Lifecycle

A registry accompanies the application from boot to shutdown: validate() catches wiring mistakes early, warmup() builds the expensive things up front, close() tears down what the registry created.

from action0.service import Registry

with Registry() as registry:
    # register(...) / load_yaml(...)
    registry.validate()
    registry.warmup()
    # ... run the application ...
    pass
# leaving the block closes the registry

validate() — fail at boot, not at first use

validate() statically checks every local definition without instantiating anything. It detects:

  • params keys that don’t exist on the provider,

  • required parameters that neither params, nor defaults, nor any registration can satisfy,

  • dangling Ref targets,

  • ambiguous injections (several candidate services, no clear default),

  • dependency cycles.

Problems are collected, not raised one at a time: the ValidationError lists all of them, one per line (also available as its problems attribute). Wire registry.validate() into application startup right after the last registration — a misconfigured service then fails the deploy instead of the 3 a.m. request that first touched it.

Validation covers what static analysis can see. Lazily registered services, providers without inspectable signatures, and unresolvable type hints are checked as far as possible and otherwise trusted.

warmup() — eager construction

Registrations are lazy by default: nothing is built until first requested. Services registered with eager=True opt into warmup(), which builds them all (in registration order) and returns the instances. Boot becomes: validate, then warm up — construction cost and construction failures both move to startup.

registry.register(ConnectionPool, eager=True)
registry.validate()
registry.warmup()  # the pool exists now, or boot has failed loudly

close() — orderly teardown

close() disposes managed instances and shuts the registry down:

  • Every scope hands over the instances visible from the calling thread and context — all singletons, the calling thread’s thread-scoped instances, the current context’s context-scoped ones. Other threads’ instances cannot be reached safely and survive.

  • Instances the registry created get their close() method called, if they have one — in reverse creation order, so dependents are closed before their dependencies. Objects handed in via register_instance() are not closed: the registry didn’t make them, it won’t break them. Transients were never stored, so they are the caller’s job too.

  • Errors from individual close() methods are logged, not raised — teardown runs to completion.

Registries are context managers; with Registry() as registry: calls close() on exit, as in the example at the top.

After closing, any use of the registry raises ServiceError. Closing twice is fine (the second call is a no-op).

Async lifecycle

Every lifecycle entry point has an async twin: awarmup(), aclose(), and async with (which calls aclose() on exit). Use them whenever the registry holds async services — the sync methods refuse async definitions, and sync close() cannot await an async disposer.

async def main() -> None:
    async with Registry() as registry:
        registry.register(make_pool)  # an async factory, eager or not
        registry.validate()
        await registry.awarmup()
        # ... run the application ...

Teardown rules match close(), with one addition: a managed instance offering an aclose() method gets that awaited, in preference to a sync close(). The reverse-creation order, the managed-only rule, and the log-don’t-raise error handling are the same.

The sync close() skips instances that only have an async aclose() — it cannot await them — and logs a warning naming the service, so a forgotten aclose() shows up in the logs rather than as a silent leak.

One registry with async services is meant to be driven from a single event loop: concurrent first requests within that loop are deduplicated, but the async machinery does not coordinate across loops or with concurrent sync resolution from other threads.