Files
wizarr/gunicorn.conf.py

164 lines
5.6 KiB
Python

# Gunicorn configuration with clean logging
import logging
import os
import sys
# Add immediate debug to check if config is loaded
print("DEBUG: gunicorn.conf.py loaded!")
# Make log level configurable for debugging production issues
# Set GUNICORN_LOG_LEVEL=info or debug for verbose output
loglevel = os.getenv("GUNICORN_LOG_LEVEL", "warning").lower()
accesslog = None # Disable access logs for clean output
errorlog = "-" # Only errors to stderr
# Make workers configurable (default 4, but allow override for resource-constrained systems)
workers = int(os.getenv("GUNICORN_WORKERS", "4"))
worker_class = "sync"
# Worker timeout - kill workers that don't respond within this time
# Increase from default 30s to 120s to account for slow library scans
timeout = int(os.getenv("GUNICORN_TIMEOUT", "120"))
# Make host and port configurable
host = os.getenv("HOST", "0.0.0.0")
port = os.getenv("PORT", "5690")
bind = f"{host}:{port}"
print(
f"DEBUG: Gunicorn config - workers={workers}, loglevel={loglevel}, timeout={timeout}s, host={host}, port={port}"
)
def on_starting(server): # noqa: ARG001
"""Called just before the master process is initialized."""
print("DEBUG: on_starting() hook called!")
def when_ready(server): # noqa: ARG001
"""Called after the server is started."""
try:
print("DEBUG: when_ready() hook called!")
# Set environment to indicate Gunicorn context
os.environ["SERVER_SOFTWARE"] = "gunicorn"
# Only run migrations if we haven't already done so
if os.getenv("WIZARR_MIGRATIONS_DONE"):
print("DEBUG: Migrations already done, skipping")
return
print("DEBUG: Setting WIZARR_MIGRATIONS_DONE flag")
os.environ["WIZARR_MIGRATIONS_DONE"] = "1"
# Import here to avoid circular imports
from app.logging_helpers import AppLogger
from app.scripts.migrate_libraries import (
run_library_migration,
update_server_verified,
)
from app.scripts.migrate_media_server import migrate_single_to_multi
logger = AppLogger("wizarr.master")
# Get the already-created app from run.py
import run
app = run.app
# Run master-only migrations and setup
with app.app_context():
logger.database_migration("server verification", "verifying media servers")
update_server_verified(app)
logger.database_migration("library migration", "updating library structure")
run_library_migration(app)
logger.database_migration(
"media server migration", "single to multi-server"
)
migrate_single_to_multi(app)
# Check Flask-APScheduler status - it handles Gunicorn coordination automatically
from app.extensions import scheduler
print("DEBUG: Checking Flask-APScheduler status...")
if scheduler and hasattr(scheduler, "scheduler") and scheduler.scheduler:
if scheduler.running:
print("DEBUG: Flask-APScheduler is running")
dev_mode = os.getenv(
"WIZARR_ENABLE_SCHEDULER", "false"
).lower() in (
"true",
"1",
"yes",
)
logger.scheduler_status(enabled=True, dev_mode=dev_mode)
else:
print("DEBUG: Flask-APScheduler exists but not running")
logger.warning("Scheduler initialized but not running")
else:
print("DEBUG: Flask-APScheduler not available")
logger.info("Scheduler disabled or not initialized")
# Complete the startup sequence
logger.complete()
except Exception as e:
print(f"DEBUG: Error in when_ready(): {e}")
import traceback
traceback.print_exc()
def post_worker_init(worker):
"""Worker process initialization - runs once per worker after spawn."""
try:
# Set worker environment
os.environ["GUNICORN_WORKER_PID"] = str(worker.pid)
print(f"DEBUG: Worker {worker.pid} initialized successfully")
# Suppress Flask app creation logs in workers
logging.getLogger("werkzeug").setLevel(logging.ERROR)
logging.getLogger("app").setLevel(logging.ERROR)
except Exception as e:
print(f"ERROR: Worker {worker.pid} failed to initialize: {e}", file=sys.stderr)
import traceback
traceback.print_exc()
raise
def worker_exit(server, worker): # noqa: ARG001
"""Called when a worker exits (crash or graceful shutdown)."""
print(f"WARNING: Worker {worker.pid} exited (age: {worker.age}s)")
def worker_abort(worker):
"""Called when a worker receives SIGABRT (usually due to timeout)."""
print(
f"ERROR: Worker {worker.pid} aborted (timeout or critical error)",
file=sys.stderr,
)
import traceback
traceback.print_stack()
def on_exit(server): # noqa: ARG001
"""Called just before the master process exits."""
print("INFO: Gunicorn master process shutting down")
# Gracefully shutdown the APScheduler to prevent "cannot schedule new futures" errors
try:
from app.extensions import scheduler
if scheduler and hasattr(scheduler, "running") and scheduler.running:
print("INFO: Shutting down APScheduler...")
# wait=False to avoid blocking on pending jobs during shutdown
scheduler.shutdown(wait=False)
print("INFO: APScheduler shut down successfully")
except Exception as e:
print(f"WARNING: Error shutting down APScheduler: {e}")