fix: address design review ACT NOW items (6 risk gaps)
- Add migrate_config() to ConfigGenerator protocol for schema version upgrades - Add per-server operation lock to ProcessManager to prevent start/stop races - Add busy_timeout retry/backoff strategy (exponential: 1s, 2s, 4s) for DB lock exhaustion - Add ConfigForm testing strategy and error boundary for malformed schemas - Add schema cache invalidation on adapter version change - Add ConfigMigrationError to typed adapter exceptions
This commit is contained in:
@@ -211,6 +211,7 @@ def start(self, server_id: int) -> dict:
|
||||
### ProcessManager (Core)
|
||||
- Singleton that owns all subprocess handles
|
||||
- Thread-safe dict: `{server_id: subprocess.Popen}`
|
||||
- **Per-server operation lock** (`_operation_locks: dict[int, threading.Lock]`) — serializes start/stop/restart for the same server. Prevents race conditions when two admins hit "start" simultaneously or when start+stop overlap. Each server gets its own lock; different servers operate independently.
|
||||
- `start()` sets `cwd=servers/{server_id}/` so relative config paths resolve correctly
|
||||
- On Windows: `terminate()` = `TerminateProcess` (hard kill, no SIGTERM) — graceful shutdown must go through adapter's RemoteAdmin
|
||||
- Provides: `start()`, `stop()`, `restart()`, `is_running()`, `send_command()`
|
||||
@@ -232,6 +233,7 @@ def start(self, server_id: int) -> dict:
|
||||
- **Validation is delegated to adapter's Pydantic models** — core never inspects config content
|
||||
- **Sensitive field encryption**: calls `adapter.get_config_generator().get_sensitive_fields(section)` to identify which JSON keys to encrypt/decrypt via Fernet
|
||||
- **Optimistic locking**: each row includes `config_version` (integer). On PUT, client sends the version they read. If version mismatch, return 409 Conflict.
|
||||
- **Schema migration**: on read, if `schema_version` differs from `adapter.get_config_version()`, core calls `adapter.migrate_config(old_version, config_json)`. On success, updates the row with migrated JSON and new `schema_version`. On `ConfigMigrationError`, keeps original config and logs a warning.
|
||||
- Provides: `get_section()`, `get_all_sections()`, `upsert_section()`, `delete_sections()`
|
||||
|
||||
### Adapter Exceptions (Standard Error Types)
|
||||
@@ -245,6 +247,7 @@ Adapters raise specific exception types so the core can handle errors precisely:
|
||||
| `LaunchArgsError` | build_launch_args() fails (missing mod, bad path) | Set server status='error', return 400 |
|
||||
| `RemoteAdminError` | Remote admin connection/command fails | Log warning, return 503 with detail |
|
||||
| `ExeNotAllowedError` | Executable not in adapter's allowlist | Return 400 with allowed list |
|
||||
| `ConfigMigrationError` | `migrate_config()` fails to transform old schema | Keep original config, log warning, server runs with old schema |
|
||||
|
||||
---
|
||||
|
||||
@@ -529,10 +532,11 @@ Steps:
|
||||
| Sync vs async DB | **Sync SQLAlchemy only** | All DB access is synchronous; background threads are non-async; no aiosqlite dependency |
|
||||
| WebSocket auth | JWT in query param on connect | Browser WS API doesn't support headers |
|
||||
| Process ownership | **ProcessManager singleton** | Single source of truth; prevents duplicate launches |
|
||||
| Server operation safety | **Per-server operation lock** | ProcessManager holds a lock per server_id that serializes start/stop/restart. Two concurrent start requests for the same server: the second waits for the first to complete, then sees the server is already running. Different servers are independent (no cross-server locking). |
|
||||
| Config files | **Adapter regenerates on each start** | Always fresh from DB; no sync drift; adapter's structured builder prevents config injection |
|
||||
| Config write failure | **Atomic write + rollback** | Adapter writes to temp files first, then atomic rename. On failure, temp files are cleaned up — original files remain untouched. Server start never proceeds with partial config. |
|
||||
| Sensitive field encryption | **Adapter declares via get_sensitive_fields()** | ConfigGenerator protocol returns list of JSON keys per section that need Fernet encryption. Core's ConfigRepository handles encrypt/decrypt transparently. |
|
||||
| Adapter schema versioning | **config_version in game_configs row** | Each config section row stores a version string. On adapter update, if version differs, adapter provides a migration function. |
|
||||
| Adapter schema versioning | **config_version in game_configs row** | Each config section row stores a version string. On adapter update, if version differs, core calls `adapter.migrate_config(old_version, config_json)` which returns the migrated dict. On migration failure (ConfigMigrationError), core keeps the original config and logs a warning — the server can still run with the old schema. |
|
||||
| Adapter error communication | **Typed adapter exceptions** | Adapters raise specific exception types (ConfigWriteError, ConfigValidationError, LaunchArgsError, RemoteAdminError). Core catches specifically and sets appropriate DB status + returns clear API errors. |
|
||||
| Remote admin thread safety | **Core wraps with lock** | Core wraps RemoteAdminClient calls with a threading.Lock. Adapter clients don't need to be thread-safe. One lock per server — API requests and poller thread share safely. |
|
||||
| Third-party adapter loading | **Setuptools entry_points** | Third-party adapters register via `languard.adapters` entry_point group. Core scans entry_points at startup and auto-registers. Built-in adapters registered on import. |
|
||||
|
||||
Reference in New Issue
Block a user