Persist matches finished by deadline timeouts
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:
@@ -12,10 +12,11 @@ import unittest
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
|
||||
from tavolo.app import game_store, platform, scheduler
|
||||
from tavolo.app import game_store, platform, scheduler, tortoise_mixin
|
||||
from tavolo.platform import GameSession, Seat
|
||||
from tavolo.platform.deadlines import encode
|
||||
from tavolo.scopone.state import PlayerState, ScoponeState
|
||||
from tavolo.platform.models import Match
|
||||
from tavolo.scopone.state import Card, PlayerState, ScoponeState
|
||||
from tests.helpers import async_test
|
||||
|
||||
PLAYERS = ("alice", "bob", "carol", "dave")
|
||||
@@ -124,6 +125,41 @@ class ConnectionIndependenceTest(unittest.TestCase):
|
||||
self.assertEqual(2, result.state.hand_number)
|
||||
self.assertEqual([], result.state.acked)
|
||||
|
||||
@async_test
|
||||
async def test_turn_timeout_finishing_match_persists_result(self) -> None:
|
||||
# The match-deciding last turn auto-played by the deadline consumer
|
||||
# runs in a task with no request context: the result must still
|
||||
# reach Postgres. Previously the write raised and the deadline
|
||||
# retried forever, leaving the game stuck on the last turn.
|
||||
session = _started_session("dl-final-1", "DLF001", turn_timeout=1)
|
||||
state = session.state
|
||||
state.players[0].hand = [Card.parse("02D")]
|
||||
for player in state.players[1:]:
|
||||
player.hand = []
|
||||
state.table = [Card.parse("02C")]
|
||||
state.turn = 0
|
||||
state.target_score = 1
|
||||
state.turn_deadline = (
|
||||
datetime.now(timezone.utc) + timedelta(seconds=1)
|
||||
).isoformat()
|
||||
|
||||
await game_store.save(session)
|
||||
await scheduler.sync_deadline(session)
|
||||
|
||||
# Nobody plays: the consumer auto-plays the last card, which ends
|
||||
# the hand and the match.
|
||||
result = await _wait_for(lambda: _phase_is(session.id, "finished"))
|
||||
self.assertIsNotNone(result, "turn deadline never finished the match")
|
||||
assert result is not None
|
||||
self.assertEqual(0, result.state.winner)
|
||||
self.assertTrue(result.stats_saved)
|
||||
|
||||
await tortoise_mixin.ensure_context()
|
||||
ctx = tortoise_mixin._ctx
|
||||
assert ctx is not None
|
||||
with ctx:
|
||||
self.assertEqual(1, await Match.all().count())
|
||||
|
||||
|
||||
async def _turn_is(game_id: str, turn: int) -> Optional[GameSession]:
|
||||
session = await game_store.load(game_id)
|
||||
|
||||
Reference in New Issue
Block a user