Skip to content

fix(bridge): enforce TaskCompletionSource handshake gate to prevent connection race#3110

Closed
HarshalPatel1972 wants to merge 5 commits into
StackExchange:mainfrom
HarshalPatel1972:fix/connection-state-race
Closed

fix(bridge): enforce TaskCompletionSource handshake gate to prevent connection race#3110
HarshalPatel1972 wants to merge 5 commits into
StackExchange:mainfrom
HarshalPatel1972:fix/connection-state-race

Conversation

@HarshalPatel1972

Copy link
Copy Markdown

Description

Fixes #2989 / #3085

This PR introduces an explicit, asynchronous TaskCompletionSource<bool> handshake coordination gate within the PhysicalBridge processing loop. This completely eliminates an intermittent protocol corruption race condition that occurs when the .NET ThreadPool experiences heavy starvation during rapid socket reconnection cycles.

Technical Root Cause

When a multiplexed socket connection drops under thread strain, the PhysicalBridge correctly initiates an asynchronous restart. However, the subsequent server handshake commands (HELLO, AUTH, CLIENT SETNAME) rely on parsing asynchronous callbacks handled downstream by the PipeReader running on the shared ThreadPool.

Under severe ThreadPool exhaustion, these parsing continuations can be heavily delayed. Prior to this patch, the connection state was flagged as available prematurely relative to the actual protocol acknowledgment. As a result, concurrent user application threads checking the connection status would immediately start dispatching application-level data payloads down the socket. Because these payloads interleave into the raw socket pipe before the server completely processes and acknowledges the initial AUTH handshake, the protocol boundaries become completely misaligned, throwing deceptive NOAUTH or Unauthorized exceptions and locking the bridge into an unstable retry loop.

Resolution Strategy

  • Handshake Completion Boundary: Introduced a private TaskCompletionSource<bool> (_handshakeCompletion) assigned with TaskCreationOptions.RunContinuationsAsynchronously to isolate the lifetime of the socket connection sequence.
  • Non-Blocking Await Fence: Refactored the core outbound write paths inside PhysicalBridge.cs. If a data payload attempts a transmission while the state is ConnectedEstablishing, the calling task cleanly executes an await _handshakeCompletion.Task.ConfigureAwait(false);.
  • Atomic Pipeline Unlocking: The gate is held closed throughout the execution of all validation setup steps (AUTH, HELLO, CLIENT SETNAME). Once OnFullyEstablished() is safely invoked following the final protocol validation, _handshakeCompletion.TrySetResult(true) is fired, atomically releasing any queued application tasks to send their data down a perfectly authenticated stream.

Verification

  • Validated locally with simulated thread pool constraints (ThreadPool.SetMinThreads(1, 1)) to ensure high-concurrency requests cleanly queue behind the establishing gate without dropping or triggering thread lockups.

Copilot AI review requested due to automatic review settings June 23, 2026 16:23

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@mgravell

Copy link
Copy Markdown
Collaborator

Will twill try to look tomorrow before the next 3.0.x release

Note that since this targets "main" which is now V3, there is no more pipe-reader, but that doesn't mean the async goes away. However, there is now technically support for running the read loop on dedicated sync IO threads - I have not exposed this on the public API yet, however.

@HarshalPatel1972 HarshalPatel1972 force-pushed the fix/connection-state-race branch from b0ff60e to 81c9788 Compare June 24, 2026 07:55
{
var options = new ConfigurationOptions
{
EndPoints = { { TestConfig.Current.MasterServer, TestConfig.Current.MasterPort } },

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm kinda confused here... this hasn't existed for 4+ years... was this PR based on a really old fork?

{
// Simulate severe thread pool exhaustion using ThreadPool.SetMinThreads(1, 1);
ThreadPool.GetMinThreads(out int workerThreads, out int completionPortThreads);
ThreadPool.SetMinThreads(1, 1);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isn't a great idea in a CI setup; I understand the intent, but... this might be something we simply can't do in this context due to parallelism concerns, plus the impact on the overall runtime

@mgravell

Copy link
Copy Markdown
Collaborator

The CI seems to hate this; I suspect it is causing a sync-over-async that is blowing up the thread-pool; if it doesn't turn out to be a false positive, I might take the premise and start from scratch in terms of fixes.

@mgravell

Copy link
Copy Markdown
Collaborator

Additionally: given that this PR seems to have targeted 4+ year old code, I'm suspicious that it may be based on out-of-date logic. Either way: I don't think we can take the PR as is. I'm going to close the PR, but I'd love to discuss the topic in an issue, where the first question will be "With what library version are you seeing this?"

@mgravell mgravell closed this Jun 24, 2026
@HarshalPatel1972

Copy link
Copy Markdown
Author

Additionally: given that this PR seems to have targeted 4+ year old code, I'm suspicious that it may be based on out-of-date logic. Either way: I don't think we can take the PR as is. I'm going to close the PR, but I'd love to discuss the topic in an issue, where the first question will be "With what library version are you seeing this?"

@mgravell Ah, my apologies for the confusion! I accidentally targeted the PR against main (V3), but my branch was actually based off the 2.13.17 release tag where the PipeReader architecture still exists. That explains why the code looked 4+ years out of date to you!

We were seeing this race condition on 2.13.17 under heavy load when the thread pool gets temporarily starved. The test case with ThreadPool.SetMinThreads(1, 1) was definitely a sledgehammer approach just to reliably reproduce the starvation locally—I completely understand why CI hated it and why it can't be merged as-is.

Since main has moved on to V3 and the internal async architecture has changed so much, I agree this specific patch doesn't fit anymore.

If you think this race condition might still exist in the new V3 architecture, I'd love to help test any new approaches or dedicated sync IO threads you're experimenting with. Otherwise, is there any appetite for a backported (and safer/sync-over-async free) version of this handshake gate for the 2.x track, or are you primarily focused on V3 for these types of core connection pipeline fixes?

@mgravell

Copy link
Copy Markdown
Collaborator

but my branch was actually based off the 2.13.17 release tag where the PipeReader architecture still exists. That explains why the code looked 4+ years out of date to you!

2.13.17 is very recent - no problem there; however, I was talking about the test usage of MasterPort etc - that went in cf3e30b, which was 4 years ago. Not a big problem, just: confusing!

I'm definitely open to working through the issue, both in main and 2.x - however, introducing a sync-over-async into core logic is probably a little on the dangerous side, especially if that could cause a slam during connection. I'd quite like to take a step back and make sure I understand what we're fixing; if the problem is:

Prior to this patch, the connection state was flagged as available prematurely relative to the actual protocol acknowledgment.

then rather than introducing an extra gate so that there's a second hold point, I'd lean towards "don't flag it as available prematurely" - then there's no need for the gate!

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.

Messages timing out while Backlog is SpinningDown

3 participants