1use super::*;
12
13pub type TaskResult = Result<(), String>;
15pub type TaskCallback = Arc<dyn Fn(TaskContext) -> TaskResult + Send + Sync + 'static>;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum SchedulerError {
21 InvalidConfig(&'static str),
23 InvalidTaskId,
25 InvalidSchedule(&'static str),
27 InvalidCron(String),
29 DuplicateTask(String),
31 CapacityExceeded {
33 max_tasks: usize,
35 },
36 Shutdown,
38 WorkerPanicked,
40}
41
42impl fmt::Display for SchedulerError {
43 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44 write!(formatter, "{self:?}")
45 }
46}
47
48impl std::error::Error for SchedulerError {}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct SchedulerConfig {
53 pub max_tasks: usize,
55 pub max_concurrent_tasks: usize,
57 pub poll_interval: Duration,
59}
60
61impl Default for SchedulerConfig {
62 fn default() -> Self {
63 Self {
64 max_tasks: 1_024,
65 max_concurrent_tasks: 4,
66 poll_interval: Duration::from_millis(25),
67 }
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
73pub enum TaskSchedule {
74 Once {
76 run_at: SystemTime,
78 },
79 Interval {
81 every: Duration,
83 start_at: Option<SystemTime>,
85 },
86 Cron {
88 expression: String,
90 },
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct RetryPolicy {
96 pub max_attempts: u32,
98 pub initial_backoff: Duration,
100 pub max_backoff: Duration,
102 pub multiplier: u32,
104 pub jitter: Duration,
106}
107
108impl Default for RetryPolicy {
109 fn default() -> Self {
110 Self {
111 max_attempts: 1,
112 initial_backoff: Duration::from_millis(100),
113 max_backoff: Duration::from_secs(30),
114 multiplier: 2,
115 jitter: Duration::ZERO,
116 }
117 }
118}
119
120#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct ScheduledTask {
123 pub id: String,
125 pub schedule: TaskSchedule,
127 pub retry: RetryPolicy,
129 pub priority: u8,
131 pub trace: Option<TraceContext>,
133}
134
135impl ScheduledTask {
136 pub fn validate(&self) -> Result<(), SchedulerError> {
138 validate_task_id(&self.id)?;
139 validate_retry(&self.retry)?;
140 validate_schedule(&self.schedule)
141 }
142}
143
144fn validate_schedule(schedule: &TaskSchedule) -> Result<(), SchedulerError> {
145 match schedule {
146 TaskSchedule::Once { .. } => Ok(()),
147 TaskSchedule::Interval { every, .. } if every.is_zero() => {
148 Err(SchedulerError::InvalidSchedule("zero interval"))
149 }
150 TaskSchedule::Interval { .. } => Ok(()),
151 TaskSchedule::Cron { expression } => Schedule::from_str(expression)
152 .map(|_| ())
153 .map_err(|error| SchedulerError::InvalidCron(error.to_string())),
154 }
155}
156
157fn validate_retry(policy: &RetryPolicy) -> Result<(), SchedulerError> {
158 if policy.max_attempts == 0 {
159 return Err(SchedulerError::InvalidSchedule(
160 "max_attempts must be positive",
161 ));
162 }
163 if policy.multiplier == 0 {
164 return Err(SchedulerError::InvalidSchedule(
165 "retry multiplier must be positive",
166 ));
167 }
168 if policy.initial_backoff > policy.max_backoff {
169 return Err(SchedulerError::InvalidSchedule(
170 "initial_backoff exceeds max_backoff",
171 ));
172 }
173 Ok(())
174}
175
176fn validate_task_id(task_id: &str) -> Result<(), SchedulerError> {
177 if task_id.is_empty()
178 || task_id.len() > MAX_TASK_ID_BYTES
179 || !task_id
180 .bytes()
181 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
182 {
183 return Err(SchedulerError::InvalidTaskId);
184 }
185 Ok(())
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
190pub struct TaskSnapshot {
191 pub id: String,
193 pub priority: u8,
195 pub running: bool,
197 pub attempts: u32,
199 pub next_run: SystemTime,
201 pub last_error: Option<String>,
203 pub trace: Option<TraceContext>,
205}
206
207#[derive(Debug, Clone, PartialEq, Eq)]
209pub struct SchedulerSnapshot {
210 pub shutdown: bool,
212 pub active_tasks: usize,
214 pub tasks: Vec<TaskSnapshot>,
216}
217pub struct TaskContext {
219 task_id: String,
220 attempt: u32,
221 cancelled: Arc<AtomicBool>,
222 shutdown: Arc<AtomicBool>,
223 trace: Option<TraceContext>,
224}
225
226impl TaskContext {
227 pub(crate) fn new(
228 task_id: String,
229 attempt: u32,
230 cancelled: Arc<AtomicBool>,
231 shutdown: Arc<AtomicBool>,
232 trace: Option<TraceContext>,
233 ) -> Self {
234 Self {
235 task_id,
236 attempt,
237 cancelled,
238 shutdown,
239 trace,
240 }
241 }
242
243 pub fn task_id(&self) -> &str {
245 &self.task_id
246 }
247
248 pub fn attempt(&self) -> u32 {
250 self.attempt
251 }
252
253 pub fn is_cancelled(&self) -> bool {
255 self.cancelled.load(Ordering::Acquire) || self.shutdown.load(Ordering::Acquire)
256 }
257
258 pub fn trace(&self) -> Option<&TraceContext> {
260 self.trace.as_ref()
261 }
262}