Worker coordination with file locks

This commit is contained in:
Jonas Linter
2025-10-15 10:07:42 +02:00
parent 0d04a546cf
commit 361611ae1b
7 changed files with 944 additions and 14 deletions

View File

@@ -45,6 +45,7 @@ from .rate_limit import (
webhook_limiter,
)
from .reservation_service import ReservationService
from .worker_coordination import is_primary_worker
# Configure logging - will be reconfigured during lifespan with actual config
_LOGGER = get_logger(__name__)
@@ -182,24 +183,16 @@ async def push_listener(customer: DBCustomer, reservation: DBReservation, hotel)
async def lifespan(app: FastAPI):
# Setup DB
# Determine if this is the primary worker
# Determine if this is the primary worker using file-based locking
# Only primary runs schedulers/background tasks
# In multi-worker setups, only one worker should run singleton services
worker_id = os.environ.get("APP_WORKER_ID", "0")
is_primary_worker = worker_id == "0"
# For uvicorn with --workers, detect if we're the main process
if not is_primary_worker:
# Check if running under uvicorn's supervisor
is_primary_worker = (
multiprocessing.current_process().name == "MainProcess"
)
is_primary, worker_lock = is_primary_worker()
_LOGGER.info(
"Worker startup: process=%s, pid=%d, primary=%s",
multiprocessing.current_process().name,
os.getpid(),
is_primary_worker,
is_primary,
)
try:
@@ -217,9 +210,9 @@ async def lifespan(app: FastAPI):
# Setup logging from config with email monitoring
# Only primary worker should have the report scheduler running
email_handler, report_scheduler = setup_logging(
config, email_service, loop, enable_scheduler=is_primary_worker
config, email_service, loop, enable_scheduler=is_primary
)
_LOGGER.info("Application startup initiated (primary_worker=%s)", is_primary_worker)
_LOGGER.info("Application startup initiated (primary_worker=%s)", is_primary)
DATABASE_URL = get_database_url(config)
engine = create_async_engine(DATABASE_URL, echo=False)
@@ -260,7 +253,7 @@ async def lifespan(app: FastAPI):
_LOGGER.info("Database tables checked/created at startup.")
# Hash any existing customers (only in primary worker to avoid race conditions)
if is_primary_worker:
if is_primary:
async with AsyncSessionLocal() as session:
customer_service = CustomerService(session)
hashed_count = await customer_service.hash_existing_customers()
@@ -311,6 +304,10 @@ async def lifespan(app: FastAPI):
await engine.dispose()
_LOGGER.info("Application shutdown complete")
# Release worker lock if this was the primary worker
if worker_lock:
worker_lock.release()
async def get_async_session(request: Request):
async_sessionmaker = request.app.state.async_sessionmaker