Skip to content

Serialize outbound frame writes to fix Stream.WriteAsync race (#211)#212

Merged
pedrosakuma merged 1 commit into
mainfrom
fix/211-serialize-outbound-writes
Jul 7, 2026
Merged

Serialize outbound frame writes to fix Stream.WriteAsync race (#211)#212
pedrosakuma merged 1 commit into
mainfrom
fix/211-serialize-outbound-writes

Conversation

@pedrosakuma

Copy link
Copy Markdown
Owner

Summary

Fixes #211 — concurrent outbound sends could race on the shared Stream.WriteAsync, letting frames land on the wire out of MsgSeqNum order or interleaved.

Changes

  • FixpClientSession: added a _writeLock (SemaphoreSlim(1,1)) wrapping every raw _stream.WriteAsync/FlushAsync call site — Negotiate, Establish, Terminate, Sequence, RetransmitRequest, and application frames — so no two callers ever write to the underlying Stream/SslStream concurrently.
  • EntryPointClient: added a _sendLock spanning the full "reserve seq → encode → write" critical section for every outbound application frame (SubmitAsync, SubmitSimpleAsync, ReplaceAsync, ReplaceSimpleAsync, CancelAsync, MassActionAsync, SubmitCrossAsync, SendQuoteRequestAsync, SendQuoteAsync, CancelQuoteAsync), via a new private SendOrderEntryFrameAsync helper. This guarantees wire order always matches seq-assignment order, closing the gap between the already-atomic NextOutboundSeqNum() reservation and the actual write.
  • Both locks are disposed alongside their owning session/client.

Tests

  • ConcurrentSendOrderingTests.SendApplicationFrameAsync_SerializesConcurrentWrites_PreservesCallOrder — deterministic test using a DelayableStream double that stalls a "slow" frame's write; verified it fails without the fix (fast frame wins the race and lands first) and passes with the fix.
  • ConcurrentSendOrderingTests.ConcurrentCancelAsync_ArrivesInGapFreeSeqOrder — end-to-end test firing 8 concurrent CancelAsync calls (mirroring the "Cancel All" trigger) over a real TCP loopback InProcessFixpTestPeer, asserting gap-free, unique MsgSeqNum with zero decode failures.

Validation

  • dotnet build SbeB3EntryPointClient.slnx -c Release — 0 warnings/errors
  • dotnet test SbeB3EntryPointClient.slnx --no-build -c Release — all unit tests pass (213), conformance tests correctly skipped (no peer env vars)
  • dotnet format SbeB3EntryPointClient.slnx --verify-no-changes --no-restore --severity warn — clean

Closes #211.

- FixpClientSession: add a _writeLock (SemaphoreSlim) guarding every raw
  _stream.WriteAsync/FlushAsync call site (Negotiate, Establish,
  Terminate, Sequence, RetransmitRequest, application frames), so no
  two callers can ever write to the underlying Stream/SslStream
  concurrently.
- EntryPointClient: add a _sendLock spanning reserve-seq -> encode ->
  write for every outbound application frame (Submit*/Replace*/Cancel*/
  MassAction/SubmitCross/Quote*), so wire order always matches the
  order MsgSeqNum was assigned, closing the gap between the atomic
  NextOutboundSeqNum() reservation and the actual write.
- Add regression tests: a deterministic FixpClientSession-level test
  proving writes no longer race/reorder, and an end-to-end test firing
  concurrent CancelAsync calls over a real TCP loopback peer asserting
  gap-free, ordered MsgSeqNum with no decode failures.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@pedrosakuma pedrosakuma merged commit 33cdcfa into main Jul 7, 2026
5 checks passed
@pedrosakuma pedrosakuma deleted the fix/211-serialize-outbound-writes branch July 7, 2026 16:50
pedrosakuma pushed a commit that referenced this pull request Jul 7, 2026
…sync race (#211) (#212)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

Concurrent outbound app-frame sends race on the raw Stream.WriteAsync (no write-side serialization)

1 participant