Skip to main content

appcore_supervisor/
supervisor_diagnostics.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: supervisor_diagnostics.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/07/24 13:18:47 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/07/24 13:18:47 by dnettoRaw
8//      ###########      S: 1.0.1-rc.8
9// =============================================================================
10
11//! Immutable diagnosis, health aggregation, and stop-state recording.
12
13use super::*;
14
15impl Supervisor {
16    /// Returns a deterministic diagnostic report without mutating services.
17    pub fn diagnose(&self) -> SupervisorDiagnosis {
18        let validation = self.validate();
19        SupervisorDiagnosis {
20            graph_valid: validation.is_ok(),
21            issues: validation
22                .err()
23                .map(|error| vec![error.to_string()])
24                .unwrap_or_default(),
25            services: self.snapshots(),
26            watchdog: self.watchdog_snapshot(now_ms()),
27            restart_executor: self.inner.restart_executor.snapshot(),
28        }
29    }
30
31    /// Returns current service snapshots in lexical order.
32    pub fn snapshots(&self) -> Vec<crate::ServiceSnapshot> {
33        let Ok(services) = self.inner.services.read() else {
34            return Vec::new();
35        };
36        let Ok(records) = self.inner.records.lock() else {
37            return Vec::new();
38        };
39        services
40            .iter()
41            .map(|(name, service)| service_snapshot(name, service, records.get(name)))
42            .collect()
43    }
44
45    /// Returns retained lifecycle events without removing them.
46    pub fn events(&self) -> Vec<SupervisorEvent> {
47        self.inner
48            .events
49            .lock()
50            .map(|events| events.iter().cloned().collect())
51            .unwrap_or_default()
52    }
53
54    /// Reports whether graph, watchdog, executor, and critical services are healthy.
55    pub fn is_healthy(&self, timestamp_ms: u64) -> bool {
56        self.validate().is_ok()
57            && self.watchdog_snapshot(timestamp_ms).is_healthy()
58            && self.inner.restart_executor.snapshot().healthy
59            && self.critical_services_healthy()
60    }
61
62    pub(super) fn critical_services_healthy(&self) -> bool {
63        self.snapshots().iter().all(|service| {
64            !service.enabled
65                || !service.critical
66                || matches!(
67                    service.health,
68                    ServiceHealth::Ready | ServiceHealth::Healthy
69                ) && !service.quarantined
70        })
71    }
72
73    pub(super) fn watchdog_snapshot(&self, timestamp_ms: u64) -> WatchdogSnapshot {
74        let watchdog = &self.inner.watchdog;
75        WatchdogSnapshot {
76            state: watchdog.state(),
77            last_reconcile_at_ms: watchdog.last_reconcile_at_ms(),
78            last_progress_at_ms: watchdog.last_progress_at_ms(),
79            reconcile_sequence: watchdog.reconcile_sequence(),
80            stalled_for_ms: watchdog.stalled_for_ms(timestamp_ms),
81            critical_services_healthy: self.critical_services_healthy(),
82            enabled: watchdog.config().enabled,
83            stall_timeout_ms: watchdog.config().stall_timeout_ms,
84        }
85    }
86
87    pub(super) fn record_stopped(&self, name: &str, timestamp_ms: u64) -> SupervisorResult<()> {
88        self.update_record(name, ServiceHealth::Unknown, ServiceRuntimeState::Stopped)?;
89        self.emit(
90            name,
91            SupervisorEventKind::ServiceStopped,
92            timestamp_ms,
93            self.restart_attempt(name),
94            ("Running", "Stopped"),
95            "lifecycle_stop",
96        );
97        Ok(())
98    }
99
100    pub(super) fn record_stop_failure(
101        &self,
102        service: &Arc<dyn ManagedService>,
103        timestamp_ms: u64,
104    ) -> SupervisorResult<()> {
105        let name = service.descriptor().name();
106        if service.runtime_state() == ServiceRuntimeState::Orphaned {
107            return self.quarantine_orphan(
108                &RestartCompletion {
109                    service_id: name.to_string(),
110                    attempt: self.restart_attempt(name),
111                    outcome: RestartOutcome::Orphaned,
112                },
113                timestamp_ms,
114            );
115        }
116        self.update_record(name, ServiceHealth::Failed, service.runtime_state())?;
117        Ok(())
118    }
119}
120
121fn service_snapshot(
122    name: &str,
123    service: &Arc<dyn ManagedService>,
124    record: Option<&RuntimeRecord>,
125) -> crate::ServiceSnapshot {
126    let activation = service.descriptor().activation();
127    let runtime_state = record
128        .map(|record| record.runtime_state)
129        .unwrap_or_else(|| service.runtime_state());
130    crate::ServiceSnapshot {
131        name: name.to_string(),
132        health: record
133            .and_then(|record| record.health)
134            .unwrap_or_else(|| service.health()),
135        dependencies: service
136            .descriptor()
137            .dependencies()
138            .iter()
139            .map(|dependency| dependency.service_id().to_string())
140            .collect(),
141        dependency_requirements: service
142            .descriptor()
143            .dependencies()
144            .iter()
145            .map(|dependency| format!("{:?}", dependency.requirement()))
146            .collect(),
147        activation,
148        enabled: activation.is_enabled(),
149        configured: activation.is_configured(),
150        running: runtime_state == ServiceRuntimeState::Running,
151        runtime_state,
152        restart_state: record
153            .map(|record| record.restart_state)
154            .unwrap_or(RestartState::None),
155        restart_count: record.map(|record| record.restart_count).unwrap_or(0),
156        operator_required: record
157            .map(|record| record.operator_required)
158            .unwrap_or(false),
159        quarantined: record.map(|record| record.quarantined).unwrap_or(false),
160        critical: service.descriptor().is_critical(),
161    }
162}