mirror of
https://github.com/frappe/gunicorn.git
synced 2026-07-05 04:01:29 +08:00
Add chunked transfer encoding support to the ASGI worker for HTTP/1.1 streaming responses that don't have a Content-Length header. This fixes SSE (Server-Sent Events) connections not closing properly. Without chunked encoding or Content-Length, HTTP/1.1 clients wait for the connection to close to determine end-of-response, causing streaming endpoints to hang. Also updates the celery_alternative example to use FastAPI with the native ASGI worker and uvloop, demonstrating async task execution with proper SSE streaming.
151 lines
5.1 KiB
Python
151 lines
5.1 KiB
Python
"""
|
|
Gunicorn Configuration - Celery Replacement Example
|
|
|
|
This configuration sets up:
|
|
1. ASGI workers to handle web requests with async I/O (using uvloop)
|
|
2. Dirty workers to handle background tasks (replacing Celery workers)
|
|
|
|
Why ASGI + Dirty Arbiters?
|
|
- ASGI: Non-blocking HTTP handling - one worker handles many concurrent requests
|
|
- Dirty: Stateful background workers - keep models/connections loaded in memory
|
|
|
|
Comparison with Celery deployment:
|
|
- Celery: gunicorn app:app + celery -A tasks worker + redis-server
|
|
- Dirty: gunicorn -c gunicorn_conf.py app:app (single command, no broker!)
|
|
"""
|
|
|
|
import multiprocessing
|
|
import os
|
|
|
|
# =============================================================================
|
|
# Basic Settings
|
|
# =============================================================================
|
|
|
|
# Bind to all interfaces on port 8000
|
|
bind = os.environ.get("GUNICORN_BIND", "0.0.0.0:8000")
|
|
|
|
# HTTP workers - handle incoming web requests
|
|
# With ASGI, fewer workers needed since each handles many concurrent requests
|
|
workers = int(os.environ.get("GUNICORN_WORKERS", min(4, multiprocessing.cpu_count() + 1)))
|
|
|
|
# Use gunicorn's native ASGI worker for async support
|
|
# This enables: await client.execute_async() without blocking
|
|
worker_class = "asgi"
|
|
|
|
# Use uvloop for better async performance
|
|
asgi_loop = "uvloop"
|
|
|
|
# Maximum concurrent connections per worker
|
|
worker_connections = 1000
|
|
|
|
# =============================================================================
|
|
# Dirty Arbiter Settings (Celery Worker Replacement)
|
|
# =============================================================================
|
|
|
|
# Task workers - these replace Celery workers
|
|
# Each dirty app can specify its own worker count via the `workers` class attribute
|
|
dirty_apps = [
|
|
# Email tasks - 2 workers (I/O bound)
|
|
"examples.celery_alternative.tasks:EmailWorker",
|
|
# Image processing - 2 workers (CPU/memory intensive)
|
|
"examples.celery_alternative.tasks:ImageWorker",
|
|
# Data processing - 4 workers (parallelizable)
|
|
"examples.celery_alternative.tasks:DataWorker",
|
|
# Scheduled tasks - 1 worker
|
|
"examples.celery_alternative.tasks:ScheduledWorker",
|
|
]
|
|
|
|
# Total dirty workers (distributed among apps based on their `workers` attribute)
|
|
# If not set, uses sum of all app worker counts
|
|
dirty_workers = int(os.environ.get("DIRTY_WORKERS", 9)) # 2+2+4+1 = 9
|
|
|
|
# Task timeout in seconds (like Celery's task_time_limit)
|
|
dirty_timeout = int(os.environ.get("DIRTY_TIMEOUT", 300))
|
|
|
|
# Threads per dirty worker (for concurrent task execution)
|
|
dirty_threads = int(os.environ.get("DIRTY_THREADS", 1))
|
|
|
|
# Graceful shutdown timeout
|
|
dirty_graceful_timeout = int(os.environ.get("DIRTY_GRACEFUL_TIMEOUT", 30))
|
|
|
|
# =============================================================================
|
|
# Timeouts & Limits
|
|
# =============================================================================
|
|
|
|
# Worker timeout (seconds)
|
|
timeout = 120
|
|
|
|
# Keep-alive connections
|
|
keepalive = 5
|
|
|
|
# Maximum requests per worker before recycling
|
|
max_requests = 1000
|
|
max_requests_jitter = 50
|
|
|
|
# =============================================================================
|
|
# Logging
|
|
# =============================================================================
|
|
|
|
# Log level
|
|
loglevel = os.environ.get("LOG_LEVEL", "info")
|
|
|
|
# Access log format
|
|
accesslog = "-"
|
|
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s'
|
|
|
|
# Error log
|
|
errorlog = "-"
|
|
|
|
# =============================================================================
|
|
# Lifecycle Hooks
|
|
# =============================================================================
|
|
|
|
def on_starting(server):
|
|
"""Called just before the master process is initialized."""
|
|
print("=" * 60)
|
|
print("Starting Gunicorn with Dirty Arbiters (Celery Replacement)")
|
|
print("Using ASGI workers with uvloop for non-blocking HTTP handling")
|
|
print("=" * 60)
|
|
|
|
|
|
def on_dirty_starting(arbiter):
|
|
"""Called when the dirty arbiter is starting."""
|
|
print(f"[Dirty] Starting dirty arbiter")
|
|
print(f"[Dirty] Registered apps: {list(arbiter.cfg.dirty_apps)}")
|
|
|
|
|
|
def dirty_post_fork(arbiter, worker):
|
|
"""Called after a dirty worker is forked."""
|
|
print(f"[Dirty] Worker {worker.pid} started")
|
|
|
|
|
|
def dirty_worker_init(worker):
|
|
"""Called when a dirty worker initializes its apps."""
|
|
print(f"[Dirty] Worker {worker.pid} initialized apps: {list(worker.apps.keys())}")
|
|
|
|
|
|
def dirty_worker_exit(arbiter, worker):
|
|
"""Called when a dirty worker exits."""
|
|
print(f"[Dirty] Worker {worker.pid} exiting")
|
|
|
|
|
|
def worker_int(worker):
|
|
"""Called when a worker receives SIGINT."""
|
|
print(f"[HTTP] Worker {worker.pid} interrupted")
|
|
|
|
|
|
def worker_exit(server, worker):
|
|
"""Called when a worker exits."""
|
|
print(f"[HTTP] Worker {worker.pid} exited")
|
|
|
|
|
|
# =============================================================================
|
|
# Development vs Production
|
|
# =============================================================================
|
|
|
|
# Reload on code changes (development only)
|
|
reload = os.environ.get("GUNICORN_RELOAD", "false").lower() == "true"
|
|
|
|
# Preload app for faster worker startup (production)
|
|
preload_app = os.environ.get("GUNICORN_PRELOAD", "false").lower() == "true"
|