feat: enforce single-worker at startup

Fail with RuntimeError when WEB_CONCURRENCY or BANGUI_WORKERS > 1.

In-memory session cache, rate-limit windows, and runtime state are
process-local. Multi-worker silently causes stale limits, ghost sessions,
inconsistent status.

Skipped when TESTING=1.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-05-03 20:33:23 +02:00
parent e1a6491ac2
commit ae9313568e
5 changed files with 919 additions and 10 deletions

View File

@@ -12,7 +12,7 @@ from httpx import ASGITransport, AsyncClient
from app.config import Settings
from app.db import init_db
from app.exceptions import ConfigValidationError, ConfigWriteError, JailNotFoundError
from app.main import CORSMiddleware, _lifespan, create_app
from app.main import CORSMiddleware, _enforce_single_worker, _lifespan, create_app
from app.services import setup_service
@@ -536,3 +536,89 @@ async def test_concurrent_requests_use_request_scoped_db_connections(tmp_path: P
assert len({id(connection) for connection in connections}) == 5
assert all(response.status_code == 200 for response in responses)
assert all(connection.close.await_count == 1 for connection in connections)
# ---------------------------------------------------------------------------
# Single-worker enforcement
# ---------------------------------------------------------------------------
def test_enforce_single_worker_allows_no_env_vars(monkeypatch: pytest.MonkeyPatch) -> None:
"""No error raised when WEB_CONCURRENCY and BANGUI_WORKERS are not set."""
monkeypatch.delenv("WEB_CONCURRENCY", raising=False)
monkeypatch.delenv("BANGUI_WORKERS", raising=False)
# Should not raise
_enforce_single_worker()
def test_enforce_single_worker_allows_workers_1(monkeypatch: pytest.MonkeyPatch) -> None:
"""WEB_CONCURRENCY=1 and BANGUI_WORKERS=1 are both allowed."""
monkeypatch.setenv("WEB_CONCURRENCY", "1")
monkeypatch.setenv("BANGUI_WORKERS", "1")
_enforce_single_worker() # Should not raise
def test_enforce_single_worker_rejects_web_concurrency_2(monkeypatch: pytest.MonkeyPatch) -> None:
"""WEB_CONCURRENCY=2 raises RuntimeError."""
monkeypatch.setenv("WEB_CONCURRENCY", "2")
monkeypatch.delenv("BANGUI_WORKERS", raising=False)
with pytest.raises(RuntimeError, match="WEB_CONCURRENCY"):
_enforce_single_worker()
def test_enforce_single_worker_rejects_web_concurrency_4(monkeypatch: pytest.MonkeyPatch) -> None:
"""WEB_CONCURRENCY=4 raises RuntimeError."""
monkeypatch.setenv("WEB_CONCURRENCY", "4")
monkeypatch.delenv("BANGUI_WORKERS", raising=False)
with pytest.raises(RuntimeError, match="WEB_CONCURRENCY"):
_enforce_single_worker()
def test_enforce_single_worker_rejects_bangui_workers_2(monkeypatch: pytest.MonkeyPatch) -> None:
"""BANGUI_WORKERS=2 raises RuntimeError."""
monkeypatch.setenv("WEB_CONCURRENCY", "1") # WEB_CONCURRENCY=1 should pass
monkeypatch.setenv("BANGUI_WORKERS", "2") # but BANGUI_WORKERS=2 should fail
with pytest.raises(RuntimeError, match="BANGUI_WORKERS"):
_enforce_single_worker()
def test_enforce_single_worker_rejects_invalid_web_concurrency(monkeypatch: pytest.MonkeyPatch) -> None:
"""Non-integer WEB_CONCURRENCY raises RuntimeError."""
monkeypatch.setenv("WEB_CONCURRENCY", "not-a-number")
with pytest.raises(RuntimeError, match="WEB_CONCURRENCY must be an integer"):
_enforce_single_worker()
def test_enforce_single_worker_error_message_mentions_docs(monkeypatch: pytest.MonkeyPatch) -> None:
"""Error message references Docs/Deployment.md."""
monkeypatch.setenv("WEB_CONCURRENCY", "2")
with pytest.raises(RuntimeError) as exc_info:
_enforce_single_worker()
assert "Deployment.md" in str(exc_info.value)
def test_create_app_raises_when_web_concurrency_gt_1(monkeypatch: pytest.MonkeyPatch) -> None:
"""create_app() raises RuntimeError when WEB_CONCURRENCY > 1 (no TESTING set)."""
monkeypatch.setenv("WEB_CONCURRENCY", "2")
with pytest.raises(RuntimeError, match="WEB_CONCURRENCY"):
create_app(settings=None) # settings=None triggers get_settings() which loads env vars
def test_create_app_skips_enforcement_when_testing_set(monkeypatch: pytest.MonkeyPatch) -> None:
"""create_app() does NOT raise when TESTING env var is set, even with workers > 1."""
monkeypatch.setenv("WEB_CONCURRENCY", "4")
monkeypatch.setenv("BANGUI_WORKERS", "4")
monkeypatch.setenv("TESTING", "1")
# Pass explicit settings to bypass get_settings() env loading.
settings = Settings(
database_path="/tmp/test.db",
fail2ban_socket="/tmp/fake_fail2ban.sock",
fail2ban_config_dir="/tmp/fail2ban",
session_secret="test-secret-key-do-not-use-in-production",
session_duration_minutes=60,
timezone="UTC",
log_level="debug",
)
# Should not raise
app = create_app(settings=settings)
assert app is not None