# 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"