diff --git a/docs/design/companion-process-manager.md b/docs/design/companion-process-manager.md index 7b11a176..e2bbfe70 100644 --- a/docs/design/companion-process-manager.md +++ b/docs/design/companion-process-manager.md @@ -675,7 +675,7 @@ No per-companion logic in Arbiter. - [x] Apply `cwd` and `env` before target. - [x] Redirect `stdout` and `stderr`. - [x] Reap exited companion processes. -- [ ] Implement `STARTING -> RUNNING` using `startsecs`. +- [x] Implement `STARTING -> RUNNING` using `startsecs`. - [ ] Implement `BACKOFF` with fixed `companion_restart_delay`. - [ ] Implement `start_process`. - [ ] Implement `stop_process`. diff --git a/gunicorn/companion/manager.py b/gunicorn/companion/manager.py index 413c46a2..36ef60c5 100644 --- a/gunicorn/companion/manager.py +++ b/gunicorn/companion/manager.py @@ -84,6 +84,24 @@ class CompanionManager: reaped.append(proc) return reaped + def promote_running(self, now: float = None) -> list: + """Move companions that survived ``startsecs`` from STARTING to RUNNING. + + A freshly spawned companion starts in STARTING. If it stays alive for + its ``startsecs`` window it is considered up and becomes RUNNING; if it + dies first, reaping handles it instead. Returns the promoted ones. + """ + now = now or time.time() + promoted = [] + for proc in self.processes.values(): + if proc.state != State.STARTING or proc.started_at is None: + continue + if now - proc.started_at >= proc.config.startsecs: + proc.state = State.RUNNING + self.log.info("companion %s running (pid %s)", proc.name, proc.pid) + promoted.append(proc) + return promoted + def _process_by_pid(self, pid: int): for proc in self.processes.values(): if proc.pid == pid: diff --git a/tests/test_companion_manager.py b/tests/test_companion_manager.py index 0169b8cf..c1c38826 100644 --- a/tests/test_companion_manager.py +++ b/tests/test_companion_manager.py @@ -125,6 +125,36 @@ def test_reap_no_children(): assert mgr.reap_processes() == [] +def test_promote_running_after_startsecs(): + mgr = make_manager("rq") + proc = mgr.processes["rq"] + proc.config.startsecs = 1 + proc.state = State.STARTING + proc.started_at = 100.0 + promoted = mgr.promote_running(now=101.5) + assert promoted == [proc] + assert proc.state == State.RUNNING + + +def test_promote_running_too_early(): + mgr = make_manager("rq") + proc = mgr.processes["rq"] + proc.config.startsecs = 5 + proc.state = State.STARTING + proc.started_at = 100.0 + assert mgr.promote_running(now=102.0) == [] + assert proc.state == State.STARTING + + +def test_promote_running_ignores_non_starting(): + mgr = make_manager("rq") + proc = mgr.processes["rq"] + proc.state = State.BACKOFF + proc.started_at = 100.0 + assert mgr.promote_running(now=999.0) == [] + assert proc.state == State.BACKOFF + + def test_spawn_parent_records_pid_and_starting(): mgr = make_manager("rq") proc = mgr.processes["rq"]