faucet_cli/schedule/
state.rs1use crate::schedule::compiled::CompiledSchedule;
5use crate::schedule::spec::{OverlapPolicy, ScheduleOnFailure};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum TickAction {
10 Dispatch,
12 Skip,
14 Queue,
16 ForbidAbort,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum RunOutcome {
23 Success,
24 Failure,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum AfterRun {
30 Continue { dispatch_pending: bool },
32 ExitOk,
34 ExitFailure { consecutive: u64 },
36}
37
38pub struct SchedulerState {
40 overlap: OverlapPolicy,
41 on_failure: ScheduleOnFailure,
42 max_runs: Option<u64>,
43 max_consecutive_failures: Option<u64>,
44 successful_runs: u64,
45 consecutive_failures: u64,
46 pending: bool,
47}
48
49impl SchedulerState {
50 pub fn new(c: &CompiledSchedule) -> Self {
51 Self {
52 overlap: c.overlap_policy,
53 on_failure: c.on_failure,
54 max_runs: c.max_runs,
55 max_consecutive_failures: c.max_consecutive_failures,
56 successful_runs: 0,
57 consecutive_failures: 0,
58 pending: false,
59 }
60 }
61
62 pub fn consecutive_failures(&self) -> u64 {
63 self.consecutive_failures
64 }
65
66 pub fn on_tick(&mut self, running: bool) -> TickAction {
68 if !running {
69 return TickAction::Dispatch;
70 }
71 match self.overlap {
72 OverlapPolicy::Skip => TickAction::Skip,
73 OverlapPolicy::Queue => {
74 self.pending = true;
75 TickAction::Queue
76 }
77 OverlapPolicy::Forbid => TickAction::ForbidAbort,
78 }
79 }
80
81 pub fn on_run_finished(&mut self, outcome: RunOutcome) -> AfterRun {
83 match outcome {
84 RunOutcome::Success => {
85 self.consecutive_failures = 0;
86 self.successful_runs += 1;
87 if let Some(max) = self.max_runs
88 && self.successful_runs >= max
89 {
90 return AfterRun::ExitOk;
91 }
92 }
93 RunOutcome::Failure => {
94 self.consecutive_failures += 1;
95 if matches!(self.on_failure, ScheduleOnFailure::Stop) {
96 return AfterRun::ExitFailure {
97 consecutive: self.consecutive_failures,
98 };
99 }
100 if let Some(max) = self.max_consecutive_failures
101 && self.consecutive_failures >= max
102 {
103 return AfterRun::ExitFailure {
104 consecutive: self.consecutive_failures,
105 };
106 }
107 }
108 }
109 let dispatch_pending = self.pending;
110 self.pending = false;
111 AfterRun::Continue { dispatch_pending }
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use crate::schedule::spec::ScheduleSpec;
119
120 fn state(yaml: &str) -> SchedulerState {
121 let spec: ScheduleSpec = serde_yaml::from_str(yaml).unwrap();
122 let compiled = CompiledSchedule::compile(&spec).unwrap();
123 SchedulerState::new(&compiled)
124 }
125
126 #[test]
127 fn dispatches_when_idle() {
128 let mut s = state("cron: \"* * * * *\"");
129 assert_eq!(s.on_tick(false), TickAction::Dispatch);
130 }
131
132 #[test]
133 fn skip_policy_drops_overlapping_tick() {
134 let mut s = state("cron: \"* * * * *\"\noverlap_policy: skip");
135 assert_eq!(s.on_tick(true), TickAction::Skip);
136 }
137
138 #[test]
139 fn queue_policy_buffers_and_dispatches_on_completion() {
140 let mut s = state("cron: \"* * * * *\"\noverlap_policy: queue");
141 assert_eq!(s.on_tick(true), TickAction::Queue);
142 assert_eq!(
143 s.on_run_finished(RunOutcome::Success),
144 AfterRun::Continue {
145 dispatch_pending: true
146 }
147 );
148 assert_eq!(
150 s.on_run_finished(RunOutcome::Success),
151 AfterRun::Continue {
152 dispatch_pending: false
153 }
154 );
155 }
156
157 #[test]
158 fn forbid_policy_aborts_on_overlap() {
159 let mut s = state("cron: \"* * * * *\"\noverlap_policy: forbid");
160 assert_eq!(s.on_tick(true), TickAction::ForbidAbort);
161 }
162
163 #[test]
164 fn max_runs_counts_successes_only() {
165 let mut s = state("cron: \"* * * * *\"\nmax_runs: 2");
166 assert_eq!(
167 s.on_run_finished(RunOutcome::Failure),
168 AfterRun::Continue {
169 dispatch_pending: false
170 }
171 );
172 assert_eq!(
173 s.on_run_finished(RunOutcome::Success),
174 AfterRun::Continue {
175 dispatch_pending: false
176 }
177 );
178 assert_eq!(s.on_run_finished(RunOutcome::Success), AfterRun::ExitOk);
179 }
180
181 #[test]
182 fn on_failure_stop_exits_on_first_failure() {
183 let mut s = state("cron: \"* * * * *\"\non_failure: stop");
184 assert_eq!(
185 s.on_run_finished(RunOutcome::Failure),
186 AfterRun::ExitFailure { consecutive: 1 }
187 );
188 }
189
190 #[test]
191 fn max_consecutive_failures_trips_and_success_resets() {
192 let mut s = state("cron: \"* * * * *\"\nmax_consecutive_failures: 3");
193 assert_eq!(
194 s.on_run_finished(RunOutcome::Failure),
195 AfterRun::Continue {
196 dispatch_pending: false
197 }
198 );
199 assert_eq!(
200 s.on_run_finished(RunOutcome::Success),
201 AfterRun::Continue {
202 dispatch_pending: false
203 }
204 );
205 assert_eq!(
207 s.on_run_finished(RunOutcome::Failure),
208 AfterRun::Continue {
209 dispatch_pending: false
210 }
211 );
212 assert_eq!(
213 s.on_run_finished(RunOutcome::Failure),
214 AfterRun::Continue {
215 dispatch_pending: false
216 }
217 );
218 assert_eq!(
219 s.on_run_finished(RunOutcome::Failure),
220 AfterRun::ExitFailure { consecutive: 3 }
221 );
222 }
223}