server: make state-save debounce timing configurable

This commit is contained in:
Jage9
2026-02-24 02:50:47 -05:00
parent f7e29ec968
commit d4dbb807da
4 changed files with 32 additions and 5 deletions

View File

@@ -39,6 +39,8 @@ class StorageConfigSection(BaseModel):
"""Persistent state file location."""
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):

View File

@@ -73,8 +73,6 @@ CLIENT_PACKET_ADAPTER = TypeAdapter(ClientPacket)
MAX_ACTIVE_PIANO_KEYS_PER_CLIENT = 12
PIANO_RECORDING_MAX_MS = 30_000
PIANO_RECORDING_MAX_EVENTS = 4096
STATE_SAVE_DEBOUNCE_MS = 200
STATE_SAVE_MAX_DELAY_MS = 1000
class SignalingServer:
@@ -89,6 +87,8 @@ class SignalingServer:
max_message_size: int = 2_000_000,
state_file: Path | None = None,
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."""
@@ -105,6 +105,8 @@ class SignalingServer:
self.grid_size = max(1, grid_size)
self.instance_id = str(uuid.uuid4())
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_started_at: float | None = None
@@ -160,13 +162,13 @@ class SignalingServer:
if self._pending_state_save_started_at is None:
self._pending_state_save_started_at = now
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()
return
if self._pending_state_save_handle is not None:
self._pending_state_save_handle.cancel()
remaining_ms = max(0, STATE_SAVE_MAX_DELAY_MS - elapsed_ms)
delay_ms = min(STATE_SAVE_DEBOUNCE_MS, remaining_ms)
remaining_ms = max(0, self.state_save_max_delay_ms - elapsed_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)
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,
state_file=state_file,
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())

View File

@@ -21,6 +21,10 @@ level = "INFO"
[storage]
# Item persistence file. Relative paths are resolved from this config file directory.
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]
# Grid width/height in cells. Valid coordinates are 0..grid_size-1.

View File

@@ -10,6 +10,8 @@ def test_load_config_defaults_when_path_none() -> None:
assert cfg.server.bind_ip == "127.0.0.1"
assert cfg.network.allow_insecure_ws is True
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
@@ -23,3 +25,18 @@ allow_insecure_ws = false
)
with pytest.raises(ValueError):
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