blackbull.server.deadline¶
blackbull.server.deadline
¶
Per-connection rescheduled deadline (Sprint 26 Phase B — scanner form).
Replaces async with asyncio.timeout(...) on the per-request hot
path. Sprint 23 used one loop.call_later TimerHandle per
connection, cancelled and rescheduled at every phase transition.
Sprint 26 Phase B replaces the per-arm call_later with a
per-process tick scanner: one singleton TimerHandle re-arms itself
every :data:_TICK_S and walks the registry of armed
:class:ConnectionDeadline instances for expirations. Per-arm cost
drops from ~1.7 µs to ~0.34 µs (WSL2 microbench, Sprint 26 Phase B
spike).
Trade-off: a fired deadline lands within [now, now + _TICK_S]
rather than at the exact requested instant. At the default
_TICK_S = 0.3 s this is ~3 % slop on the tightest configurable
deadline (BB_HEADER_TIMEOUT default 10 s) and ~1 % on the
body_timeout default 30 s. Tune via BB_DEADLINE_TICK_MS
(milliseconds, default 300, floor 10).
The scanner is loop-scoped and lazily started on the first arm;
it quiesces (cancels its own handle) when the registry empties and
auto-resurrects on the next arm. Each worker process has its
own scanner.
ConnectionDeadline
¶
One reusable deadline per connection.
The instance binds to the task that constructed it (in practice, the
connection actor's task). When the per-process scanner observes
that the deadline's monotonic _deadline_at has passed, the bound
task is cancelled — the cancellation propagates into whichever
reader.readuntil / read / readexactly is currently
awaiting. Call sites translate the cancellation into
TimeoutError via :meth:guard (the common case) or manually
by checking :attr:fired.
Why a scanner instead of one call_later per arm? At
saturation the per-arm path costs ~1.7 µs (TimerHandle + heap
push + cancel). The scanner replaces that with one
loop.time() call + one comparison + one set membership check
on the registry (~0.34 µs). Sprint 26 Phase B microbench: 80 %
per-call reduction; cascade-multiplied estimate ~15–25 % B1.
arm(seconds)
¶
(Re-)set the deadline; seconds <= 0 disables it.
Safe to call repeatedly. Resets :attr:fired so a recovered
deadline can be reused across phases on the same connection.
disarm()
¶
Drop the deadline. Idempotent.
guard(seconds)
¶
Arm the deadline and return self as a context manager.
Caller pattern::
with dl.guard(cfg.header_timeout):
await reader.readuntil(...)
Matches the observable behaviour of async with asyncio.timeout(d):
— a fired deadline manifests as TimeoutError. The
same-loop-iteration race where the underlying read completes
and the deadline fires in the same tick is treated as a
timeout (same convention as asyncio.timeout).
Returns self rather than allocating a wrapper object. Safe
because each connection owns its own ConnectionDeadline
and uses it sequentially from a single task.