mirror of
https://github.com/frappe/gunicorn.git
synced 2026-07-07 13:11:30 +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)
|
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:
|
def format_response(command: str, data: dict) -> str:
|
||||||
"""
|
"""
|
||||||
Format response data based on command.
|
Format response data based on command.
|
||||||
@ -182,7 +240,9 @@ def format_response(command: str, data: dict) -> str:
|
|||||||
cmd_lower = command.lower().strip()
|
cmd_lower = command.lower().strip()
|
||||||
|
|
||||||
# Route to specific formatters
|
# 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)
|
return format_workers(data)
|
||||||
elif cmd_lower == "show dirty":
|
elif cmd_lower == "show dirty":
|
||||||
return format_dirty(data)
|
return format_dirty(data)
|
||||||
|
|||||||
@ -405,6 +405,83 @@ class CommandHandlers:
|
|||||||
|
|
||||||
return {"status": "shutting_down", "mode": mode}
|
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:
|
def help(self) -> dict:
|
||||||
"""
|
"""
|
||||||
Return list of available commands.
|
Return list of available commands.
|
||||||
@ -413,6 +490,7 @@ class CommandHandlers:
|
|||||||
Dictionary with commands and descriptions
|
Dictionary with commands and descriptions
|
||||||
"""
|
"""
|
||||||
commands = {
|
commands = {
|
||||||
|
"show all": "Show all processes (arbiter, web workers, dirty workers)",
|
||||||
"show workers": "List HTTP workers with their status",
|
"show workers": "List HTTP workers with their status",
|
||||||
"show dirty": "List dirty workers and apps",
|
"show dirty": "List dirty workers and apps",
|
||||||
"show config": "Show current effective configuration",
|
"show config": "Show current effective configuration",
|
||||||
|
|||||||
@ -242,11 +242,13 @@ class ControlSocketServer:
|
|||||||
def _handle_show(self, args: list) -> dict:
|
def _handle_show(self, args: list) -> dict:
|
||||||
"""Handle 'show' commands."""
|
"""Handle 'show' commands."""
|
||||||
if not args:
|
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()
|
target = args[0].lower()
|
||||||
|
|
||||||
if target == "workers":
|
if target == "all":
|
||||||
|
return self.handlers.show_all()
|
||||||
|
elif target == "workers":
|
||||||
return self.handlers.show_workers()
|
return self.handlers.show_workers()
|
||||||
elif target == "dirty":
|
elif target == "dirty":
|
||||||
return self.handlers.show_dirty()
|
return self.handlers.show_dirty()
|
||||||
|
|||||||
@ -356,6 +356,47 @@ class TestShutdown:
|
|||||||
mock_kill.assert_called_once_with(12345, signal.SIGINT)
|
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:
|
class TestHelp:
|
||||||
"""Tests for help command."""
|
"""Tests for help command."""
|
||||||
|
|
||||||
@ -368,6 +409,7 @@ class TestHelp:
|
|||||||
|
|
||||||
assert "commands" in result
|
assert "commands" in result
|
||||||
commands = result["commands"]
|
commands = result["commands"]
|
||||||
|
assert "show all" in commands
|
||||||
assert "show workers" in commands
|
assert "show workers" in commands
|
||||||
assert "worker add [N]" in commands
|
assert "worker add [N]" in commands
|
||||||
assert "reload" in commands
|
assert "reload" in commands
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user