perf(battlesnake): defer ordered lifecycle bookkeeping
Build and Push Docker Container / build-and-push (push) Successful in 10m12s
Build and Push Docker Container / build-and-push (push) Successful in 10m12s
- Acknowledge start and end without waiting for persistence or dashboards. - Keep runtime creation and finished-game removal on the response path. - Serialize start, turn, and end bookkeeping per game in Quart tasks. - Capture turn thinking before scheduling deferred persistence. - Defer start metrics, logging, and stale-game pruning. - Run legacy history exports in a worker thread. - Record results and end metrics even when history export fails. - Drain pending bookkeeping before closing storage during shutdown. - Test sub-500 ms responses with slow backends and real SQLite. - Cover event ordering, export failures, and rejected user agents.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
"""Ordered, application-managed game bookkeeping outside HTTP response budgets."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
import os
|
||||
|
||||
from quart_common.web.logger import await_log
|
||||
from server.database import StorageLoader
|
||||
|
||||
class GameLifecycleService:
|
||||
def __init__(self, server):
|
||||
self.server = server
|
||||
self.pending: dict[str, asyncio.Future] = {}
|
||||
|
||||
def submit(self, game_id:str, operation:Callable[[], Awaitable[None]]) -> None:
|
||||
previous = self.pending.get(game_id)
|
||||
completed = asyncio.get_running_loop().create_future()
|
||||
self.pending[game_id] = completed
|
||||
|
||||
async def run():
|
||||
try:
|
||||
if previous is not None:
|
||||
await asyncio.shield(previous)
|
||||
await operation()
|
||||
except Exception:
|
||||
self.server.app.logger.exception('Game bookkeeping failed for %s', game_id)
|
||||
finally:
|
||||
if not completed.done():
|
||||
completed.set_result(None)
|
||||
if self.pending.get(game_id) is completed:
|
||||
self.pending.pop(game_id, None)
|
||||
|
||||
self.server.app.add_background_task(run)
|
||||
|
||||
async def drain(self) -> None:
|
||||
while self.pending:
|
||||
await asyncio.gather(*(asyncio.shield(task) for task in list(self.pending.values())))
|
||||
|
||||
async def started(self, game_state:dict, game_board, active_count:int) -> None:
|
||||
server = self.server
|
||||
await server.metrics_collector.record_game_started(active_count)
|
||||
await server.gameplay_tracking.record_gameplay_start(game_state, game_board)
|
||||
await server.dashboard_query.push_dashboard_games_update(game_state, trigger='game_started')
|
||||
await await_log(server.logger.info(f'GAME START: {game_state["game"]}'))
|
||||
await server.game_runtime.prune_stale_games()
|
||||
|
||||
async def ended(self, game_state:dict, game_board) -> None:
|
||||
server = self.server
|
||||
try:
|
||||
if server.store_game_state and game_board is not None:
|
||||
kwargs = {
|
||||
'file_path': os.path.join(server.data_path, 'data'),
|
||||
'database': os.getenv('EDGEDB_DATABASE'),
|
||||
}
|
||||
if server.check_tls_security:
|
||||
kwargs['tls_security'] = None
|
||||
# Legacy exporters include synchronous serialization and database calls.
|
||||
# Keep those off the request event loop as well as off the response path.
|
||||
def export():
|
||||
asyncio.run(game_board.save(StorageLoader.build(server.storage_type), **kwargs))
|
||||
|
||||
await asyncio.to_thread(export)
|
||||
finally:
|
||||
# A history export failure must not suppress the game result or metrics.
|
||||
await server.gameplay_tracking.record_gameplay_end(game_state)
|
||||
await server.metrics_collector.record_game_end(game_state)
|
||||
await server.dashboard_query.push_dashboard_games_update(game_state)
|
||||
await await_log(server.logger.info(
|
||||
f'GAME ENDED: Winner is {[snake["name"] for snake in game_state["board"]["snakes"]]}'
|
||||
))
|
||||
|
||||
async def turn(self, game_state:dict, move:str|None, thinking:dict|None) -> None:
|
||||
server = self.server
|
||||
await server.gameplay_tracking.record_gameplay_turn_snapshot(game_state, move, thinking)
|
||||
await server.dashboard_query.push_dashboard_game_replay_update(game_state['game']['id'])
|
||||
Reference in New Issue
Block a user