Skip to main content

faucet_cli/schedule/
spec.rs

1//! Config types for the `schedule:` block.
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6/// Top-level `schedule:` block. Presence of this block is what makes a config
7/// runnable by `faucet schedule`.
8#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
9#[serde(deny_unknown_fields)]
10pub struct ScheduleSpec {
11    /// Cron expression. 5-field standard Unix cron (`minute hour day-of-month
12    /// month day-of-week`), or 6-field with a leading seconds field for
13    /// sub-minute schedules.
14    pub cron: String,
15
16    /// IANA timezone name (e.g. `America/Los_Angeles`). Default `UTC`.
17    #[serde(default = "default_timezone")]
18    pub timezone: String,
19
20    /// What to do when a tick fires while the previous run is still in flight.
21    #[serde(default)]
22    pub overlap_policy: OverlapPolicy,
23
24    /// Stop cleanly after this many *successful* runs. `None` = run forever.
25    #[serde(default)]
26    pub max_runs: Option<u64>,
27
28    /// Exit non-zero after this many *consecutive* failed runs (so a supervisor
29    /// restarts / pages). A success resets the counter. `None` = never exit on
30    /// failure (alert via the `consecutive_failures` gauge instead).
31    #[serde(default)]
32    pub max_consecutive_failures: Option<u64>,
33
34    /// Per-run failure policy.
35    #[serde(default)]
36    pub on_failure: ScheduleOnFailure,
37
38    /// Run once on startup before waiting for the first scheduled tick.
39    #[serde(default)]
40    pub start_immediately: bool,
41
42    /// Optional per-run kill switch (seconds). A run exceeding this is aborted
43    /// and counts as a failed run.
44    #[serde(default)]
45    pub run_timeout_secs: Option<u64>,
46
47    /// On SIGTERM/SIGINT, await the in-flight run this many seconds before
48    /// aborting it. Default 30 (matches Kubernetes' default termination grace).
49    #[serde(default = "default_shutdown_grace_secs")]
50    pub shutdown_grace_secs: u64,
51}
52
53/// Behaviour when a tick fires while a run is still in progress.
54#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
55#[serde(rename_all = "lowercase")]
56pub enum OverlapPolicy {
57    /// Drop the overlapping tick (default).
58    #[default]
59    Skip,
60    /// Buffer one missed tick and run it when the current run finishes.
61    Queue,
62    /// Treat an overlap as fatal — exit non-zero.
63    Forbid,
64}
65
66/// Per-run failure policy.
67#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
68#[serde(rename_all = "lowercase")]
69pub enum ScheduleOnFailure {
70    /// Log the failure and wait for the next tick (default).
71    #[default]
72    Continue,
73    /// Exit non-zero on the first failed run.
74    Stop,
75}
76
77fn default_timezone() -> String {
78    "UTC".to_string()
79}
80fn default_shutdown_grace_secs() -> u64 {
81    30
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn defaults_apply_when_only_cron_given() {
90        let spec: ScheduleSpec = serde_yaml::from_str("cron: \"0 2 * * *\"").unwrap();
91        assert_eq!(spec.cron, "0 2 * * *");
92        assert_eq!(spec.timezone, "UTC");
93        assert_eq!(spec.overlap_policy, OverlapPolicy::Skip);
94        assert_eq!(spec.on_failure, ScheduleOnFailure::Continue);
95        assert_eq!(spec.max_runs, None);
96        assert_eq!(spec.max_consecutive_failures, None);
97        assert!(!spec.start_immediately);
98        assert_eq!(spec.run_timeout_secs, None);
99        assert_eq!(spec.shutdown_grace_secs, 30);
100    }
101
102    #[test]
103    fn enums_use_lowercase_wire_form() {
104        let spec: ScheduleSpec =
105            serde_yaml::from_str("cron: \"* * * * *\"\noverlap_policy: queue\non_failure: stop\n")
106                .unwrap();
107        assert_eq!(spec.overlap_policy, OverlapPolicy::Queue);
108        assert_eq!(spec.on_failure, ScheduleOnFailure::Stop);
109    }
110}