Lesson 6 — understand the application lifecycle¶
What you will build¶
An application that fails to start, on purpose. The interesting part of a lifecycle is not the happy path — it is what happens to the half-built application when one provider cannot be constructed.
What you need¶
The setup from lesson 1. This lesson uses nothing beyond the core package.
The states¶
| State | Reached when |
|---|---|
new |
App() has been created and providers registered |
validated |
app.validate() has checked the graph without building anything |
starting |
start() is resolving eager providers |
ready |
everything app-scoped and eager exists, and requests are accepted |
stopping |
stop() is releasing what the application owns |
stopped |
nothing is owned any more; stop() on a stopped application does nothing |
failed |
startup raised; whatever was built has been closed |
The code¶
examples/learn_fundamentals/fundamentals06_lifecycle.py
"""Lesson 6 — what happens between start and stop.
A failing eager provider is the interesting case: the application does not end up half-started. What
was already constructed is closed, the state becomes ``failed``, and ``stop()`` still returns the
application to ``stopped``. ``failed`` is a state you can leave, which is what makes it a state
rather than an exit.
"""
from __future__ import annotations
import asyncio
from xyberos import App
from xyberos.errors import DependencyResolutionError
class BrokenService:
"""A service that cannot be constructed, so startup has to unwind."""
def __init__(self) -> None:
raise RuntimeError("the service could not be built")
class Connection:
"""A service that owns a resource, so the runtime is responsible for closing it."""
async def aclose(self) -> None:
"""Release the resource."""
print("connection released")
async def connection_factory() -> Connection:
"""Build the connection that the application will own."""
print("connection acquired")
return Connection()
async def main() -> None:
"""Watch a failed startup clean up after itself."""
app = App()
app.provide(Connection, factory=connection_factory, eager=True)
app.provide(BrokenService, eager=True)
print("initial:", app.state.value)
try:
await app.start()
except DependencyResolutionError as error:
print("startup failed:", error)
print("after the failure:", app.state.value)
await app.stop()
print("after stop:", app.state.value)
if __name__ == "__main__":
asyncio.run(main())
How to run it¶
What it prints¶
initial: new
connection acquired
connection released
startup failed: failed to construct BrokenService from application
after the failure: failed
after stop: stopped
Four things are happening in those six lines, and each one is a decision:
- The connection was acquired before the failure. Eager providers are built in registration order, so the first one exists by the time the second raises.
- It was released anyway.
connection releasedappears before the failure is reported, because the runtime closes what it already built instead of leaving it to the garbage collector. - The error names the contract, not the line.
failed to construct BrokenService from applicationtells you which provider failed and which source registered it. The original exception is the cause of that error, soexcept ... as error: error.__cause__is the traceback you want. failedis a state you can leave.stop()still moves the application tostopped; the alternative would be a lifecycle you cannot recover from in a long-running process.
Common mistakes¶
- Catching the wrong exception. A failing provider surfaces as
DependencyResolutionError, not as whatever the factory raised. Catch the framework's error and read__cause__for the original. - Leaving a resource out of the runtime's ownership. A provider built with
instance=is given to the application rather than owned by it, so nothing closes it. Own it with a factory, or close it yourself. - Treating
failedas fatal to the process. It is a state: fix the wiring, or stop cleanly and exit — but the application will not resolve anything while it isfailed.
Exercise¶
Remove the failing provider and predict the output before running it.
Show the answer
examples/learn_fundamentals/fundamentals06_exercise.py
Two differences from the lesson, and both are the point:
- The state after startup is `ready` rather than `failed`, so the application is one you can use.
- `connection released` now happens inside `stop()` rather than during a failed startup — because a
provider that started successfully is closed when the application stops, not when it starts. Note
the position: the release happens *before* the state becomes `stopped`, which is what makes
`stopped` mean "nothing is owned any more".
"""Lesson 6, exercise — the same application without the failing provider.
The answer to the lesson's exercise, kept as a file so the output the page claims is the output this
module really produces. The only difference from the lesson is which provider was removed.
"""
from __future__ import annotations
import asyncio
from xyberos import App
class Connection:
"""A service that owns a resource, so the runtime is responsible for closing it."""
async def aclose(self) -> None:
"""Release the resource."""
print("connection released")
async def connection_factory() -> Connection:
"""Build the connection that the application will own."""
print("connection acquired")
return Connection()
async def main() -> None:
"""Run the exercise's version of lesson 6."""
app = App()
app.provide(Connection, factory=connection_factory, eager=True)
print("initial:", app.state.value)
await app.start()
print("after start:", app.state.value)
await app.stop()
print("after stop:", app.state.value)
if __name__ == "__main__":
asyncio.run(main())
Next¶
Lesson 7 is where configuration becomes real: a settings file, the environment, and explicit arguments, in the order that decides which one wins. Until it is published, the roadmap shows where each lesson stands.