Skip to content

Fix memory leak from undisposed sockets and event handler retention during server restart#3608

Merged
marcschier merged 4 commits into
OPCFoundation:masterfrom
RicoSuter:fix/dispose-socket-and-event-leaks
Mar 11, 2026
Merged

Fix memory leak from undisposed sockets and event handler retention during server restart#3608
marcschier merged 4 commits into
OPCFoundation:masterfrom
RicoSuter:fix/dispose-socket-and-event-leaks

Conversation

@RicoSuter

@RicoSuter RicoSuter commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

When an OPC UA server is stopped and restarted in-process (e.g. for configuration changes), TcpServerChannel instances from previous server lifetimes were never garbage collected, causing a steady heap growth per restart cycle.

Root cause: UaSCBinaryChannel.Dispose() did not close the underlying Socket. Pending ReceiveAsync operations kept the channels pinned via SocketAsyncEngine, preventing GC of the entire channel object graph (certificate caches, protocol buffers, etc.).

Changes

  • UaSCBinaryChannel.Dispose() — Close socket to cancel pending async I/O and allow GC of disposed channels
  • ServerBase.Dispose() — Unsubscribe ConnectionStatusChanged from transport listeners before disposing them

Verification

Tested with an in-process server restart loop over 786 cycles (~17 hours). Without the fix, heap grew steadily (~15 KB/cycle). With the fix, heap stabilized at 80-81 MB after initial warmup and remained flat for the entire run.

Test plan

  • Verified heap stability over 786 restart cycles
  • Confirmed via GC dumps that zombie TcpServerChannel count stays at active-only (3-5) instead of accumulating

Notes

Do I need to add tests for this?

Disclaimer: This bug has been found with hours of manual work with AI and my local OPC UA stress test application and should be safe - please review the changes as I don't really know the OPC UA code base in depth.

…uring server restart

When an OPC UA server is stopped and restarted in-process, TcpServerChannel
instances were never garbage collected because UaSCBinaryChannel.Dispose() did
not close the underlying socket. Pending ReceiveAsync operations kept the
channels pinned via SocketAsyncEngine, causing a steady heap growth per cycle.

Changes:
- Close socket in UaSCBinaryChannel.Dispose() to cancel pending async I/O
- Unsubscribe CertificateValidator.CertificateUpdate in StandardServer.Dispose()
- Unsubscribe ConfigurationWatcher.Changed in StandardServer.Dispose()
- Unsubscribe ConnectionStatusChanged from TransportListeners in ServerBase.Dispose()

Verified over 786 restart cycles with stable heap.
@codecov

codecov Bot commented Mar 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 50.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 66.06%. Comparing base (d25caff) to head (d9f8f17).
⚠️ Report is 140 commits behind head on master.

Files with missing lines Patch % Lines
Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           master    #3608       +/-   ##
===========================================
+ Coverage   51.86%   66.06%   +14.19%     
===========================================
  Files         370      459       +89     
  Lines       78618    97822    +19204     
  Branches    13650    16383     +2733     
===========================================
+ Hits        40779    64630    +23851     
+ Misses      33705    28143     -5562     
- Partials     4134     5049      +915     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@marcschier
marcschier merged commit eae41a5 into OPCFoundation:master Mar 11, 2026
138 of 141 checks passed
jnsjll pushed a commit to jnsjll/UA-.NETStandard that referenced this pull request Mar 12, 2026
…uring server restart (OPCFoundation#3608)

* Fix memory leak from undisposed sockets and retained event handlers during server restart

When an OPC UA server is stopped and restarted in-process, TcpServerChannel
instances were never garbage collected because UaSCBinaryChannel.Dispose() did
not close the underlying socket. Pending ReceiveAsync operations kept the
channels pinned via SocketAsyncEngine, causing a steady heap growth per cycle.

Changes:
- Close socket in UaSCBinaryChannel.Dispose() to cancel pending async I/O
- Unsubscribe CertificateValidator.CertificateUpdate in StandardServer.Dispose()
- Unsubscribe ConfigurationWatcher.Changed in StandardServer.Dispose()
- Unsubscribe ConnectionStatusChanged from TransportListeners in ServerBase.Dispose()

Verified over 786 restart cycles with stable heap.

* Remove verbose comment

* Remove redundant event unsubscribe before dispose

* Remove duplicate CertificateUpdate unsubscribe (already existed in master)
mrsuciu pushed a commit to mrsuciu/UA-.NETStandard that referenced this pull request Mar 19, 2026
…uring server restart (OPCFoundation#3608)

* Fix memory leak from undisposed sockets and retained event handlers during server restart

When an OPC UA server is stopped and restarted in-process, TcpServerChannel
instances were never garbage collected because UaSCBinaryChannel.Dispose() did
not close the underlying socket. Pending ReceiveAsync operations kept the
channels pinned via SocketAsyncEngine, causing a steady heap growth per cycle.

Changes:
- Close socket in UaSCBinaryChannel.Dispose() to cancel pending async I/O
- Unsubscribe CertificateValidator.CertificateUpdate in StandardServer.Dispose()
- Unsubscribe ConfigurationWatcher.Changed in StandardServer.Dispose()
- Unsubscribe ConnectionStatusChanged from TransportListeners in ServerBase.Dispose()

Verified over 786 restart cycles with stable heap.

* Remove verbose comment

* Remove redundant event unsubscribe before dispose

* Remove duplicate CertificateUpdate unsubscribe (already existed in master)

(cherry picked from commit eae41a5)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants