SPB Git forge

spb/ka-guardian

Public
26commits 1branches 0releases
4.5 MBsize
maindefault branch
19 days agolast push
Python 57.1% Shell 13.8% CSS 12% JavaScript 10% HTML 7.2%

feat(orchestrateur): damper d événement de masse + fermeture des incidents retired

- Damper : si plus de mass_incident_threshold (20, policy) nouveaux incidents
  seraient créés d un coup pour un service, c est une panne GLOBALE de l app
  (sync mort, /api/stats cassé), pas autant de pannes individuelles — on ne
  crée rien et on alerte au journal/dashboard. Les sources encore cassées une
  fois la vague retombée reçoivent leurs incidents normalement (2026-08-24 :
  794 incidents jobka créés après ~17 h d arrêt de job-ka-sync).
- Statut retired (nouveau côté api-ka d10b2f6) : un incident actif dont la
  source est retirée de la supervision est fermé en abandoned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 24, 2026) parent 9ba737d

2 changed files +31 −6

modified orchestrator/main.py +29 −5
@@ -441,6 +441,7 @@ async def poll_once() -> None:
441 441 for service, block in mine.items():
442 442 if service not in SERVICES:
443 443 continue # service inconnu de la topologie: visible au dashboard, pas d'action
444 + to_create: list[tuple[str, str, dict[str, Any]]] = []
444 445 for conn in block.get("connectors", []):
445 446 source, status = conn["source"], conn["status"]
446 447 inc = active_incident(c, service, source)
@@ -457,11 +458,7 @@ async def poll_once() -> None:
457 458 (service, source)).fetchone()
458 459 if ab and now() - ab["updated"] < POLICY.get("abandoned_retry_hours", 168) * 3600:
459 460 continue
460 − iid = uuid.uuid4().hex[:10]
461 − c.execute("INSERT INTO incidents(id,service,source,status_detected,state,created,updated,detail) "
462 − "VALUES(?,?,?,?,?,?,?,?)",
463 − (iid, service, source, status, "open", now(), now(), jdump(conn)))
464 − incident_event(iid, service, source, "open", f"détecté {status}")
461 + to_create.append((source, status, conn))
465 462 elif status in trigger and inc is not None and inc["state"] in ("open", "cooldown"):
466 463 # Ne pas rafraîchir watching/fixing: `updated` sert de chrono
467 464 # à la fenêtre de surveillance et au cooldown.
@@ -474,6 +471,33 @@ async def poll_once() -> None:
474 471 elif inc["state"] in ("open", "cooldown"):
475 472 set_incident(c, inc["id"], state="self_healed", resolved=now())
476 473 incident_event(inc["id"], service, source, "self_healed", "revenu à ok sans intervention")
474 + elif status == "retired" and inc is not None and inc["state"] in ("open", "cooldown", "watching"):
475 + # api-ka a retiré la source de la supervision (désactivée
476 + # côté app ou disparue du journal) : plus rien à réparer.
477 + set_incident(c, inc["id"], state="abandoned")
478 + incident_event(inc["id"], service, source, "abandoned", "source retirée de la supervision api-ka")
479 +
480 + # Damper d'événement de masse : des centaines de sources qui
481 + # basculent d'un coup signalent une panne GLOBALE de l'app (sync
482 + # mort, /api/stats cassé), pas autant de pannes individuelles —
483 + # missionner source par source serait long et coûteux pour rien
484 + # (2026-08-24 : 794 incidents jobka créés après ~17 h d'arrêt de
485 + # job-ka-sync). On n'ouvre rien : la panne d'app se voit au
486 + # dashboard (bloc _app) et les sources encore cassées une fois la
487 + # vague retombée sous le seuil recevront leurs incidents.
488 + limit = POLICY.get("mass_incident_threshold", 20)
489 + if len(to_create) > limit:
490 + msg = (f"vague de {len(to_create)} connecteurs en panne sur {service} "
491 + f"(seuil {limit}) — panne globale probable de l'app, aucun incident créé")
492 + print(f"[poll] {msg}", flush=True)
493 + hub.publish_sync({"kind": "log", "level": "error", "msg": msg, "ts": now()})
494 + continue
495 + for source, status, conn in to_create:
496 + iid = uuid.uuid4().hex[:10]
497 + c.execute("INSERT INTO incidents(id,service,source,status_detected,state,created,updated,detail) "
498 + "VALUES(?,?,?,?,?,?,?,?)",
499 + (iid, service, source, status, "open", now(), now(), jdump(conn)))
500 + incident_event(iid, service, source, "open", f"détecté {status}")
477 501
478 502
479 503 async def tick() -> None:
modified topology.json +2 −1
@@ -180,6 +180,7 @@
180 180 "mission_timeout_seconds": 3600,
181 181 "effort_max_turns": 150,
182 182 "effort_timeout_seconds": 7200,
183 − "abandoned_retry_hours": 168
183 + "abandoned_retry_hours": 168,
184 + "mass_incident_threshold": 20
184 185 }
185 186 }
186 187