Skip to main content

improv_core_model/
schedule.rs

1//! Pure refresh-scheduling decisions: given each external-sourced measure's
2//! [`RefreshPolicy`] and when it last ran, decide which
3//! measures are *due* to refresh now. This is intentionally I/O-free and
4//! clock-free (times are plain seconds) so a daemon can drive it and tests can
5//! exercise it deterministically. The daemon (CLI `serve-refresh`) does the
6//! actual query/eval; this only picks what and when.
7
8use crate::{MeasureId, RefreshPolicy};
9use std::collections::HashMap;
10
11/// Decide whether a single measure is due to refresh.
12///
13/// * `Manual` — never scheduled (only `refresh`/`refresh-all` run it).
14/// * `OnLoad` — due once, when it has never run in this daemon session
15///   (`last_run` is `None`).
16/// * `Interval { secs }` — due when it has never run, or when `now_secs` is at
17///   least `secs` past `last_run`.
18pub fn is_due(policy: RefreshPolicy, now_secs: u64, last_run: Option<u64>) -> bool {
19    match policy {
20        RefreshPolicy::Manual => false,
21        RefreshPolicy::OnLoad => last_run.is_none(),
22        RefreshPolicy::Interval { secs } => match last_run {
23            None => true,
24            Some(last) => now_secs.saturating_sub(last) >= secs,
25        },
26    }
27}
28
29/// Return the ids of every measure whose policy makes it due at `now_secs`,
30/// given the policy per measure and the last run time per measure. Ids are
31/// returned sorted for determinism.
32pub fn due_measures(
33    policies: &HashMap<MeasureId, RefreshPolicy>,
34    now_secs: u64,
35    last_run: &HashMap<MeasureId, u64>,
36) -> Vec<MeasureId> {
37    let mut due: Vec<MeasureId> = policies
38        .iter()
39        .filter(|(id, &p)| is_due(p, now_secs, last_run.get(id).copied()))
40        .map(|(id, _)| *id)
41        .collect();
42    due.sort_by_key(|m| m.0);
43    due
44}
45
46#[cfg(test)]
47#[cfg_attr(coverage_nightly, coverage(off))]
48mod tests {
49    use super::*;
50
51    #[test]
52    fn manual_is_never_due() {
53        assert!(!is_due(RefreshPolicy::Manual, 100, None));
54        assert!(!is_due(RefreshPolicy::Manual, 100, Some(0)));
55    }
56
57    #[test]
58    fn on_load_is_due_only_once() {
59        assert!(is_due(RefreshPolicy::OnLoad, 0, None));
60        assert!(!is_due(RefreshPolicy::OnLoad, 999, Some(0)));
61    }
62
63    #[test]
64    fn interval_respects_elapsed() {
65        let p = RefreshPolicy::Interval { secs: 30 };
66        assert!(is_due(p, 0, None)); // never run -> due
67        assert!(!is_due(p, 20, Some(0))); // only 20s elapsed
68        assert!(is_due(p, 30, Some(0))); // exactly 30s -> due
69        assert!(is_due(p, 61, Some(30))); // 31s since last -> due
70    }
71
72    #[test]
73    fn due_measures_filters_and_sorts() {
74        let mut policies = HashMap::new();
75        policies.insert(MeasureId(3), RefreshPolicy::OnLoad);
76        policies.insert(MeasureId(1), RefreshPolicy::Manual);
77        policies.insert(MeasureId(2), RefreshPolicy::Interval { secs: 10 });
78        let mut last = HashMap::new();
79        last.insert(MeasureId(2), 100u64); // ran at 100
80                                           // now=105: measure 2 not yet due (5s<10), measure 3 due (never ran),
81                                           // measure 1 manual -> never.
82        assert_eq!(due_measures(&policies, 105, &last), vec![MeasureId(3)]);
83        // now=115: measure 2 now due too; sorted.
84        assert_eq!(
85            due_measures(&policies, 115, &last),
86            vec![MeasureId(2), MeasureId(3)]
87        );
88    }
89}