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 TerminalStatePolicy {
134    #[default]
135    Retain,
136    Delete,
137}
138
139#[derive(Debug, Clone)]
140pub struct SchedulerConfig {
141    /// The timezone forwarded to each [`crate::RunContext`] and used to
142    /// evaluate [`Schedule::Cron`] expressions.
143    ///
144    /// This does not rewrite [`Schedule::AtTimes`] values, which already carry
145    /// their own timezone-aware timestamps.
146    pub timezone: Tz,
147    /// The maximum number of [`crate::RunRecord`] items kept in memory.
148    pub history_limit: usize,
149    /// Controls how persisted state is handled once a job reaches a terminal
150    /// state (`next_run_at == None` and no further work is pending).
151    pub terminal_state_policy: TerminalStatePolicy,
152}
153
154impl Default for SchedulerConfig {
155    fn default() -> Self {
156        Self {
157            timezone: Shanghai,
158            history_limit: 32,
159            terminal_state_policy: TerminalStatePolicy::Retain,
160        }
161    }
162}
163
164impl Schedule {
165    /// Create a staggered interval schedule that defaults to the job id as
166    /// the phase seed.
167    pub fn staggered_interval(every: Duration) -> Self {
168        Self::StaggeredInterval(StaggeredIntervalSchedule::new(every))
169    }
170
171    /// Create a staggered interval schedule with an explicit phase seed.
172    pub fn staggered_interval_with_seed(every: Duration, seed: impl Into<String>) -> Self {
173        Self::StaggeredInterval(StaggeredIntervalSchedule::new(every).with_seed(seed))
174    }
175
176    /// Create a grouped interval schedule with evenly spaced members.
177    pub fn grouped_interval(every: Duration, group_size: u32, member_index: u32) -> Self {
178        Self::GroupedInterval(GroupedIntervalSchedule::new(
179            every,
180            group_size,
181            member_index,
182        ))
183    }
184
185    /// Create a grouped interval schedule and rotate the whole group with an
186    /// explicit seed.
187    pub fn grouped_interval_with_seed(
188        every: Duration,
189        group_size: u32,
190        member_index: u32,
191        group_seed: impl Into<String>,
192    ) -> Self {
193        Self::GroupedInterval(
194            GroupedIntervalSchedule::new(every, group_size, member_index)
195                .with_group_seed(group_seed),
196        )
197    }
198
199    /// Create a windowed interval schedule.
200    pub fn windowed_interval(default_every: Option<Duration>) -> Self {
201        Self::WindowedInterval(WindowedIntervalSchedule::new(default_every))
202    }
203}
204
205pub(crate) fn utc_time(value: DateTime<Tz>) -> DateTime<Utc> {
206    value.with_timezone(&Utc)
207}