Skip to content

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

$env:PYTHONPATH = "src;examples"
python -m learn_fundamentals.fundamentals06_lifecycle
PYTHONPATH=src:examples python -m learn_fundamentals.fundamentals06_lifecycle

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:

  1. 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.
  2. It was released anyway. connection released appears before the failure is reported, because the runtime closes what it already built instead of leaving it to the garbage collector.
  3. The error names the contract, not the line. failed to construct BrokenService from application tells you which provider failed and which source registered it. The original exception is the cause of that error, so except ... as error: error.__cause__ is the traceback you want.
  4. failed is a state you can leave. stop() still moves the application to stopped; 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 failed as 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 is failed.

Exercise

Remove the failing provider and predict the output before running it.

Show the answer
examples/learn_fundamentals/fundamentals06_exercise.py
"""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())
initial: new
connection acquired
after start: ready
connection released
after stop: stopped
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".

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.