create MemoryGameBoardStore Class and rework Building of Game Board Storage
This commit is contained in:
+8
-7
@@ -1,6 +1,6 @@
|
|||||||
from server.Files import read_file
|
from server.Files import read_file
|
||||||
|
from server.game_board_stats import GameBoardStoreBuilder
|
||||||
from server.GameBoard import GameBoard
|
from server.GameBoard import GameBoard
|
||||||
from server.GameStateStore import GameStateStore
|
|
||||||
from snakes import SnakeBuilder
|
from snakes import SnakeBuilder
|
||||||
from quart_common.web.logger import await_log
|
from quart_common.web.logger import await_log
|
||||||
from quart_common.web.logger import build_logger
|
from quart_common.web.logger import build_logger
|
||||||
@@ -31,7 +31,7 @@ class Server:
|
|||||||
self.check_tls_security = check_tls_security
|
self.check_tls_security = check_tls_security
|
||||||
|
|
||||||
self.store_game_state = False
|
self.store_game_state = False
|
||||||
self.game_state_store = GameStateStore(
|
self.game_state_store = GameBoardStoreBuilder.build(
|
||||||
backend=game_state_backend,
|
backend=game_state_backend,
|
||||||
redis_url=game_state_redis_url,
|
redis_url=game_state_redis_url,
|
||||||
ttl_seconds=game_state_ttl_sec,
|
ttl_seconds=game_state_ttl_sec,
|
||||||
@@ -104,7 +104,7 @@ class Server:
|
|||||||
self._record_http_request('move')
|
self._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_board = await self._get_game_board(game_state)
|
game_board = cast(GameBoard, await self._get_game_board(game_state))
|
||||||
next_move = game_board.snake_neat_make_a_move()
|
next_move = game_board.snake_neat_make_a_move()
|
||||||
await self._persist_game_board(game_state['game']['id'], game_board)
|
await self._persist_game_board(game_state['game']['id'], game_board)
|
||||||
elapsed_ms = (time.perf_counter() - move_started) * 1000.0
|
elapsed_ms = (time.perf_counter() - move_started) * 1000.0
|
||||||
@@ -133,7 +133,7 @@ class Server:
|
|||||||
self._prune_stale_games()
|
self._prune_stale_games()
|
||||||
game_state = await request.get_json()
|
game_state = await request.get_json()
|
||||||
if self.store_game_state:
|
if self.store_game_state:
|
||||||
game_board = await self._get_game_board(game_state, end=True)
|
game_board = cast(GameBoard, await self._get_game_board(game_state, end=True))
|
||||||
if self.check_tls_security:
|
if self.check_tls_security:
|
||||||
await game_board.save(
|
await game_board.save(
|
||||||
StorageLoader.build(self.storage_type),
|
StorageLoader.build(self.storage_type),
|
||||||
@@ -228,7 +228,7 @@ class Server:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return 180
|
return 180
|
||||||
|
|
||||||
async def _create_game_board(self, game_state: dict):
|
async def _create_game_board(self, game_state:dict) -> GameBoard:
|
||||||
game_id = game_state['game']['id']
|
game_id = game_state['game']['id']
|
||||||
new_game_board = GameBoard(
|
new_game_board = GameBoard(
|
||||||
game_id=game_id,
|
game_id=game_id,
|
||||||
@@ -264,14 +264,15 @@ class Server:
|
|||||||
self.game_last_seen_unix.pop(game_id, None)
|
self.game_last_seen_unix.pop(game_id, None)
|
||||||
await self.game_state_store.delete(game_id)
|
await self.game_state_store.delete(game_id)
|
||||||
|
|
||||||
async def _get_game_board(self, game_state:dict, end:bool=False):
|
async def _get_game_board(self, game_state:dict, end:bool=False) -> GameBoard:
|
||||||
game_id = game_state['game']['id']
|
game_id = game_state['game']['id']
|
||||||
|
game_board:GameBoard
|
||||||
try:
|
try:
|
||||||
game_board = self.running_games[game_id]
|
game_board = self.running_games[game_id]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
persisted_board = await self.game_state_store.load(game_id)
|
persisted_board = await self.game_state_store.load(game_id)
|
||||||
if persisted_board is not None:
|
if persisted_board is not None:
|
||||||
game_board = persisted_board
|
game_board = cast(GameBoard, persisted_board)
|
||||||
self.running_games[game_id] = game_board
|
self.running_games[game_id] = game_board
|
||||||
else:
|
else:
|
||||||
game_board = await self._create_game_board(game_state)
|
game_board = await self._create_game_board(game_state)
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
from server.GameBoard import GameBoard
|
||||||
|
|
||||||
|
class MemoryGameBoardStore:
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self._state:dict[str, object] = {}
|
||||||
|
|
||||||
|
async def save(self, game_id:str, game_board:GameBoard) -> None:
|
||||||
|
self._state[game_id] = game_board
|
||||||
|
|
||||||
|
async def load(self, game_id:str):
|
||||||
|
return self._state.get(game_id)
|
||||||
|
|
||||||
|
async def delete(self, game_id:str) -> None:
|
||||||
|
self._state.pop(game_id, None)
|
||||||
|
|
||||||
|
async def close(self) -> None:
|
||||||
|
return None
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
|
from server.GameBoard import GameBoard
|
||||||
import pickle
|
import pickle
|
||||||
|
|
||||||
class GameStateStore:
|
class RedisGameBoardStore:
|
||||||
def __init__(self, backend:str="memory", redis_url:str="redis://localhost:6379/0", key_prefix:str="snake:gameboard", ttl_seconds:int=900,):
|
def __init__(self, redis_url:str="redis://localhost:6379/0", key_prefix:str="snake:gameboard", ttl_seconds:int=900, **kwargs):
|
||||||
self.backend = (backend or "memory").strip().lower()
|
|
||||||
self.redis_url = redis_url
|
self.redis_url = redis_url
|
||||||
self.key_prefix = key_prefix
|
self.key_prefix = key_prefix
|
||||||
self.ttl_seconds = max(60, int(ttl_seconds))
|
self.ttl_seconds = max(60, int(ttl_seconds))
|
||||||
@@ -13,28 +13,22 @@ class GameStateStore:
|
|||||||
return self._redis
|
return self._redis
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from redis.asyncio import from_url # type: ignore[import-not-found]
|
import aioredis # type: ignore[import-not-found]
|
||||||
except ImportError as error: # pragma: no cover
|
except ImportError as error: # pragma: no cover
|
||||||
raise RuntimeError("Redis backend selected but 'redis' package is not installed") from error
|
raise RuntimeError("Redis backend selected but 'aioredis' package is not installed") from error
|
||||||
|
|
||||||
self._redis = from_url(self.redis_url)
|
self._redis = aioredis.from_url(self.redis_url)
|
||||||
return self._redis
|
return self._redis
|
||||||
|
|
||||||
def _key(self, game_id:str) -> str:
|
def _key(self, game_id:str) -> str:
|
||||||
return f"{self.key_prefix}:{game_id}"
|
return f"{self.key_prefix}:{game_id}"
|
||||||
|
|
||||||
async def save(self, game_id:str, game_board) -> None:
|
async def save(self, game_id:str, game_board:GameBoard) -> None:
|
||||||
if self.backend != "redis":
|
|
||||||
return
|
|
||||||
|
|
||||||
redis = await self._get_redis()
|
redis = await self._get_redis()
|
||||||
payload = pickle.dumps(game_board, protocol=pickle.HIGHEST_PROTOCOL)
|
payload = pickle.dumps(game_board, protocol=pickle.HIGHEST_PROTOCOL)
|
||||||
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):
|
||||||
if self.backend != "redis":
|
|
||||||
return None
|
|
||||||
|
|
||||||
redis = await self._get_redis()
|
redis = await self._get_redis()
|
||||||
payload = await redis.get(self._key(game_id))
|
payload = await redis.get(self._key(game_id))
|
||||||
if payload is None:
|
if payload is None:
|
||||||
@@ -42,14 +36,18 @@ class GameStateStore:
|
|||||||
return pickle.loads(payload)
|
return pickle.loads(payload)
|
||||||
|
|
||||||
async def delete(self, game_id:str) -> None:
|
async def delete(self, game_id:str) -> None:
|
||||||
if self.backend != "redis":
|
|
||||||
return
|
|
||||||
|
|
||||||
redis = await self._get_redis()
|
redis = await self._get_redis()
|
||||||
await redis.delete(self._key(game_id))
|
await redis.delete(self._key(game_id))
|
||||||
|
|
||||||
async def close(self) -> None:
|
async def close(self) -> None:
|
||||||
if self._redis is None:
|
if self._redis is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if hasattr(self._redis, "aclose"):
|
||||||
await self._redis.aclose()
|
await self._redis.aclose()
|
||||||
|
elif hasattr(self._redis, "close"):
|
||||||
|
close_result = self._redis.close()
|
||||||
|
if close_result is not None:
|
||||||
|
await close_result
|
||||||
|
|
||||||
self._redis = None
|
self._redis = None
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from server.game_board_stats.MemoryGameBoardStore import MemoryGameBoardStore
|
||||||
|
from server.game_board_stats.RedisGameBoardStore import RedisGameBoardStore
|
||||||
|
|
||||||
|
class GameBoardStoreBuilder:
|
||||||
|
@classmethod
|
||||||
|
def build(self, backend:str="memory", **kwargs) -> MemoryGameBoardStore|RedisGameBoardStore:
|
||||||
|
selected = (backend or "memory").strip().lower()
|
||||||
|
if selected == "redis":
|
||||||
|
return RedisGameBoardStore(**kwargs)
|
||||||
|
return MemoryGameBoardStore(**kwargs)
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
from server.GameBoard import GameBoard
|
from server.GameBoard import GameBoard
|
||||||
from server.GameStateStore import GameStateStore
|
from server.game_board_stats import GameBoardStoreBuilder
|
||||||
|
from server.game_board_stats.MemoryGameBoardStore import MemoryGameBoardStore
|
||||||
|
from server.game_board_stats.RedisGameBoardStore import RedisGameBoardStore
|
||||||
from snakes.TemplateSnake import TemplateSnake
|
from snakes.TemplateSnake import TemplateSnake
|
||||||
|
|
||||||
|
|
||||||
class _FakeRedis:
|
class _FakeRedis:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.data = {}
|
self.data = {}
|
||||||
@@ -70,21 +72,33 @@ class TestGameStateStore(unittest.IsolatedAsyncioTestCase):
|
|||||||
)
|
)
|
||||||
return board
|
return board
|
||||||
|
|
||||||
async def test_memory_backend_returns_none(self):
|
def test_builder_selects_store_backend(self):
|
||||||
store = GameStateStore(backend="memory")
|
memory_store = GameBoardStoreBuilder.build(backend="memory")
|
||||||
|
redis_store = GameBoardStoreBuilder.build(backend="redis")
|
||||||
|
default_store = GameBoardStoreBuilder.build(backend="unknown")
|
||||||
|
|
||||||
|
self.assertIsInstance(memory_store, MemoryGameBoardStore)
|
||||||
|
self.assertIsInstance(redis_store, RedisGameBoardStore)
|
||||||
|
self.assertIsInstance(default_store, MemoryGameBoardStore)
|
||||||
|
|
||||||
|
async def test_memory_backend_roundtrip(self):
|
||||||
|
store = MemoryGameBoardStore()
|
||||||
board = self._build_board()
|
board = self._build_board()
|
||||||
await store.save("game-1", board)
|
await store.save("game-1", board)
|
||||||
|
|
||||||
loaded = await store.load("game-1")
|
loaded = cast(GameBoard, await store.load("game-1"))
|
||||||
self.assertIsNone(loaded)
|
self.assertIsNotNone(loaded)
|
||||||
|
self.assertEqual(loaded.id, "game-1")
|
||||||
|
await store.delete("game-1")
|
||||||
|
self.assertIsNone(await store.load("game-1"))
|
||||||
|
|
||||||
async def test_redis_backend_roundtrip(self):
|
async def test_redis_backend_roundtrip(self):
|
||||||
store = GameStateStore(backend="redis")
|
store = RedisGameBoardStore()
|
||||||
store._redis = _FakeRedis()
|
store._redis = cast(Any, _FakeRedis())
|
||||||
board = self._build_board()
|
board = self._build_board()
|
||||||
|
|
||||||
await store.save("game-1", board)
|
await store.save("game-1", board)
|
||||||
loaded = await store.load("game-1")
|
loaded = cast(GameBoard, await store.load("game-1"))
|
||||||
self.assertIsNotNone(loaded)
|
self.assertIsNotNone(loaded)
|
||||||
self.assertEqual(loaded.id, "game-1")
|
self.assertEqual(loaded.id, "game-1")
|
||||||
self.assertEqual(loaded.get_turn(), 3)
|
self.assertEqual(loaded.get_turn(), 3)
|
||||||
|
|||||||
Reference in New Issue
Block a user