Lifecycle
We've been name-dropping "lifecycle hooks" since the very first extension. Time to make good
on it. Every WebFluid app has a managed lifecycle: it boots, serves, and shuts down again,
all kicked off by the single fluid.mix() call you've used since Getting Started.
What mix() really does
mix() is just a thin wrapper that runs the async start() coroutine.
From there the framework opens a logging session, runs your startup hooks,
spins up the server, and then sits on a shutdown flag. When a stop signal arrives it serves
its last request, runs your shutdown hooks and exits cleanly. You never
manage that loop yourself — you only register the things that should happen at each
end.
Startup and shutdown hooks
A hook is a plain (sync or async) callable that takes no required arguments. You register it
with startup_hook or shutdown_hook. This is the proper home for the
table creation we hard-coded under if __name__ == "__main__" back in the
SQLAlchemy chapter:
from webfluid import Fluid
from webfluid.core.ext import db
def prepare_fluid() -> Fluid:
app = Fluid(__name__)
from fluid.app import app_router
from fluid.api import api_router
app.include_router(app_router)
app.include_router(api_router)
from fluid.events.models import register as register_events
app.startup_hook(register_events)
@app.startup_hook
async def create_tables():
bind = db.get_bind_for_model(db.Model)
db.Model.metadata.create_all(bind.sync_engine)
@app.shutdown_hook
def goodbye():
from webfluid.utils.logging import factory as log
log.log("The app is cooling down.")
return app
if __name__ == "__main__":
prepare_fluid().mix()
The register_events line is the one we already wrote back in the
Events chapter, and now you can see why it had to be
a hook: a startup hook is simply "code that runs once the loop exists and before the first
request arrives", which is exactly what a broadcast channel needs.
Extensions and Additives use exactly this mechanism under the hood — the scheduler
starts on a startup hook, the proxy client closes on a shutdown hook, database pools are
disposed on one, and even the Additive registration itself is a startup hook. So when you
register your own, you're joining the same queue the framework uses — which is also why
wf run shows you a progress bar for each phase: it is counting the same hooks you
just added to.
The shutdown half is worth taking as seriously as the startup half, because it is the only
place a process gets to tidy up after itself. Every database engine the app opened is
disposed there, which is exactly the kind of work that is invisible while it happens and
expensive when it does not.
Startup hooks run one after another in the order you register them, and shutdown hooks in reverse registration order. Later hooks can rely on earlier ones having finished — but a hook that blocks also holds up every hook queued after it, so keep them focused and let long running work happen elsewhere.
Hooks can only be added before the server is up. Trying to register one after startup raises, which is the framework's way of catching a mistake early. The practical rule: register hooks while building your app (in the factory), not from inside a request.
Graceful shutdown
The runtime installs handlers for SIGINT and SIGTERM, so a
Ctrl+C or a container stop flips the shutdown flag instead of killing the
process mid-request. The in-flight request finishes, your shutdown hooks run, and only then
does the server stop — which is why you'll always see the "Running shutdown hooks..."
line before the app exits.
This whole sequence lives in mix(), not in the ASGI lifespan protocol. So an
external ASGI server that imports your app and serves it directly — a bare
uvicorn main:fluid, or gunicorn — never runs your startup or shutdown
hooks: no tables, no scheduler, no Additives. Run your apps through wf run, which
is what the whole CLI is built around.
Continue reading
From here you can continue straight with Runtime.