Clamp the overwrite discard to what the buffer holds - #27
Conversation
In overwrite mode write() advances the read pointer by len(p) - avail to
make room. When len(p) is larger than the buffer, that count exceeds the
data actually present and the % size wraps it to an arbitrary offset,
leaving bytes readable that were never written.
New(4).SetOverwrite(true).Write([]byte("abcdef")) returned 2 and left
"\x00\x00ab" readable with Length() == 4.
TestRingBuffer_OverwriteMode_TwiceFull already asserts the intended
semantic - an oversize write keeps the first size bytes - but only
exercises a length that is an exact multiple of the size, so the wrap
cancels out.
Review: LGTM — mergingI reviewed the diff, audited the overwrite path in The bug is realIn overwrite mode, The fix is correct and minimalClamping Scope is clean
Nice, tightly-scoped fix. Merging. |
In overwrite mode,
write()makes room by advancing the read pointer (ring_buffer.go:664):When
len(p)is larger than the buffer,neededis bigger than the data actually present, and% r.sizewraps it to an arbitrary offset rather than clamping. The read pointer ends up past space that was never written.Two NUL bytes that were never written are readable, and
Length()reports a full buffer.Measured at master, four buffer states:
The last row still has
"cd"from the previous write.Your own test already declares the right answer
TestRingBuffer_OverwriteMode_TwiceFull(ring_buffer_test.go:1930) writes 8 bytes into a full 4-byte buffer and asserts"efgh"-- an oversize write keeps the firstsizebytes and drops everything older. That is the semantic I am matching.It passes today only because
8 % 4 == 0, so the wrap happens to land back on the same offset. Change the 8 to a 6 and the identical call returns 2 bytes with"cd"still readable.writeByte(ring_buffer.go:764) is the other half of the pair: it advancesr.ronly insideif r.w == r.r && r.isFull, so it never moves the read pointer past data that exists. Only the bulk path does.The fix
Clamp
neededtor.size - avail, the amount actually held. After it, all four rows above given=4and agree withTwiceFull:"abcd","abcd","cdef","efgh".What I deliberately did not change
The semantic for an oversize write stays yours -- the first
sizebytes are kept andErrTooMuchDataToWriteis still returned. Making it keep the newestsizebytes instead would be a design proposal and would contradictTwiceFull. This only makes every buffer state produce the answer that test already declares.I also left alone the fact that a blocking overwrite
Writewaits onErrTooMuchDataToWrite(ring_buffer.go:414); that is a separate question.Verification
go test -race -count=1 ./...(the workflow's line) --ok github.com/smallnest/ringbuffer 21.960s.gofmt -l .empty,go vet ./...clean.TestIssue23, the regression test from the closed issue in the same overwrite family, passes.I checked the clamp from three directions rather than only confirming the new rows go green:
r.size - avail - 1-- yourTwiceFullfails,Expected 'efgh', got 'defg'r.size - avail + 1-- yourTwiceFullfails,Expected 'efgh', got 'bcde'Your existing row failing on both sides is what pins the value exactly, and it means this adds a clamp arm rather than moving a threshold.
Observable output changes only for
len(p) > Capacity()in overwrite mode. The whole suite, including all eightOverwriteMode_*tests, is green unmodified -- no existing assertion had to change.Disclosure: AI-assisted. I found and prepared this with an AI assistant, and I ran and verified everything above myself.