Persist matches finished by deadline timeouts
CI / Build and push docker image (push) Successful in 1m15s
CI / Build and push docker image (push) Successful in 1m15s
The deadline consumer runs in a long-lived task outside any request, so a timeout that ended the match raised 'No TortoiseContext is currently active' in save_match_result before the state was saved: the game stayed stuck on the last turn and the entry retried forever. It only surfaced on the match-deciding turn; ordinary timeouts and human plays were fine. Bind the Tortoise context before persisting a finished match (optional context_binder wired to TortoiseMixin.ensure_context), and back off to the heartbeat when a due entry fails instead of hot-looping on it.
This commit is contained in:
@@ -75,6 +75,28 @@ class ConnectionIndependenceTest(unittest.TestCase):
|
||||
await platform.scheduler.sync_deadline(session)
|
||||
self.assertIsNone(await platform.game_store.next_deadline())
|
||||
|
||||
@async_test
|
||||
async def test_tick_finishing_match_persists_result_with_no_connections(self) -> None:
|
||||
# A deadline that ends the match must persist the result from the
|
||||
# consumer task, which has no request context of its own.
|
||||
from tavolo.platform.models import Match
|
||||
|
||||
_, platform, tortoise_mixin = make_platform()
|
||||
store = platform.game_store
|
||||
scheduler = platform.scheduler
|
||||
session = _started_session(target=1, deadline_in_seconds=0.05)
|
||||
await store.save(session)
|
||||
await scheduler.sync_deadline(session)
|
||||
|
||||
# Nobody ever connects: the consumer must finish the match and
|
||||
# write it to Postgres.
|
||||
result = await _wait_for(lambda: _finished(store, session.id))
|
||||
self.assertIsNotNone(result, "deadline never finished the match")
|
||||
|
||||
ctx = await use_db(tortoise_mixin)
|
||||
with ctx:
|
||||
self.assertEqual(1, await Match.all().count())
|
||||
|
||||
|
||||
async def _plays_is(store, game_id: str, count: int) -> Optional[GameSession]:
|
||||
session = await store.load(game_id)
|
||||
@@ -83,6 +105,13 @@ async def _plays_is(store, game_id: str, count: int) -> Optional[GameSession]:
|
||||
return None
|
||||
|
||||
|
||||
async def _finished(store, game_id: str) -> Optional[GameSession]:
|
||||
session = await store.load(game_id)
|
||||
if session is not None and session.state["finished"]:
|
||||
return session
|
||||
return None
|
||||
|
||||
|
||||
class ProcessDueTest(unittest.TestCase):
|
||||
"""Direct ``process_due`` behaviour: engine revalidation and idempotency."""
|
||||
|
||||
@@ -163,10 +192,11 @@ class ProcessDueTest(unittest.TestCase):
|
||||
@async_test
|
||||
async def test_finished_match_is_persisted_on_tick(self) -> None:
|
||||
# A tick that completes the match writes the result to Postgres.
|
||||
# No context is bound in this task: the scheduler must bind its
|
||||
# own, exactly like its consumer task at app startup.
|
||||
from tavolo.platform.models import Match
|
||||
|
||||
_, platform, tortoise_mixin = make_platform()
|
||||
ctx = await use_db(tortoise_mixin)
|
||||
scheduler = platform.scheduler
|
||||
store = platform.game_store
|
||||
session = _started_session(target=1, deadline_in_seconds=3600)
|
||||
@@ -178,9 +208,10 @@ class ProcessDueTest(unittest.TestCase):
|
||||
"kind": deadline.kind,
|
||||
"token": deadline.token,
|
||||
})
|
||||
await scheduler.process_due(member)
|
||||
ctx = await use_db(tortoise_mixin)
|
||||
with ctx:
|
||||
await scheduler.process_due(member)
|
||||
self.assertEqual(1, await Match.all().count())
|
||||
self.assertEqual(1, await Match.all().count())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user