feat(companion): Add states and CompanionConfig with config hash

This commit is contained in:
Tanmoy Sarkar 2026-06-09 15:34:50 +05:30
parent 3f479157d7
commit 2241dd4031
3 changed files with 97 additions and 2 deletions

View File

@ -664,8 +664,8 @@ No per-companion logic in Arbiter.
## 20. Implementation Tasks
- [ ] Add companion config settings in `gunicorn/config.py`.
- [ ] Add config validation for `companion_workers`.
- [x] Add companion config settings in `gunicorn/config.py`.
- [x] Add config validation for `companion_workers`.
- [ ] Add `CompanionConfig` and config hash generation.
- [ ] Add public process states.
- [ ] Add `CompanionProcess` runtime state.

View File

@ -0,0 +1,10 @@
#
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.
"""Companion process manager.
Gunicorn manages one extra child, the Companion Manager, which manages all
configured non-HTTP companion processes (RQ workers, scheduler, socket.io,
custom daemons). See ``docs/design/companion-process-manager.md``.
"""

View File

@ -0,0 +1,85 @@
#
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.
import hashlib
import json
# Public states, mimicking ``supervisorctl status``. The manager never
# exposes EXITED/FATAL/UNKNOWN; an exited companion is either STOPPED (manual)
# or BACKOFF (waiting to restart).
STOPPED = "STOPPED"
STARTING = "STARTING"
RUNNING = "RUNNING"
BACKOFF = "BACKOFF"
STOPPING = "STOPPING"
PUBLIC_STATES = (STOPPED, STARTING, RUNNING, BACKOFF, STOPPING)
class CompanionConfig:
"""Validated, normalized config for a single companion.
Built from one entry of ``companion_workers`` with global defaults already
applied. ``config_hash`` is a stable digest of every field; the manager
restarts a companion whenever its hash changes on reread.
"""
def __init__(
self,
name,
target,
cwd=None,
env=None,
stop_signal="SIGTERM",
stop_timeout=60,
reload_timeout=60,
stdout=None,
stderr=None,
startsecs=1,
):
self.name = name
self.target = target
self.cwd = cwd
self.env = dict(env or {})
self.stop_signal = stop_signal
self.stop_timeout = stop_timeout
self.reload_timeout = reload_timeout
self.stdout = stdout
self.stderr = stderr
self.startsecs = startsecs
def to_dict(self):
return {
"name": self.name,
"target": self.target,
"cwd": self.cwd,
"env": self.env,
"stop_signal": self.stop_signal,
"stop_timeout": self.stop_timeout,
"reload_timeout": self.reload_timeout,
"stdout": self.stdout,
"stderr": self.stderr,
"startsecs": self.startsecs,
}
@property
def config_hash(self):
# Sort keys so dict ordering never changes the digest. A callable
# target has no stable repr across runs, so use its qualified name.
data = self.to_dict()
data["target"] = self._target_key(self.target)
blob = json.dumps(data, sort_keys=True, default=str)
return hashlib.sha256(blob.encode("utf-8")).hexdigest()
@staticmethod
def _target_key(target):
if callable(target):
mod = getattr(target, "__module__", "")
qual = getattr(target, "__qualname__", repr(target))
return "%s:%s" % (mod, qual)
return str(target)
def __repr__(self):
return "<CompanionConfig %s>" % self.name