1use crate::ids::{GenerationId, Hash, ProbeId, Timestamp};
4use crate::unit::{Condition, Evidence, Override, OverrideDesired, ProbeResult, ProbeRole, UnitDefs, UnitId, UnitKind};
5use serde::{Deserialize, Serialize};
6use std::collections::{BTreeMap, BTreeSet};
7
8#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
9pub enum Observed {
10 Unknown {
12 since: Timestamp,
13 reason: String,
14 },
15 Transitioning {
16 from: Box<Observed>,
17 since: Timestamp,
18 },
19 Absent,
20 Detected,
21 Bound,
22 Active,
23 Failed(Reason),
24}
25
26#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
27pub enum Reason {
28 ExitCode(i32),
29 DriverError(String),
30 Backend(String),
31}
32
33#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
39pub enum UnitStatus {
40 Running,
41 Degraded(Vec<ProbeId>),
42 Stopped,
43 Overridden(OverrideDesired),
44 Unknown,
45 Failed(Reason),
46 Absent,
47 Detected,
48 Bound,
49 Transitioning,
50 NotExpected,
51}
52
53#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
55pub struct Resources {
56 pub mem_total_mb: u64,
57 pub mem_available_mb: u64,
58 pub fs_free_bytes: BTreeMap<UnitId, u64>,
60}
61
62#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
63pub struct SystemState {
64 pub generation: GenerationId,
65 pub known_good: GenerationId,
67 pub live_tested: Option<GenerationId>,
69 pub accepted_baseline: Option<GenerationId>,
71 #[serde(default)]
73 pub security_floor: Option<GenerationId>,
74 pub units: BTreeMap<UnitId, (Observed, Vec<Evidence>)>,
75 pub overrides: Vec<Override>,
76 pub conditions: BTreeSet<Condition>,
78 pub resources: Resources,
80 pub core_hash: Hash,
81 #[serde(default)]
85 pub holds: BTreeSet<UnitId>,
86 pub observed_at: Timestamp,
88}
89
90impl SystemState {
91 pub fn override_of(&self, id: &UnitId) -> Option<&Override> {
93 self.overrides.iter().find(|s| &s.unit == id && s.holds_at(self.observed_at))
94 }
95
96 pub fn overridden(&self, id: &UnitId) -> bool {
97 self.override_of(id).is_some()
98 }
99
100 pub fn expected(&self, id: &UnitId, defs: &UnitDefs) -> bool {
103 match defs.get(id) {
104 Some(def) => !self.overridden(id) && def.desired.when.holds(&self.conditions),
105 None => false,
106 }
107 }
108
109 pub fn failing_readiness(&self, id: &UnitId, defs: &UnitDefs) -> Vec<ProbeId> {
114 let (Some(def), Some((_, evidence))) = (defs.get(id), self.units.get(id)) else { return vec![] };
115 def.probes
116 .iter()
117 .filter(|p| p.role == ProbeRole::Readiness)
118 .filter(|p| {
119 evidence
120 .iter()
121 .filter(|e| e.probe == p.id)
122 .max_by_key(|e| e.at)
123 .is_some_and(|e| e.fresh_at(self.observed_at) && matches!(e.result, ProbeResult::Fail(_)))
124 })
125 .map(|p| p.id.clone())
126 .collect()
127 }
128
129 pub fn passing_readiness(&self, id: &UnitId, defs: &UnitDefs) -> Vec<ProbeId> {
132 let (Some(def), Some((_, evidence))) = (defs.get(id), self.units.get(id)) else { return vec![] };
133 def.probes
134 .iter()
135 .filter(|p| p.role == ProbeRole::Readiness)
136 .filter(|p| {
137 evidence
138 .iter()
139 .filter(|e| e.probe == p.id)
140 .max_by_key(|e| e.at)
141 .is_some_and(|e| e.fresh_at(self.observed_at) && matches!(e.result, ProbeResult::Pass))
142 })
143 .map(|p| p.id.clone())
144 .collect()
145 }
146
147 pub fn status(&self, id: &UnitId, defs: &UnitDefs) -> UnitStatus {
152 if let Some(o) = self.override_of(id) {
153 return UnitStatus::Overridden(o.desired);
154 }
155 let Some(def) = defs.get(id) else {
156 return UnitStatus::NotExpected;
157 };
158 if !def.desired.when.holds(&self.conditions) {
159 return UnitStatus::NotExpected;
160 }
161 let Some((observed, evidence)) = self.units.get(id) else {
162 return UnitStatus::Unknown;
163 };
164 if let Observed::Failed(r) = observed {
165 return UnitStatus::Failed(r.clone());
166 }
167
168 let mut failed = Vec::new();
169 for probe in def.probes.iter().filter(|p| p.role == ProbeRole::Readiness) {
170 let latest = evidence.iter().filter(|e| e.probe == probe.id).max_by_key(|e| e.at);
171 match latest {
172 Some(e) if e.fresh_at(self.observed_at) => match &e.result {
173 ProbeResult::Pass => {}
174 ProbeResult::Fail(_) => failed.push(probe.id.clone()),
175 ProbeResult::Timeout | ProbeResult::Abandoned | ProbeResult::Error(_) => {
176 return UnitStatus::Unknown;
177 }
178 },
179 _ => return UnitStatus::Unknown,
180 }
181 }
182
183 match observed {
184 Observed::Active if failed.is_empty() => UnitStatus::Running,
185 Observed::Active => UnitStatus::Degraded(failed),
186 Observed::Unknown { .. } => UnitStatus::Unknown,
187 Observed::Transitioning { .. } => UnitStatus::Transitioning,
188 Observed::Absent => UnitStatus::Absent,
189 Observed::Detected => UnitStatus::Detected,
190 Observed::Bound if def.id.kind == UnitKind::Device => UnitStatus::Bound,
193 Observed::Bound => UnitStatus::Stopped,
194 Observed::Failed(_) => unreachable!("handled above"),
195 }
196 }
197
198 pub fn digest(&self) -> Hash {
204 let view = DigestView {
205 generation: self.generation,
206 known_good: self.known_good,
207 live_tested: self.live_tested,
208 accepted_baseline: self.accepted_baseline,
209 units: self
210 .units
211 .iter()
212 .map(|(id, (obs, ev))| {
213 let mut results: Vec<(&ProbeId, &ProbeResult)> = ev.iter().map(|e| (&e.probe, &e.result)).collect();
214 results.sort_by(|a, b| a.0.cmp(b.0));
215 (id, ObservedView::of(obs), results)
216 })
217 .collect(),
218 overrides: {
219 let mut s: Vec<&Override> = self.overrides.iter().collect();
220 s.sort();
221 s
222 },
223 conditions: &self.conditions,
224 core_hash: self.core_hash,
225 holds: &self.holds,
226 };
227 Hash::of(&serde_json::to_vec(&view).expect("digest view serializes"))
228 }
229}
230
231type UnitView<'a> = (&'a UnitId, ObservedView<'a>, Vec<(&'a ProbeId, &'a ProbeResult)>);
232
233#[derive(Serialize)]
234struct DigestView<'a> {
235 generation: GenerationId,
236 known_good: GenerationId,
237 live_tested: Option<GenerationId>,
238 accepted_baseline: Option<GenerationId>,
239 units: Vec<UnitView<'a>>,
240 overrides: Vec<&'a Override>,
241 conditions: &'a BTreeSet<Condition>,
242 core_hash: Hash,
243 holds: &'a BTreeSet<UnitId>,
244}
245
246#[derive(Serialize, PartialEq)]
247enum ObservedView<'a> {
248 Unknown { reason: &'a str },
249 Transitioning { from: Box<ObservedView<'a>> },
250 Absent,
251 Detected,
252 Bound,
253 Active,
254 Failed(&'a Reason),
255}
256
257impl<'a> ObservedView<'a> {
258 fn of(o: &'a Observed) -> ObservedView<'a> {
259 match o {
260 Observed::Unknown { reason, .. } => ObservedView::Unknown { reason },
261 Observed::Transitioning { from, .. } => {
262 ObservedView::Transitioning { from: Box::new(ObservedView::of(from)) }
263 }
264 Observed::Absent => ObservedView::Absent,
265 Observed::Detected => ObservedView::Detected,
266 Observed::Bound => ObservedView::Bound,
267 Observed::Active => ObservedView::Active,
268 Observed::Failed(r) => ObservedView::Failed(r),
269 }
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use crate::ids::Millis;
277 use crate::unit::*;
278
279 fn probe(id: &str, role: ProbeRole) -> ProbeDef {
280 ProbeDef {
281 id: ProbeId(id.into()),
282 kind: ProbeKind::SystemdActive,
283 timeout: Millis::secs(10),
284 origin: ProbeOrigin::Canonical,
285 role,
286 cadence: if role == ProbeRole::Readiness { Cadence::EveryTick } else { Cadence::OnDemand },
287 cost: ProbeCost::Trivial,
288 side_effects: if role == ProbeRole::Readiness { SideEffects::None } else { SideEffects::Audible },
289 establishes: String::new(),
290 }
291 }
292
293 fn def(id: &UnitId, when: Condition, probes: Vec<ProbeDef>) -> UnitDef {
294 UnitDef {
295 id: id.clone(),
296 desired: Desired { status: DesiredStatus::Running, when },
297 edges: vec![],
298 sensitivity: Sensitivities::default(),
299 restart_disruption: Disruption::Brief,
300 probes,
301 synonyms: vec![],
302 systemd_restart: None,
303 startup_grace: None,
304 runtime_repair: None,
305 stops_are_faults: false,
306 holds: false,
307 hold_allow: vec![],
308 hold_interfaces: vec![],
309 }
310 }
311
312 fn ev(id: &str, result: ProbeResult, at: u64) -> Evidence {
313 Evidence { probe: ProbeId(id.into()), result, at: Timestamp(at), stale_after: Millis(30_000) }
314 }
315
316 fn state(now: u64) -> SystemState {
317 SystemState {
318 generation: GenerationId(2),
319 known_good: GenerationId(1),
320 live_tested: None,
321 accepted_baseline: None,
322 security_floor: None,
323 units: BTreeMap::new(),
324 overrides: vec![],
325 conditions: BTreeSet::new(),
326 resources: Resources::default(),
327 core_hash: Hash::ZERO,
328 holds: BTreeSet::new(),
329 observed_at: Timestamp(now),
330 }
331 }
332
333 fn web() -> (UnitId, UnitDefs) {
334 let id = UnitId::service("gym-web");
335 let d = def(
336 &id,
337 Condition::Always,
338 vec![probe("http", ProbeRole::Readiness), probe("tone", ProbeRole::Diagnostic)],
339 );
340 (id.clone(), [(id, d)].into())
341 }
342
343 #[test]
344 fn running_needs_fresh_readiness_only() {
345 let (id, defs) = web();
346 let mut s = state(100_000);
347 s.units.insert(
349 id.clone(),
350 (Observed::Active, vec![ev("http", ProbeResult::Pass, 90_000), ev("tone", ProbeResult::Pass, 1)]),
351 );
352 assert_eq!(s.status(&id, &defs), UnitStatus::Running);
353 s.observed_at = Timestamp(200_000);
354 assert_eq!(s.status(&id, &defs), UnitStatus::Unknown);
355 }
356
357 #[test]
358 fn failed_beats_stale() {
359 let (id, defs) = web();
361 let mut s = state(1_000_000);
362 s.units.insert(id.clone(), (Observed::Failed(Reason::ExitCode(137)), vec![ev("http", ProbeResult::Pass, 1)]));
363 assert_eq!(s.status(&id, &defs), UnitStatus::Failed(Reason::ExitCode(137)));
364 }
365
366 #[test]
367 fn timeout_is_unknown_not_failed() {
368 let (id, defs) = web();
369 let mut s = state(100_000);
370 s.units.insert(id.clone(), (Observed::Active, vec![ev("http", ProbeResult::Timeout, 99_000)]));
371 assert_eq!(s.status(&id, &defs), UnitStatus::Unknown);
372 }
373
374 #[test]
375 fn degraded_and_stopped() {
376 let (id, defs) = web();
377 let mut s = state(100_000);
378 s.units.insert(id.clone(), (Observed::Active, vec![ev("http", ProbeResult::Fail("503".into()), 99_000)]));
379 assert_eq!(s.status(&id, &defs), UnitStatus::Degraded(vec![ProbeId("http".into())]));
380 s.units.insert(id.clone(), (Observed::Bound, vec![ev("http", ProbeResult::Fail("refused".into()), 99_000)]));
381 assert_eq!(s.status(&id, &defs), UnitStatus::Stopped);
382 }
383
384 #[test]
385 fn suppressed_and_not_expected() {
386 let id = UnitId::target("graphical-session");
387 let defs: UnitDefs =
388 [(id.clone(), def(&id, Condition::SessionEstablished { uid: None, seated: false }, vec![]))].into();
389 let mut s = state(100_000);
390 s.units.insert(id.clone(), (Observed::Bound, vec![]));
391 assert_eq!(s.status(&id, &defs), UnitStatus::NotExpected);
393 assert!(!s.expected(&id, &defs));
394 s.conditions.insert(Condition::SessionEstablished { uid: Some(1000), seated: false });
395 assert_eq!(s.status(&id, &defs), UnitStatus::Stopped);
396 assert!(s.expected(&id, &defs));
397 s.overrides.push(Override {
398 unit: id.clone(),
399 until: Until::At(Timestamp(200_000)),
400 reason: String::new(),
401 by: Actor::user(1000),
402 desired: OverrideDesired::LeaveAlone,
403 attribution: Attribution::Attested,
404 acknowledged: false,
405 });
406 assert_eq!(s.status(&id, &defs), UnitStatus::Overridden(OverrideDesired::LeaveAlone));
407 s.observed_at = Timestamp(200_000);
408 assert_eq!(s.status(&id, &defs), UnitStatus::Stopped);
409 }
410
411 #[test]
412 fn digest_ignores_timestamps_and_resources() {
413 let (id, _) = web();
414 let mut a = state(100_000);
415 a.units.insert(id.clone(), (Observed::Active, vec![ev("http", ProbeResult::Pass, 99_000)]));
416 let mut b = a.clone();
417 b.observed_at = Timestamp(555_555);
418 b.resources.mem_available_mb = 7;
419 b.units.insert(id.clone(), (Observed::Active, vec![ev("http", ProbeResult::Pass, 1)]));
420 assert_eq!(a.digest(), b.digest());
421 b.units.insert(id.clone(), (Observed::Active, vec![ev("http", ProbeResult::Fail("x".into()), 1)]));
422 assert_ne!(a.digest(), b.digest());
423 let mut c = a.clone();
424 c.generation = GenerationId(3);
425 assert_ne!(a.digest(), c.digest());
426 }
427
428 mod digest_props {
432 use super::*;
433 use proptest::prelude::*;
434 use proptest::test_runner::{Config, FailurePersistence};
435
436 fn observed() -> impl Strategy<Value = Observed> {
437 prop_oneof![
438 Just(Observed::Active),
439 Just(Observed::Bound),
440 Just(Observed::Absent),
441 (0..3i32).prop_map(|c| Observed::Failed(Reason::ExitCode(c))),
442 "[a-c]{1,3}".prop_map(|r| Observed::Unknown { since: Timestamp(1), reason: r }),
443 Just(Observed::Transitioning { from: Box::new(Observed::Active), since: Timestamp(1) }),
444 ]
445 }
446
447 fn result() -> impl Strategy<Value = ProbeResult> {
448 prop_oneof![
449 Just(ProbeResult::Pass),
450 Just(ProbeResult::Timeout),
451 Just(ProbeResult::Abandoned),
452 "[xy]{1,2}".prop_map(ProbeResult::Fail)
453 ]
454 }
455
456 prop_compose! {
457 fn system()(
458 generation in 1..4u64,
459 units in proptest::collection::btree_map(
460 "[a-d]{1,2}",
461 (observed(), proptest::collection::vec(("[pq]", result()), 0..3)),
462 0..4,
463 ),
464 ) -> SystemState {
465 let mut s = state(1_000);
466 s.generation = GenerationId(generation);
467 for (name, (o, ev)) in units {
468 let ev = ev.into_iter().map(|(p, r)| super::ev(&p, r, 900)).collect();
469 s.units.insert(UnitId::service(&name), (o, ev));
470 }
471 s
472 }
473 }
474
475 fn perturb(s: &SystemState, by: u64) -> SystemState {
477 let mut t = s.clone();
478 t.observed_at = Timestamp(t.observed_at.0 + by);
479 t.resources.mem_available_mb += by;
480 t.resources.mem_total_mb += 2 * by;
481 for (o, ev) in t.units.values_mut() {
482 match o {
483 Observed::Unknown { since, .. } | Observed::Transitioning { since, .. } => since.0 += by,
484 _ => {}
485 }
486 for e in ev {
487 e.at.0 += by;
488 }
489 }
490 t
491 }
492
493 proptest! {
494 #![proptest_config(Config { cases: 256, failure_persistence: None::<Box<dyn FailurePersistence>>, ..Config::default() })]
495
496 #[test]
497 fn timestamps_and_resources_do_not_count(s in system(), by in 1..1_000_000u64) {
498 prop_assert_eq!(s.digest(), perturb(&s, by).digest());
499 }
500
501 #[test]
502 fn what_the_state_says_does(s in system(), o in observed()) {
503 let mut t = s.clone();
504 t.generation = GenerationId(s.generation.0 + 10);
505 prop_assert_ne!(s.digest(), t.digest());
506 let mut u = s.clone();
507 u.overrides.push(Override {
508 unit: UnitId::service("z"),
509 until: Until::Explicit,
510 reason: String::new(),
511 by: Actor::Daemon,
512 desired: OverrideDesired::LeaveAlone,
513 attribution: Attribution::Attested,
514 acknowledged: false,
515 });
516 prop_assert_ne!(s.digest(), u.digest());
517 if let Some((id, (old, ev))) = s.units.iter().next() {
518 let same = ObservedView::of(old) == ObservedView::of(&o);
519 let mut v = s.clone();
520 v.units.insert(id.clone(), (o, ev.clone()));
521 prop_assert_eq!(s.digest() == v.digest(), same);
522 }
523 }
524 }
525 }
526}