Wake deadline consumers on enqueue instead of polling every second

This commit is contained in:
2026-09-29 23:39:42 +00:00
parent e344ce2e30
commit b43e074728
7 changed files with 162 additions and 19 deletions
@@ -220,6 +220,7 @@ class DummyEngine(GameEngine):
def make_platform(
engines: Sequence[GameEngine] = (DummyEngine(),),
heartbeat_ms: int = 50,
) -> Tuple[KayaApp, Platform, TortoiseMixin]:
"""Build a throwaway app + platform wired with in-memory stores."""
registry = GameRegistry(engines)
@@ -251,7 +252,7 @@ def make_platform(
scheduler = DeadlineScheduler(
game_store,
registry,
heartbeat_ms=50,
heartbeat_ms=heartbeat_ms,
context_binder=tortoise_mixin.ensure_context,
)
platform = Platform(
@@ -112,6 +112,38 @@ async def _finished(store, game_id: str) -> Optional[GameSession]:
return None
class NotificationTest(unittest.TestCase):
"""Deadline-enqueue notifications wake consumers on other workers."""
@async_test
async def test_notification_wakes_a_long_heartbeat_consumer(self) -> None:
# The consumer's heartbeat is a minute away and nothing sets its
# local wake event: only the pub/sub notification can deliver the
# deadline in time. This is the cross-worker path.
_, platform, _ = make_platform(heartbeat_ms=60_000)
store = platform.game_store
scheduler = platform.scheduler
session = _started_session(deadline_in_seconds=0.1)
await store.save(session)
scheduler.ensure_consumer()
# Let the listener task subscribe before announcing.
await asyncio.sleep(0.05)
engine = platform.registry.require(session.game_type)
deadline = engine.next_deadline(session)
assert deadline is not None
member = encode({
"game_id": session.id,
"kind": deadline.kind,
"token": deadline.token,
})
await store.add_deadline(member, deadline.due_at.timestamp())
await store.notify_deadline()
result = await _wait_for(lambda: _plays_is(store, session.id, 1))
self.assertIsNotNone(result, "notification did not wake the consumer")
class ProcessDueTest(unittest.TestCase):
"""Direct ``process_due`` behaviour: engine revalidation and idempotency."""
@@ -270,6 +270,17 @@ class InMemoryGameStoreTest(unittest.TestCase):
self.assertEqual(60.0, await store.next_deadline())
self.assertEqual(["b"], await store.due_deadlines(now=100.0))
@async_test
async def test_deadline_notifications_reach_subscribers(self) -> None:
store = InMemoryGameStore(_registry())
async with store.subscribe_deadlines() as events:
await store.notify_deadline()
signal = await asyncio.wait_for(anext(events), timeout=1.0)
self.assertIsNone(signal)
# The subscription is gone after the context exits: notifying
# again is a no-op rather than an error.
await store.notify_deadline()
class RedisGameStoreTest(unittest.TestCase):
@async_test