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:
@@ -727,7 +727,7 @@ BanGUI maintains its **own SQLite database** (separate from the fail2ban databas
|
||||
- The backend `dependencies.py` provides an `authenticated` dependency that validates the session cookie on every protected endpoint.
|
||||
- **Session validation cache** (`InMemorySessionCache` in `app.utils.session_cache`) — validated session tokens are cached in memory for 10 seconds (configurable via `session_cache_ttl_seconds`) to avoid a SQLite round-trip on every request from the same browser. The cache is invalidated immediately on logout. **⚠️ This cache is process-local and not safe for multi-worker or distributed deployments.** In single-worker mode (enforced by TASK-002), this is safe and improves performance. For multi-worker deployments, replace `InMemorySessionCache` with a shared backend (Redis, database, shared memory) implementing the `SessionCache` protocol. See `app/utils/session_cache.py` module docstring for implementation details.
|
||||
- **GeoCache** — `GeoCache` instance is created at startup with a configurable `allow_http_fallback` flag and stored on `app.state.geo_cache`. It implements a primary + fallback resolution strategy: (1) try local MaxMind GeoLite2-Country MMDB database (primary, encrypted, no network traffic), (2) if unavailable/no result and allowed, fall back to ip-api.com HTTP API (unencrypted, disabled by default for security). Encapsulates in-memory lookup cache, negative cache for unresolvable IPs (5-minute TTL), dirty set for persistence, and thread-safe async locking. Cache is loaded from the `geo_cache` SQLite table on startup. New resolutions are accumulated in memory and periodically flushed to the database by the `geo_cache_flush` background task. Stale entries are re-resolved by the `geo_re_resolve` task. Injected into routes and tasks via FastAPI's dependency system. See Backend-Development.md § IP Geolocation Resolution for setup and security details.
|
||||
- **Runtime state** (`RuntimeState` in `app.utils.runtime_state`) — stores mutable application state: `server_status` (fail2ban online/offline), `last_activation` (jail activation tracking), `pending_recovery` (crash detection), and `runtime_settings` (effective configuration). **⚠️ RuntimeState is process-local and only safe when BanGUI runs as a single asyncio worker.** Mutations must not span `await` points (cooperative scheduling within a single event loop is safe). In multi-worker deployments, each process has its own copy — logouts from worker A don't affect worker B's cache, health status updates are per-worker, and activation tracking is unreliable. BanGUI enforces single-worker mode (TASK-002) to prevent this issue. For future multi-worker support, replace RuntimeState with a shared coordination backend (Redis, shared memory, database). See `app/utils/runtime_state.py` module docstring for details.
|
||||
- **Runtime state** (`RuntimeState` in `app.utils.runtime_state`) — stores mutable application state: `server_status` (fail2ban online/offline), `last_activation` (jail activation tracking), `pending_recovery` (crash detection), `runtime_settings` (effective configuration), and service-specific state holders like `jail_service_state` (`JailServiceState` for jail capability detection cache). RuntimeState fields are managed through dedicated functions (e.g., `record_activation()`, `clear_pending_recovery()`) and via dependency injection to services. Service-specific state (like `JailServiceState`) is nested within `RuntimeState` to keep all mutable state in one controlled location. **⚠️ RuntimeState is process-local and only safe when BanGUI runs as a single asyncio worker.** Mutations must not span `await` points (cooperative scheduling within a single event loop is safe). In multi-worker deployments, each process has its own copy — logouts from worker A don't affect worker B's cache, health status updates are per-worker, and activation tracking is unreliable. BanGUI enforces single-worker mode (TASK-002) to prevent this issue. For future multi-worker support, replace RuntimeState with a shared coordination backend (Redis, shared memory, database). See `app/utils/runtime_state.py` module docstring for details.
|
||||
- **Setup-completion flag** — once `is_setup_complete()` returns `True`, the result is stored in `app.state._setup_complete_cached`. The `SetupRedirectMiddleware` skips the DB query on all subsequent requests, removing 1 SQL query per request for the common post-setup case. The completion flag is only written after the runtime database is successfully initialized and all initial setup settings are persisted, preventing a failed setup from permanently bypassing the setup wizard.
|
||||
|
||||
### 6.1 CSRF Protection
|
||||
|
||||
@@ -174,6 +174,44 @@ async def get_history(
|
||||
|
||||
This pattern prevents circular imports, makes services testable, and allows easy mocking in tests.
|
||||
|
||||
### Mutable Runtime State
|
||||
|
||||
All mutable runtime state (state that changes during the application's lifetime) **must** be stored in `RuntimeState` defined in `app/utils/runtime_state.py`. This centralizes state management, prevents accidental global mutable variables, and makes state management testable and synchronization-safe.
|
||||
|
||||
**Allowed locations for mutable state:**
|
||||
|
||||
1. **RuntimeState fields** — Core application state (e.g., `server_status`, `last_activation`, `pending_recovery`, `runtime_settings`). Managed through dedicated functions (e.g., `record_activation()`, `clear_pending_recovery()`).
|
||||
2. **Nested service state** — Service-specific mutable state (e.g., `JailServiceState` for jail capability detection cache) is nested within `RuntimeState` as a field. Services receive their state via dependency injection.
|
||||
3. **Controlled via dependencies** — State is injected into services and routers using FastAPI `Depends()`. This ensures single-source-of-truth and testability.
|
||||
|
||||
**Example — jail_service state management:**
|
||||
|
||||
```python
|
||||
# Define service-specific state (in app/utils/runtime_state.py)
|
||||
@dataclass
|
||||
class JailServiceState:
|
||||
backend_cmd_supported: bool | None = None
|
||||
backend_cmd_lock: asyncio.Lock | None = None
|
||||
|
||||
# Nested in RuntimeState
|
||||
@dataclass
|
||||
class RuntimeState:
|
||||
jail_service_state: JailServiceState = field(default_factory=JailServiceState)
|
||||
...
|
||||
|
||||
# Injected into services via dependency
|
||||
async def list_jails(socket_path: str, state: JailServiceState) -> JailListResponse:
|
||||
backend_cmd_is_supported = await _check_backend_cmd_supported(client, name, state)
|
||||
...
|
||||
|
||||
# Routers inject state through FastAPI dependencies
|
||||
@router.get("/api/jails")
|
||||
async def get_jails(state: JailServiceStateDep) -> JailListResponse:
|
||||
return await jail_service.list_jails(socket_path, state)
|
||||
```
|
||||
|
||||
**Why:** Centralizing mutable state prevents race conditions, makes concurrency boundaries explicit, simplifies testing (each test gets a fresh state object), and prepares for multi-worker deployments (shared state would need to be extracted to Redis, database, or shared memory).
|
||||
|
||||
---
|
||||
|
||||
## 4. FastAPI Conventions
|
||||
|
||||
@@ -1,23 +1,3 @@
|
||||
## 3) Blocklist import flow mixes too many responsibilities
|
||||
- Where found:
|
||||
- [backend/app/services/blocklist_service.py](backend/app/services/blocklist_service.py)
|
||||
- Why this is needed:
|
||||
- One function handling download, validation, persistence, and banning is hard to test and evolve.
|
||||
- Goal:
|
||||
- Split into focused components with clear boundaries.
|
||||
- What to do:
|
||||
- Extract downloader, parser/validator, ban executor, and persistence coordinator.
|
||||
- Keep orchestration in a thin workflow service.
|
||||
- Possible traps and issues:
|
||||
- Behavior changes in retry/error aggregation during split.
|
||||
- Docs changes needed:
|
||||
- Add blocklist import sequence diagram and component ownership.
|
||||
- Doc references:
|
||||
- [Docs/Architekture.md](Docs/Architekture.md)
|
||||
- [Docs/Features.md](Docs/Features.md)
|
||||
|
||||
---
|
||||
|
||||
## 4) Module-level mutable runtime flags in service layer
|
||||
- Where found:
|
||||
- [backend/app/services/jail_service.py](backend/app/services/jail_service.py)
|
||||
|
||||
Reference in New Issue
Block a user