Skip to content

fix: address PR #198 review — lock Deactivate, avoid stale _client in status callback - #199

Merged
anthony-lopez-pd merged 1 commit into
maintenance-1x-tcp-client-dispose-socketfrom
maintenance-1x-tcp-client-dispose-socket-review-fixes
Sep 7, 2026
Merged

anthony-lopez-pd merged 1 commit into
maintenance-1x-tcp-client-dispose-socketfrom
maintenance-1x-tcp-client-dispose-socket-review-fixes

Conversation

@anthony-lopez-pd

Copy link
Copy Markdown

Addresses all three review comments on #198. Targets #198's branch rather than maintenance-1x
so merging this updates that PR in place and keeps its review threads intact.

Direct pushes to the #198 branch are refused by branch protection ("Changes must be made through
a pull request"), which is why this arrives as a stacked PR rather than another commit.

Comment Verdict Fix
Deactivate() doesn't hold connectLock valid wrapped in connectLock/try-finally
Client_SocketStatusChange uses _client not the callback's client valid uses the argument, plus null and ReferenceEquals guards
dispose catch logs ex.Message valid logs the full exception

1. Deactivate() under connectLock

It disposed RetryTimer and the client without the lock, so it could race
Connect()/Disconnect()/Reconnect() and WaitAndTryReconnect's lock-protected
RetryTimer.Reset. My disposal change made this materially worse by nulling _client.

Verified no same-thread re-entry: nothing DisposeClient() calls takes connectLock.

2. Stale _client in the status callback

The connected path called _client.ReceiveDataAsync(...) on the field rather than the
callback's own TCPClient. Now that DisposeClient() nulls _client, a queued status change
arriving afterwards would throw — a failure mode my change introduced.

Now uses the client argument with a null check and a ReferenceEquals(client, _client) test,
so events from a superseded socket don't re-arm receive on a dead client.

The ConnectionChange event still fires as before — the guard only skips the receive
re-arm, not the notification. I deliberately avoided an early return, which would have changed
event semantics beyond what the comment asked for.

3. Log the full exception

A disposal race is exactly the case where the stack trace and any inner exception matter.

One note on that comment's premise: it said the sibling GenericSecureTcpIpClient "frequently"
logs the full exception. It's mixed — full ex in one place, ex.Message in two others. The
underlying argument holds regardless, so I made the change.

Compiles clean under MSBuild. Line endings unchanged (LF-only, matching the file).

…callback

Three review comments on the disposal change, all valid.

1. Deactivate() disposed RetryTimer and the client without holding
   connectLock, so it could race Connect()/Disconnect()/Reconnect() and
   WaitAndTryReconnect's lock-protected RetryTimer.Reset. Now wrapped in
   connectLock/try-finally like the other lifecycle methods. Verified no
   same-thread re-entry: nothing DisposeClient() calls takes the lock.

2. Client_SocketStatusChange's connected path called
   _client.ReceiveDataAsync(...) on the field rather than the callback's
   own TCPClient. Since DisposeClient() now nulls _client, a queued
   status change could arrive afterwards and throw. It now uses the
   `client` argument, guarded with a null check and a ReferenceEquals
   test so events from a superseded socket don't re-arm receive on a dead
   client. The ConnectionChange event still fires as before — the guard
   only skips the receive re-arm, not the notification.

3. The dispose catch logged ex.Message, dropping the stack trace and any
   inner exception. Now logs the full exception, which is exactly what a
   disposal race needs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@anthony-lopez-pd
anthony-lopez-pd merged commit 9045b33 into maintenance-1x-tcp-client-dispose-socket Sep 7, 2026
2 checks passed
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