mirror of
https://github.com/frappe/gunicorn.git
synced 2026-07-02 18:51:31 +08:00
feat(companion): Redirect companion stdout and stderr
Child calls _redirect_output after env setup: each configured log path is opened append-mode and dup2'd onto fd 1/2. None/inherit keeps the inherited fd; stderr stdout shares stdout's fd. Rotation stays external. Add tests for inherit, append flags, file dup2, and stderr-to-stdout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ea2748a209
commit
2bf7e1b1fb
@ -673,7 +673,7 @@ No per-companion logic in Arbiter.
|
|||||||
- [x] Add `CompanionManager` skeleton.
|
- [x] Add `CompanionManager` skeleton.
|
||||||
- [x] Spawn one companion process from the manager.
|
- [x] Spawn one companion process from the manager.
|
||||||
- [x] Apply `cwd` and `env` before target.
|
- [x] Apply `cwd` and `env` before target.
|
||||||
- [ ] Redirect `stdout` and `stderr`.
|
- [x] Redirect `stdout` and `stderr`.
|
||||||
- [ ] Reap exited companion processes.
|
- [ ] Reap exited companion processes.
|
||||||
- [ ] Implement `STARTING -> RUNNING` using `startsecs`.
|
- [ ] Implement `STARTING -> RUNNING` using `startsecs`.
|
||||||
- [ ] Implement `BACKOFF` with fixed `companion_restart_delay`.
|
- [ ] Implement `BACKOFF` with fixed `companion_restart_delay`.
|
||||||
|
|||||||
@ -48,6 +48,7 @@ class CompanionManager:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
self._apply_environment(proc.config)
|
self._apply_environment(proc.config)
|
||||||
|
self._redirect_output(proc.config)
|
||||||
target = self._resolve_target(proc.config.target)
|
target = self._resolve_target(proc.config.target)
|
||||||
target()
|
target()
|
||||||
except SystemExit:
|
except SystemExit:
|
||||||
@ -70,6 +71,33 @@ class CompanionManager:
|
|||||||
if config.env:
|
if config.env:
|
||||||
os.environ.update(config.env)
|
os.environ.update(config.env)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _redirect_output(config: CompanionConfig) -> None:
|
||||||
|
"""Send the companion's stdout and stderr to its configured log files.
|
||||||
|
|
||||||
|
By default a companion just inherits the manager's stdout/stderr, so
|
||||||
|
leaving these unset (or ``"inherit"``) keeps that. Give a file path and
|
||||||
|
we append the output there instead. For stderr you can also pass
|
||||||
|
``"stdout"`` to fold the two streams into one file.
|
||||||
|
"""
|
||||||
|
out = CompanionManager._open_output(config.stdout)
|
||||||
|
if out is not None:
|
||||||
|
os.dup2(out, 1)
|
||||||
|
if config.stderr == "stdout":
|
||||||
|
os.dup2(1, 2)
|
||||||
|
else:
|
||||||
|
err = CompanionManager._open_output(config.stderr)
|
||||||
|
if err is not None:
|
||||||
|
os.dup2(err, 2)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _open_output(value):
|
||||||
|
"""Open one log file for writing, or return None to leave the stream
|
||||||
|
as-is when the companion should keep inheriting it."""
|
||||||
|
if value in (None, "inherit"):
|
||||||
|
return None
|
||||||
|
return os.open(value, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o644)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _resolve_target(target: Union[Callable, str]) -> Callable:
|
def _resolve_target(target: Union[Callable, str]) -> Callable:
|
||||||
"""Return the zero-arg callable for a companion target.
|
"""Return the zero-arg callable for a companion target.
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
# This file is part of gunicorn released under the MIT license.
|
# This file is part of gunicorn released under the MIT license.
|
||||||
# See the NOTICE for more information.
|
# See the NOTICE for more information.
|
||||||
|
|
||||||
|
import os
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -54,6 +55,46 @@ def test_apply_environment_noop_without_cwd_env():
|
|||||||
chdir.assert_not_called()
|
chdir.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_open_output_inherit_returns_none():
|
||||||
|
assert CompanionManager._open_output(None) is None
|
||||||
|
assert CompanionManager._open_output("inherit") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_open_output_path_opens_append():
|
||||||
|
with mock.patch("os.open", return_value=9) as op:
|
||||||
|
fd = CompanionManager._open_output("/var/log/rq.log")
|
||||||
|
assert fd == 9
|
||||||
|
flags = op.call_args.args[1]
|
||||||
|
assert flags & os.O_APPEND and flags & os.O_CREAT
|
||||||
|
|
||||||
|
|
||||||
|
def test_redirect_output_files():
|
||||||
|
config = CompanionConfig(name="rq", target=lambda: None,
|
||||||
|
stdout="/o.log", stderr="/e.log")
|
||||||
|
with mock.patch("os.open", side_effect=[10, 11]), \
|
||||||
|
mock.patch("os.dup2") as dup2:
|
||||||
|
CompanionManager._redirect_output(config)
|
||||||
|
dup2.assert_any_call(10, 1)
|
||||||
|
dup2.assert_any_call(11, 2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_redirect_output_stderr_to_stdout():
|
||||||
|
config = CompanionConfig(name="rq", target=lambda: None,
|
||||||
|
stdout="/o.log", stderr="stdout")
|
||||||
|
with mock.patch("os.open", return_value=10), \
|
||||||
|
mock.patch("os.dup2") as dup2:
|
||||||
|
CompanionManager._redirect_output(config)
|
||||||
|
dup2.assert_any_call(10, 1)
|
||||||
|
dup2.assert_any_call(1, 2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_redirect_output_inherit_noop():
|
||||||
|
config = CompanionConfig(name="rq", target=lambda: None)
|
||||||
|
with mock.patch("os.dup2") as dup2:
|
||||||
|
CompanionManager._redirect_output(config)
|
||||||
|
dup2.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_spawn_parent_records_pid_and_starting():
|
def test_spawn_parent_records_pid_and_starting():
|
||||||
mgr = make_manager("rq")
|
mgr = make_manager("rq")
|
||||||
proc = mgr.processes["rq"]
|
proc = mgr.processes["rq"]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user