Skip to main content

scheduler/model/
schedule.rs

1use super::{CronSchedule, JobTimeWindow};
2use chrono::{DateTime, Utc};
3use chrono_tz::{Asia::Shanghai, Tz};
4use std::time::Duration;
5
6/// Interval schedule with a stable phase derived from a seed.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct StaggeredIntervalSchedule {
9    pub every: Duration,
10    pub seed: Option<String>,
11}
12
13impl StaggeredIntervalSchedule {
14    pub fn new(every: Duration) -> Self {
15        Self { every, seed: None }
16    }
17
18    pub fn with_seed(mut self, seed: impl Into<String>) -> Self {
19        self.seed = Some(seed.into());
20        self
21    }
22}
23
24/// Interval schedule that spreads members evenly across the interval.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct GroupedIntervalSchedule {
27    pub every: Duration,
28    pub group_size: u32,
29    pub member_index: u32,
30    pub group_seed: Option<String>,
31}
32
33impl GroupedIntervalSchedule {
34    pub fn new(every: Duration, group_size: u32, member_index: u32) -> Self {
35        Self {
36            every,
37            group_size,
38            member_index,
39            group_seed: None,
40        }
41    }
42
43    pub fn with_group_seed(mut self, group_seed: impl Into<String>) -> Self {
44        self.group_seed = Some(group_seed.into());
45        self
46    }
47}
48
49/// Interval override for a matching time window.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct IntervalWindow {
52    pub window: JobTimeWindow,
53    pub every: Option<Duration>,
54}
55
56impl IntervalWindow {
57    pub fn new(window: JobTimeWindow, every: Option<Duration>) -> Self {
58        Self { window, every }
59    }
60}
61
62/// Interval schedule with per-window frequencies.
63///
64/// `None` means no trigger is produced while that frequency is active.
65/// Windows are evaluated in order; the first matching window wins.
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct WindowedIntervalSchedule {
68    pub default_every: Option<Duration>,
69    pub windows: Vec<IntervalWindow>,
70}
71
72impl WindowedIntervalSchedule {
73    pub fn new(default_every: Option<Duration>) -> Self {
74        Self {
75            default_every,
76            windows: Vec::new(),
77        }
78    }
79
80    pub fn with_window(mut self, window: JobTimeWindow, every: Option<Duration>) -> Self {
81        self.windows.push(IntervalWindow::new(window, every));
82        self
83    }
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub enum Schedule {
88    /// Trigger repeatedly after the given interval.
89    Interval(Duration),
90    /// Trigger repeatedly after the given interval with a stable phase.
91    ///
92    /// The first run is aligned to a deterministic phase derived from the
93    /// seed. If no seed is provided, the job id is used.
94    StaggeredInterval(StaggeredIntervalSchedule),
95    /// Trigger repeatedly after the given interval with evenly spaced group
96    /// members.
97    ///
98    /// The first run is aligned to a stable slot determined by the member
99    /// index. An optional group seed can rotate the entire group without
100    /// changing the spacing between members.
101    GroupedInterval(GroupedIntervalSchedule),
102    /// Trigger repeatedly with frequencies selected by local time windows.
103    WindowedInterval(WindowedIntervalSchedule),
104    /// Trigger at the listed wall-clock times.
105    ///
106    /// The list is sorted before execution starts. An empty list is treated as
107    /// a no-op schedule and exits without running.
108    AtTimes(Vec<DateTime<Tz>>),
109    /// Trigger using a standard 5-field cron expression.
110    ///
111    /// The scheduler timezone is used to calculate the next matching wall-clock
112    /// time. The expression itself does not carry timezone information.
113    Cron(CronSchedule),
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
117pub enum MissedRunPolicy {
118    Skip,
119    #[default]
120    CatchUpOnce,
121    ReplayAll,
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
125pub enum OverlapPolicy {
126    #[default]
127    Forbid,
128    QueueOne,
129    AllowParallel,
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
133pub enum TimeWindowAlignment {
134    #[default]
135    Disabled,
136    AlignToNextWindow,
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
140pub enum TerminalStatePolicy {
141    #[default]
142    Retain,
143    Delete,
144}
145
146#[derive(Debug, Clone)]
147pub struct SchedulerConfig {
148    /// The timezone forwarded to each [`crate::RunContext`] and used to
149    /// evaluate [`Schedule::Cron`] expressions.
150    ///
151    /// This does not rewrite [`Schedule::AtTimes`] values, which already carry
152    /// their own timezone-aware timestamps.
153    pub timezone: Tz,
154    /// The maximum number of [`crate::RunRecord`] items kept in memory.
155    pub history_limit: usize,
156    /// Controls how persisted state is handled once a job reaches a terminal
157    /// state (`next_run_at == None` and no further work is pending).
158    pub terminal_state_policy: TerminalStatePolicy,
159}
160
161impl Default for SchedulerConfig {
162    fn default() -> Self {
163        Self {
164            timezone: Shanghai,
165            history_limit: 32,
166            terminal_state_policy: TerminalStatePolicy::Retain,
167        }
168    }
169}
170
171impl Schedule {
172    /// Create a staggered interval schedule that defaults to the job id as
173    /// the phase seed.
174    pub fn staggered_interval(every: Duration) -> Self {
175        Self::StaggeredInterval(StaggeredIntervalSchedule::new(every))
176    }
177
178    /// Create a staggered interval schedule with an explicit phase seed.
179    pub fn staggered_interval_with_seed(every: Duration, seed: impl Into<String>) -> Self {
180        Self::StaggeredInterval(StaggeredIntervalSchedule::new(every).with_seed(seed))
181    }
182
183    /// Create a grouped interval schedule with evenly spaced members.
184    pub fn grouped_interval(every: Duration, group_size: u32, member_index: u32) -> Self {
185        Self::GroupedInterval(GroupedIntervalSchedule::new(
186            every,
187            group_size,
188            member_index,
189        ))
190    }
191
192    /// Create a grouped interval schedule and rotate the whole group with an
193    /// explicit seed.
194    pub fn grouped_interval_with_seed(
195        every: Duration,
196        group_size: u32,
197        member_index: u32,
198        group_seed: impl Into<String>,
199    ) -> Self {
200        Self::GroupedInterval(
201            GroupedIntervalSchedule::new(every, group_size, member_index)
202                .with_group_seed(group_seed),
203        )
204    }
205
206    /// Create a windowed interval schedule.
207    pub fn windowed_interval(default_every: Option<Duration>) -> Self {
208        Self::WindowedInterval(WindowedIntervalSchedule::new(default_every))
209    }
210}
211
212pub(crate) fn utc_time(value: DateTime<Tz>) -> DateTime<Utc> {
213    value.with_timezone(&Utc)
214}