Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions docs/content/reference/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,17 @@ A WSGI application path in pattern ``$(MODULE_NAME):$(VARIABLE_NAME)``.

**Command line:** `--control-socket PATH`

**Default:** `'gunicorn.ctl'`
**Default:** `'/run/gunicorn.ctl'`

Unix socket path for control interface.

The control socket allows runtime management of Gunicorn via the
``gunicornc`` command-line tool. Commands include viewing worker
status, adjusting worker count, and graceful reload/shutdown.

By default, creates ``gunicorn.ctl`` in the working directory.
Set an absolute path for a fixed location (e.g., ``/var/run/gunicorn.ctl``).
By default, creates ``/run/gunicorn.ctl`` (requires write access to
``/run``). For user-level deployments, specify a different path such
as ``/tmp/gunicorn.ctl`` or ``~/.gunicorn.ctl``.

Use ``--no-control-socket`` to disable.

Expand Down
9 changes: 7 additions & 2 deletions gunicorn/arbiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,8 @@ def start(self):
if self.cfg.dirty_workers > 0 and self.cfg.dirty_apps:
self.spawn_dirty_arbiter()

# Start control socket server
self._start_control_server()
# Note: control socket server is started after initial workers spawn
# to avoid fork deadlocks with asyncio

self.cfg.when_ready(self)

Expand Down Expand Up @@ -222,6 +222,10 @@ def run(self):
try:
self.manage_workers()

# Start control socket server after initial workers are spawned
# to avoid fork deadlocks with asyncio
self._start_control_server()

while True:
self.maybe_promote_master()

Expand Down Expand Up @@ -686,6 +690,7 @@ def spawn_worker(self):
self.app, self.timeout / 2.0,
self.cfg, self.log)
self.cfg.pre_fork(self, worker)

pid = os.fork()
if pid != 0:
worker.pid = pid
Expand Down
7 changes: 4 additions & 3 deletions gunicorn/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3123,16 +3123,17 @@ class ControlSocket(Setting):
cli = ["--control-socket"]
meta = "PATH"
validator = validate_string
default = "gunicorn.ctl"
default = "/run/gunicorn.ctl"
desc = """\
Unix socket path for control interface.

The control socket allows runtime management of Gunicorn via the
``gunicornc`` command-line tool. Commands include viewing worker
status, adjusting worker count, and graceful reload/shutdown.

By default, creates ``gunicorn.ctl`` in the working directory.
Set an absolute path for a fixed location (e.g., ``/var/run/gunicorn.ctl``).
By default, creates ``/run/gunicorn.ctl`` (requires write access to
``/run``). For user-level deployments, specify a different path such
as ``/tmp/gunicorn.ctl`` or ``~/.gunicorn.ctl``.

Use ``--no-control-socket`` to disable.

Expand Down
92 changes: 91 additions & 1 deletion gunicorn/ctl/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@

Runs in the arbiter process and accepts commands via Unix socket.
Uses asyncio in a background thread to handle client connections.

Fork Safety:
This server uses os.register_at_fork() to properly handle fork() calls.
Before fork: the asyncio thread is stopped to prevent lock issues.
After fork in parent: the server is restarted.
After fork in child: references are cleared (workers don't need the control server).
"""

import asyncio
Expand All @@ -22,12 +28,53 @@
)


# Module-level tracking of active control server instances for fork handling.
# This is necessary because os.register_at_fork() callbacks are process-level.
_active_servers = set()
_module_state = {"fork_handlers_registered": False}


def _register_fork_handlers():
"""Register fork handlers once at module level."""
if _module_state["fork_handlers_registered"]:
return
_module_state["fork_handlers_registered"] = True

os.register_at_fork(
before=_before_fork,
after_in_parent=_after_fork_parent,
after_in_child=_after_fork_child,
)


def _before_fork():
"""Called before fork() - stop all active control servers."""
for server in list(_active_servers):
server._stop_for_fork()


def _after_fork_parent():
"""Called in parent after fork() - restart all control servers."""
for server in list(_active_servers):
server._restart_after_fork()


def _after_fork_child():
"""Called in child after fork() - cleanup references."""
# In the child process (worker), we don't need the control server.
# Just clear the references without trying to stop anything.
_active_servers.clear()


class ControlSocketServer:
"""
Control socket server running in arbiter process.

The server runs an asyncio event loop in a background thread,
accepting connections and dispatching commands to handlers.

Fork safety is handled via os.register_at_fork() - the server
automatically stops before fork and restarts after in the parent.
"""

def __init__(self, arbiter, socket_path, socket_mode=0o600):
Expand All @@ -48,6 +95,10 @@ def __init__(self, arbiter, socket_path, socket_mode=0o600):
self._loop = None
self._thread = None
self._running = False
self._was_running_before_fork = False

# Ensure fork handlers are registered
_register_fork_handlers()

def start(self):
"""Start server in background thread with asyncio event loop."""
Expand All @@ -58,8 +109,14 @@ def start(self):
self._thread = threading.Thread(target=self._run_loop, daemon=True)
self._thread.start()

# Track this server for fork handling
_active_servers.add(self)

def stop(self):
"""Stop server and cleanup socket."""
# Remove from active servers tracking
_active_servers.discard(self)

if not self._running:
return

Expand All @@ -80,6 +137,39 @@ def stop(self):
except OSError:
pass

def _stop_for_fork(self):
"""Stop server before fork (called by fork handler)."""
if not self._running:
self._was_running_before_fork = False
return

self._was_running_before_fork = True
self._running = False

if self._loop and self._server:
try:
self._loop.call_soon_threadsafe(self._shutdown)
except RuntimeError:
# Loop may already be closed
pass

if self._thread:
self._thread.join(timeout=2.0)
self._thread = None

self._loop = None
self._server = None

def _restart_after_fork(self):
"""Restart server in parent after fork (called by fork handler)."""
if not self._was_running_before_fork:
return

self._was_running_before_fork = False
self._running = True
self._thread = threading.Thread(target=self._run_loop, daemon=True)
self._thread.start()

def _shutdown(self):
"""Shutdown server (called from event loop thread)."""
if self._server:
Expand All @@ -90,7 +180,7 @@ def _run_loop(self):
try:
asyncio.run(self._serve())
except Exception as e:
if self.arbiter.log:
if self._running and self.arbiter.log:
self.arbiter.log.error("Control server error: %s", e)

async def _serve(self):
Expand Down
6 changes: 4 additions & 2 deletions gunicorn/workers/gtornado.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,12 @@ def run(self):
not isinstance(app, tornado.web.Application):
app = WSGIContainer(app)

worker = self

class _HTTPServer(tornado.httpserver.HTTPServer):

def on_close(instance, server_conn):
self.handle_request()
def on_close(self, server_conn):
worker.handle_request()
super().on_close(server_conn)

if self.cfg.is_ssl:
Expand Down
Loading