Skip to content

add PeekAt(): read at an offset past the read pointer - #31

Merged
smallnest merged 1 commit into
smallnest:masterfrom
Arrayscape:feature/peekat
Sep 9, 2026
Merged

add PeekAt(): read at an offset past the read pointer#31
smallnest merged 1 commit into
smallnest:masterfrom
Arrayscape:feature/peekat

Conversation

@arraytad

@arraytad arraytad commented Sep 9, 2026

Copy link
Copy Markdown

Problem

Peek always starts at the read pointer. That is the right default, but it
leaves out a consumer that has to run ahead of what it can release.

Where bytes stay buffered until something confirms them — a retransmission
window, a stream that may have to be replayed after a reconnect — the read
pointer sits wherever confirmation has reached, which is not where reading
should resume. The two are separated by everything in flight.

Such a consumer keeps its own cursor, and today the only way to read at it is
Peek(p) with len(p) >= cursor + n, then discard the first cursor bytes. So
the destination buffer has to be as large as the entire unreleased span, and
every read copies that whole prefix again. The cost grows with whatever is
unacknowledged, which is exactly the quantity that grows when a consumer falls
behind.

In our case (buffering a server's output so a browser that loses its WebSocket
can resume the byte stream exactly where it left off) this put a ceiling on
delivery: once the unacknowledged span exceeded the read buffer, the same prefix
came back every time and the stream stopped advancing.

API

func (r *RingBuffer) PeekAt(off int, p []byte) (n int, err error)

Reads up to len(p) bytes starting off bytes past the read pointer, without
moving it. PeekAt(0, p) is Peek(p).

Semantics

  • off at or past the end of the buffered data returns (0, nil). Not an
    error, and deliberately not ErrIsEmpty. It means the consumer has taken
    everything there is — an ordinary thing to ask, and something a caught-up
    consumer asks on every poll.
  • A genuinely empty buffer returns ErrIsEmpty, matching Peek.

Otherwise: short reads at the end return what is there; the wrap is handled with
the same two-copy split as copyFromBuffer; off < 0 is treated like an empty
p.

PeekAt takes only r.mu and touches only committed-region state (length(),
r, size). It never takes writeMu, so it is consistent with the rule #29
established — the read/inspection side stays independent of the write side, and
there is no lock-order interaction to get wrong.

Tests

peekat_test.go:

  • TestPeekAtReadsFromTheOffset — the basic contract, and that the read pointer does not move
  • TestPeekAtShortAtTheEnd — fewer bytes available than requested
  • TestPeekAtPastTheEndIsNotAnError — the (0, nil) case above
  • TestPeekAtAcrossTheWrap — offset and span straddling the end of the backing array
  • TestPeekAtOnAFullBuffer — the boundary where r == w means full, not empty
  • TestPeekAtEmptyAndDegenerate — empty buffer, empty p, negative offset
  • TestPeekAtAgreesWithPeek — differential: PeekAt(0, p) matches Peek(p) across sizes

gofmt -l . empty, go vet ./... clean, go test -race -count=1 ./... green
against current master (go1.26.3), including #29's rewritten locking.

@smallnest
smallnest merged commit 6e2b059 into smallnest:master Sep 9, 2026
@arraytad

arraytad commented Sep 9, 2026

Copy link
Copy Markdown
Author

Thank you!

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.

3 participants