Test failure #66
Labels
No labels
breaking-change
bug
documentation
duplicate
enhancement
good first issue
help wanted
performance
question
wontfix
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Grandiras/ATProto.NET#66
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
[xUnit.net 00:00:31.27] ATProtoNet.Tests.Pds.PdsFederationEndpointTests.SubscribeRepos_StreamsLiveCommitEvents [FAIL]
[xUnit.net 00:00:31.27] System.OperationCanceledException : The operation was canceled.
[xUnit.net 00:00:31.27] Stack Trace:
[xUnit.net 00:00:31.28] at System.Threading.CancellationToken.ThrowOperationCanceledException()
[xUnit.net 00:00:31.28] at System.Threading.SemaphoreSlim.WaitUntilCountOrTimeoutAsync(TaskNode asyncWaiter, Int64 millisecondsTimeout, CancellationToken cancellationToken)
[xUnit.net 00:00:31.28] at Microsoft.AspNetCore.TestHost.TestWebSocket.ReceiverSenderBuffer.ReceiveAsync(CancellationToken cancellationToken)
[xUnit.net 00:00:31.28] at Microsoft.AspNetCore.TestHost.TestWebSocket.ReceiveAsync(ArraySegment
1 buffer, CancellationToken cancellationToken) [xUnit.net 00:00:31.28] /workspace/Grandiras/ATProto.NET/tests/ATProtoNet.Tests/Pds/PdsFederationEndpointTests.cs(503,0): at ATProtoNet.Tests.Pds.PdsFederationEndpointTests.ReceiveFrameAsync(WebSocket socket, CancellationToken cancellationToken) [xUnit.net 00:00:31.28] /workspace/Grandiras/ATProto.NET/tests/ATProtoNet.Tests/Pds/PdsFederationEndpointTests.cs(437,0): at ATProtoNet.Tests.Pds.PdsFederationEndpointTests.SubscribeRepos_StreamsLiveCommitEvents() [xUnit.net 00:00:31.28] --- End of stack trace from previous location --- Failed ATProtoNet.Tests.Pds.PdsFederationEndpointTests.SubscribeRepos_StreamsLiveCommitEvents [30 s] Error Message: System.OperationCanceledException : The operation was canceled. Stack Trace: at System.Threading.CancellationToken.ThrowOperationCanceledException() at System.Threading.SemaphoreSlim.WaitUntilCountOrTimeoutAsync(TaskNode asyncWaiter, Int64 millisecondsTimeout, CancellationToken cancellationToken) at Microsoft.AspNetCore.TestHost.TestWebSocket.ReceiverSenderBuffer.ReceiveAsync(CancellationToken cancellationToken) at Microsoft.AspNetCore.TestHost.TestWebSocket.ReceiveAsync(ArraySegment1 buffer, CancellationToken cancellationToken)at ATProtoNet.Tests.Pds.PdsFederationEndpointTests.ReceiveFrameAsync(WebSocket socket, CancellationToken cancellationToken) in /workspace/Grandiras/ATProto.NET/tests/ATProtoNet.Tests/Pds/PdsFederationEndpointTests.cs:line 503
at ATProtoNet.Tests.Pds.PdsFederationEndpointTests.SubscribeRepos_StreamsLiveCommitEvents() in /workspace/Grandiras/ATProto.NET/tests/ATProtoNet.Tests/Pds/PdsFederationEndpointTests.cs:line 437
--- End of stack trace from previous location ---
🤖 On it — follow along in the action run; the reply will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the task runs.
Pushed branch
claude/issue-66.Root cause
Not a flaky test — a real race in the PDS firehose handler.
subscribeReposaccepted the WebSocket and then enumeratedPdsSequencer.SubscribeAsync. Because that method is anasync IAsyncEnumerable, its body — including the_subscribers.Add(...)registration — doesn't run until the first read. Meanwhile, withTestServer(and Kestrel), the client'sConnectAsyncreturns as soon as the server callsAcceptWebSocketAsync. That leaves a window where the peer believes it is connected and can already provoke events, but nothing is listening yet.The test does exactly that: connect, then immediately
createRecord. If the#commitis published in that window it lands in neither the replay snapshot (there is none — no cursor) nor the live channel, and the live-only subscriber waits out the full 30 s for a frame that is already gone.Verification
I confirmed the mechanism rather than guessing. The test passes 8/8 in isolation, so I temporarily inserted
await Task.Delay(500)between accept and subscribe — that reproduced the reported failure exactly: sameOperationCanceledException, sameReceiveFrameAsyncstack frame, same 30 s duration. With the fix and that same 500 ms delay still in place, all 26PdsFederationEndpointTestspass, which is the real proof the window is closed.Fix
PdsSequencer.Subscribe(cursor), which registers eagerly and returns a disposablePdsFirehoseSubscription. Events published between registering and the first read are buffered on its channel instead of dropped.AcceptWebSocketAsync, so "connected" and "receiving" become the same instant.CurrentSeq/OldestAvailableSeqsnapshotted under the same lock as the registration, and theFutureCursor/OutdatedCursorchecks now read those instead of re-reading the live sequencer. That closes a second, narrower TOCTOU where a publish racing the handshake could make a valid cursor look like a future one.SubscribeAsyncis unchanged and delegates to the new method — no breaking change; existing callers and the five existingSubscribeAsynctests still pass untouched.Tests
Added four deterministic
PdsSequencerTestscovering the contract at the level where it can be asserted without timing: buffering across the registration→read gap, the snapshot not moving under a later publish, idempotent disposal, and rejecting double enumeration.Full suite: 1398/1398 passing, Release config, three consecutive runs. Build has 0 errors and no new warnings.
One note on scope: I fixed the server rather than the test, because the same hole is reachable by a real relay — the test just happens to hit the timing reliably enough to expose it. A cursor-based relay would recover on reconnect, but a live-only subscriber silently loses the event.
⏱ 8m 7s · 44 turns · tokens: 61.0k in / 21.2k out (+2148.3k cache-read)
📜 Full session transcript
→ Opened PR: #67