mirror of
https://github.com/frappe/gunicorn.git
synced 2026-07-03 19:21:29 +08:00
feat(ctl): add 'show all' command for process overview
Displays complete hierarchy: arbiter PID, web workers with their PIDs/status, dirty arbiter PID, and dirty workers with their apps.
This commit is contained in:
parent
3963e850ea
commit
9f7000ff63
@ -168,6 +168,64 @@ def format_help(data: dict) -> str:
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def format_all(data: dict) -> str:
|
||||
"""Format show all output for display."""
|
||||
lines = []
|
||||
|
||||
# Arbiter
|
||||
arbiter = data.get("arbiter", {})
|
||||
lines.append("ARBITER (master)")
|
||||
lines.append(f" PID: {arbiter.get('pid', '?')}")
|
||||
lines.append("")
|
||||
|
||||
# Web workers
|
||||
web_workers = data.get("web_workers", [])
|
||||
lines.append(f"WEB WORKERS ({data.get('web_worker_count', 0)})")
|
||||
if web_workers:
|
||||
lines.append(f" {'PID':<10} {'AGE':<6} {'BOOTED':<8} {'LAST_BEAT'}")
|
||||
lines.append(f" {'-' * 38}")
|
||||
for w in web_workers:
|
||||
pid = w.get("pid", "?")
|
||||
age = w.get("age", "?")
|
||||
booted = "yes" if w.get("booted") else "no"
|
||||
hb = w.get("last_heartbeat")
|
||||
hb_str = f"{hb}s ago" if hb is not None else "n/a"
|
||||
lines.append(f" {pid:<10} {age:<6} {booted:<8} {hb_str}")
|
||||
else:
|
||||
lines.append(" (none)")
|
||||
lines.append("")
|
||||
|
||||
# Dirty arbiter
|
||||
dirty_arbiter = data.get("dirty_arbiter")
|
||||
if dirty_arbiter:
|
||||
lines.append("DIRTY ARBITER")
|
||||
lines.append(f" PID: {dirty_arbiter.get('pid', '?')}")
|
||||
lines.append("")
|
||||
|
||||
# Dirty workers
|
||||
dirty_workers = data.get("dirty_workers", [])
|
||||
lines.append(f"DIRTY WORKERS ({data.get('dirty_worker_count', 0)})")
|
||||
if dirty_workers:
|
||||
lines.append(f" {'PID':<10} {'AGE':<6} {'APPS':<30} {'LAST_BEAT'}")
|
||||
lines.append(f" {'-' * 58}")
|
||||
for w in dirty_workers:
|
||||
pid = w.get("pid", "?")
|
||||
age = w.get("age", "?")
|
||||
apps = ", ".join(w.get("apps", []))
|
||||
if len(apps) > 28:
|
||||
apps = apps[:25] + "..."
|
||||
hb = w.get("last_heartbeat")
|
||||
hb_str = f"{hb}s ago" if hb is not None else "n/a"
|
||||
lines.append(f" {pid:<10} {age:<6} {apps:<30} {hb_str}")
|
||||
else:
|
||||
lines.append(" (none)")
|
||||
else:
|
||||
lines.append("DIRTY ARBITER")
|
||||
lines.append(" (not running)")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def format_response(command: str, data: dict) -> str:
|
||||
"""
|
||||
Format response data based on command.
|
||||
@ -182,7 +240,9 @@ def format_response(command: str, data: dict) -> str:
|
||||
cmd_lower = command.lower().strip()
|
||||
|
||||
# Route to specific formatters
|
||||
if cmd_lower == "show workers":
|
||||
if cmd_lower == "show all":
|
||||
return format_all(data)
|
||||
elif cmd_lower == "show workers":
|
||||
return format_workers(data)
|
||||
elif cmd_lower == "show dirty":
|
||||
return format_dirty(data)
|
||||
|
||||
@ -405,6 +405,83 @@ class CommandHandlers:
|
||||
|
||||
return {"status": "shutting_down", "mode": mode}
|
||||
|
||||
def show_all(self) -> dict:
|
||||
"""
|
||||
Return overview of all processes (arbiter, web workers, dirty arbiter, dirty workers).
|
||||
|
||||
Returns:
|
||||
Dictionary with complete process hierarchy
|
||||
"""
|
||||
now = time.monotonic()
|
||||
|
||||
# Arbiter info
|
||||
arbiter_info = {
|
||||
"pid": self.arbiter.pid,
|
||||
"type": "arbiter",
|
||||
"role": "master",
|
||||
}
|
||||
|
||||
# Web workers (HTTP workers)
|
||||
web_workers = []
|
||||
for pid, worker in self.arbiter.WORKERS.items():
|
||||
try:
|
||||
last_update = worker.tmp.last_update()
|
||||
last_heartbeat = round(now - last_update, 2)
|
||||
except (OSError, ValueError):
|
||||
last_heartbeat = None
|
||||
|
||||
web_workers.append({
|
||||
"pid": pid,
|
||||
"type": "web",
|
||||
"age": worker.age,
|
||||
"booted": worker.booted,
|
||||
"last_heartbeat": last_heartbeat,
|
||||
})
|
||||
|
||||
# Sort by age
|
||||
web_workers.sort(key=lambda w: w["age"])
|
||||
|
||||
# Dirty arbiter and workers
|
||||
dirty_arbiter_info = None
|
||||
dirty_workers = []
|
||||
|
||||
if self.arbiter.dirty_arbiter_pid:
|
||||
dirty_arbiter_info = {
|
||||
"pid": self.arbiter.dirty_arbiter_pid,
|
||||
"type": "dirty_arbiter",
|
||||
"role": "dirty master",
|
||||
}
|
||||
|
||||
# Get dirty workers if we have access
|
||||
dirty_arbiter = getattr(self.arbiter, 'dirty_arbiter', None)
|
||||
if dirty_arbiter and hasattr(dirty_arbiter, 'workers'):
|
||||
for pid, worker in dirty_arbiter.workers.items():
|
||||
try:
|
||||
last_update = worker.tmp.last_update()
|
||||
last_heartbeat = round(now - last_update, 2)
|
||||
except (OSError, ValueError, AttributeError):
|
||||
last_heartbeat = None
|
||||
|
||||
dirty_workers.append({
|
||||
"pid": pid,
|
||||
"type": "dirty",
|
||||
"age": worker.age,
|
||||
"apps": getattr(worker, 'app_paths', []),
|
||||
"booted": getattr(worker, 'booted', False),
|
||||
"last_heartbeat": last_heartbeat,
|
||||
})
|
||||
|
||||
dirty_workers.sort(key=lambda w: w["age"])
|
||||
|
||||
return {
|
||||
"arbiter": arbiter_info,
|
||||
"web_workers": web_workers,
|
||||
"web_worker_count": len(web_workers),
|
||||
"dirty_arbiter": dirty_arbiter_info,
|
||||
"dirty_workers": dirty_workers,
|
||||
"dirty_worker_count": len(dirty_workers),
|
||||
}
|
||||
|
||||
def help(self) -> dict:
|
||||
"""
|
||||
Return list of available commands.
|
||||
@ -413,6 +490,7 @@ class CommandHandlers:
|
||||
Dictionary with commands and descriptions
|
||||
"""
|
||||
commands = {
|
||||
"show all": "Show all processes (arbiter, web workers, dirty workers)",
|
||||
"show workers": "List HTTP workers with their status",
|
||||
"show dirty": "List dirty workers and apps",
|
||||
"show config": "Show current effective configuration",
|
||||
|
||||
@ -242,11 +242,13 @@ class ControlSocketServer:
|
||||
def _handle_show(self, args: list) -> dict:
|
||||
"""Handle 'show' commands."""
|
||||
if not args:
|
||||
raise ValueError("Missing show target (workers|dirty|config|stats|listeners)")
|
||||
raise ValueError("Missing show target (all|workers|dirty|config|stats|listeners)")
|
||||
|
||||
target = args[0].lower()
|
||||
|
||||
if target == "workers":
|
||||
if target == "all":
|
||||
return self.handlers.show_all()
|
||||
elif target == "workers":
|
||||
return self.handlers.show_workers()
|
||||
elif target == "dirty":
|
||||
return self.handlers.show_dirty()
|
||||
|
||||
@ -356,6 +356,47 @@ class TestShutdown:
|
||||
mock_kill.assert_called_once_with(12345, signal.SIGINT)
|
||||
|
||||
|
||||
class TestShowAll:
|
||||
"""Tests for show all command."""
|
||||
|
||||
def test_show_all_basic(self):
|
||||
"""Test show all command."""
|
||||
arbiter = MockArbiter()
|
||||
arbiter.WORKERS = {
|
||||
1001: MockWorker(1001, 1),
|
||||
1002: MockWorker(1002, 2),
|
||||
}
|
||||
handlers = CommandHandlers(arbiter)
|
||||
|
||||
result = handlers.show_all()
|
||||
|
||||
assert "arbiter" in result
|
||||
assert result["arbiter"]["pid"] == 12345
|
||||
assert result["arbiter"]["type"] == "arbiter"
|
||||
|
||||
assert "web_workers" in result
|
||||
assert result["web_worker_count"] == 2
|
||||
assert len(result["web_workers"]) == 2
|
||||
|
||||
assert "dirty_arbiter" in result
|
||||
assert result["dirty_arbiter"] is None
|
||||
|
||||
assert "dirty_workers" in result
|
||||
assert result["dirty_worker_count"] == 0
|
||||
|
||||
def test_show_all_with_dirty(self):
|
||||
"""Test show all with dirty arbiter running."""
|
||||
arbiter = MockArbiter()
|
||||
arbiter.dirty_arbiter_pid = 2000
|
||||
handlers = CommandHandlers(arbiter)
|
||||
|
||||
result = handlers.show_all()
|
||||
|
||||
assert result["dirty_arbiter"] is not None
|
||||
assert result["dirty_arbiter"]["pid"] == 2000
|
||||
assert result["dirty_arbiter"]["type"] == "dirty_arbiter"
|
||||
|
||||
|
||||
class TestHelp:
|
||||
"""Tests for help command."""
|
||||
|
||||
@ -368,6 +409,7 @@ class TestHelp:
|
||||
|
||||
assert "commands" in result
|
||||
commands = result["commands"]
|
||||
assert "show all" in commands
|
||||
assert "show workers" in commands
|
||||
assert "worker add [N]" in commands
|
||||
assert "reload" in commands
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user