mirror of
https://github.com/frappe/gunicorn.git
synced 2026-07-02 18:51:31 +08:00
refactor(companion): Spell out abbreviated identifiers
No behaviour change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
5db503295c
commit
9f3762d6b6
@ -25,19 +25,19 @@ def decode_command(line):
|
|||||||
JSON object carrying a string ``cmd``; anything else is a ``CommandError``.
|
JSON object carrying a string ``cmd``; anything else is a ``CommandError``.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
obj = json.loads(line)
|
command = json.loads(line)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
raise CommandError("invalid JSON")
|
raise CommandError("invalid JSON")
|
||||||
if not isinstance(obj, dict):
|
if not isinstance(command, dict):
|
||||||
raise CommandError("request must be a JSON object")
|
raise CommandError("request must be a JSON object")
|
||||||
if not isinstance(obj.get("cmd"), str):
|
if not isinstance(command.get("cmd"), str):
|
||||||
raise CommandError("missing 'cmd'")
|
raise CommandError("missing 'cmd'")
|
||||||
return obj
|
return command
|
||||||
|
|
||||||
|
|
||||||
def encode_response(obj):
|
def encode_response(response):
|
||||||
"""Encode a response dict as one newline-terminated JSON line of bytes."""
|
"""Encode a response dict as one newline-terminated JSON line of bytes."""
|
||||||
return (json.dumps(obj) + "\n").encode("utf-8")
|
return (json.dumps(response) + "\n").encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
class ControlServer:
|
class ControlServer:
|
||||||
@ -58,7 +58,7 @@ class ControlServer:
|
|||||||
self.mode = mode
|
self.mode = mode
|
||||||
self.log = log
|
self.log = log
|
||||||
self.backlog = backlog
|
self.backlog = backlog
|
||||||
self.sock = None
|
self.listener = None
|
||||||
|
|
||||||
def create(self):
|
def create(self):
|
||||||
"""Bind and listen on the Unix socket, replacing any stale one.
|
"""Bind and listen on the Unix socket, replacing any stale one.
|
||||||
@ -69,18 +69,18 @@ class ControlServer:
|
|||||||
"""
|
"""
|
||||||
if os.path.exists(self.path):
|
if os.path.exists(self.path):
|
||||||
os.unlink(self.path)
|
os.unlink(self.path)
|
||||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
sock.bind(self.path)
|
listener.bind(self.path)
|
||||||
os.chmod(self.path, self.mode)
|
os.chmod(self.path, self.mode)
|
||||||
sock.listen(self.backlog)
|
listener.listen(self.backlog)
|
||||||
self.sock = sock
|
self.listener = listener
|
||||||
return sock
|
return listener
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Close the listening socket and remove its file."""
|
"""Close the listening socket and remove its file."""
|
||||||
if self.sock is not None:
|
if self.listener is not None:
|
||||||
self.sock.close()
|
self.listener.close()
|
||||||
self.sock = None
|
self.listener = None
|
||||||
if os.path.exists(self.path):
|
if os.path.exists(self.path):
|
||||||
os.unlink(self.path)
|
os.unlink(self.path)
|
||||||
|
|
||||||
@ -93,25 +93,25 @@ class ControlServer:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
response = self.dispatch(decode_command(line))
|
response = self.dispatch(decode_command(line))
|
||||||
except CommandError as e:
|
except CommandError as error:
|
||||||
response = {"ok": False, "error": str(e)}
|
response = {"ok": False, "error": str(error)}
|
||||||
return encode_response(response)
|
return encode_response(response)
|
||||||
|
|
||||||
def serve_connection(self, conn):
|
def serve_connection(self, connection):
|
||||||
"""Serve newline-delimited requests on one accepted connection.
|
"""Serve newline-delimited requests on one accepted connection.
|
||||||
|
|
||||||
Reads until the client hangs up, buffering partial reads and answering
|
Reads until the client hangs up, buffering partial reads and answering
|
||||||
each complete line as it arrives. A trailing fragment without a newline
|
each complete line as it arrives. A trailing fragment without a newline
|
||||||
is ignored.
|
is ignored.
|
||||||
"""
|
"""
|
||||||
buf = b""
|
buffer = b""
|
||||||
with conn:
|
with connection:
|
||||||
while True:
|
while True:
|
||||||
chunk = conn.recv(65536)
|
chunk = connection.recv(65536)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
break
|
break
|
||||||
buf += chunk
|
buffer += chunk
|
||||||
while b"\n" in buf:
|
while b"\n" in buffer:
|
||||||
line, buf = buf.split(b"\n", 1)
|
line, buffer = buffer.split(b"\n", 1)
|
||||||
if line.strip():
|
if line.strip():
|
||||||
conn.sendall(self.handle_line(line))
|
connection.sendall(self.handle_line(line))
|
||||||
|
|||||||
@ -35,7 +35,7 @@ class CompanionManager:
|
|||||||
# validates companion config, returning a fresh CompanionConfig list.
|
# validates companion config, returning a fresh CompanionConfig list.
|
||||||
self.config_loader = None
|
self.config_loader = None
|
||||||
|
|
||||||
def handle_command(self, obj: dict) -> dict:
|
def handle_command(self, command: dict) -> dict:
|
||||||
"""Route a decoded control command to its action.
|
"""Route a decoded control command to its action.
|
||||||
|
|
||||||
This is the ``dispatch`` the control socket calls. ``status`` returns a
|
This is the ``dispatch`` the control socket calls. ``status`` returns a
|
||||||
@ -44,37 +44,37 @@ class CompanionManager:
|
|||||||
commands need a string ``name``, and anything else raises ``CommandError`` so the
|
commands need a string ``name``, and anything else raises ``CommandError`` so the
|
||||||
socket replies with an error envelope.
|
socket replies with an error envelope.
|
||||||
"""
|
"""
|
||||||
cmd = obj["cmd"]
|
command_name = command["cmd"]
|
||||||
if cmd == "status":
|
if command_name == "status":
|
||||||
return {"ok": True, "companions": self.status()}
|
return {"ok": True, "companions": self.status()}
|
||||||
if cmd == "reread":
|
if command_name == "reread":
|
||||||
if self.config_loader is None:
|
if self.config_loader is None:
|
||||||
raise CommandError("reread not configured")
|
raise CommandError("reread not configured")
|
||||||
try:
|
try:
|
||||||
new_configs = self.config_loader()
|
new_configs = self.config_loader()
|
||||||
except Exception as e:
|
except Exception as error:
|
||||||
return {"ok": False, "error": "invalid config: %s" % e,
|
return {"ok": False, "error": "invalid config: %s" % error,
|
||||||
"kept_old_config": True}
|
"kept_old_config": True}
|
||||||
return self.reread_config(new_configs)
|
return self.reread_config(new_configs)
|
||||||
|
|
||||||
# Every remaining command acts on one named companion.
|
# Every remaining command acts on one named companion.
|
||||||
name = obj.get("name")
|
name = command.get("name")
|
||||||
if not isinstance(name, str):
|
if not isinstance(name, str):
|
||||||
raise CommandError("'%s' requires a 'name'" % cmd)
|
raise CommandError("'%s' requires a 'name'" % command_name)
|
||||||
if cmd == "start":
|
if command_name == "start":
|
||||||
ok, message = self.start_process(name)
|
ok, message = self.start_process(name)
|
||||||
elif cmd == "stop":
|
elif command_name == "stop":
|
||||||
ok, message = self.stop_process(name)
|
ok, message = self.stop_process(name)
|
||||||
elif cmd == "restart":
|
elif command_name == "restart":
|
||||||
ok, message = self.restart_process(name)
|
ok, message = self.restart_process(name)
|
||||||
else:
|
else:
|
||||||
raise CommandError("unknown command %r" % cmd)
|
raise CommandError("unknown command %r" % command_name)
|
||||||
return {"ok": ok, "message": message}
|
return {"ok": ok, "message": message}
|
||||||
|
|
||||||
def status(self, now: float = None) -> list:
|
def status(self, now: float = None) -> list:
|
||||||
"""Status entry for every companion, for the ``status`` command."""
|
"""Status entry for every companion, for the ``status`` command."""
|
||||||
now = now or time.time()
|
now = now or time.time()
|
||||||
return [proc.status_dict(now) for proc in self.processes.values()]
|
return [process.status_dict(now) for process in self.processes.values()]
|
||||||
|
|
||||||
def reread_config(self, new_configs) -> dict:
|
def reread_config(self, new_configs) -> dict:
|
||||||
"""Transactionally apply a fresh set of companion configs.
|
"""Transactionally apply a fresh set of companion configs.
|
||||||
@ -101,18 +101,18 @@ class CompanionManager:
|
|||||||
removed.append(name)
|
removed.append(name)
|
||||||
|
|
||||||
for name in new_names - old_names:
|
for name in new_names - old_names:
|
||||||
proc = CompanionProcess(new_by_name[name])
|
process = CompanionProcess(new_by_name[name])
|
||||||
self.processes[name] = proc
|
self.processes[name] = process
|
||||||
self.spawn_process(proc)
|
self.spawn_process(process)
|
||||||
added.append(name)
|
added.append(name)
|
||||||
|
|
||||||
for name in new_names & old_names:
|
for name in new_names & old_names:
|
||||||
proc = self.processes[name]
|
process = self.processes[name]
|
||||||
if proc.config.config_hash == new_by_name[name].config_hash:
|
if process.config.config_hash == new_by_name[name].config_hash:
|
||||||
unchanged.append(name)
|
unchanged.append(name)
|
||||||
continue
|
continue
|
||||||
proc.config = new_by_name[name]
|
process.config = new_by_name[name]
|
||||||
if proc.manual_stop:
|
if process.manual_stop:
|
||||||
unchanged.append(name)
|
unchanged.append(name)
|
||||||
else:
|
else:
|
||||||
self.restart_process(name)
|
self.restart_process(name)
|
||||||
@ -132,7 +132,7 @@ class CompanionManager:
|
|||||||
by_name[config.name] = config
|
by_name[config.name] = config
|
||||||
return by_name
|
return by_name
|
||||||
|
|
||||||
def spawn_process(self, proc: CompanionProcess) -> int:
|
def spawn_process(self, process: CompanionProcess) -> int:
|
||||||
"""Fork one companion.
|
"""Fork one companion.
|
||||||
|
|
||||||
Parent records the pid and moves the companion to STARTING. Child
|
Parent records the pid and moves the companion to STARTING. Child
|
||||||
@ -146,21 +146,21 @@ class CompanionManager:
|
|||||||
"""
|
"""
|
||||||
pid = os.fork()
|
pid = os.fork()
|
||||||
if pid != 0:
|
if pid != 0:
|
||||||
proc.pid = pid
|
process.pid = pid
|
||||||
proc.state = State.STARTING
|
process.state = State.STARTING
|
||||||
proc.started_at = time.time()
|
process.started_at = time.time()
|
||||||
self.log.info("companion %s started (pid %s)", proc.name, pid)
|
self.log.info("companion %s started (pid %s)", process.name, pid)
|
||||||
return pid
|
return pid
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._apply_environment(proc.config)
|
self._apply_environment(process.config)
|
||||||
self._redirect_output(proc.config)
|
self._redirect_output(process.config)
|
||||||
target = self._resolve_target(proc.config.target)
|
target = self._resolve_target(process.config.target)
|
||||||
target()
|
target()
|
||||||
except SystemExit:
|
except SystemExit:
|
||||||
raise
|
raise
|
||||||
except BaseException:
|
except BaseException:
|
||||||
self.log.exception("companion %s crashed", proc.name)
|
self.log.exception("companion %s crashed", process.name)
|
||||||
os._exit(1)
|
os._exit(1)
|
||||||
os._exit(0)
|
os._exit(0)
|
||||||
|
|
||||||
@ -173,16 +173,16 @@ class CompanionManager:
|
|||||||
without doing anything. STOPPING is rejected so the caller polls status
|
without doing anything. STOPPING is rejected so the caller polls status
|
||||||
and retries once the old child is gone. Returns ``(ok, message)``.
|
and retries once the old child is gone. Returns ``(ok, message)``.
|
||||||
"""
|
"""
|
||||||
proc = self.processes.get(name)
|
process = self.processes.get(name)
|
||||||
if proc is None:
|
if process is None:
|
||||||
return False, "unknown companion %s" % name
|
return False, "unknown companion %s" % name
|
||||||
if proc.state in (State.RUNNING, State.STARTING):
|
if process.state in (State.RUNNING, State.STARTING):
|
||||||
return True, "%s already %s" % (name, proc.state.lower())
|
return True, "%s already %s" % (name, process.state.lower())
|
||||||
if proc.state == State.STOPPING:
|
if process.state == State.STOPPING:
|
||||||
return False, "%s is stopping; retry" % name
|
return False, "%s is stopping; retry" % name
|
||||||
proc.manual_stop = False
|
process.manual_stop = False
|
||||||
proc.next_retry_at = None
|
process.next_retry_at = None
|
||||||
self.spawn_process(proc)
|
self.spawn_process(process)
|
||||||
return True, "%s started" % name
|
return True, "%s started" % name
|
||||||
|
|
||||||
def stop_process(self, name: str, now: float = None):
|
def stop_process(self, name: str, now: float = None):
|
||||||
@ -195,21 +195,21 @@ class CompanionManager:
|
|||||||
settles in STOPPED. STOPPED and STOPPING are already-there success
|
settles in STOPPED. STOPPED and STOPPING are already-there success
|
||||||
no-ops. Returns ``(ok, message)``.
|
no-ops. Returns ``(ok, message)``.
|
||||||
"""
|
"""
|
||||||
proc = self.processes.get(name)
|
process = self.processes.get(name)
|
||||||
if proc is None:
|
if process is None:
|
||||||
return False, "unknown companion %s" % name
|
return False, "unknown companion %s" % name
|
||||||
proc.manual_stop = True
|
process.manual_stop = True
|
||||||
if proc.state in (State.STOPPED, State.STOPPING):
|
if process.state in (State.STOPPED, State.STOPPING):
|
||||||
return True, "%s already %s" % (name, proc.state.lower())
|
return True, "%s already %s" % (name, process.state.lower())
|
||||||
if proc.state == State.BACKOFF:
|
if process.state == State.BACKOFF:
|
||||||
proc.next_retry_at = None
|
process.next_retry_at = None
|
||||||
proc.state = State.STOPPED
|
process.state = State.STOPPED
|
||||||
return True, "%s stopped" % name
|
return True, "%s stopped" % name
|
||||||
now = now or time.time()
|
now = now or time.time()
|
||||||
os.kill(proc.pid, self._signal_number(proc.config.stop_signal))
|
os.kill(process.pid, self._signal_number(process.config.stop_signal))
|
||||||
proc.state = State.STOPPING
|
process.state = State.STOPPING
|
||||||
proc.stop_deadline = now + proc.config.stop_timeout
|
process.stop_deadline = now + process.config.stop_timeout
|
||||||
self.log.info("companion %s stopping (pid %s)", name, proc.pid)
|
self.log.info("companion %s stopping (pid %s)", name, process.pid)
|
||||||
return True, "%s stopping" % name
|
return True, "%s stopping" % name
|
||||||
|
|
||||||
def restart_process(self, name: str, now: float = None):
|
def restart_process(self, name: str, now: float = None):
|
||||||
@ -222,26 +222,26 @@ class CompanionManager:
|
|||||||
STOPPED start again immediately. STOPPING is rejected so the caller
|
STOPPED start again immediately. STOPPING is rejected so the caller
|
||||||
retries. This never rereads config. Returns ``(ok, message)``.
|
retries. This never rereads config. Returns ``(ok, message)``.
|
||||||
"""
|
"""
|
||||||
proc = self.processes.get(name)
|
process = self.processes.get(name)
|
||||||
if proc is None:
|
if process is None:
|
||||||
return False, "unknown companion %s" % name
|
return False, "unknown companion %s" % name
|
||||||
if proc.state == State.STOPPING:
|
if process.state == State.STOPPING:
|
||||||
return False, "%s is stopping; retry" % name
|
return False, "%s is stopping; retry" % name
|
||||||
proc.manual_stop = False
|
process.manual_stop = False
|
||||||
if proc.state in (State.RUNNING, State.STARTING):
|
if process.state in (State.RUNNING, State.STARTING):
|
||||||
now = now or time.time()
|
now = now or time.time()
|
||||||
proc.restart_pending = True
|
process.restart_pending = True
|
||||||
os.kill(proc.pid, self._signal_number(proc.config.stop_signal))
|
os.kill(process.pid, self._signal_number(process.config.stop_signal))
|
||||||
proc.state = State.STOPPING
|
process.state = State.STOPPING
|
||||||
proc.stop_deadline = now + proc.config.reload_timeout
|
process.stop_deadline = now + process.config.reload_timeout
|
||||||
self.log.info("companion %s restarting (pid %s)", name, proc.pid)
|
self.log.info("companion %s restarting (pid %s)", name, process.pid)
|
||||||
return True, "%s restarting" % name
|
return True, "%s restarting" % name
|
||||||
proc.next_retry_at = None
|
process.next_retry_at = None
|
||||||
self.spawn_process(proc)
|
self.spawn_process(process)
|
||||||
return True, "%s started" % name
|
return True, "%s started" % name
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _signal_number(sig) -> int:
|
def _signal_number(stop_signal) -> int:
|
||||||
"""Resolve a stop signal to its number, e.g. ``"SIGTERM"`` -> 15.
|
"""Resolve a stop signal to its number, e.g. ``"SIGTERM"`` -> 15.
|
||||||
|
|
||||||
Accepts a signal name or a raw number and validates both against the
|
Accepts a signal name or a raw number and validates both against the
|
||||||
@ -249,9 +249,11 @@ class CompanionManager:
|
|||||||
than silently sending the wrong signal (or none).
|
than silently sending the wrong signal (or none).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return signal.Signals[sig] if isinstance(sig, str) else signal.Signals(sig)
|
if isinstance(stop_signal, str):
|
||||||
|
return signal.Signals[stop_signal]
|
||||||
|
return signal.Signals(stop_signal)
|
||||||
except (KeyError, ValueError):
|
except (KeyError, ValueError):
|
||||||
raise ValueError("unknown stop signal %r" % (sig,))
|
raise ValueError("unknown stop signal %r" % (stop_signal,))
|
||||||
|
|
||||||
def reap_processes(self) -> list:
|
def reap_processes(self) -> list:
|
||||||
"""Reap any companions that have exited and record their exit info.
|
"""Reap any companions that have exited and record their exit info.
|
||||||
@ -282,14 +284,14 @@ class CompanionManager:
|
|||||||
break
|
break
|
||||||
if pid == 0:
|
if pid == 0:
|
||||||
break
|
break
|
||||||
proc = self._process_by_pid(pid)
|
process = self._process_by_pid(pid)
|
||||||
if proc is not None:
|
if process is not None:
|
||||||
self._record_exit(proc, status)
|
self._record_exit(process, status)
|
||||||
self.handle_exit(proc)
|
self.handle_exit(process)
|
||||||
reaped.append(proc)
|
reaped.append(process)
|
||||||
return reaped
|
return reaped
|
||||||
|
|
||||||
def handle_exit(self, proc: CompanionProcess, now: float = None) -> None:
|
def handle_exit(self, process: CompanionProcess, now: float = None) -> None:
|
||||||
"""Decide a companion's fate after it exits: restart, stop, or back off.
|
"""Decide a companion's fate after it exits: restart, stop, or back off.
|
||||||
|
|
||||||
A pending restart wins: the old child was asked to stop only so a fresh
|
A pending restart wins: the old child was asked to stop only so a fresh
|
||||||
@ -300,19 +302,19 @@ class CompanionManager:
|
|||||||
backoff, no retry cap).
|
backoff, no retry cap).
|
||||||
"""
|
"""
|
||||||
now = now or time.time()
|
now = now or time.time()
|
||||||
if proc.restart_pending:
|
if process.restart_pending:
|
||||||
proc.restart_pending = False
|
process.restart_pending = False
|
||||||
proc.restart_count += 1
|
process.restart_count += 1
|
||||||
self.spawn_process(proc)
|
self.spawn_process(process)
|
||||||
return
|
return
|
||||||
if proc.manual_stop:
|
if process.manual_stop:
|
||||||
proc.state = State.STOPPED
|
process.state = State.STOPPED
|
||||||
proc.next_retry_at = None
|
process.next_retry_at = None
|
||||||
return
|
return
|
||||||
proc.state = State.BACKOFF
|
process.state = State.BACKOFF
|
||||||
proc.next_retry_at = now + proc.restart_delay
|
process.next_retry_at = now + process.restart_delay
|
||||||
self.log.info("companion %s exited, retrying in %ss",
|
self.log.info("companion %s exited, retrying in %ss",
|
||||||
proc.name, proc.restart_delay)
|
process.name, process.restart_delay)
|
||||||
|
|
||||||
def retry_backoff(self, now: float = None) -> list:
|
def retry_backoff(self, now: float = None) -> list:
|
||||||
"""Respawn BACKOFF companions whose fixed retry delay has elapsed.
|
"""Respawn BACKOFF companions whose fixed retry delay has elapsed.
|
||||||
@ -322,14 +324,14 @@ class CompanionManager:
|
|||||||
"""
|
"""
|
||||||
now = now or time.time()
|
now = now or time.time()
|
||||||
retried = []
|
retried = []
|
||||||
for proc in self.processes.values():
|
for process in self.processes.values():
|
||||||
if proc.state != State.BACKOFF or proc.next_retry_at is None:
|
if process.state != State.BACKOFF or process.next_retry_at is None:
|
||||||
continue
|
continue
|
||||||
if now >= proc.next_retry_at:
|
if now >= process.next_retry_at:
|
||||||
proc.restart_count += 1
|
process.restart_count += 1
|
||||||
proc.next_retry_at = None
|
process.next_retry_at = None
|
||||||
self.spawn_process(proc)
|
self.spawn_process(process)
|
||||||
retried.append(proc)
|
retried.append(process)
|
||||||
return retried
|
return retried
|
||||||
|
|
||||||
def promote_running(self, now: float = None) -> list:
|
def promote_running(self, now: float = None) -> list:
|
||||||
@ -341,23 +343,23 @@ class CompanionManager:
|
|||||||
"""
|
"""
|
||||||
now = now or time.time()
|
now = now or time.time()
|
||||||
promoted = []
|
promoted = []
|
||||||
for proc in self.processes.values():
|
for process in self.processes.values():
|
||||||
if proc.state != State.STARTING or proc.started_at is None:
|
if process.state != State.STARTING or process.started_at is None:
|
||||||
continue
|
continue
|
||||||
if now - proc.started_at >= proc.config.startsecs:
|
if now - process.started_at >= process.config.startsecs:
|
||||||
proc.state = State.RUNNING
|
process.state = State.RUNNING
|
||||||
self.log.info("companion %s running (pid %s)", proc.name, proc.pid)
|
self.log.info("companion %s running (pid %s)", process.name, process.pid)
|
||||||
promoted.append(proc)
|
promoted.append(process)
|
||||||
return promoted
|
return promoted
|
||||||
|
|
||||||
def _process_by_pid(self, pid: int):
|
def _process_by_pid(self, pid: int):
|
||||||
for proc in self.processes.values():
|
for process in self.processes.values():
|
||||||
if proc.pid == pid:
|
if process.pid == pid:
|
||||||
return proc
|
return process
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _record_exit(proc: CompanionProcess, status: int) -> None:
|
def _record_exit(process: CompanionProcess, status: int) -> None:
|
||||||
"""Store how a companion died: signal number or exit code, plus time.
|
"""Store how a companion died: signal number or exit code, plus time.
|
||||||
|
|
||||||
``status`` is the packed value from ``waitpid``. ``WIFSIGNALED`` tells
|
``status`` is the packed value from ``waitpid``. ``WIFSIGNALED`` tells
|
||||||
@ -366,14 +368,14 @@ class CompanionManager:
|
|||||||
code. Only one of the two is ever set, so the other is cleared.
|
code. Only one of the two is ever set, so the other is cleared.
|
||||||
"""
|
"""
|
||||||
if os.WIFSIGNALED(status):
|
if os.WIFSIGNALED(status):
|
||||||
proc.last_exit_signal = os.WTERMSIG(status)
|
process.last_exit_signal = os.WTERMSIG(status)
|
||||||
proc.last_exit_code = None
|
process.last_exit_code = None
|
||||||
else:
|
else:
|
||||||
proc.last_exit_code = os.WEXITSTATUS(status)
|
process.last_exit_code = os.WEXITSTATUS(status)
|
||||||
proc.last_exit_signal = None
|
process.last_exit_signal = None
|
||||||
proc.exited_at = time.time()
|
process.exited_at = time.time()
|
||||||
proc.exit_count += 1
|
process.exit_count += 1
|
||||||
proc.pid = None
|
process.pid = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _apply_environment(config: CompanionConfig) -> None:
|
def _apply_environment(config: CompanionConfig) -> None:
|
||||||
@ -397,15 +399,15 @@ class CompanionManager:
|
|||||||
we append the output there instead. For stderr you can also pass
|
we append the output there instead. For stderr you can also pass
|
||||||
``"stdout"`` to fold the two streams into one file.
|
``"stdout"`` to fold the two streams into one file.
|
||||||
"""
|
"""
|
||||||
out = CompanionManager._open_output(config.stdout)
|
stdout_fd = CompanionManager._open_output(config.stdout)
|
||||||
if out is not None:
|
if stdout_fd is not None:
|
||||||
os.dup2(out, 1)
|
os.dup2(stdout_fd, 1)
|
||||||
if config.stderr == "stdout":
|
if config.stderr == "stdout":
|
||||||
os.dup2(1, 2)
|
os.dup2(1, 2)
|
||||||
else:
|
else:
|
||||||
err = CompanionManager._open_output(config.stderr)
|
stderr_fd = CompanionManager._open_output(config.stderr)
|
||||||
if err is not None:
|
if stderr_fd is not None:
|
||||||
os.dup2(err, 2)
|
os.dup2(stderr_fd, 2)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _open_output(value):
|
def _open_output(value):
|
||||||
@ -424,7 +426,7 @@ class CompanionManager:
|
|||||||
"""
|
"""
|
||||||
if callable(target):
|
if callable(target):
|
||||||
return target
|
return target
|
||||||
module, sep, attr = target.partition(":")
|
module_name, separator, attribute = target.partition(":")
|
||||||
if not sep:
|
if not separator:
|
||||||
raise ValueError("companion target %r must be 'module:callable'" % target)
|
raise ValueError("companion target %r must be 'module:callable'" % target)
|
||||||
return getattr(importlib.import_module(module), attr)
|
return getattr(importlib.import_module(module_name), attribute)
|
||||||
|
|||||||
@ -78,15 +78,15 @@ class CompanionConfig:
|
|||||||
# target has no stable repr across runs, so use its qualified name.
|
# target has no stable repr across runs, so use its qualified name.
|
||||||
data = self.to_dict()
|
data = self.to_dict()
|
||||||
data["target"] = self._target_key(self.target)
|
data["target"] = self._target_key(self.target)
|
||||||
blob = json.dumps(data, sort_keys=True, default=str)
|
payload = json.dumps(data, sort_keys=True, default=str)
|
||||||
return hashlib.sha256(blob.encode("utf-8")).hexdigest()
|
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _target_key(target):
|
def _target_key(target):
|
||||||
if callable(target):
|
if callable(target):
|
||||||
mod = getattr(target, "__module__", "")
|
module = getattr(target, "__module__", "")
|
||||||
qual = getattr(target, "__qualname__", repr(target))
|
qualified_name = getattr(target, "__qualname__", repr(target))
|
||||||
return "%s:%s" % (mod, qual)
|
return "%s:%s" % (module, qualified_name)
|
||||||
return str(target)
|
return str(target)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
@ -146,8 +146,8 @@ class CompanionProcess:
|
|||||||
format_uptime(self.uptime(now) or 0),
|
format_uptime(self.uptime(now) or 0),
|
||||||
)
|
)
|
||||||
if self.state == State.BACKOFF and self.next_retry_at is not None:
|
if self.state == State.BACKOFF and self.next_retry_at is not None:
|
||||||
left = max(0, int(self.next_retry_at - now))
|
seconds_left = max(0, int(self.next_retry_at - now))
|
||||||
return "exited with %s, retrying in %ds" % (self._exit_status(), left)
|
return "exited with %s, retrying in %ds" % (self._exit_status(), seconds_left)
|
||||||
if self.state == State.BACKOFF:
|
if self.state == State.BACKOFF:
|
||||||
return "exited with %s" % self._exit_status()
|
return "exited with %s" % self._exit_status()
|
||||||
if self.state == State.STOPPED and self.manual_stop:
|
if self.state == State.STOPPED and self.manual_stop:
|
||||||
|
|||||||
@ -652,10 +652,10 @@ def unquote_to_wsgi_str(string):
|
|||||||
def format_uptime(seconds):
|
def format_uptime(seconds):
|
||||||
"""Render a duration like supervisor: ``2 days, 03:12:44`` or ``0:05:12``."""
|
"""Render a duration like supervisor: ``2 days, 03:12:44`` or ``0:05:12``."""
|
||||||
seconds = int(seconds)
|
seconds = int(seconds)
|
||||||
days, rem = divmod(seconds, 86400)
|
days, remainder = divmod(seconds, 86400)
|
||||||
hours, rem = divmod(rem, 3600)
|
hours, remainder = divmod(remainder, 3600)
|
||||||
minutes, secs = divmod(rem, 60)
|
minutes, remaining_seconds = divmod(remainder, 60)
|
||||||
if days:
|
if days:
|
||||||
unit = "day" if days == 1 else "days"
|
unit = "day" if days == 1 else "days"
|
||||||
return "%d %s, %02d:%02d:%02d" % (days, unit, hours, minutes, secs)
|
return "%d %s, %02d:%02d:%02d" % (days, unit, hours, minutes, remaining_seconds)
|
||||||
return "%d:%02d:%02d" % (hours, minutes, secs)
|
return "%d:%02d:%02d" % (hours, minutes, remaining_seconds)
|
||||||
|
|||||||
@ -35,51 +35,53 @@ def test_decode_command_missing_cmd():
|
|||||||
|
|
||||||
|
|
||||||
def test_encode_response_newline_terminated():
|
def test_encode_response_newline_terminated():
|
||||||
out = encode_response({"ok": True})
|
response = encode_response({"ok": True})
|
||||||
assert out.endswith(b"\n")
|
assert response.endswith(b"\n")
|
||||||
assert json.loads(out) == {"ok": True}
|
assert json.loads(response) == {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
def test_handle_line_dispatches():
|
def test_handle_line_dispatches():
|
||||||
server = ControlServer(dispatch=lambda obj: {"ok": True, "echo": obj["cmd"]},
|
server = ControlServer(
|
||||||
|
dispatch=lambda command: {"ok": True, "echo": command["cmd"]},
|
||||||
path="/tmp/x.sock")
|
path="/tmp/x.sock")
|
||||||
out = server.handle_line('{"cmd": "status"}')
|
response = server.handle_line('{"cmd": "status"}')
|
||||||
assert json.loads(out) == {"ok": True, "echo": "status"}
|
assert json.loads(response) == {"ok": True, "echo": "status"}
|
||||||
|
|
||||||
|
|
||||||
def test_handle_line_bad_json_error_envelope():
|
def test_handle_line_bad_json_error_envelope():
|
||||||
server = ControlServer(dispatch=lambda obj: {"ok": True}, path="/tmp/x.sock")
|
server = ControlServer(dispatch=lambda command: {"ok": True}, path="/tmp/x.sock")
|
||||||
out = json.loads(server.handle_line("garbage"))
|
response = json.loads(server.handle_line("garbage"))
|
||||||
assert out["ok"] is False and "JSON" in out["error"]
|
assert response["ok"] is False and "JSON" in response["error"]
|
||||||
|
|
||||||
|
|
||||||
def test_handle_line_dispatch_command_error():
|
def test_handle_line_dispatch_command_error():
|
||||||
def dispatch(obj):
|
def dispatch(command):
|
||||||
raise CommandError("unknown command")
|
raise CommandError("unknown command")
|
||||||
server = ControlServer(dispatch=dispatch, path="/tmp/x.sock")
|
server = ControlServer(dispatch=dispatch, path="/tmp/x.sock")
|
||||||
out = json.loads(server.handle_line('{"cmd": "bogus"}'))
|
response = json.loads(server.handle_line('{"cmd": "bogus"}'))
|
||||||
assert out["ok"] is False and out["error"] == "unknown command"
|
assert response["ok"] is False and response["error"] == "unknown command"
|
||||||
|
|
||||||
|
|
||||||
def test_create_unlinks_stale_and_chmods():
|
def test_create_unlinks_stale_and_chmods():
|
||||||
server = ControlServer(dispatch=lambda o: {}, path="/tmp/x.sock", mode=0o600)
|
server = ControlServer(dispatch=lambda command: {}, path="/tmp/x.sock",
|
||||||
sock = mock.Mock()
|
mode=0o600)
|
||||||
|
listener = mock.Mock()
|
||||||
with mock.patch("os.path.exists", return_value=True), \
|
with mock.patch("os.path.exists", return_value=True), \
|
||||||
mock.patch("os.unlink") as unlink, \
|
mock.patch("os.unlink") as unlink, \
|
||||||
mock.patch("socket.socket", return_value=sock), \
|
mock.patch("socket.socket", return_value=listener), \
|
||||||
mock.patch("os.chmod") as chmod:
|
mock.patch("os.chmod") as chmod:
|
||||||
server.create()
|
server.create()
|
||||||
unlink.assert_called_once_with("/tmp/x.sock")
|
unlink.assert_called_once_with("/tmp/x.sock")
|
||||||
sock.bind.assert_called_once_with("/tmp/x.sock")
|
listener.bind.assert_called_once_with("/tmp/x.sock")
|
||||||
chmod.assert_called_once_with("/tmp/x.sock", 0o600)
|
chmod.assert_called_once_with("/tmp/x.sock", 0o600)
|
||||||
sock.listen.assert_called_once()
|
listener.listen.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
def test_close_unlinks():
|
def test_close_unlinks():
|
||||||
server = ControlServer(dispatch=lambda o: {}, path="/tmp/x.sock")
|
server = ControlServer(dispatch=lambda command: {}, path="/tmp/x.sock")
|
||||||
server.sock = mock.Mock()
|
server.listener = mock.Mock()
|
||||||
with mock.patch("os.path.exists", return_value=True), \
|
with mock.patch("os.path.exists", return_value=True), \
|
||||||
mock.patch("os.unlink") as unlink:
|
mock.patch("os.unlink") as unlink:
|
||||||
server.close()
|
server.close()
|
||||||
unlink.assert_called_once_with("/tmp/x.sock")
|
unlink.assert_called_once_with("/tmp/x.sock")
|
||||||
assert server.sock is None
|
assert server.listener is None
|
||||||
|
|||||||
@ -19,9 +19,9 @@ def make_manager(*names):
|
|||||||
|
|
||||||
|
|
||||||
def test_manager_builds_one_process_per_config():
|
def test_manager_builds_one_process_per_config():
|
||||||
mgr = make_manager("rq", "scheduler")
|
manager = make_manager("rq", "scheduler")
|
||||||
assert set(mgr.processes) == {"rq", "scheduler"}
|
assert set(manager.processes) == {"rq", "scheduler"}
|
||||||
assert mgr.processes["rq"].state == State.STOPPED
|
assert manager.processes["rq"].state == State.STOPPED
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_target_accepts_callable():
|
def test_resolve_target_accepts_callable():
|
||||||
@ -63,10 +63,10 @@ def test_open_output_inherit_returns_none():
|
|||||||
|
|
||||||
|
|
||||||
def test_open_output_path_opens_append():
|
def test_open_output_path_opens_append():
|
||||||
with mock.patch("os.open", return_value=9) as op:
|
with mock.patch("os.open", return_value=9) as open_mock:
|
||||||
fd = CompanionManager._open_output("/var/log/rq.log")
|
fd = CompanionManager._open_output("/var/log/rq.log")
|
||||||
assert fd == 9
|
assert fd == 9
|
||||||
flags = op.call_args.args[1]
|
flags = open_mock.call_args.args[1]
|
||||||
assert flags & os.O_APPEND and flags & os.O_CREAT
|
assert flags & os.O_APPEND and flags & os.O_CREAT
|
||||||
|
|
||||||
|
|
||||||
@ -98,12 +98,12 @@ def test_redirect_output_inherit_noop():
|
|||||||
|
|
||||||
|
|
||||||
def test_reap_records_exit_code():
|
def test_reap_records_exit_code():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
proc = mgr.processes["rq"]
|
proc = manager.processes["rq"]
|
||||||
proc.pid = 4321
|
proc.pid = 4321
|
||||||
# exit code 1 -> status 1<<8; second call drains the queue.
|
# exit code 1 -> status 1<<8; second call drains the queue.
|
||||||
with mock.patch("os.waitpid", side_effect=[(4321, 1 << 8), (0, 0)]):
|
with mock.patch("os.waitpid", side_effect=[(4321, 1 << 8), (0, 0)]):
|
||||||
reaped = mgr.reap_processes()
|
reaped = manager.reap_processes()
|
||||||
assert reaped == [proc]
|
assert reaped == [proc]
|
||||||
assert proc.last_exit_code == 1
|
assert proc.last_exit_code == 1
|
||||||
assert proc.last_exit_signal is None
|
assert proc.last_exit_signal is None
|
||||||
@ -112,229 +112,229 @@ def test_reap_records_exit_code():
|
|||||||
|
|
||||||
|
|
||||||
def test_reap_records_signal():
|
def test_reap_records_signal():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
proc = mgr.processes["rq"]
|
proc = manager.processes["rq"]
|
||||||
proc.pid = 4321
|
proc.pid = 4321
|
||||||
with mock.patch("os.waitpid", side_effect=[(4321, 9), (0, 0)]):
|
with mock.patch("os.waitpid", side_effect=[(4321, 9), (0, 0)]):
|
||||||
mgr.reap_processes()
|
manager.reap_processes()
|
||||||
assert proc.last_exit_signal == 9
|
assert proc.last_exit_signal == 9
|
||||||
assert proc.last_exit_code is None
|
assert proc.last_exit_code is None
|
||||||
|
|
||||||
|
|
||||||
def test_reap_no_children():
|
def test_reap_no_children():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
with mock.patch("os.waitpid", side_effect=ChildProcessError):
|
with mock.patch("os.waitpid", side_effect=ChildProcessError):
|
||||||
assert mgr.reap_processes() == []
|
assert manager.reap_processes() == []
|
||||||
|
|
||||||
|
|
||||||
def test_status_lists_all_companions():
|
def test_status_lists_all_companions():
|
||||||
mgr = make_manager("rq", "scheduler")
|
manager = make_manager("rq", "scheduler")
|
||||||
entries = mgr.status(now=100.0)
|
entries = manager.status(now=100.0)
|
||||||
assert {e["name"] for e in entries} == {"rq", "scheduler"}
|
assert {e["name"] for e in entries} == {"rq", "scheduler"}
|
||||||
assert all("state" in e and "description" in e for e in entries)
|
assert all("state" in e and "description" in e for e in entries)
|
||||||
|
|
||||||
|
|
||||||
def test_handle_command_status():
|
def test_handle_command_status():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
resp = mgr.handle_command({"cmd": "status"})
|
resp = manager.handle_command({"cmd": "status"})
|
||||||
assert resp["ok"] is True
|
assert resp["ok"] is True
|
||||||
assert resp["companions"][0]["name"] == "rq"
|
assert resp["companions"][0]["name"] == "rq"
|
||||||
|
|
||||||
|
|
||||||
def test_handle_command_start_routes():
|
def test_handle_command_start_routes():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
with mock.patch.object(mgr, "start_process",
|
with mock.patch.object(manager, "start_process",
|
||||||
return_value=(True, "rq started")) as sp:
|
return_value=(True, "rq started")) as start_mock:
|
||||||
resp = mgr.handle_command({"cmd": "start", "name": "rq"})
|
resp = manager.handle_command({"cmd": "start", "name": "rq"})
|
||||||
sp.assert_called_once_with("rq")
|
start_mock.assert_called_once_with("rq")
|
||||||
assert resp == {"ok": True, "message": "rq started"}
|
assert resp == {"ok": True, "message": "rq started"}
|
||||||
|
|
||||||
|
|
||||||
def test_handle_command_stop_and_restart_route():
|
def test_handle_command_stop_and_restart_route():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
with mock.patch.object(mgr, "stop_process", return_value=(True, "s")) as st, \
|
with mock.patch.object(manager, "stop_process", return_value=(True, "s")) as stop_mock, \
|
||||||
mock.patch.object(mgr, "restart_process", return_value=(True, "r")) as rt:
|
mock.patch.object(manager, "restart_process", return_value=(True, "r")) as restart_mock:
|
||||||
mgr.handle_command({"cmd": "stop", "name": "rq"})
|
manager.handle_command({"cmd": "stop", "name": "rq"})
|
||||||
mgr.handle_command({"cmd": "restart", "name": "rq"})
|
manager.handle_command({"cmd": "restart", "name": "rq"})
|
||||||
st.assert_called_once_with("rq")
|
stop_mock.assert_called_once_with("rq")
|
||||||
rt.assert_called_once_with("rq")
|
restart_mock.assert_called_once_with("rq")
|
||||||
|
|
||||||
|
|
||||||
def test_handle_command_missing_name():
|
def test_handle_command_missing_name():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
with pytest.raises(CommandError):
|
with pytest.raises(CommandError):
|
||||||
mgr.handle_command({"cmd": "start"})
|
manager.handle_command({"cmd": "start"})
|
||||||
|
|
||||||
|
|
||||||
def test_handle_command_unknown():
|
def test_handle_command_unknown():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
with pytest.raises(CommandError):
|
with pytest.raises(CommandError):
|
||||||
mgr.handle_command({"cmd": "reread"})
|
manager.handle_command({"cmd": "reread"})
|
||||||
|
|
||||||
|
|
||||||
def cfg(name, **kw):
|
def make_config(name, **kwargs):
|
||||||
return CompanionConfig(name=name, target=lambda: None, **kw)
|
return CompanionConfig(name=name, target=lambda: None, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def test_reread_adds_new():
|
def test_reread_adds_new():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
new = [cfg("rq"), cfg("scheduler")]
|
new = [make_config("rq"), make_config("scheduler")]
|
||||||
with mock.patch("os.fork", return_value=10):
|
with mock.patch("os.fork", return_value=10):
|
||||||
result = mgr.reread_config(new)
|
result = manager.reread_config(new)
|
||||||
assert result["added"] == ["scheduler"]
|
assert result["added"] == ["scheduler"]
|
||||||
assert "scheduler" in mgr.processes
|
assert "scheduler" in manager.processes
|
||||||
assert mgr.processes["scheduler"].state == State.STARTING
|
assert manager.processes["scheduler"].state == State.STARTING
|
||||||
|
|
||||||
|
|
||||||
def test_reread_removes_missing():
|
def test_reread_removes_missing():
|
||||||
mgr = make_manager("rq", "scheduler")
|
manager = make_manager("rq", "scheduler")
|
||||||
mgr.processes["scheduler"].state = State.RUNNING
|
manager.processes["scheduler"].state = State.RUNNING
|
||||||
mgr.processes["scheduler"].pid = 11
|
manager.processes["scheduler"].pid = 11
|
||||||
with mock.patch("os.kill"):
|
with mock.patch("os.kill"):
|
||||||
result = mgr.reread_config([cfg("rq")])
|
result = manager.reread_config([make_config("rq")])
|
||||||
assert result["removed"] == ["scheduler"]
|
assert result["removed"] == ["scheduler"]
|
||||||
assert "scheduler" not in mgr.processes
|
assert "scheduler" not in manager.processes
|
||||||
|
|
||||||
|
|
||||||
def test_reread_restarts_changed():
|
def test_reread_restarts_changed():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
mgr.processes["rq"].state = State.RUNNING
|
manager.processes["rq"].state = State.RUNNING
|
||||||
mgr.processes["rq"].pid = 12
|
manager.processes["rq"].pid = 12
|
||||||
changed = cfg("rq", env={"X": "1"}) # different hash
|
changed = make_config("rq", env={"X": "1"}) # different hash
|
||||||
with mock.patch("os.kill"):
|
with mock.patch("os.kill"):
|
||||||
result = mgr.reread_config([changed])
|
result = manager.reread_config([changed])
|
||||||
assert result["restarted"] == ["rq"]
|
assert result["restarted"] == ["rq"]
|
||||||
assert mgr.processes["rq"].config is changed
|
assert manager.processes["rq"].config is changed
|
||||||
assert mgr.processes["rq"].state == State.STOPPING
|
assert manager.processes["rq"].state == State.STOPPING
|
||||||
|
|
||||||
|
|
||||||
def test_reread_changed_manual_stop_keeps_stopped():
|
def test_reread_changed_manual_stop_keeps_stopped():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
proc = mgr.processes["rq"]
|
proc = manager.processes["rq"]
|
||||||
proc.manual_stop = True
|
proc.manual_stop = True
|
||||||
proc.state = State.STOPPED
|
proc.state = State.STOPPED
|
||||||
changed = cfg("rq", env={"X": "1"})
|
changed = make_config("rq", env={"X": "1"})
|
||||||
result = mgr.reread_config([changed])
|
result = manager.reread_config([changed])
|
||||||
assert result["unchanged"] == ["rq"]
|
assert result["unchanged"] == ["rq"]
|
||||||
assert proc.config is changed and proc.state == State.STOPPED
|
assert proc.config is changed and proc.state == State.STOPPED
|
||||||
|
|
||||||
|
|
||||||
def test_reread_unchanged_noop():
|
def test_reread_unchanged_noop():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
same = mgr.processes["rq"].config
|
same = manager.processes["rq"].config
|
||||||
result = mgr.reread_config([same])
|
result = manager.reread_config([same])
|
||||||
assert result["unchanged"] == ["rq"]
|
assert result["unchanged"] == ["rq"]
|
||||||
assert result["restarted"] == []
|
assert result["restarted"] == []
|
||||||
|
|
||||||
|
|
||||||
def test_reread_duplicate_name_keeps_old():
|
def test_reread_duplicate_name_keeps_old():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
result = mgr.reread_config([cfg("rq"), cfg("rq")])
|
result = manager.reread_config([make_config("rq"), make_config("rq")])
|
||||||
assert result["ok"] is False and result["kept_old_config"] is True
|
assert result["ok"] is False and result["kept_old_config"] is True
|
||||||
assert "duplicate" in result["error"]
|
assert "duplicate" in result["error"]
|
||||||
|
|
||||||
|
|
||||||
def test_handle_command_reread_no_loader():
|
def test_handle_command_reread_no_loader():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
with pytest.raises(CommandError):
|
with pytest.raises(CommandError):
|
||||||
mgr.handle_command({"cmd": "reread"})
|
manager.handle_command({"cmd": "reread"})
|
||||||
|
|
||||||
|
|
||||||
def test_handle_command_reread_runs_loader():
|
def test_handle_command_reread_runs_loader():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
mgr.config_loader = lambda: [mgr.processes["rq"].config]
|
manager.config_loader = lambda: [manager.processes["rq"].config]
|
||||||
resp = mgr.handle_command({"cmd": "reread"})
|
resp = manager.handle_command({"cmd": "reread"})
|
||||||
assert resp["ok"] is True and resp["unchanged"] == ["rq"]
|
assert resp["ok"] is True and resp["unchanged"] == ["rq"]
|
||||||
|
|
||||||
|
|
||||||
def test_handle_command_reread_bad_config():
|
def test_handle_command_reread_bad_config():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
def boom():
|
def boom():
|
||||||
raise ValueError("duplicate companion name rq")
|
raise ValueError("duplicate companion name rq")
|
||||||
mgr.config_loader = boom
|
manager.config_loader = boom
|
||||||
resp = mgr.handle_command({"cmd": "reread"})
|
resp = manager.handle_command({"cmd": "reread"})
|
||||||
assert resp["ok"] is False and resp["kept_old_config"] is True
|
assert resp["ok"] is False and resp["kept_old_config"] is True
|
||||||
|
|
||||||
|
|
||||||
def test_start_process_stopped_spawns():
|
def test_start_process_stopped_spawns():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
proc = mgr.processes["rq"]
|
proc = manager.processes["rq"]
|
||||||
with mock.patch("os.fork", return_value=70) as fork:
|
with mock.patch("os.fork", return_value=70) as fork:
|
||||||
ok, _ = mgr.start_process("rq")
|
ok, _ = manager.start_process("rq")
|
||||||
fork.assert_called_once()
|
fork.assert_called_once()
|
||||||
assert ok and proc.state == State.STARTING and proc.manual_stop is False
|
assert ok and proc.state == State.STARTING and proc.manual_stop is False
|
||||||
|
|
||||||
|
|
||||||
def test_start_process_backoff_cancels_retry():
|
def test_start_process_backoff_cancels_retry():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
proc = mgr.processes["rq"]
|
proc = manager.processes["rq"]
|
||||||
proc.state = State.BACKOFF
|
proc.state = State.BACKOFF
|
||||||
proc.next_retry_at = 999.0
|
proc.next_retry_at = 999.0
|
||||||
proc.manual_stop = True
|
proc.manual_stop = True
|
||||||
with mock.patch("os.fork", return_value=71):
|
with mock.patch("os.fork", return_value=71):
|
||||||
ok, _ = mgr.start_process("rq")
|
ok, _ = manager.start_process("rq")
|
||||||
assert ok and proc.state == State.STARTING
|
assert ok and proc.state == State.STARTING
|
||||||
assert proc.next_retry_at is None and proc.manual_stop is False
|
assert proc.next_retry_at is None and proc.manual_stop is False
|
||||||
|
|
||||||
|
|
||||||
def test_start_process_running_is_noop():
|
def test_start_process_running_is_noop():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
mgr.processes["rq"].state = State.RUNNING
|
manager.processes["rq"].state = State.RUNNING
|
||||||
with mock.patch("os.fork") as fork:
|
with mock.patch("os.fork") as fork:
|
||||||
ok, _ = mgr.start_process("rq")
|
ok, _ = manager.start_process("rq")
|
||||||
assert ok
|
assert ok
|
||||||
fork.assert_not_called()
|
fork.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_start_process_stopping_rejected():
|
def test_start_process_stopping_rejected():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
mgr.processes["rq"].state = State.STOPPING
|
manager.processes["rq"].state = State.STOPPING
|
||||||
ok, msg = mgr.start_process("rq")
|
ok, msg = manager.start_process("rq")
|
||||||
assert not ok and "stopping" in msg
|
assert not ok and "stopping" in msg
|
||||||
|
|
||||||
|
|
||||||
def test_start_process_unknown():
|
def test_start_process_unknown():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
ok, _ = mgr.start_process("nope")
|
ok, _ = manager.start_process("nope")
|
||||||
assert not ok
|
assert not ok
|
||||||
|
|
||||||
|
|
||||||
def test_stop_process_running_signals_and_stopping():
|
def test_stop_process_running_signals_and_stopping():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
proc = mgr.processes["rq"]
|
proc = manager.processes["rq"]
|
||||||
proc.state = State.RUNNING
|
proc.state = State.RUNNING
|
||||||
proc.pid = 80
|
proc.pid = 80
|
||||||
proc.config.stop_timeout = 60
|
proc.config.stop_timeout = 60
|
||||||
with mock.patch("os.kill") as kill:
|
with mock.patch("os.kill") as kill:
|
||||||
ok, _ = mgr.stop_process("rq", now=200.0)
|
ok, _ = manager.stop_process("rq", now=200.0)
|
||||||
kill.assert_called_once_with(80, signal.SIGTERM)
|
kill.assert_called_once_with(80, signal.SIGTERM)
|
||||||
assert ok and proc.state == State.STOPPING
|
assert ok and proc.state == State.STOPPING
|
||||||
assert proc.manual_stop is True and proc.stop_deadline == 260.0
|
assert proc.manual_stop is True and proc.stop_deadline == 260.0
|
||||||
|
|
||||||
|
|
||||||
def test_stop_process_backoff_to_stopped():
|
def test_stop_process_backoff_to_stopped():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
proc = mgr.processes["rq"]
|
proc = manager.processes["rq"]
|
||||||
proc.state = State.BACKOFF
|
proc.state = State.BACKOFF
|
||||||
proc.next_retry_at = 999.0
|
proc.next_retry_at = 999.0
|
||||||
with mock.patch("os.kill") as kill:
|
with mock.patch("os.kill") as kill:
|
||||||
ok, _ = mgr.stop_process("rq")
|
ok, _ = manager.stop_process("rq")
|
||||||
kill.assert_not_called()
|
kill.assert_not_called()
|
||||||
assert ok and proc.state == State.STOPPED
|
assert ok and proc.state == State.STOPPED
|
||||||
assert proc.next_retry_at is None and proc.manual_stop is True
|
assert proc.next_retry_at is None and proc.manual_stop is True
|
||||||
|
|
||||||
|
|
||||||
def test_stop_process_already_stopped():
|
def test_stop_process_already_stopped():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
with mock.patch("os.kill") as kill:
|
with mock.patch("os.kill") as kill:
|
||||||
ok, _ = mgr.stop_process("rq")
|
ok, _ = manager.stop_process("rq")
|
||||||
kill.assert_not_called()
|
kill.assert_not_called()
|
||||||
assert ok and mgr.processes["rq"].manual_stop is True
|
assert ok and manager.processes["rq"].manual_stop is True
|
||||||
|
|
||||||
|
|
||||||
def test_stop_process_unknown():
|
def test_stop_process_unknown():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
ok, _ = mgr.stop_process("nope")
|
ok, _ = manager.stop_process("nope")
|
||||||
assert not ok
|
assert not ok
|
||||||
|
|
||||||
|
|
||||||
@ -349,14 +349,14 @@ def test_signal_number_rejects_bad():
|
|||||||
|
|
||||||
|
|
||||||
def test_restart_process_running_stops_with_reload_timeout():
|
def test_restart_process_running_stops_with_reload_timeout():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
proc = mgr.processes["rq"]
|
proc = manager.processes["rq"]
|
||||||
proc.state = State.RUNNING
|
proc.state = State.RUNNING
|
||||||
proc.pid = 90
|
proc.pid = 90
|
||||||
proc.config.reload_timeout = 30
|
proc.config.reload_timeout = 30
|
||||||
proc.manual_stop = True
|
proc.manual_stop = True
|
||||||
with mock.patch("os.kill") as kill:
|
with mock.patch("os.kill") as kill:
|
||||||
ok, _ = mgr.restart_process("rq", now=300.0)
|
ok, _ = manager.restart_process("rq", now=300.0)
|
||||||
kill.assert_called_once_with(90, signal.SIGTERM)
|
kill.assert_called_once_with(90, signal.SIGTERM)
|
||||||
assert ok and proc.state == State.STOPPING
|
assert ok and proc.state == State.STOPPING
|
||||||
assert proc.restart_pending is True and proc.stop_deadline == 330.0
|
assert proc.restart_pending is True and proc.stop_deadline == 330.0
|
||||||
@ -364,14 +364,14 @@ def test_restart_process_running_stops_with_reload_timeout():
|
|||||||
|
|
||||||
|
|
||||||
def test_restart_pending_reap_respawns_immediately():
|
def test_restart_pending_reap_respawns_immediately():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
proc = mgr.processes["rq"]
|
proc = manager.processes["rq"]
|
||||||
proc.state = State.STOPPING
|
proc.state = State.STOPPING
|
||||||
proc.restart_pending = True
|
proc.restart_pending = True
|
||||||
proc.pid = 91
|
proc.pid = 91
|
||||||
with mock.patch("os.waitpid", side_effect=[(91, 0), (0, 0)]), \
|
with mock.patch("os.waitpid", side_effect=[(91, 0), (0, 0)]), \
|
||||||
mock.patch("os.fork", return_value=92):
|
mock.patch("os.fork", return_value=92):
|
||||||
mgr.reap_processes()
|
manager.reap_processes()
|
||||||
assert proc.state == State.STARTING
|
assert proc.state == State.STARTING
|
||||||
assert proc.pid == 92
|
assert proc.pid == 92
|
||||||
assert proc.restart_pending is False
|
assert proc.restart_pending is False
|
||||||
@ -379,90 +379,90 @@ def test_restart_pending_reap_respawns_immediately():
|
|||||||
|
|
||||||
|
|
||||||
def test_restart_process_stopped_starts_now():
|
def test_restart_process_stopped_starts_now():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
proc = mgr.processes["rq"]
|
proc = manager.processes["rq"]
|
||||||
with mock.patch("os.fork", return_value=93), mock.patch("os.kill") as kill:
|
with mock.patch("os.fork", return_value=93), mock.patch("os.kill") as kill:
|
||||||
ok, _ = mgr.restart_process("rq")
|
ok, _ = manager.restart_process("rq")
|
||||||
kill.assert_not_called()
|
kill.assert_not_called()
|
||||||
assert ok and proc.state == State.STARTING
|
assert ok and proc.state == State.STARTING
|
||||||
|
|
||||||
|
|
||||||
def test_restart_process_backoff_starts_now():
|
def test_restart_process_backoff_starts_now():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
proc = mgr.processes["rq"]
|
proc = manager.processes["rq"]
|
||||||
proc.state = State.BACKOFF
|
proc.state = State.BACKOFF
|
||||||
proc.next_retry_at = 999.0
|
proc.next_retry_at = 999.0
|
||||||
with mock.patch("os.fork", return_value=94):
|
with mock.patch("os.fork", return_value=94):
|
||||||
ok, _ = mgr.restart_process("rq")
|
ok, _ = manager.restart_process("rq")
|
||||||
assert ok and proc.state == State.STARTING and proc.next_retry_at is None
|
assert ok and proc.state == State.STARTING and proc.next_retry_at is None
|
||||||
|
|
||||||
|
|
||||||
def test_restart_process_stopping_rejected():
|
def test_restart_process_stopping_rejected():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
mgr.processes["rq"].state = State.STOPPING
|
manager.processes["rq"].state = State.STOPPING
|
||||||
ok, msg = mgr.restart_process("rq")
|
ok, msg = manager.restart_process("rq")
|
||||||
assert not ok and "stopping" in msg
|
assert not ok and "stopping" in msg
|
||||||
|
|
||||||
|
|
||||||
def test_manual_stop_preserved_through_exit():
|
def test_manual_stop_preserved_through_exit():
|
||||||
# stop a running companion, then reap its child: it must settle in STOPPED
|
# stop a running companion, then reap its child: it must settle in STOPPED
|
||||||
# with manual_stop still set so it is not auto-restarted.
|
# with manual_stop still set so it is not auto-restarted.
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
proc = mgr.processes["rq"]
|
proc = manager.processes["rq"]
|
||||||
proc.state = State.RUNNING
|
proc.state = State.RUNNING
|
||||||
proc.pid = 60
|
proc.pid = 60
|
||||||
with mock.patch("os.kill"):
|
with mock.patch("os.kill"):
|
||||||
mgr.stop_process("rq", now=10.0)
|
manager.stop_process("rq", now=10.0)
|
||||||
with mock.patch("os.waitpid", side_effect=[(60, 0), (0, 0)]), \
|
with mock.patch("os.waitpid", side_effect=[(60, 0), (0, 0)]), \
|
||||||
mock.patch("os.fork") as fork:
|
mock.patch("os.fork") as fork:
|
||||||
mgr.reap_processes()
|
manager.reap_processes()
|
||||||
fork.assert_not_called()
|
fork.assert_not_called()
|
||||||
assert proc.state == State.STOPPED and proc.manual_stop is True
|
assert proc.state == State.STOPPED and proc.manual_stop is True
|
||||||
|
|
||||||
|
|
||||||
def test_start_clears_manual_stop():
|
def test_start_clears_manual_stop():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
proc = mgr.processes["rq"]
|
proc = manager.processes["rq"]
|
||||||
proc.manual_stop = True
|
proc.manual_stop = True
|
||||||
with mock.patch("os.fork", return_value=61):
|
with mock.patch("os.fork", return_value=61):
|
||||||
mgr.start_process("rq")
|
manager.start_process("rq")
|
||||||
assert proc.manual_stop is False
|
assert proc.manual_stop is False
|
||||||
|
|
||||||
|
|
||||||
def test_spawn_does_not_touch_manual_stop():
|
def test_spawn_does_not_touch_manual_stop():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
proc = mgr.processes["rq"]
|
proc = manager.processes["rq"]
|
||||||
proc.manual_stop = True
|
proc.manual_stop = True
|
||||||
with mock.patch("os.fork", return_value=62):
|
with mock.patch("os.fork", return_value=62):
|
||||||
mgr.spawn_process(proc)
|
manager.spawn_process(proc)
|
||||||
assert proc.manual_stop is True
|
assert proc.manual_stop is True
|
||||||
|
|
||||||
|
|
||||||
def test_handle_exit_unexpected_backoff():
|
def test_handle_exit_unexpected_backoff():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
proc = mgr.processes["rq"]
|
proc = manager.processes["rq"]
|
||||||
proc.restart_delay = 5
|
proc.restart_delay = 5
|
||||||
mgr.handle_exit(proc, now=100.0)
|
manager.handle_exit(proc, now=100.0)
|
||||||
assert proc.state == State.BACKOFF
|
assert proc.state == State.BACKOFF
|
||||||
assert proc.next_retry_at == 105.0
|
assert proc.next_retry_at == 105.0
|
||||||
|
|
||||||
|
|
||||||
def test_handle_exit_manual_stop_stays_stopped():
|
def test_handle_exit_manual_stop_stays_stopped():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
proc = mgr.processes["rq"]
|
proc = manager.processes["rq"]
|
||||||
proc.manual_stop = True
|
proc.manual_stop = True
|
||||||
mgr.handle_exit(proc, now=100.0)
|
manager.handle_exit(proc, now=100.0)
|
||||||
assert proc.state == State.STOPPED
|
assert proc.state == State.STOPPED
|
||||||
assert proc.next_retry_at is None
|
assert proc.next_retry_at is None
|
||||||
|
|
||||||
|
|
||||||
def test_retry_backoff_respawns_when_due():
|
def test_retry_backoff_respawns_when_due():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
proc = mgr.processes["rq"]
|
proc = manager.processes["rq"]
|
||||||
proc.state = State.BACKOFF
|
proc.state = State.BACKOFF
|
||||||
proc.next_retry_at = 100.0
|
proc.next_retry_at = 100.0
|
||||||
with mock.patch("os.fork", return_value=555):
|
with mock.patch("os.fork", return_value=555):
|
||||||
retried = mgr.retry_backoff(now=101.0)
|
retried = manager.retry_backoff(now=101.0)
|
||||||
assert retried == [proc]
|
assert retried == [proc]
|
||||||
assert proc.restart_count == 1
|
assert proc.restart_count == 1
|
||||||
assert proc.state == State.STARTING
|
assert proc.state == State.STARTING
|
||||||
@ -470,59 +470,59 @@ def test_retry_backoff_respawns_when_due():
|
|||||||
|
|
||||||
|
|
||||||
def test_retry_backoff_waits_until_due():
|
def test_retry_backoff_waits_until_due():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
proc = mgr.processes["rq"]
|
proc = manager.processes["rq"]
|
||||||
proc.state = State.BACKOFF
|
proc.state = State.BACKOFF
|
||||||
proc.next_retry_at = 100.0
|
proc.next_retry_at = 100.0
|
||||||
assert mgr.retry_backoff(now=99.0) == []
|
assert manager.retry_backoff(now=99.0) == []
|
||||||
assert proc.state == State.BACKOFF
|
assert proc.state == State.BACKOFF
|
||||||
|
|
||||||
|
|
||||||
def test_reap_unexpected_exit_enters_backoff():
|
def test_reap_unexpected_exit_enters_backoff():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
proc = mgr.processes["rq"]
|
proc = manager.processes["rq"]
|
||||||
proc.pid = 4321
|
proc.pid = 4321
|
||||||
with mock.patch("os.waitpid", side_effect=[(4321, 1 << 8), (0, 0)]):
|
with mock.patch("os.waitpid", side_effect=[(4321, 1 << 8), (0, 0)]):
|
||||||
mgr.reap_processes()
|
manager.reap_processes()
|
||||||
assert proc.state == State.BACKOFF
|
assert proc.state == State.BACKOFF
|
||||||
assert proc.next_retry_at is not None
|
assert proc.next_retry_at is not None
|
||||||
|
|
||||||
|
|
||||||
def test_promote_running_after_startsecs():
|
def test_promote_running_after_startsecs():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
proc = mgr.processes["rq"]
|
proc = manager.processes["rq"]
|
||||||
proc.config.startsecs = 1
|
proc.config.startsecs = 1
|
||||||
proc.state = State.STARTING
|
proc.state = State.STARTING
|
||||||
proc.started_at = 100.0
|
proc.started_at = 100.0
|
||||||
promoted = mgr.promote_running(now=101.5)
|
promoted = manager.promote_running(now=101.5)
|
||||||
assert promoted == [proc]
|
assert promoted == [proc]
|
||||||
assert proc.state == State.RUNNING
|
assert proc.state == State.RUNNING
|
||||||
|
|
||||||
|
|
||||||
def test_promote_running_too_early():
|
def test_promote_running_too_early():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
proc = mgr.processes["rq"]
|
proc = manager.processes["rq"]
|
||||||
proc.config.startsecs = 5
|
proc.config.startsecs = 5
|
||||||
proc.state = State.STARTING
|
proc.state = State.STARTING
|
||||||
proc.started_at = 100.0
|
proc.started_at = 100.0
|
||||||
assert mgr.promote_running(now=102.0) == []
|
assert manager.promote_running(now=102.0) == []
|
||||||
assert proc.state == State.STARTING
|
assert proc.state == State.STARTING
|
||||||
|
|
||||||
|
|
||||||
def test_promote_running_ignores_non_starting():
|
def test_promote_running_ignores_non_starting():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
proc = mgr.processes["rq"]
|
proc = manager.processes["rq"]
|
||||||
proc.state = State.BACKOFF
|
proc.state = State.BACKOFF
|
||||||
proc.started_at = 100.0
|
proc.started_at = 100.0
|
||||||
assert mgr.promote_running(now=999.0) == []
|
assert manager.promote_running(now=999.0) == []
|
||||||
assert proc.state == State.BACKOFF
|
assert proc.state == State.BACKOFF
|
||||||
|
|
||||||
|
|
||||||
def test_spawn_parent_records_pid_and_starting():
|
def test_spawn_parent_records_pid_and_starting():
|
||||||
mgr = make_manager("rq")
|
manager = make_manager("rq")
|
||||||
proc = mgr.processes["rq"]
|
proc = manager.processes["rq"]
|
||||||
with mock.patch("os.fork", return_value=4321):
|
with mock.patch("os.fork", return_value=4321):
|
||||||
pid = mgr.spawn_process(proc)
|
pid = manager.spawn_process(proc)
|
||||||
assert pid == 4321
|
assert pid == 4321
|
||||||
assert proc.pid == 4321
|
assert proc.pid == 4321
|
||||||
assert proc.state == State.STARTING
|
assert proc.state == State.STARTING
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user