speed up loading and saving to redis that the move request are getting a answer when switching workers, strip data that neats to get recomuted every turn
Build and Push Docker Container / build-and-push (push) Successful in 4m49s

This commit is contained in:
2026-04-07 12:13:11 +02:00
parent f479541c04
commit f6e19e18e6
6 changed files with 58 additions and 7 deletions
+5
View File
@@ -151,6 +151,11 @@ class GameBoard:
return {"name": self.type, "is_ladder": self.is_ladder} return {"name": self.type, "is_ladder": self.is_ladder}
def __getstate__(self):
state = self.__dict__.copy()
state['turns'] = [] # strip turn history — grows linearly, not needed for move computation
return state
async def save(self, store_class, **kwargs): async def save(self, store_class, **kwargs):
store = store_class(**kwargs) store = store_class(**kwargs)
await store.save(self) await store.save(self)
+21 -3
View File
@@ -1,5 +1,5 @@
from typing import TYPE_CHECKING, cast from typing import TYPE_CHECKING, cast
import json, time, os import asyncio, json, time, os
from quart import Blueprint, request, jsonify from quart import Blueprint, request, jsonify
@@ -61,9 +61,27 @@ def create_battlesnake_blueprint(server:'Server') -> Blueprint:
server.metrics_collector.record_http_request('move') server.metrics_collector.record_http_request('move')
game_state = await request.get_json() game_state = await request.get_json()
move_started = time.perf_counter() move_started = time.perf_counter()
game_id = game_state['game']['id']
timeout_ms = int(game_state.get('game', {}).get('timeout', 500))
budget_sec = max(0.05, (timeout_ms - 50) / 1000.0)
next_move = None
move_completed = False
game_board = None
try:
async with asyncio.timeout(budget_sec):
game_board = cast(GameBoard, await server.game_runtime.get_game_board(game_state)) game_board = cast(GameBoard, await server.game_runtime.get_game_board(game_state))
next_move = game_board.snake_neat_make_a_move() loop = asyncio.get_running_loop()
await server.game_runtime.persist_game_board(game_state['game']['id'], game_board) next_move = await loop.run_in_executor(None, game_board.snake_neat_make_a_move)
move_completed = True
except TimeoutError:
await await_log(server.logger.warning(f'MOVE TIMEOUT: turn={game_state.get("turn")}, game={game_id}, returning fallback {next_move!r}'))
if move_completed:
await server.game_runtime.persist_game_board(game_id, game_board)
await server.gameplay_tracking.record_gameplay_turn(game_state, next_move, game_board) await server.gameplay_tracking.record_gameplay_turn(game_state, next_move, game_board)
elapsed_ms = (time.perf_counter() - move_started) * 1000.0 elapsed_ms = (time.perf_counter() - move_started) * 1000.0
await server.metrics_collector.record_move(next_move, elapsed_ms) await server.metrics_collector.record_move(next_move, elapsed_ms)
@@ -1,5 +1,5 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import inspect, pickle import inspect, pickle, zlib
if TYPE_CHECKING: if TYPE_CHECKING:
from server.GameBoard import GameBoard from server.GameBoard import GameBoard
@@ -28,7 +28,7 @@ class RedisGameBoardStore:
async def save(self, game_id:str, game_board:'GameBoard') -> None: async def save(self, game_id:str, game_board:'GameBoard') -> None:
redis = await self._get_redis() redis = await self._get_redis()
payload = pickle.dumps(game_board, protocol=pickle.HIGHEST_PROTOCOL) payload = zlib.compress(pickle.dumps(game_board, protocol=pickle.HIGHEST_PROTOCOL), level=1)
await redis.set(self._key(game_id), payload, ex=self.ttl_seconds) await redis.set(self._key(game_id), payload, ex=self.ttl_seconds)
async def load(self, game_id:str): async def load(self, game_id:str):
@@ -36,6 +36,9 @@ class RedisGameBoardStore:
payload = await redis.get(self._key(game_id)) payload = await redis.get(self._key(game_id))
if payload is None: if payload is None:
return None return None
try:
return pickle.loads(zlib.decompress(payload))
except zlib.error:
return pickle.loads(payload) return pickle.loads(payload)
async def delete(self, game_id:str) -> None: async def delete(self, game_id:str) -> None:
+5
View File
@@ -79,6 +79,11 @@ class ApexBattleSnake(TemplateSnake):
# RL bootstrap dataset recorder # RL bootstrap dataset recorder
self.rl_bootstrap = RLBootstrapDataset() self.rl_bootstrap = RLBootstrapDataset()
def __getstate__(self):
state = super().__getstate__()
state['_game_phase'] = 0.0 # A3: per-turn scalar, recomputed in choose_move
return state
# ── Env helpers ────────────────────────────────────────────────────────────── # ── Env helpers ──────────────────────────────────────────────────────────────
def _get_timeout_buffer_ms(self) -> int: def _get_timeout_buffer_ms(self) -> int:
+9
View File
@@ -202,3 +202,12 @@ class TemplateSnake:
def set_target_food(self, target_food:dict): def set_target_food(self, target_food:dict):
self.target_food = target_food self.target_food = target_food
return True return True
def __getstate__(self):
state = self.__dict__.copy()
state['history'] = [] # strip history — grows per turn, not needed for moves
state.pop('game_board', None) # re-set at top of every choose_move; circular ref
state.pop('calculations', None) # re-initialised at top of every choose_move
state.pop('eat_the_snake_overwrite', None) # re-initialised at top of every choose_move
state.pop('kill_the_snake', None) # per-call transient
return state
+11
View File
@@ -100,6 +100,17 @@ class UltimateBattleSnake(TemplateSnake):
# RL bootstrap dataset recorder # RL bootstrap dataset recorder
self.rl_bootstrap = RLBootstrapDataset() self.rl_bootstrap = RLBootstrapDataset()
def __getstate__(self):
state = super().__getstate__()
# strip per-turn precomputed state — all re-assigned at the top of choose_move
state['_enemy_dmaps'] = []
state['_enemy_heads'] = []
state['_base_blocked'] = set()
state['_is_snail'] = False
state['_bfs_cache'] = {}
state['_bfs_cache_turn'] = -1
return state
# ── Env helpers ────────────────────────────────────────────────────────────── # ── Env helpers ──────────────────────────────────────────────────────────────
def _get_timeout_buffer_ms(self) -> int: def _get_timeout_buffer_ms(self) -> int: