1use planarian_incident::divergent;
11use planarian_schema::spec::{precondition, predict};
12use planarian_schema::{
13 ChangeId, Confidence, DriverRef, EdgeKind, Expectation, Incident, ProbeResult, ProbeRole, SystemState, Timestamp,
14 Transition, UnitDefs, UnitId, UnitKind, UnitStatus, Value,
15};
16use std::collections::{BTreeMap, BTreeSet};
17
18pub const DIAGNOSTIC_EDGE_MIN: Confidence = Confidence(700);
19
20pub struct Inputs<'a> {
21 pub state: &'a SystemState,
22 pub defs: &'a UnitDefs,
23 pub failed_since: &'a BTreeMap<UnitId, Timestamp>,
25 pub known_drivers: &'a BTreeMap<UnitId, DriverRef>,
27 pub current_planarian_built: bool,
30 pub alternatives: &'a BTreeMap<String, Vec<Value>>,
33 pub lost_changes: &'a BTreeMap<UnitId, (ChangeId, BTreeMap<String, Value>)>,
36}
37
38#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct Candidate {
40 pub target: UnitId,
41 pub transition: Transition,
42 pub expected: Expectation,
43 pub reason: String,
44}
45
46fn diverges(u: &UnitId, i: &Inputs<'_>) -> bool {
47 let Some(def) = i.defs.get(u) else { return false };
48 i.state.expected(u, i.defs) && divergent(def.desired.status, &i.state.status(u, i.defs))
49}
50
51fn walkable(kind: &EdgeKind) -> bool {
52 match kind {
53 EdgeKind::Requires => true,
54 EdgeKind::Diagnostic { confidence } => *confidence >= DIAGNOSTIC_EDGE_MIN,
55 EdgeKind::Ordering | EdgeKind::Wants => false,
56 }
57}
58
59fn subgraph(target: &UnitId, i: &Inputs<'_>) -> BTreeMap<UnitId, Vec<UnitId>> {
62 let mut g: BTreeMap<UnitId, Vec<UnitId>> = BTreeMap::new();
63 let mut frontier = vec![target.clone()];
64 while let Some(u) = frontier.pop() {
65 if g.contains_key(&u) {
66 continue;
67 }
68 let next: Vec<UnitId> = i
69 .defs
70 .get(&u)
71 .map(|d| d.edges.iter().filter(|e| walkable(&e.kind) && diverges(&e.to, i)).map(|e| e.to.clone()).collect())
72 .unwrap_or_default();
73 frontier.extend(next.iter().cloned());
74 g.insert(u, next);
75 }
76 g
77}
78
79fn sccs(g: &BTreeMap<UnitId, Vec<UnitId>>) -> (Vec<Vec<UnitId>>, BTreeMap<UnitId, usize>) {
81 struct T<'g> {
82 g: &'g BTreeMap<UnitId, Vec<UnitId>>,
83 index: BTreeMap<UnitId, usize>,
84 low: BTreeMap<UnitId, usize>,
85 on: BTreeSet<UnitId>,
86 stack: Vec<UnitId>,
87 comps: Vec<Vec<UnitId>>,
88 n: usize,
89 }
90 fn visit(t: &mut T<'_>, v: &UnitId) {
91 t.index.insert(v.clone(), t.n);
92 t.low.insert(v.clone(), t.n);
93 t.n += 1;
94 t.stack.push(v.clone());
95 t.on.insert(v.clone());
96 for w in t.g.get(v).cloned().unwrap_or_default() {
97 if !t.index.contains_key(&w) {
98 visit(t, &w);
99 let lw = t.low[&w];
100 let lv = t.low.get_mut(v).expect("visited");
101 *lv = (*lv).min(lw);
102 } else if t.on.contains(&w) {
103 let iw = t.index[&w];
104 let lv = t.low.get_mut(v).expect("visited");
105 *lv = (*lv).min(iw);
106 }
107 }
108 if t.low[v] == t.index[v] {
109 let mut comp = vec![];
110 while let Some(w) = t.stack.pop() {
111 t.on.remove(&w);
112 comp.push(w.clone());
113 if &w == v {
114 break;
115 }
116 }
117 comp.sort();
118 t.comps.push(comp);
119 }
120 }
121 let mut t =
122 T { g, index: BTreeMap::new(), low: BTreeMap::new(), on: BTreeSet::new(), stack: vec![], comps: vec![], n: 0 };
123 for v in g.keys() {
124 if !t.index.contains_key(v) {
125 visit(&mut t, v);
126 }
127 }
128 let mut of = BTreeMap::new();
129 for (ci, c) in t.comps.iter().enumerate() {
130 for u in c {
131 of.insert(u.clone(), ci);
132 }
133 }
134 (t.comps, of)
135}
136
137fn rank(u: &UnitId, i: &Inputs<'_>) -> (u64, std::cmp::Reverse<usize>, UnitId) {
140 let failed_at = i.failed_since.get(u).map(|t| t.0).unwrap_or(u64::MAX);
141 let failed_probes = match (i.defs.get(u), i.state.units.get(u)) {
142 (Some(d), Some((_, ev))) => d
143 .probes
144 .iter()
145 .filter(|p| p.role == ProbeRole::Readiness)
146 .filter(|p| ev.iter().any(|e| e.probe == p.id && matches!(e.result, ProbeResult::Fail(_))))
147 .count(),
148 _ => 0,
149 };
150 (failed_at, std::cmp::Reverse(failed_probes), u.clone())
151}
152
153pub fn root_cause(target: &UnitId, i: &Inputs<'_>) -> UnitId {
155 let g = subgraph(target, i);
156 let (comps, of) = sccs(&g);
157 let sinks: Vec<usize> =
159 (0..comps.len()).filter(|ci| comps[*ci].iter().all(|u| g[u].iter().all(|w| of[w] == *ci))).collect();
160 sinks.iter().flat_map(|ci| comps[*ci].iter()).min_by_key(|u| rank(u, i)).cloned().unwrap_or_else(|| target.clone())
161}
162
163fn est_tier(t: &Transition) -> u8 {
166 match t {
167 Transition::RestartUnit(_) | Transition::CollectGarbage { .. } | Transition::SetRuntimeOption { .. } => 1,
168 Transition::BindDriver { .. } | Transition::ApplyChange { .. } => 2,
169 _ => 3,
170 }
171}
172
173pub fn candidates(incident: &Incident, i: &Inputs<'_>) -> Vec<Candidate> {
175 let root = root_cause(&incident.target, i);
176 if i.state.overridden(&root) {
177 return vec![]; }
179 let Some(def) = i.defs.get(&root) else { return vec![] };
180 let status = i.state.status(&root, i.defs);
181 let mut out: Vec<(Transition, String)> = vec![];
182 let via = if root == incident.target { String::new() } else { format!(" (root cause of {})", incident.target) };
183
184 if let Some(opt) = &def.runtime_repair
190 && divergent(def.desired.status, &status)
191 && let Some(v) = i.alternatives.get(opt).and_then(|a| a.first())
192 {
193 let shown = match v {
194 Value::Str(s) => s.clone(),
195 other => format!("{other:?}"),
196 };
197 out.push((
198 Transition::SetRuntimeOption { path: opt.clone(), value: v.clone() },
199 format!("{root} is {status:?}; switch {opt} to {shown}{via}"),
200 ));
201 }
202
203 match def.id.kind {
204 UnitKind::Device if root.name.starts_with("fs-") => {
205 let emergency = root.name == "fs-journal" || root.name == "fs-nix";
206 out.push((Transition::CollectGarbage { emergency }, format!("{root} is low on space{via}")));
207 }
208 UnitKind::Device => {
209 if let Some(driver) = i.known_drivers.get(&root)
210 && matches!(status, UnitStatus::Detected | UnitStatus::Failed(_))
211 {
212 out.push((
213 Transition::BindDriver { device: root.clone(), driver: driver.clone() },
214 format!("{root} lost its driver {}{via}", driver.name),
215 ));
216 }
217 }
218 _ if root.name.starts_with("external:") => {
219 }
221 _ => match status {
222 UnitStatus::Failed(_) | UnitStatus::Stopped | UnitStatus::Degraded(_) => {
223 out.push((Transition::RestartUnit(root.clone()), format!("{root} is {status:?}{via}")));
224 }
225 UnitStatus::Absent => out.push((
226 Transition::ApplyChange {
227 change: ChangeId("repair:reenable-predicted".into()),
228 args: [("service".to_string(), Value::Str(root.name.clone()))].into(),
229 },
230 format!("{root} is missing though the manifest predicts it{via}"),
231 )),
232 _ => {}
233 },
234 }
235
236 if divergent(def.desired.status, &status)
239 && let Some((change, args)) = i.lost_changes.get(&root)
240 {
241 let mut a = args.clone();
242 a.insert("change".into(), Value::Str(change.0.clone()));
243 out.push((
244 Transition::ApplyChange { change: ChangeId("repair:reapply".into()), args: a },
245 format!("{root} is {status:?} and the change {} committed for it is no longer applied{via}", change.0),
246 ));
247 }
248
249 if i.current_planarian_built && i.state.generation != i.state.known_good {
250 out.push((Transition::Rollback, format!("roll back to the known-good generation{via}")));
251 }
252
253 out.retain(|(t, _)| precondition(t, i.state, i.defs));
254 out.sort_by_key(|(t, _)| {
255 let rollback = matches!(t, Transition::Rollback);
256 (rollback, est_tier(t), def.restart_disruption)
257 });
258 out.into_iter()
259 .map(|(t, reason)| Candidate {
260 target: root.clone(),
261 expected: predict(&t, i.state, i.defs),
262 transition: t,
263 reason,
264 })
265 .collect()
266}
267
268pub fn next(incident: &Incident, i: &Inputs<'_>) -> Option<Candidate> {
272 let c = candidates(incident, i);
273 if c.is_empty() {
274 return None;
275 }
276 let n = incident.attempts.len() % c.len();
277 c.into_iter().nth(n)
278}
279
280#[cfg(test)]
281mod tests;