Serve a moonasgi application with a uvicorn-style process model: a graceful
shutdown path over a single acceptor that spawns a concurrent handler per
connection.
The lifespan protocol runs startup before the listener binds and shutdown on
the way out. A single acceptor task spawns one handler task per connection, so
requests are served concurrently, each through mooncat's HTTP/1.1 codec and,
on an upgrade, its WebSocket bridge. serve, serve_config and
serve_tls_config run this same loop; this entry point adds the handle.
Shutdown is triggered by handle.shutdown() / handle.request_stop(), by
reaching Config::limit_max_requests, or by a signal the runtime turns into
global cancellation (see the boundary note below). All three converge on one
sequence, run under protect_from_cancel so a signal can't abort it midway:
stop accepting (cancel the acceptor), drain in-flight requests (bounded by
Config::graceful_timeout), run the ASGI lifespan shutdown, then close the
listener.
The acceptor honours the connection ceilings: max_connections gates
accepting, so a full server leaves the next client in the listen queue rather
than accepting it and sitting on it,
and allow_failure decides whether one bad connection is contained or brings
the server down. limit_concurrency is the other kind of ceiling — a request
arriving past it is answered 503 rather than queued.
Multi-worker boundary
uvicorn's --workers binds the port once in the parent (SO_REUSEADDR, an
inheritable socket) and hands that socket to N worker processes it starts with
multiprocessing's spawn context. moonbitlang/async can start a process but
cannot give it a listening socket, and its event loop allows only one
outstanding accept per listener handle (wait_read guards on a single
waiter), so even N in-process acceptor tasks on one shared listener aren't
expressible — a second concurrent accept on the same listener aborts.
mooncat therefore serves from one acceptor that spawns a concurrent handler
per connection, which is the concurrency a single uvicorn worker provides on
its event loop. Multiple workers land when the async layer can pass a
listening socket to a child process.
Signal boundary
The only signal hook moonbitlang/async exposes is
@signal.set_global_cancellation_signals, which cancels the whole task tree
on SIGINT/SIGTERM. mooncat catches that cancellation and still runs lifespan
shutdown and closes the listener under protect_from_cancel; but a signal
also cancels the in-flight handler tasks, so drain-before-close is only fully
honoured on the programmatic ShutdownHandle path. That matches uvicorn's
own escalation: a first signal drains, a second forces exit.