Refactor: Move module-level mutable flags to JailServiceState
TASK-004: Replace module-level mutable runtime flags in service layer with injected state holder, eliminating hidden global state and improving testability and synchronization boundaries. Changes: - Create JailServiceState dataclass in app/utils/runtime_state.py to hold backend capability cache and synchronization lock - Add JailServiceState as a field in RuntimeState (with default_factory) - Remove module-level _backend_cmd_supported and _backend_cmd_lock from jail_service.py - Refactor _check_backend_cmd_supported() to accept state parameter - Inject JailServiceState into list_jails() and _fetch_jail_summary() via parameters - Add get_jail_service_state() dependency provider in app/dependencies.py - Add JailServiceStateDep type alias for router injection - Update jails router to receive and pass state to service functions - Update all tests to use jail_service_state fixture and pass state to functions - Remove duplicate _MAX_PAGE_SIZE constant definition - Document mutable state management in Backend-Development.md - Update Architecture.md to describe JailServiceState and state nesting pattern Benefits: - Eliminates global mutable state and associated race conditions - Makes state visible to callers (not hidden in module scope) - Enables test isolation (each test gets fresh state) - Prepares codebase for multi-worker deployments (state can be extracted to shared backend) - Synchronization boundaries are now explicit (state.get_backend_cmd_lock()) Compliance: - All tests pass (17 passed in TestListJails, TestGetJail, TestLockInitialization) - No ruff linting errors - Type-safe: JailServiceState properly typed with asyncio.Lock, bool | None Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -40,6 +40,7 @@ acceptable and keeps the implementation simple.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Any
|
||||
@@ -69,10 +70,43 @@ _RUNTIME_ATTRIBUTES: frozenset[str] = frozenset(
|
||||
"pending_recovery",
|
||||
"last_activation",
|
||||
"runtime_settings",
|
||||
"jail_service_state",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class JailServiceState:
|
||||
"""Mutable runtime state for the jail service.
|
||||
|
||||
Stores capability detection results and synchronization primitives used by
|
||||
jail operations. This state is initialized once and shared across all
|
||||
service calls within a single worker process.
|
||||
"""
|
||||
|
||||
backend_cmd_supported: bool | None = None
|
||||
backend_cmd_lock: asyncio.Lock | None = None
|
||||
|
||||
def get_backend_cmd_lock(self) -> asyncio.Lock:
|
||||
"""Return the shared backend capability probe lock, initialising lazily.
|
||||
|
||||
The caller must already be running inside the event loop when the lock
|
||||
is created, which is true for all service entry points.
|
||||
"""
|
||||
if self.backend_cmd_lock is None:
|
||||
self.backend_cmd_lock = asyncio.Lock()
|
||||
return self.backend_cmd_lock
|
||||
|
||||
async def reset_backend_capability_cache(self) -> None:
|
||||
"""Reset the cached backend/idle capability detection state.
|
||||
|
||||
This is intended for test isolation and scenarios where the cached
|
||||
probe result must be invalidated before the next detection attempt.
|
||||
"""
|
||||
async with self.get_backend_cmd_lock():
|
||||
self.backend_cmd_supported = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class RuntimeState:
|
||||
"""Mutable runtime state for the current application instance."""
|
||||
@@ -82,6 +116,7 @@ class RuntimeState:
|
||||
pending_recovery: PendingRecovery | None = None
|
||||
last_activation: ActivationRecord | None = None
|
||||
runtime_settings: Settings | None = None
|
||||
jail_service_state: JailServiceState = field(default_factory=JailServiceState)
|
||||
|
||||
|
||||
class ApplicationState(State):
|
||||
|
||||
Reference in New Issue
Block a user