File indexing completed on 2026-08-12 09:36:12
0001
0002 """
0003 Canary site-health agent.
0004
0005 An always-on agent built on the shared testbed agent infrastructure
0006 (`swf_common_lib.base_agent.BaseAgent`) that runs the site-canary
0007 assessment cycle on the platform host: the passive assessor over PanDA
0008 accounting and the policy evaluator over stored evidence (site-canary
0009 docs/SWF_INTEGRATION.md, bootstrap step 5). It grows the credentialed
0010 canary capabilities to come — probe submission, queue-status actuation —
0011 as handlers, in the prod-ops agent pattern
0012 (swf-epicprod docs/EPICPROD_OPS_AGENT.md).
0013
0014 It is event-driven, not polled. Requests arrive as JSON messages on an
0015 anycast control queue; each action is a `msg_type` dispatched to a
0016 `_handle_<msg_type>` method. The doer is the site-canary CLI itself —
0017 already a standalone committed tool — run as a bounded subprocess on
0018 BaseAgent's worker pool via ``run_in_background`` so the single receiver
0019 thread is never blocked. The hourly cadence is a cron-enqueued
0020 `assess_refresh` (scripts/enqueue-ops-message.py); the same command by
0021 hand is the on-demand trigger.
0022
0023 This is a system-level singleton (not a per-user testbed agent). It runs
0024 under a fixed 'canary' namespace (from canary.toml) so it is identifiable
0025 in the monitor and every caller addresses it explicitly, and is managed by
0026 systemd like the other platform singletons.
0027
0028 Capabilities:
0029 assess_refresh — run `canary assess --panda --write` (per-queue passive
0030 samples from PanDA accounting into the canary store),
0031 then `canary evaluate --write` (policy verdicts and
0032 health-state transitions over the stored evidence;
0033 recorded, not actuated).
0034 health_ping — liveness probe; replies 'pong' to reply_to.
0035 shutdown — deliberate stop; exits EXIT_DELIBERATE so systemd
0036 leaves the singleton down instead of restarting it.
0037 """
0038 import json
0039 import os
0040 import signal
0041 import subprocess
0042 import sys
0043 import time
0044
0045 from pathlib import Path
0046
0047 from swf_common_lib.base_agent import BaseAgent
0048
0049
0050 CANARY_QUEUE = os.environ.get("CANARY_OPS_QUEUE", "/queue/canary.ops")
0051
0052
0053
0054 ASSESS_TIMEOUT = int(os.environ.get("CANARY_ASSESS_TIMEOUT", "300"))
0055 EVALUATE_TIMEOUT = int(os.environ.get("CANARY_EVALUATE_TIMEOUT", "120"))
0056
0057
0058
0059
0060 CANARY_CONFIG = Path(__file__).resolve().parent / "canary.toml"
0061
0062
0063
0064
0065 EXIT_DELIBERATE = 100
0066
0067
0068 class CanaryAgent(BaseAgent):
0069 """Canary site-health agent — dispatches canary messages to handlers."""
0070
0071 KNOWN_TYPES = {"assess_refresh", "health_ping", "shutdown"}
0072
0073 def __init__(self):
0074
0075
0076
0077 super().__init__(agent_type="CANARY", subscription_queue=CANARY_QUEUE,
0078 config_path=str(CANARY_CONFIG))
0079 self._deliberate = False
0080
0081 def on_message(self, frame):
0082 message_data, msg_type = self.log_received_message(frame, known_types=self.KNOWN_TYPES)
0083 if message_data is None:
0084 return
0085 handler = getattr(self, f"_handle_{msg_type}", None)
0086 if handler is None:
0087 self.logger.warning(f"CANARY: no handler for msg_type '{msg_type}'")
0088 return
0089
0090
0091
0092
0093 try:
0094 handler(message_data)
0095 except Exception as e:
0096 self.logger.error(f"CANARY: handler '{msg_type}' raised: {e}")
0097
0098
0099
0100 def _handle_health_ping(self, m):
0101 """Liveness probe: reply 'pong' to the caller's reply_to queue."""
0102 reply_to = m.get("reply_to")
0103 if not reply_to:
0104 self.logger.warning("CANARY health_ping: no reply_to, dropping")
0105 return
0106 pong = {"msg_type": "pong", "agent": self.agent_name, "pid": self.pid}
0107 self.conn.send(destination=reply_to, body=json.dumps(pong))
0108 self.logger.info(f"CANARY health_ping -> pong to {reply_to}")
0109
0110 def _handle_shutdown(self, m):
0111 """Deliberate-shutdown back door: unwind through BaseAgent's normal
0112 SIGTERM path; main() then exits EXIT_DELIBERATE so systemd
0113 (RestartPreventExitStatus) leaves it stopped instead of restarting."""
0114 self.logger.warning(
0115 f"CANARY: deliberate shutdown requested by {m.get('sender', '?')}")
0116 self._deliberate = True
0117 os.kill(self.pid, signal.SIGTERM)
0118
0119 def _handle_assess_refresh(self, m):
0120 """Enqueue one assessment cycle on the worker pool — it blocks on the
0121 PanDA accounting query. Deduped so overlapping triggers (cron plus an
0122 on-demand run) never run two cycles at once."""
0123 self.run_in_background(
0124 self._do_assess_refresh, m,
0125 dedup_key="assess_refresh", label="assess_refresh")
0126
0127 def _do_assess_refresh(self, m):
0128 """Run the site-canary CLI doers: assess (write passive samples), then
0129 evaluate (record verdicts and health-state transitions)."""
0130 created_by = str(m.get("created_by") or "?")
0131 self.logger.info(f"CANARY assess_refresh: starting (by {created_by})")
0132 t0 = time.monotonic()
0133 if not self._run_doer(["assess", "--panda", "--write"], ASSESS_TIMEOUT):
0134 self._emit_complete(ok=False, stage="assess")
0135 return
0136 if not self._run_doer(["evaluate", "--write"], EVALUATE_TIMEOUT):
0137 self._emit_complete(ok=False, stage="evaluate")
0138 return
0139 elapsed = time.monotonic() - t0
0140 self.logger.info(f"CANARY assess_refresh done in {elapsed:.1f}s")
0141 self._emit_complete(ok=True)
0142
0143
0144
0145 def _run_doer(self, canary_args, timeout):
0146 """Run one site-canary CLI subprocess, bounded; relay its output and
0147 surface every failure. Returns True on success."""
0148 cmd = [sys.executable, "-m", "canary"] + canary_args
0149 name = canary_args[0]
0150 try:
0151 p = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
0152 except subprocess.TimeoutExpired:
0153 self.logger.error(f"CANARY {name} TIMEOUT after {timeout}s")
0154 return False
0155 for line in (p.stderr or "").splitlines():
0156 self.logger.info(f" canary {name}: {line}")
0157 if p.returncode != 0:
0158 tail = (p.stdout or "").strip().splitlines()[-1:] or ["(no output)"]
0159 self.logger.error(
0160 f"CANARY {name} FAILED rc={p.returncode}: {tail[0]}")
0161 return False
0162 return True
0163
0164 def _emit_complete(self, ok, stage=None):
0165 """Publish the cycle outcome to the SSE topic so pages can refresh
0166 live; a failed stage is named, never silent."""
0167 event = {"msg_type": "canary_assess_complete", "ok": ok}
0168 if stage:
0169 event["failed_stage"] = stage
0170 self.send_message('/topic/epictopic', event)
0171
0172
0173 def main():
0174 agent = CanaryAgent()
0175 agent.run()
0176
0177
0178 sys.exit(EXIT_DELIBERATE if agent._deliberate else 0)
0179
0180
0181 if __name__ == "__main__":
0182 main()