diff --git a/server/src/tavolo/deadlines.py b/server/src/tavolo/deadlines.py index 4d238a8..356e236 100644 --- a/server/src/tavolo/deadlines.py +++ b/server/src/tavolo/deadlines.py @@ -214,14 +214,20 @@ async def _fire_hand_end(store: GameStore, state: GameState, entry: Dict[str, An # --- consumer lifecycle ------------------------------------------------------- -def ensure_consumer(store: GameStore) -> None: - """Start the deadline consumer on the running loop if not yet running. +def ensure_consumer( + store: GameStore, loop: Optional[asyncio.AbstractEventLoop] = None +) -> None: + """Start the deadline consumer on the given (or running) loop if not + yet running. Called lazily whenever a deadline is enqueued (the ASGI test transport never fires the lifespan hooks, so the mixin's ``setup`` alone is not - enough) and on application startup. + enough) and on application startup. The explicit ``loop`` matters at + startup: under RSGI granian calls ``setup`` before the loop runs, so + ``asyncio.get_running_loop()`` would fail there. """ - loop = asyncio.get_running_loop() + if loop is None: + loop = asyncio.get_running_loop() for old in list(_consumers): if old.is_closed(): _consumers.pop(old, None) @@ -284,7 +290,7 @@ class DeadlineSchedulerMixin(KayaMixin): pass def setup(self, loop: asyncio.AbstractEventLoop) -> None: - ensure_consumer(self._store) + ensure_consumer(self._store, loop) def shutdown(self, loop: asyncio.AbstractEventLoop) -> None: stop_consumer(loop)