Add multi-worker detection for APScheduler safety
- Add _check_single_worker_mode() to startup.py that detects and rejects multi-worker configurations, raising a clear RuntimeError with instructions - Set BANGUI_WORKERS=1 as default in Dockerfile.backend - Document single-worker requirement in compose.prod.yml - Add 'Deployment Constraints' section to Architekture.md explaining why single-worker mode is required and detailing future multi-worker support - Add '9.1 Background Tasks and Scheduler Architecture' section to Backend-Development.md documenting task structure and single-worker requirement - Add comprehensive test suite (test_startup.py) covering all scenarios: allows single worker, rejects multi-worker, validates config format, and verifies informative error messages This fix addresses TASK-002 which identified that in-process APScheduler is unsafe in multi-worker deployments due to each worker creating independent scheduler instances, causing duplicate background job execution. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,7 @@ in ``app.main`` delegates resource creation and task registration here.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -32,6 +33,39 @@ if TYPE_CHECKING:
|
||||
log: structlog.stdlib.BoundLogger = structlog.get_logger()
|
||||
|
||||
|
||||
def _check_single_worker_mode() -> None:
|
||||
"""Verify that the application is running with a single worker.
|
||||
|
||||
APScheduler's AsyncIOScheduler is bound to a single asyncio event loop
|
||||
and cannot be safely shared across multiple worker processes. If each
|
||||
worker starts its own scheduler instance, all background jobs execute N
|
||||
times (where N is the number of workers), resulting in duplicate blocklist
|
||||
imports, duplicate ban operations, duplicate history writes, and SQLite
|
||||
lock contention.
|
||||
|
||||
This function detects multi-worker configurations and raises a clear
|
||||
RuntimeError with instructions.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the app would run with multiple workers.
|
||||
"""
|
||||
# Check for explicit worker count env var (convention used in deployment)
|
||||
workers_env = os.environ.get("BANGUI_WORKERS")
|
||||
if workers_env is not None:
|
||||
try:
|
||||
worker_count = int(workers_env)
|
||||
if worker_count > 1:
|
||||
raise RuntimeError(
|
||||
"BanGUI background scheduler cannot run with multiple workers.\n"
|
||||
f"BANGUI_WORKERS is set to {worker_count}. Set it to 1 or remove it.\n"
|
||||
"See Architekture.md § Deployment Constraints for details."
|
||||
)
|
||||
except ValueError as e:
|
||||
raise RuntimeError(
|
||||
f"BANGUI_WORKERS environment variable must be an integer, got: {workers_env}"
|
||||
) from e
|
||||
|
||||
|
||||
async def _ensure_database_schema(database_path: str) -> None:
|
||||
"""Create the configured runtime database if it does not already exist."""
|
||||
db = await open_db(database_path)
|
||||
@@ -70,6 +104,8 @@ async def startup_shared_resources(
|
||||
Returns:
|
||||
A tuple of ``(http_session, scheduler)``.
|
||||
"""
|
||||
_check_single_worker_mode()
|
||||
|
||||
db_path: Path = Path(settings.database_path)
|
||||
await run_blocking(db_path.parent.mkdir, parents=True, exist_ok=True)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user