Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-08-12 09:36:12

0001 #!/usr/bin/env python3
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 # Anycast control queue: one consumer handles each request exactly once.
0050 CANARY_QUEUE = os.environ.get("CANARY_OPS_QUEUE", "/queue/canary.ops")
0051 
0052 # Backstops on the doer subprocesses. The assessor is one bounded PanDA
0053 # accounting query plus store writes; the evaluator reads the store only.
0054 ASSESS_TIMEOUT = int(os.environ.get("CANARY_ASSESS_TIMEOUT", "300"))
0055 EVALUATE_TIMEOUT = int(os.environ.get("CANARY_EVALUATE_TIMEOUT", "120"))
0056 
0057 # Dedicated namespace config shipped beside the agent. A fixed 'canary'
0058 # namespace makes the singleton identifiable in the monitor and lets callers
0059 # address it explicitly — every message to this agent carries namespace=canary.
0060 CANARY_CONFIG = Path(__file__).resolve().parent / "canary.toml"
0061 
0062 # Deliberate-shutdown sentinel: main() exits with this so systemd's
0063 # RestartPreventExitStatus knows the stop was on purpose and must not restart.
0064 # Distinct from 0 — a persistent agent exiting 0 unbidden is still a failure.
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         # System-level singleton (not a per-user testbed agent): its namespace
0075         # is the fixed 'canary' from CANARY_CONFIG, so it is identifiable in
0076         # the monitor and callers address it explicitly.
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:          # namespace-filtered — ignore
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         # Handlers return immediately: control messages act inline; work
0090         # messages validate here and enqueue their doer via run_in_background,
0091         # so the receiver thread is never blocked. The worker pool drives
0092         # PROCESSING state — no processing() wrap.
0093         try:
0094             handler(message_data)
0095         except Exception as e:
0096             self.logger.error(f"CANARY: handler '{msg_type}' raised: {e}")
0097 
0098     # -- handlers ------------------------------------------------------------
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)   # reuse BaseAgent's graceful unwind
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     # -- doers ---------------------------------------------------------------
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     # A deliberate bus 'shutdown' exits with the sentinel so systemd does not
0177     # restart it; any other exit is a failure and is restarted (burst-capped).
0178     sys.exit(EXIT_DELIBERATE if agent._deliberate else 0)
0179 
0180 
0181 if __name__ == "__main__":
0182     main()