-
Notifications
You must be signed in to change notification settings - Fork 863
Expand file tree
/
Copy pathgunicorn.config.py
More file actions
209 lines (161 loc) · 8.49 KB
/
Copy pathgunicorn.config.py
File metadata and controls
209 lines (161 loc) · 8.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# -*- coding: utf-8 -*-
"""
Copyright 2025
SPDX-License-Identifier: Apache-2.0
Authors: Mihai Criveti
Description: GUNICORN CONFIGURATION
Reference: https://docs.gunicorn.org/en/stable/settings.html
Notes:
- Set bind = "0.0.0.0:4444" to set the IP (all) and port (4444)
- Set a timeout to avoid worker timeout in containers, as the workers
will have to wait a long time for queries
Reference: https://stackoverflow.com/questions/10855197/frequent-worker-timeout
"""
# Standard
import os
import platform
# macOS fork safety fix: Must be set before any workers fork
# On macOS, Objective-C is not fork-safe. When gunicorn forks workers after
# certain libraries have initialized Objective-C, it causes crashes.
if platform.system() == "Darwin":
os.environ["OBJC_DISABLE_INITIALIZE_FORK_SAFETY"] = "YES"
# First-Party
# Import Pydantic Settings singleton
from mcpgateway.config import settings
# import multiprocessing
# Bind to exactly what .env (or defaults) says
bind = f"{settings.host}:{settings.port}"
workers = 2 # A positive integer generally in the 2-4 x $(NUM_CORES)
timeout = 600 # Set a timeout of 600
loglevel = "info" # debug info warning error critical
max_requests = 100000 # The maximum number of requests a worker will process before restarting
max_requests_jitter = 100 # The maximum jitter to add to the max_requests setting.
# Optimization https://docs.gunicorn.org/en/stable/settings.html#preload-app
# Disable preload on macOS due to fork-safety issues with async libraries
# On macOS, fork is unsafe with many async frameworks (SQLAlchemy, uvicorn, etc.)
preload_app = platform.system() != "Darwin"
reuse_port = True # Set the SO_REUSEPORT flag on the listening socket
# Server model: https://docs.gunicorn.org/en/stable/design.html
# worker-class = "eventlet" # Requires eventlet >= 0.24.1, pip install gunicorn[eventlet]
# worker-class = "gevent" # Requires gevent >= 1.4, pip install gunicorn[gevent]
# worker_class = "tornado" # Requires tornado >= 0.2, pip install gunicorn[tornado]
# threads = 2 # A positive integer generally in the 2-4 x $(NUM_CORES) range.
# gevent
# pidfile = '/tmp/gunicorn-pidfile'
# errorlog = '/tmp/gunicorn-errorlog'
# accesslog = '/tmp/gunicorn-accesslog'
# access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
# SSL/TLS Configuration
# Note: certfile and keyfile are set via command-line arguments in run-gunicorn.sh
# If a passphrase is provided via SSL_KEY_PASSWORD environment variable,
# the key will be decrypted by the SSL key manager before Gunicorn starts.
# certfile = 'certs/cert.pem'
# keyfile = 'certs/key.pem'
#
# Inbound mTLS (client certificate verification) is wired the same way, via
# run-gunicorn.sh command-line arguments driven by two environment variables:
# CA_CERTS -> --ca-certs <path> CA bundle used to verify client certificates
# CERT_REQS -> --cert-reqs <0|1|2> 0=none (default), 1=optional, 2=required
# Both are forwarded verbatim through UvicornWorker's ssl_options into uvicorn's
# SSL context. Only the CA_CERTS bundle is trusted; the system CA store is never
# loaded. No on_starting() handling is required - unlike passphrase-protected
# keys, these values need no pre-processing.
# Global variable to store the prepared key file path
_prepared_key_file = None
# server hooks
def on_starting(server):
"""Called just before the master process is initialized.
This is where we handle passphrase-protected SSL keys by decrypting
them to a temporary file before Gunicorn workers start.
"""
global _prepared_key_file
# Check if SSL is enabled via environment variable (set by run-gunicorn.sh)
# and a passphrase is provided
ssl_enabled = os.environ.get("SSL", "false").lower() == "true"
ssl_key_password = os.environ.get("SSL_KEY_PASSWORD")
# If ssl_version is passed as a string from CLI, normalize to integer for Uvicorn
if ssl_enabled and hasattr(server, "cfg"):
try:
ssl_ver = server.cfg.ssl_version
if isinstance(ssl_ver, str):
import ssl as _ssl
if ssl_ver.isdigit():
server.cfg.set("ssl_version", int(ssl_ver))
elif ssl_ver.startswith("PROTOCOL_") and hasattr(_ssl, ssl_ver):
server.cfg.set("ssl_version", getattr(_ssl, ssl_ver))
else:
server.log.warning("Unrecognized SSL_VERSION value %r; leaving unchanged (boot may fail downstream)", ssl_ver)
except Exception as e:
server.log.warning("Failed to normalize Gunicorn ssl_version setting: %s", e)
if ssl_enabled and ssl_key_password:
try:
from mcpgateway.utils.ssl_key_manager import prepare_ssl_key
# Get the key file path from environment (set by run-gunicorn.sh)
key_file = os.environ.get("KEY_FILE", "certs/key.pem")
server.log.info(f"Preparing passphrase-protected SSL key: {key_file}")
# Decrypt the key and get the temporary file path
_prepared_key_file = prepare_ssl_key(key_file, ssl_key_password)
server.log.info(f"SSL key prepared successfully: {_prepared_key_file}")
# Update the keyfile setting to use the decrypted temporary file
# This is a bit of a hack, but Gunicorn doesn't provide a better way
# to modify the keyfile after it's been set via command line
if hasattr(server, "cfg"):
server.cfg.set("keyfile", _prepared_key_file)
except Exception as e:
server.log.error(f"Failed to prepare SSL key: {e}")
raise
def when_ready(server):
server.log.info("Server is ready. Spawning workers")
def post_fork(server, worker):
server.log.info("Worker spawned (pid: %s)", worker.pid)
# Dispose SQLAlchemy engine to ensure clean pool separation per worker
# Using dispose(close=False) to prevent interference with parent process connections
# per SQLAlchemy's official fork recipe (1.4.33+). This de-references the parent's
# connection pool without actively closing inherited sockets.
try:
from mcpgateway.db import engine
engine.dispose(close=False)
server.log.info("SQLAlchemy engine pool reset for worker %s", worker.pid)
except Exception as e:
server.log.warning("Failed to reset SQLAlchemy engine pool: %s", e)
# Reset Redis client state so each worker creates its own connection
# This is necessary because --preload causes the client to be initialized
# in the master process, but each forked worker needs its own event loop
try:
from mcpgateway.utils.redis_client import _reset_client
_reset_client()
except ImportError:
pass
# Recompute the session-affinity WORKER_ID per worker, but only when the feature
# is enabled. Captured at import time, so under --preload every worker would
# otherwise inherit the master's id ({hostname}:1) and subscribe to the same Redis
# channel, collapsing point-to-point forwarding into a per-container broadcast that
# fans every request out to all workers and degrades throughput by an order of
# magnitude. With the affinity flag off, no request path reads WORKER_ID, so gating
# the rebind (and the session_affinity import at fork) keeps "flag off" a clean
# no-op for the affinity machinery.
if settings.mcpgateway_session_affinity_enabled:
try:
import socket
from mcpgateway.services import session_affinity
session_affinity.WORKER_ID = f"{socket.gethostname()}:{worker.pid}"
except Exception as exc: # noqa: BLE001 - fail loud, never crash the worker
# Silent fallback would re-introduce the per-container broadcast amplification.
server.log.warning(
"post_fork(pid=%s): failed to rebind session_affinity.WORKER_ID (%s: %s) — "
"workers may share the master's WORKER_ID and session-affinity forwarding "
"may broadcast each request to every worker in the container.",
worker.pid,
type(exc).__name__,
exc,
)
def post_worker_init(worker):
worker.log.info("worker initialization completed")
def worker_int(worker):
worker.log.info("worker received INT or QUIT signal")
def worker_abort(worker):
worker.log.info("worker received SIGABRT signal")
def worker_exit(server, worker):
server.log.info("Worker exit (pid: %s)", worker.pid)
def child_exit(server, worker):
server.log.info("Worker child exit (pid: %s)", worker.pid)