Skip to content

blackbull.server.read_buffer

blackbull.server.read_buffer

Single connection-owned byte buffer.

Keep protocol verdicts and receive policy outside this class. It reports bytes and drained boundaries; the reader decides when to release capacity. Not safe for concurrent readers or cross-thread mutation.

ReadBuffer

A cursor-addressed byte buffer fed by asyncio.BufferedProtocol.

available property

Unconsumed bytes currently resident.

capacity property

Size of the underlying allocation (diagnostics and tests).

examined_bytes property

Cumulative bytes the head scan has looked at on this connection.

Exposed so the linear-scan invariant is assertable rather than merely intended: a scan restarting from the front on every arrival would make this quadratic in the number of segments, a peer-chosen CPU cost. Overlap of up to three bytes per resumption is the straddled-terminator back-off, not a restart.

buffer_updated(nbytes)

Declare how much of the last get_buffer was written.

Returns the new resident count, and updates peak_avail on the way — but only for a grown buffer, since a floor-sized one can never reach the release decision that reads it.

compact()

Move the unconsumed tail to the front.

Called on message boundaries. Without it the cursors walk forward for the life of a keep-alive connection and the allocation grows to every byte ever received on it. Compacting to empty is the one moment a message is provably gone, so it raises drained_boundary.

consume(n)

Advance past n bytes handed out by view.

consume_boundary()

Take the raised boundary, and start the next message's accounting.

The reader polls drained_boundary and calls this when it is set. Clearing the flag and the peak has to happen here rather than at the call site: a consumer resetting them itself makes an edge-triggered signal whose edge goes to whichever consumer reaches it first.

find(sep, start=0)

Offset of sep within the resident bytes, or -1.

Scans from the read cursor every call, unlike find_head_end: its callers change the search target between calls, so a carried scan offset would be wrong rather than merely wasteful. start is a relative offset for a caller resuming its own scan on a separator that is still pending.

find_head_end(limit=0)

Return head length including terminator, -1 if incomplete, or LIMIT_EXCEEDED.

limit=0 is unbounded. Resume across split terminators without rescanning the whole head; reset scan state at each message boundary.

get_buffer(sizehint, *, want=0)

Space for the transport to read into.

The returned buffer is never empty — an empty one stalls the connection permanently — even though asyncio passes sizehint = -1 when it has no preference.

sizehint is advisory and is never honoured; the Internals page says why. Growth follows arriving bytes and want, the caller's pending read size: a parked read of n gets a span of up to min(n, high-water) so one recv feeds most of it, and want = 0 stays at the floor.

release_to_floor()

Hand a grown allocation back. True when it was given up.

The mechanism behind the reader's hysteretic release policy: a single large upload must not leave every connection that served one holding its peak allocation for the rest of its keep-alive life.

Refuses while bytes are resident. That is the one invariant this container owes its caller, not a policy check — drained_boundary is also raised on the arrival path, where a delivery lands immediately afterwards and reallocating would discard bytes the transport has already handed over.

take(n)

Materialize and consume the next n bytes.

Use the size gate for copying. Release temporary memoryviews before returning: a live export prevents the next buffer resize and can cause BufferError.

view(n)

Return an unconsumed view of the next n resident bytes; callers own its lifetime.