Async¶
What you’ll learn¶
Register an
async deffactory and resolve it viaawait container.aresolve(...).Use an async-generator provider for deterministic async cleanup within a scope.
Async factory + aresolve¶
Run locally¶
uv run python examples/ex_18_async/01_async_factory_aresolve.py
"""Focused example: async factory registration with ``await container.aresolve(...)``."""
from __future__ import annotations
import asyncio
from diwire import Container
class AsyncService:
def __init__(self, value: str) -> None:
self.value = value
async def main() -> None:
container = Container()
async def build_async_service() -> AsyncService:
await asyncio.sleep(0)
return AsyncService(value="ok")
container.add_factory(build_async_service, provides=AsyncService)
service = await container.aresolve(AsyncService)
print(f"async_factory_value={service.value}") # => async_factory_value=ok
if __name__ == "__main__":
asyncio.run(main())
Async generator cleanup¶
Run locally¶
uv run python examples/ex_18_async/02_async_generator_cleanup.py
"""Focused example: async-generator provider cleanup on async scope exit."""
from __future__ import annotations
import asyncio
from collections.abc import AsyncGenerator
from diwire import Container, Lifetime, Scope
class AsyncResource:
pass
async def main() -> None:
container = Container()
state = {"closed": 0}
async def provide_async_resource() -> AsyncGenerator[AsyncResource, None]:
try:
yield AsyncResource()
finally:
await asyncio.sleep(0)
state["closed"] += 1
container.add_generator(
provide_async_resource,
provides=AsyncResource,
scope=Scope.REQUEST,
lifetime=Lifetime.SCOPED,
)
async with container.enter_scope() as request_scope:
_ = await request_scope.aresolve(AsyncResource)
closed_before_exit = state["closed"]
print(
f"async_cleanup_after_exit={closed_before_exit == 0 and state['closed'] == 1}",
) # => async_cleanup_after_exit=True
if __name__ == "__main__":
asyncio.run(main())
Cancelled job cleanup¶
A background job can be cancelled while it waits for input. Give each job
its own request scope. Keep the scope inside the task that uses the resource.
An async-generator provider can then return a borrowed connection in its
finally block when the job stops.
Cancel the task once, then await it. Catch asyncio.CancelledError outside
the job’s scope. In this example, release:done appears before cancelled.
The event signals that the job has acquired its connection before cancellation.
No timed delay is needed to select that point.
The connection is an in-memory stand-in. Replace release() with the real
driver’s async release operation. Use the async scope on success and failure
too. A scoped connection is reused inside one job; the next job gets a new one.
This example covers one cancellation during work. A second cancellation during
cleanup, a failed release operation, or process termination needs a separate
shutdown policy. Do not assume that a finally block makes cleanup immune
to these events.
Run locally¶
uv run python examples/ex_18_async/03_cancelled_job_cleanup.py
"""Return a borrowed connection when an async job is cancelled.
Cancel once and await the task. The job owns its request scope, so async
cleanup finishes before the caller receives CancelledError. This in-memory
connection needs no database or network service.
"""
from __future__ import annotations
import asyncio
from collections.abc import AsyncGenerator
from dataclasses import dataclass
from diwire import Container, Lifetime, Scope
@dataclass
class Connection:
events: list[str]
released: bool = False
async def release(self) -> None:
self.events.append("release:start")
# A real driver would await its connection release operation here.
await asyncio.sleep(0)
self.released = True
self.events.append("release:done")
def create_container(events: list[str]) -> Container:
container = Container()
async def borrow_connection() -> AsyncGenerator[Connection, None]:
connection = Connection(events)
events.append("acquire")
try:
yield connection
finally:
await connection.release()
container.add_generator(
borrow_connection,
provides=Connection,
lifetime=Lifetime.SCOPED,
scope=Scope.REQUEST,
)
return container
async def run_job(
container: Container,
started: asyncio.Event,
finish: asyncio.Event,
) -> None:
async with container.enter_scope(Scope.REQUEST) as job:
connection = await job.aresolve(Connection)
connection.events.append("run")
started.set()
# Stand in for work that waits for more input.
await finish.wait()
connection.events.append("complete")
async def main() -> None:
events: list[str] = []
container = create_container(events)
started = asyncio.Event()
task = asyncio.create_task(run_job(container, started, asyncio.Event()))
try:
await started.wait()
task.cancel()
try:
await task
except asyncio.CancelledError:
events.append("cancelled")
finally:
await container.aclose()
print(
f"lifecycle={','.join(events)}"
) # => lifecycle=acquire,run,release:start,release:done,cancelled
if __name__ == "__main__":
asyncio.run(main())