tests: pin the PipeReader contract and the ring buffers behind it - #238
Merged
Merged
Conversation
TcpConnectionPipeReader had no coverage of the two things #226 asks about, and both are the kind that fail silently: get examined wrong and a "read until I have a whole frame" loop spins hot on the same bytes; get consumed wrong and either the buffer never returns to the group or it returns while the caller is still holding a slice of it. Three tests, all against a real connection: examined-to-the-end parks the next read instead of respinning it. Consume nothing, examine everything, read again - the read must not come back until there are NEW bytes, and when they arrive it must carry the unconsumed prefix with them (AB then C reads back as ABC, not C). consuming part of one recv leaves the rest readable. HEAD|BODY in a single recv, consume five bytes, read again: BODY comes back from what is already held. This is the case AdvanceTo rebases offsets for, and the rebase is load-bearing - ReadOnlySequence.GetOffset measures from the segment's RunningIndex, not from the sequence's logical start, so a sequence over segment(RunningIndex 4096)[30..] reports GetOffset(Start) == 4126 rather than 0. Without the rebase the held-byte counter goes negative and the next read parks forever on bytes that already arrived. recv buffers go back to the ring as consumption passes them. 2,000 round trips through an eight-slot buffer group: if a consumed buffer did not return, the group is exhausted within a handful of messages. Deleting the ReturnBuffer call in AdvanceTo fails this with "Unable to read data from the transport connection: Connection timed out", so it is not vacuous. No src changes: the investigation found the reader correct on both counts. Each handler ignores a connection that sends nothing and closes. TestServer proves a port is listening by connecting and dropping it, so every server here serves one probe that is not the test's - and on a reader that is a real mistake rather than a nuisance, because a probe connection reports IsEmpty and IsCompleted on its first read, which is exactly the shape a finished stream has. E2E 188 passed, 0 failed.
MDA2AV
force-pushed
the
test/pipereader-contract
branch
from
September 20, 2026 21:10
804e8a1 to
0b61cb6
Compare
Merged
MDA2AV
added a commit
that referenced
this pull request
Sep 20, 2026
All twelve published packages share one version, as always. First release since #237, so the bump really is only the twelve Version elements: IoxideRuntime.Version is generated from ioxide.csproj now, and the README's blockquote literal is gone in favour of the nuget badge that was already there. Nothing else in the tree states the number. Carries #239 (recv buffers a reader or stream still holds are reclaimed at teardown, on both TCP and QUIC), #238 (the PipeReader contract under test) and #237 itself. E2E 194, Unit 46, Http 44, Tls 142, Chaos 47, File 4.
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Answers #226 — see the investigation comment for the full write-up. Tests only; no
src/changes, because both halves of the question turned out to be working.What had no coverage
TcpConnectionPipeReaderis the zero-copy reader: it hands out ring memory rather than copies. Both of the things #226 asks about fail silently when they are wrong.Get
examinedwrong andReadAsynchands back the same bytes forever, so an ordinary "read until I have a whole frame" loop spins hot. Getconsumedwrong and either the buffer never returns to the group, or it returns while the caller is still holding a slice of it.The three tests
examined-to-the-end parks the next read instead of respinning it. Consume nothing, examine everything, read again — the read must not come back until there are new bytes, and when they arrive it must still carry the unconsumed prefix.ABthenCreads back asABC, notC.Consuming part of one recv leaves the rest readable.
HEAD|BODYin a single recv, consume five bytes, read again:BODYcomes back from what is already held, without parking.This is the case
AdvanceTorebases offsets for, and I checked the assumption underneath it because the comment there is surprising enough to be worth confirming:GetOffsetmeasures from the segment'sRunningIndex, not from the sequence's logical start. So the- startOffsetrebase is load-bearing: without it the held-byte counter goes negative and the next read parks forever on bytes that already arrived — the hang the existing comment describes.Recv buffers go back to the ring as consumption passes them. 2,000 request/response round trips through an eight-slot buffer group. If a consumed buffer did not return, the group is exhausted within a handful of messages.
Non-vacuous — deleting the
ReturnBuffercall inAdvanceTo:E2E 188 passed, 0 failed.
One harness wrinkle, handled
Every handler here ignores a connection that sends nothing and then closes.
TestServerproves a port is listening by connecting aTcpClientand dropping it, so every server serves one probe that is not the test's.On a
PipeReaderthat is a real trap rather than a nuisance: the probe's first read reportsIsEmpty && IsCompleted, which is exactly the shape a finished stream has. My first draft of the buffer test was filled in by the probe and reportedserved 0before the test's own connection had said a word.Not in this PR
Two things the investigation turned up that need source changes, deliberately left out:
TcpConnectionDualPipehas noIAsyncDisposable.DrainRecv()at recycle drains the connection's SPSC ring only, so slices already pulled into the reader's chain come back only viaComplete(). That makes "Complete the reader beforeDecRef" load-bearing and unenforced — sharper in incremental mode, where recycleAlignedFreesBufSlabwholesale and the generation guard drops a late return. The TLS dual pipe already has a disposal; this one is two properties.TlsProloguePipeReaderreportsIsCompletedgoing back to false after a partial consume once the peer is gone. Already pinned by the pending test atPrologueReaderTests.cs:145, so not re-filed.On closing #226
Left without a closing keyword. The literal questions are answered and now pinned, but the dual-pipe disposal gap above came out of the same investigation, so whether to close it or keep it open for that is your call.