Skip to content

blackbull.server

blackbull.server

Server

An asyncio socket server that dispatches each connection through the app's :class:~blackbull.server.protocol_registry.ProtocolRegistry.

The shared HTTP listener detects HTTP/1.1 vs HTTP/2 (and upgrades to WebSocket); port-bound non-ASGI protocols registered via :meth:BlackBull.raw_handler get their own listening socket. When ssl_context or certfile is set, the HTTP listener runs as HTTPS.

Formerly ASGIServer — that name remains as a backward-compat alias.

ssl_context property writable

The context a listener built from certfile/keyfile uses.

Assigning it re-points every bound listener that was carrying the previous one — which is what makes configuring mTLS after open_socket() work, and what keeps bound_listeners honest about what is actually being served. A listener the caller stated carries its own context and is left alone, because identity is the difference between "the server's" and "its own".

client_connected_cb(reader, writer) async

Accept callback for the shared HTTP listener.

connection_protocol_factory(bound_binding=None)

Factory for loop.create_server — one buffered protocol per accept.

Replaces the start_server callback pair: instead of a StreamReader and StreamWriter over asyncio's own buffering, the connection owns a single buffer the kernel writes into, and the actor reads by cursor.

The protocol spawns the serving task itself because a protocol factory is synchronous. connection_made fires after the TLS handshake on an SSL transport, so ALPN is already decided by the time the task runs — same ordering the callback form relied on.

open_socket(port=0, unix_path=None, inherited_fd=None)

Bind every listener this server was asked for.

A caller that named listeners= gets those. A caller that said it the old way — a port, a Unix path, or an inherited fd — gets one listener built from those arguments, so there is one binding path and not two.

run(port=80) async

Run an asyncio socket server with the setting in this object.

shutdown() async

Drive the ASGI lifespan shutdown handshake.

startup() async

Drive the ASGI lifespan startup handshake.

Launches the app's lifespan task, delivers 'lifespan.startup', and waits for 'lifespan.startup.complete'. Raises RuntimeError on 'lifespan.startup.failed'. Stores the context manager so that shutdown() can deliver 'lifespan.shutdown' to the same task.

stop(drain_timeout=8.0) async

Stop accepting, then let the connections already being served finish.

Nothing in flight is cancelled while drain_timeout lasts: a cancelled handler leaves a client holding a half-written response. Whatever is left at the deadline is cancelled — a shutdown that must complete still completes. Keep the budget inside MultiWorkerServer.shutdown_timeout so the drain ends here and not in a SIGKILL.