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_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.