Skip to main content

scheduler/model/
schedule.rs

1use super::CronSchedule;
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#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum Schedule {
51    /// Trigger repeatedly after the given interval.
52    Interval(Duration),
53    /// Trigger repeatedly after the given interval with a stable phase.
54    ///
55    /// The first run is aligned to a deterministic phase derived from the
56    /// seed. If no seed is provided, the job id is used.
57    StaggeredInterval(StaggeredIntervalSchedule),
58    /// Trigger repeatedly after the given interval with evenly spaced group
59    /// members.
60    ///
61    /// The first run is aligned to a stable slot determined by the member
62    /// index. An optional group seed can rotate the entire group without
63    /// changing the spacing between members.
64    GroupedInterval(GroupedIntervalSchedule),
65    /// Trigger at the listed wall-clock times.
66    ///
67    /// The list is sorted before execution starts. An empty list is treated as
68    /// a no-op schedule and exits without running.
69    AtTimes(Vec<DateTime<Tz>>),
70    /// Trigger using a standard 5-field cron expression.
71    ///
72    /// The scheduler timezone is used to calculate the next matching wall-clock
73    /// time. The expression itself does not carry timezone information.
74    Cron(CronSchedule),
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
78pub enum MissedRunPolicy {
79    Skip,
80    #[default]
81    CatchUpOnce,
82    ReplayAll,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
86pub enum OverlapPolicy {
87    #[default]
88    Forbid,
89    QueueOne,
90    AllowParallel,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
94pub enum TerminalStatePolicy {
95    #[default]
96    Retain,
97    Delete,
98}
99
100#[derive(Debug, Clone)]
101pub struct SchedulerConfig {
102    /// The timezone forwarded to each [`crate::RunContext`] and used to
103    /// evaluate [`Schedule::Cron`] expressions.
104    ///
105    /// This does not rewrite [`Schedule::AtTimes`] values, which already carry
106    /// their own timezone-aware timestamps.
107    pub timezone: Tz,
108    /// The maximum number of [`crate::RunRecord`] items kept in memory.
109    pub history_limit: usize,
110    /// Controls how persisted state is handled once a job reaches a terminal
111    /// state (`next_run_at == None` and no further work is pending).
112    pub terminal_state_policy: TerminalStatePolicy,
113}
114
115impl Default for SchedulerConfig {
116    fn default() -> Self {
117        Self {
118            timezone: Shanghai,
119            history_limit: 32,
120            terminal_state_policy: TerminalStatePolicy::Retain,
121        }
122    }
123}
124
125impl Schedule {
126    /// Create a staggered interval schedule that defaults to the job id as
127    /// the phase seed.
128    pub fn staggered_interval(every: Duration) -> Self {
129        Self::StaggeredInterval(StaggeredIntervalSchedule::new(every))
130    }
131
132    /// Create a staggered interval schedule with an explicit phase seed.
133    pub fn staggered_interval_with_seed(every: Duration, seed: impl Into<String>) -> Self {
134        Self::StaggeredInterval(StaggeredIntervalSchedule::new(every).with_seed(seed))
135    }
136
137    /// Create a grouped interval schedule with evenly spaced members.
138    pub fn grouped_interval(every: Duration, group_size: u32, member_index: u32) -> Self {
139        Self::GroupedInterval(GroupedIntervalSchedule::new(
140            every,
141            group_size,
142            member_index,
143        ))
144    }
145
146    /// Create a grouped interval schedule and rotate the whole group with an
147    /// explicit seed.
148    pub fn grouped_interval_with_seed(
149        every: Duration,
150        group_size: u32,
151        member_index: u32,
152        group_seed: impl Into<String>,
153    ) -> Self {
154        Self::GroupedInterval(
155            GroupedIntervalSchedule::new(every, group_size, member_index)
156                .with_group_seed(group_seed),
157        )
158    }
159}
160
161pub(crate) fn utc_time(value: DateTime<Tz>) -> DateTime<Utc> {
162    value.with_timezone(&Utc)
163}