update Dashboard with new keyboard bindings fix errors with hazards, set win or loss on stale games (older) and push new games real time over the websocket connection when they are finished
Build and Push Docker Container / build-and-push (push) Failing after 12m39s
Build and Push Docker Container / build-and-push (push) Failing after 12m39s
This commit is contained in:
+84
-4
@@ -14,8 +14,15 @@ from server.metrics import (
|
||||
MetricsCollector,
|
||||
)
|
||||
|
||||
from quart import Quart, request, jsonify, render_template, send_from_directory
|
||||
import logging, json, os, re, time
|
||||
from quart import (
|
||||
Quart,
|
||||
request,
|
||||
jsonify,
|
||||
render_template,
|
||||
send_from_directory,
|
||||
websocket,
|
||||
)
|
||||
import asyncio, logging, json, os, re, time
|
||||
from typing import cast
|
||||
|
||||
class Server:
|
||||
@@ -51,6 +58,8 @@ class Server:
|
||||
self.running_games:dict[str, GameBoard] = {}
|
||||
self.game_move_counts:dict[str, int] = {}
|
||||
self.game_last_seen_unix:dict[str, int] = {}
|
||||
self.dashboard_game_subscribers:set[asyncio.Queue[str]] = set()
|
||||
self.dashboard_game_subscribers_lock=asyncio.Lock()
|
||||
|
||||
self.metrics_collector = MetricsCollector(
|
||||
metrics_manager=MetricsStoreBuilder.build(
|
||||
@@ -68,6 +77,7 @@ class Server:
|
||||
)
|
||||
self.clear_worker_metrics_on_startup = self._env_bool('METRICS_CLEAR_WORKERS_ON_STARTUP', True)
|
||||
self.worker_metrics_startup_lock_ttl_sec = self._env_int('METRICS_STARTUP_CLEANUP_LOCK_TTL_SEC', 300)
|
||||
self.dashboard_running_game_stale_sec = 600
|
||||
self._startup_worker_metrics_cleared = False
|
||||
|
||||
self.logger = build_logger('Battlesnake', debug_env_var='DEBUG_SERVER')
|
||||
@@ -145,6 +155,7 @@ class Server:
|
||||
)
|
||||
|
||||
await self._record_gameplay_end(game_state)
|
||||
await self._push_dashboard_games_update(game_state)
|
||||
await await_log(self.logger.info(f'GAME ENDED: Winner is {[x['name'] for x in game_state['board']['snakes']]}'))
|
||||
await self._delete_game_board(game_state)
|
||||
await self.metrics_collector.record_game_end(game_state)
|
||||
@@ -215,6 +226,19 @@ class Server:
|
||||
customization_root = os.path.join(self.data_path, 'server', 'static', 'customizations')
|
||||
return await send_from_directory(customization_root, asset_path)
|
||||
|
||||
@self.app.websocket('/dashboard/ws/games')
|
||||
async def dashboard_games_ws():
|
||||
subscriber_queue: asyncio.Queue[str] = asyncio.Queue(maxsize=20)
|
||||
await self._register_dashboard_game_subscriber(subscriber_queue)
|
||||
try:
|
||||
initial_payload = await self._build_dashboard_games_event()
|
||||
await websocket.send(json.dumps(initial_payload))
|
||||
while True:
|
||||
event_payload = await subscriber_queue.get()
|
||||
await websocket.send(event_payload)
|
||||
finally:
|
||||
await self._unregister_dashboard_game_subscriber(subscriber_queue)
|
||||
|
||||
async def run(self, host:str='0.0.0.0', port:int=8000, debug:bool=False):
|
||||
logging.getLogger('werkzeug').setLevel(logging.ERROR)
|
||||
|
||||
@@ -402,28 +426,84 @@ class Server:
|
||||
except Exception as error:
|
||||
await await_log(self.logger.warning(f'Gameplay DB end record failed:{error}'))
|
||||
|
||||
async def _register_dashboard_game_subscriber(self, subscriber_queue:asyncio.Queue[str]) -> None:
|
||||
async with self.dashboard_game_subscribers_lock:
|
||||
self.dashboard_game_subscribers.add(subscriber_queue)
|
||||
|
||||
async def _unregister_dashboard_game_subscriber(self, subscriber_queue:asyncio.Queue[str]) -> None:
|
||||
async with self.dashboard_game_subscribers_lock:
|
||||
self.dashboard_game_subscribers.discard(subscriber_queue)
|
||||
|
||||
async def _broadcast_dashboard_game_event(self, payload:dict) -> None:
|
||||
encoded_payload = json.dumps(payload)
|
||||
async with self.dashboard_game_subscribers_lock:
|
||||
subscribers = tuple(self.dashboard_game_subscribers)
|
||||
|
||||
for subscriber_queue in subscribers:
|
||||
if subscriber_queue.full():
|
||||
try:
|
||||
subscriber_queue.get_nowait()
|
||||
except asyncio.QueueEmpty:
|
||||
pass
|
||||
|
||||
try:
|
||||
subscriber_queue.put_nowait(encoded_payload)
|
||||
except asyncio.QueueFull:
|
||||
continue
|
||||
|
||||
async def _build_dashboard_games_event(self, game_state:dict|None=None) -> dict:
|
||||
games_payload = await self._get_dashboard_games(limit=100)
|
||||
summary_payload = await self._get_dashboard_summary()
|
||||
game_id = None
|
||||
if game_state is not None:
|
||||
game_id = game_state.get('game', {}).get('id')
|
||||
|
||||
return {
|
||||
'type': 'dashboard_games_update',
|
||||
'trigger': 'game_saved' if game_id else 'snapshot',
|
||||
'game_id': game_id,
|
||||
'games': games_payload,
|
||||
'summary': summary_payload,
|
||||
}
|
||||
|
||||
async def _push_dashboard_games_update(self, game_state:dict|None=None) -> None:
|
||||
if self.gameplay_database is None:
|
||||
return
|
||||
event_payload = await self._build_dashboard_games_event(game_state)
|
||||
await self._broadcast_dashboard_game_event(event_payload)
|
||||
|
||||
async def _get_dashboard_summary(self) -> dict:
|
||||
if self.gameplay_database is None:
|
||||
return {'enabled': False}
|
||||
try:
|
||||
await self._finalize_stale_dashboard_games()
|
||||
summary = await self.gameplay_database.get_summary()
|
||||
summary['enabled'] = True
|
||||
return summary
|
||||
except Exception as error:
|
||||
await await_log(self.logger.warning(f'Gameplay DB summary failed:{error}'))
|
||||
return {'enabled': True, 'error':' summary_unavailable'}
|
||||
return {'enabled': True, 'error': ' summary_unavailable'}
|
||||
|
||||
async def _get_dashboard_games(self, limit:int=50) -> dict:
|
||||
if self.gameplay_database is None:
|
||||
return {'enabled': False, 'games': []}
|
||||
try:
|
||||
await self._finalize_stale_dashboard_games()
|
||||
games = await self.gameplay_database.list_games(limit=limit)
|
||||
return {'enabled': True, 'games': games}
|
||||
except Exception as error:
|
||||
await await_log(self.logger.warning(f'Gameplay DB game list failed:{error}'))
|
||||
return {'enabled': True, 'error': 'games_unavailable', 'games': []}
|
||||
|
||||
async def _get_dashboard_game_replay(self, game_id:str) -> dict | None:
|
||||
async def _finalize_stale_dashboard_games(self) -> None:
|
||||
if self.gameplay_database is None:
|
||||
return
|
||||
try:
|
||||
await self.gameplay_database.finalize_stale_running_games(stale_after_seconds=self.dashboard_running_game_stale_sec)
|
||||
except Exception as error:
|
||||
await await_log(self.logger.warning(f'Gameplay DB stale running game finalize failed:{error}'))
|
||||
|
||||
async def _get_dashboard_game_replay(self, game_id:str) -> dict|None:
|
||||
if self.gameplay_database is None:
|
||||
return {'enabled': False, 'error': 'database_disabled', 'game_id': game_id}
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user