Fix #28: ReadFrom no longer holds the lock while blocked in rd.Read - #29
Merged
Conversation
ReadFrom took r.mu once and held it via defer for the whole loop, calling rd.Read while the mutex was still held. Whenever the source was idle (e.g. an idle TCP connection), rd.Read blocked with the lock held and froze the entire buffer: Read, Peek, Length, Free and IsEmpty all blocked until bytes arrived. This is reachable without naming the method, since io.Copy(rb, rd) dispatches to ReadFrom via io.ReaderFrom. Fix, preserving the zero-copy contract (ReadFrom reads directly into r.buf): - Add writeMu to serialize the write side. ReadFrom holds it for its whole duration and reserves the contiguous free window beyond the write pointer, releasing r.mu during rd.Read. Because the reserved window is disjoint from the committed [r, w) region, readers and inspectors stay responsive and correct while a read is in flight. - Guard Write/WriteByte/TryWrite/TryWriteByte with writeMu (lock order is always writeMu -> r.mu) so no concurrent writer scribbles into the reserved window or moves r.w out from under the snapshot. - Snapshot the generation before releasing r.mu; if Reset() lands while blocked in rd.Read, the stale bytes are dropped and the loop recomputes against the fresh state. Add readfrom_idle_test.go: a regression test proving inspectors return promptly while ReadFrom is parked in an idle read (deadlocks before this fix), plus concurrent slow-source and Reset stress tests under -race. Also bump go.mod to 1.26 and modernize (WaitGroup.Go, testing.B.Loop, any).
This was referenced Sep 9, 2026
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.
Problem (#28)
ReadFromacquiredr.muonce and held it viadeferfor the entire loop, callingrd.Readwhile the mutex was still held. When the source is idle (e.g. an idle TCP connection),rd.Readblocks with the lock held, freezing the whole buffer —Read,Peek,Length,FreeandIsEmptyall block until bytes arrive.This is reachable without ever naming the method:
io.Copy(rb, rd)dispatches toReadFromthroughio.ReaderFrom.Fix
Release
r.mufor the duration ofrd.Readwhile preservingReadFrom's zero-copy contract (it reads directly intor.buf).writeMuserializes the write side.ReadFromholds it for its whole duration and reserves the contiguous free window beyond the write pointer, releasingr.muduringrd.Read. The reserved window is disjoint from the committed[r, w)region that readers andPeek/Lengthtouch, so the read/inspection side stays responsive and correct while a read is in flight.writeMu(Write/WriteByte/TryWrite/TryWriteByte), lock order alwayswriteMu → r.mu, so no concurrent writer scribbles into the reserved window or movesr.wout from under the snapshot. Readers andResetnever takewriteMu, so they remain independent.r.mu; ifReset()lands while blocked inrd.Read, the stale bytes are dropped and the loop recomputes against the fresh state.Tests
readfrom_idle_test.go:TestReadFromIdleSourceDoesNotBlockInspectors— regression test: inspectors return promptly whileReadFromis parked in an idle read. Deadlocks before this fix.TestReadFromConcurrentWithInspectorsAndDrain— slow multi-chunk source vs. concurrent inspect + drain.TestReadFromConcurrentWithReset— hammersReset()mid-copy to exercise the generation snapshot/discard path.All green under
go test -race -count=1 ./...;gofmt -l .empty;go vet ./...clean.Note
This PR also bumps
go.modto1.26and applies matching modernizations (WaitGroup.Go,testing.B.Loop,interface{}→any). If forcing a Go version bump on downstream users is undesirable, the fix itself is independent of the bump and can be rebased ontogo 1.19with the regression test rewritten in the older idioms — happy to split it out.Closes #28