blackbull.server.warmup¶
blackbull.server.warmup
¶
Pre-fork warm-up — run an app's warm-up hooks once in the master before it binds a listening socket or forks workers, so every worker is born warm.
Motivation¶
BlackBull runs one asyncio event loop per worker; that loop services both
accept() and request processing. A cold CPU-bound coroutine (a fresh
gRPC codec encoding thousands of streams, or a burst of cold TLS handshakes)
can hold the loop long enough that the accept reader callback never runs,
the kernel listen backlog overflows, and a colocated load generator's redial
storm turns a cold-start transient into an ECONNREFUSED collapse. The
empirical tell is that the second run of the same process is always clean —
warmth, not structure, is the differentiator.
The fix is to reach that warmth before the listening socket exists. Warm-up
registered via :meth:BlackBull.on_warmup runs here, once, in the master:
- Before bind + fork (multi-worker): forked workers inherit the warmed heap
via copy-on-write. PEP 659's adaptive specialization lives in the code
objects on the heap, so it survives
fork();gc.collect()+gc.freeze()keep the warmed pages COW-shared instead of being dirtied by the first post-fork collection. - Before serving (single-worker): the one process is warm before it binds.
Safety¶
Warm-up is best-effort and must never crash the master: every failure is logged
and swallowed, degrading to today's cold start. It uses only in-process ASGI
drives and in-memory (:class:ssl.MemoryBIO) handshakes — it never creates a
socket, a live connection, or a lingering event loop that a subsequent
fork() could inherit (the classic preload_app hazard). The temporary
loop used by :func:run_warmup is closed before it returns.
run_warmup(app, ssl_context=None)
¶
Synchronous pre-fork entry point (called from serve before bind/fork).
No-op when app registered no :meth:BlackBull.on_warmup hooks. Otherwise
runs the hooks (plus the built-in TLS warm-up when ssl_context is set) in a
temporary event loop that is closed before returning, so a subsequent
fork() never inherits a live loop. Ends with gc.collect() +
gc.freeze() to keep the warmed pages copy-on-write-shared across fork.
warm_tls(ssl_context, *, n=_DEFAULT_TLS_N)
async
¶
Prime the TLS handshake path with n in-memory handshakes (no socket).
Drives full handshakes between the server ssl_context and a throwaway
client context over paired :class:ssl.MemoryBIO buffers, faulting in the
OpenSSL / RSA / ALPN code the TLS benchmark profiles hit cold on run-1.
Self-contained: creates no listener and no file descriptor.
warmup_inline(app, ssl_context=None)
async
¶
Async warm-up used by the single-worker path (runs on the serving loop).
Same behaviour as :func:run_warmup minus the temporary-loop management:
the single-worker process never forks, so warming on the loop that will go
on to serve is both correct and cheaper. Never raises.