server: make state-save debounce timing configurable
This commit is contained in:
@@ -39,6 +39,8 @@ class StorageConfigSection(BaseModel):
|
|||||||
"""Persistent state file location."""
|
"""Persistent state file location."""
|
||||||
|
|
||||||
state_file: str = "runtime/items.json"
|
state_file: str = "runtime/items.json"
|
||||||
|
state_save_debounce_ms: int = Field(default=200, gt=0)
|
||||||
|
state_save_max_delay_ms: int = Field(default=1000, gt=0)
|
||||||
|
|
||||||
|
|
||||||
class WorldConfigSection(BaseModel):
|
class WorldConfigSection(BaseModel):
|
||||||
|
|||||||
@@ -73,8 +73,6 @@ CLIENT_PACKET_ADAPTER = TypeAdapter(ClientPacket)
|
|||||||
MAX_ACTIVE_PIANO_KEYS_PER_CLIENT = 12
|
MAX_ACTIVE_PIANO_KEYS_PER_CLIENT = 12
|
||||||
PIANO_RECORDING_MAX_MS = 30_000
|
PIANO_RECORDING_MAX_MS = 30_000
|
||||||
PIANO_RECORDING_MAX_EVENTS = 4096
|
PIANO_RECORDING_MAX_EVENTS = 4096
|
||||||
STATE_SAVE_DEBOUNCE_MS = 200
|
|
||||||
STATE_SAVE_MAX_DELAY_MS = 1000
|
|
||||||
|
|
||||||
|
|
||||||
class SignalingServer:
|
class SignalingServer:
|
||||||
@@ -89,6 +87,8 @@ class SignalingServer:
|
|||||||
max_message_size: int = 2_000_000,
|
max_message_size: int = 2_000_000,
|
||||||
state_file: Path | None = None,
|
state_file: Path | None = None,
|
||||||
grid_size: int = 41,
|
grid_size: int = 41,
|
||||||
|
state_save_debounce_ms: int = 200,
|
||||||
|
state_save_max_delay_ms: int = 1000,
|
||||||
):
|
):
|
||||||
"""Initialize runtime state, TLS context, and item service."""
|
"""Initialize runtime state, TLS context, and item service."""
|
||||||
|
|
||||||
@@ -105,6 +105,8 @@ class SignalingServer:
|
|||||||
self.grid_size = max(1, grid_size)
|
self.grid_size = max(1, grid_size)
|
||||||
self.instance_id = str(uuid.uuid4())
|
self.instance_id = str(uuid.uuid4())
|
||||||
self.server_version = self._resolve_server_version()
|
self.server_version = self._resolve_server_version()
|
||||||
|
self.state_save_debounce_ms = max(1, int(state_save_debounce_ms))
|
||||||
|
self.state_save_max_delay_ms = max(self.state_save_debounce_ms, int(state_save_max_delay_ms))
|
||||||
self._pending_state_save_handle: asyncio.TimerHandle | None = None
|
self._pending_state_save_handle: asyncio.TimerHandle | None = None
|
||||||
self._pending_state_save_started_at: float | None = None
|
self._pending_state_save_started_at: float | None = None
|
||||||
|
|
||||||
@@ -160,13 +162,13 @@ class SignalingServer:
|
|||||||
if self._pending_state_save_started_at is None:
|
if self._pending_state_save_started_at is None:
|
||||||
self._pending_state_save_started_at = now
|
self._pending_state_save_started_at = now
|
||||||
elapsed_ms = int((now - self._pending_state_save_started_at) * 1000)
|
elapsed_ms = int((now - self._pending_state_save_started_at) * 1000)
|
||||||
if elapsed_ms >= STATE_SAVE_MAX_DELAY_MS:
|
if elapsed_ms >= self.state_save_max_delay_ms:
|
||||||
self._flush_state_save()
|
self._flush_state_save()
|
||||||
return
|
return
|
||||||
if self._pending_state_save_handle is not None:
|
if self._pending_state_save_handle is not None:
|
||||||
self._pending_state_save_handle.cancel()
|
self._pending_state_save_handle.cancel()
|
||||||
remaining_ms = max(0, STATE_SAVE_MAX_DELAY_MS - elapsed_ms)
|
remaining_ms = max(0, self.state_save_max_delay_ms - elapsed_ms)
|
||||||
delay_ms = min(STATE_SAVE_DEBOUNCE_MS, remaining_ms)
|
delay_ms = min(self.state_save_debounce_ms, remaining_ms)
|
||||||
self._pending_state_save_handle = loop.call_later(delay_ms / 1000, self._flush_state_save)
|
self._pending_state_save_handle = loop.call_later(delay_ms / 1000, self._flush_state_save)
|
||||||
|
|
||||||
def _is_nickname_taken(self, nickname: str, exclude_client_id: str | None = None) -> bool:
|
def _is_nickname_taken(self, nickname: str, exclude_client_id: str | None = None) -> bool:
|
||||||
@@ -1344,5 +1346,7 @@ def run() -> None:
|
|||||||
max_message_size=config.network.max_message_bytes,
|
max_message_size=config.network.max_message_bytes,
|
||||||
state_file=state_file,
|
state_file=state_file,
|
||||||
grid_size=config.world.grid_size,
|
grid_size=config.world.grid_size,
|
||||||
|
state_save_debounce_ms=config.storage.state_save_debounce_ms,
|
||||||
|
state_save_max_delay_ms=config.storage.state_save_max_delay_ms,
|
||||||
)
|
)
|
||||||
asyncio.run(server.start())
|
asyncio.run(server.start())
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ level = "INFO"
|
|||||||
[storage]
|
[storage]
|
||||||
# Item persistence file. Relative paths are resolved from this config file directory.
|
# Item persistence file. Relative paths are resolved from this config file directory.
|
||||||
state_file = "runtime/items.json"
|
state_file = "runtime/items.json"
|
||||||
|
# Debounce window for coalesced item-state writes.
|
||||||
|
state_save_debounce_ms = 200
|
||||||
|
# Maximum delay before a pending state write is forced.
|
||||||
|
state_save_max_delay_ms = 1000
|
||||||
|
|
||||||
[world]
|
[world]
|
||||||
# Grid width/height in cells. Valid coordinates are 0..grid_size-1.
|
# Grid width/height in cells. Valid coordinates are 0..grid_size-1.
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ def test_load_config_defaults_when_path_none() -> None:
|
|||||||
assert cfg.server.bind_ip == "127.0.0.1"
|
assert cfg.server.bind_ip == "127.0.0.1"
|
||||||
assert cfg.network.allow_insecure_ws is True
|
assert cfg.network.allow_insecure_ws is True
|
||||||
assert cfg.storage.state_file == "runtime/items.json"
|
assert cfg.storage.state_file == "runtime/items.json"
|
||||||
|
assert cfg.storage.state_save_debounce_ms == 200
|
||||||
|
assert cfg.storage.state_save_max_delay_ms == 1000
|
||||||
assert cfg.world.grid_size == 41
|
assert cfg.world.grid_size == 41
|
||||||
|
|
||||||
|
|
||||||
@@ -23,3 +25,18 @@ allow_insecure_ws = false
|
|||||||
)
|
)
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
load_config(config_path)
|
load_config(config_path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_reads_state_save_timing(tmp_path: Path) -> None:
|
||||||
|
config_path = tmp_path / "config.toml"
|
||||||
|
config_path.write_text(
|
||||||
|
"""
|
||||||
|
[storage]
|
||||||
|
state_file = "runtime/items.json"
|
||||||
|
state_save_debounce_ms = 150
|
||||||
|
state_save_max_delay_ms = 900
|
||||||
|
""".strip()
|
||||||
|
)
|
||||||
|
cfg = load_config(config_path)
|
||||||
|
assert cfg.storage.state_save_debounce_ms == 150
|
||||||
|
assert cfg.storage.state_save_max_delay_ms == 900
|
||||||
|
|||||||
Reference in New Issue
Block a user