Files
mcp-server/gunicorn.conf.py
T
Claude 6bcf3b9ac9 feat: run FastAPI under gunicorn with uvicorn ASGI workers
Production entrypoint switch:
  - pyproject.toml: add gunicorn>=23.0 dep
  - gunicorn.conf.py: env-var-driven config (bind, workers, threads,
    timeout, graceful_timeout, keepalive, log level, access-log format)
  - Dockerfile: CMD gunicorn main:app (auto-loads gunicorn.conf.py from
    WORKDIR /app)
  - docker-compose.yml: forward GUNICORN_WORKERS / GUNICORN_TIMEOUT /
    GUNICORN_BIND
  - .env.example: document the new tunables

Why gunicorn over standalone uvicorn for production:
  - Process supervision: master restarts crashed workers, restarts on
    memory leaks
  - Graceful shutdown: SIGTERM drains workers in-flight
  - Multi-worker: concurrent requests actually run in parallel
  - Standard ops: k8s readiness probes, log aggregators, etc. all know
    gunicorn

Why uvicorn workers (not sync workers): gunicorn can't natively serve
ASGI; uvicorn.workers.UvicornWorker is the canonical way to run an
ASGI app under gunicorn.

Defaults:
  - 2 workers (small MCP service; raise for high concurrency)
  - 1 thread per worker (no blocking I/O)
  - 120s timeout (yarn logs can be slow; uvicorn's 30s default is too
    tight)

Verified: gunicorn boots, lifespan runs (14 tools logged), MCP
initialize + tools/list + tools/call all work, /openapi.json = 200,
multiple gunicorn worker processes visible in ps.

uv sync picked up gunicorn 26.0.0. Tests still 116/116.
2026-06-25 10:44:25 +08:00

51 lines
1.8 KiB
Python

# coding=utf-8
"""
@Time :2026/6/24
@Author :tao.chen
Gunicorn config for the Spark Executor MCP service.
ASGI workers (uvicorn.workers.UvicornWorker) so we get gunicorn's process
supervision, graceful shutdown, and graceful reload semantics on top of
uvicorn's ASGI implementation.
All knobs are env-var driven so the same image runs in dev (workers=1)
and prod (workers=2-4) without rebuilding.
"""
import os
# --- Network ---
bind = os.environ.get("GUNICORN_BIND", "0.0.0.0:8000")
# --- Process model ---
# 2 is a sensible default for a small containerized MCP server: lets a slow
# yarn logs request run in parallel with a status check. Scale up by
# setting GUNICORN_WORKERS at deploy time.
workers = int(os.environ.get("GUNICORN_WORKERS", "2"))
worker_class = "uvicorn.workers.UvicornWorker"
# 1 thread per worker is enough for ASGI handlers (no blocking I/O).
threads = int(os.environ.get("GUNICORN_THREADS", "1"))
# --- Lifecycle ---
# Generous timeout because the slowest tool call here is yarn logs (which
# can take 30+ seconds on a busy cluster). uvicorn standalone defaults to
# 30s; gunicorn's default is 30s too — both too tight for log fetch.
timeout = int(os.environ.get("GUNICORN_TIMEOUT", "120"))
graceful_timeout = int(os.environ.get("GUNICORN_GRACEFUL_TIMEOUT", "30"))
keepalive = int(os.environ.get("GUNICORN_KEEPALIVE", "5"))
# --- Logging ---
# Stream access + error to stdout/stderr so docker logs / k8s logs capture
# them. gunicorn's "[INFO] Booting worker" lines interleave with loguru's
# output — both go to stderr.
accesslog = "-"
errorlog = "-"
loglevel = os.environ.get("GUNICORN_LOGLEVEL", "info")
access_log_format = (
'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(L)s'
)
# --- Process naming (visible in `ps aux`) ---
proc_name = "spark-executor-mcp"