Skip to content

Make DrivineAssembly pi-pilgrim 0.4's AsyncPilgrimAssembly, so await close() closes the pools again - #236

Merged
jasperblues merged 6 commits into
mainfrom
fix/228-async-close
Oct 28, 2026
Merged

jasperblues merged 6 commits into
mainfrom
fix/228-async-close

Conversation

@jasperblues

@jasperblues jasperblues commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Fixes #228

What was wrong

0.2.0's README said to shut down with await assembly.drivine.close(). #195 moved DrivineAssembly onto pi-pilgrim's lifecycle and removed its async close(). So in 0.3.0 that call reaches pi-pilgrim's sync close(). Once the registry is made, that raises PilgrimError ... Close it with await aclose() and leaves the drivers and pools open.

The fix

pi-pilgrim 0.4.0 (liberation-data/pi-pilgrim#10) adds AsyncPilgrimAssembly, for an assembly whose teardown is async. Its close() is a native async def that awaits aclose(), and its with refuses. It closes held and owned child assemblies through their own lifecycle. It also adds AsyncTeardownRequired, a PilgrimError.

  • pi-pilgrim is now >=0.4.0,<0.5, and the lock is updated.
  • The async DrivineAssembly subclasses AsyncPilgrimAssembly. So await close(), asyncio.run(close()), await aclose() and async with all close it, whether or not the registry was made. An application assembly closes it whether it holds it with AssemblyHolder.shared(DrivineAssembly) or as a component, self.shared(DrivineAssembly). A sync close of either assembly closes Drivine while it has no registry. Once Drivine has a registry, that sync close refuses with AsyncTeardownRequired before closing anything. A subclass of DrivineAssembly that overrides aclose() refuses a sync close from the start, since its own teardown is async.
  • with DrivineAssembly() (async) raises AsyncTeardownRequired. DrivineAssembly overrides only __enter__, so that the refusal also points to drivine.sync.DrivineAssembly. __exit__ is pi-pilgrim's.
  • The sync assembly is unchanged. It's still a plain PilgrimAssembly, and drivine/sync/drivine_assembly.py is byte for byte the same as on main. scripts/unasync.py maps AsyncPilgrimAssembly to PilgrimAssembly. Its ASYNC_ONLY table leaves DrivineAssembly.__enter__ out of the sync code, and that's the only entry it has.
  • Docs. docs/09-with-pi-pilgrim.md says that the async assembly is an AsyncPilgrimAssembly and lists what closes it. Its "Upgrading from 0.2.0" section is updated to match. docs/design.md says the same.

One behaviour to know about: close() called without await closes nothing, even with no registry made, and Python warns that a coroutine was never awaited. That's how a native coroutine behaves, and the docs say to await it.

Tests

In tests/unit/test_assembly_lifecycle.py, which passes under -W error:

  • DrivineAssembly is an AsyncPilgrimAssembly, and the sync one isn't.
  • await close() ends the providers, and the assembly makes no more components. Calling it twice, or after aclose(), ends them only once. With no registry made, it closes the assembly.
  • asyncio.run(close()) closes it, with or without a registry.
  • close() left unawaited closes nothing and warns, with or without a registry.
  • Holding Drivine as a component (self.shared): a sync close closes a Drivine with no registry. Once Drivine has a registry, the sync close refuses with AsyncTeardownRequired, leaves everything open, and aclose() then closes it. await aclose() on the holder closes Drivine.
  • The 0.2.0 README pattern works: AssemblyHolder.shared(DrivineAssembly), then await app.drivine.close(). The application assembly's sync close() closes a Drivine with no registry, and refuses once Drivine has one. await AssemblyHolder.aclose() closes it.
  • with on the async assembly raises AsyncTeardownRequired, and the message names the sync API.
  • The sync assembly's close() still closes and returns None.

drivine4j and Drivine for TypeScript have no DrivineAssembly, so there's nothing to port.

  • A subclass that overrides aclose(): await close() runs the override, and a sync close refuses even with no registry.
  • A provider that fails while closing doesn't stop the others closing (test_await_close_ends_every_provider_when_one_fails).
  • tests/unit/test_unasync.py: ASYNC_ONLY leaves out exactly the definitions it names, and refuses a name the module doesn't define.

…ses the pools again

0.3.0 moved DrivineAssembly onto pi-pilgrim's lifecycle and dropped its async close(), so 0.2.0's documented
shutdown, await assembly.drivine.close(), reached pi-pilgrim's sync close(), which refuses once the registry is
made, and left the drivers and pools open.

close() is back on the async assembly. It closes the assembly at once when nothing needs awaiting, and otherwise
returns aclose() to await, so an application assembly's sync close(), which calls it without awaiting, still closes
it. with DrivineAssembly() now refuses and points to async with. scripts/unasync.py leaves these out of the sync
assembly, whose close() is pi-pilgrim's. The README says what changed from 0.2.0.

Fixes #228
…n what close() returns, keep the refusal text out of the sync copy, and say when close() must be awaited
…close() closes it and so does a holder's sync close

pi-pilgrim 0.4.0 has an AsyncPilgrimAssembly whose close() is aclose(), and closes a held or owned assembly through
its own lifecycle. On it, DrivineAssembly drops its sync close() override, the finished coroutine it returned, and the
match on pi-pilgrim's refusal text. An assembly holding Drivine as a component, self.shared(DrivineAssembly), now closes
it with a sync close() while it has no registry, where it raised an ExceptionGroup. A refusal is now
AsyncTeardownRequired. __enter__ stays, to name the sync API in its refusal, so unasync's ASYNC_ONLY keeps only it;
the sync DrivineAssembly is unchanged.
@jasperblues jasperblues changed the title Bring back await DrivineAssembly.close(), 0.2.0's shutdown, so it closes the pools again Make DrivineAssembly pi-pilgrim 0.4's AsyncPilgrimAssembly, so await close() closes the pools again Oct 28, 2026
…rom the start, check the sync assembly is a plain one, and keep uv.lock's revision
@jasperblues
jasperblues merged commit 21447f8 into main Oct 28, 2026
16 checks passed
@jasperblues jasperblues mentioned this pull request Oct 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

await DrivineAssembly.close(), the shutdown 0.2.0 documented, raises PilgrimError in 0.3.0 and leaves the pools open

1 participant