forked from IBM/mcp-context-forge
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgunicorn.config.py
More file actions
186 lines (139 loc) · 7.15 KB
/
Copy pathgunicorn.config.py
File metadata and controls
186 lines (139 loc) · 7.15 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
# -*- 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'
# ca-certs = '/etc/ca_bundle.crt'
# 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_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)