Skip to content

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.