foxtive_cron/contracts.rs
1use crate::{CronError, CronResult};
2use chrono::{DateTime, Utc};
3use chrono_tz::Tz;
4use serde::{Deserialize, Serialize};
5use std::borrow::Cow;
6use std::collections::HashMap;
7use std::str::FromStr;
8use std::sync::Arc;
9use std::time::Duration;
10use tokio::sync::RwLock;
11
12/// Policies for handling missed job executions.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
14pub enum MisfirePolicy {
15 /// Skip all missed runs and wait for the next scheduled occurrence.
16 #[default]
17 Skip,
18 /// Execute a single missed run as soon as possible, then resume regular schedule.
19 FireOnce,
20 /// Execute all missed runs as soon as possible, one by one.
21 FireAll,
22}
23
24/// Policies for retrying failed job runs.
25#[derive(Debug, Clone, Serialize, Deserialize, Default)]
26pub enum RetryPolicy {
27 /// No retries.
28 #[default]
29 None,
30 /// Retry a fixed number of times with a specific interval.
31 Fixed {
32 max_retries: usize,
33 interval: Duration,
34 },
35 /// Exponential backoff retry strategy.
36 Exponential {
37 max_retries: usize,
38 initial_interval: Duration,
39 max_interval: Duration,
40 },
41}
42
43/// Events emitted by the scheduler during the job lifecycle.
44#[derive(Debug, Clone)]
45pub enum JobEvent {
46 /// Emitted when a job is about to start.
47 Started { id: String, name: String },
48 /// Emitted when a job completes successfully.
49 Completed {
50 id: String,
51 name: String,
52 duration: Duration,
53 },
54 /// Emitted when a job fails.
55 Failed {
56 id: String,
57 name: String,
58 error: String,
59 },
60 /// Emitted when a job is scheduled for retry.
61 Retrying {
62 id: String,
63 name: String,
64 attempt: usize,
65 delay: Duration,
66 },
67 /// Emitted when a scheduled job misfires.
68 Misfired {
69 id: String,
70 name: String,
71 scheduled_time: DateTime<Utc>,
72 },
73}
74
75/// Trait for listening to scheduler events.
76#[async_trait::async_trait]
77pub trait JobEventListener: Send + Sync {
78 /// Called when an event occurs.
79 async fn on_event(&self, event: JobEvent);
80}
81
82/// Trait for exporting metrics.
83pub trait MetricsExporter: Send + Sync {
84 /// Record a job start.
85 fn record_start(&self, id: &str, name: &str);
86 /// Record a job completion.
87 fn record_completion(&self, id: &str, name: &str, duration: Duration);
88 /// Record a job failure.
89 fn record_failure(&self, id: &str, name: &str);
90 /// Record a job retry.
91 fn record_retry(&self, id: &str, name: &str);
92 /// Record a job misfire.
93 fn record_misfire(&self, id: &str, name: &str);
94}
95
96/// Information about a job's execution state for persistence.
97#[derive(Debug, Clone, Serialize, Deserialize, Default)]
98pub struct JobState {
99 pub last_run: Option<DateTime<Utc>>,
100 pub last_success: Option<DateTime<Utc>>,
101 pub last_failure: Option<DateTime<Utc>>,
102 pub consecutive_failures: usize,
103}
104
105/// Trait for persisting job definitions and states.
106#[async_trait::async_trait]
107pub trait JobStore: Send + Sync {
108 /// Save or update a job's state.
109 async fn save_state(&self, id: &str, state: &JobState) -> CronResult<()>;
110 /// Retrieve a job's state.
111 async fn get_state(&self, id: &str) -> CronResult<Option<JobState>>;
112}
113
114/// A simple in-memory implementation of [`JobStore`].
115#[derive(Default)]
116pub struct InMemoryJobStore {
117 states: Arc<RwLock<HashMap<String, JobState>>>,
118}
119
120impl InMemoryJobStore {
121 pub fn new() -> Self {
122 Self::default()
123 }
124}
125
126#[async_trait::async_trait]
127impl JobStore for InMemoryJobStore {
128 async fn save_state(&self, id: &str, state: &JobState) -> CronResult<()> {
129 let mut states = self.states.write().await;
130 states.insert(id.to_string(), state.clone());
131 Ok(())
132 }
133
134 async fn get_state(&self, id: &str) -> CronResult<Option<JobState>> {
135 let states = self.states.read().await;
136 Ok(states.get(id).cloned())
137 }
138}
139
140/// A validated cron schedule, parsed at construction time to prevent
141/// runtime errors from malformed expressions.
142///
143/// Wraps [`cron::Schedule`] and is the required return type of [`JobContract::schedule`].
144/// By accepting only `ValidatedSchedule` values, the scheduler guarantees that
145/// no job can be registered with an invalid cron expression.
146#[derive(Debug, Clone)]
147pub struct ValidatedSchedule(pub(crate) cron::Schedule);
148
149impl ValidatedSchedule {
150 /// Parse and validate a cron expression.
151 ///
152 /// # Errors
153 /// Returns an error if the expression is not a valid cron format.
154 ///
155 /// # Example
156 /// ```rust
157 /// use foxtive_cron::contracts::ValidatedSchedule;
158 ///
159 /// let schedule = ValidatedSchedule::parse("*/5 * * * * * *").unwrap();
160 /// ```
161 pub fn parse(expr: &str) -> CronResult<Self> {
162 let schedule = cron::Schedule::from_str(expr)
163 .map_err(|e| CronError::InvalidSchedule(format!("{}: {}", expr, e)))?;
164 Ok(Self(schedule))
165 }
166
167 /// Returns the next occurrence after the given time, in the specified timezone.
168 pub fn next_after(&self, after: &DateTime<Utc>, tz: Tz) -> Option<DateTime<Utc>> {
169 let local_after = after.with_timezone(&tz);
170 self.0
171 .after(&local_after)
172 .next()
173 .map(|dt| dt.with_timezone(&Utc))
174 }
175}
176
177/// A trait that defines a schedule for a job.
178pub trait Schedule: Send + Sync {
179 /// Returns the next scheduled time after the given timestamp.
180 fn next_after(&self, after: &DateTime<Utc>, tz: Tz) -> Option<DateTime<Utc>>;
181}
182
183impl Schedule for ValidatedSchedule {
184 fn next_after(&self, after: &DateTime<Utc>, tz: Tz) -> Option<DateTime<Utc>> {
185 self.next_after(after, tz)
186 }
187}
188
189/// A type of job.
190#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
191pub enum JobType {
192 /// A job that runs according to a cron schedule.
193 Recurring,
194 /// A job that runs once at a specific time.
195 Once,
196}
197
198/// A trait representing a schedulable job for the Cron system.
199///
200/// Any struct that implements this trait can be scheduled and executed
201/// according to its provided cron schedule.
202///
203/// ## Required Methods
204/// - `run`: The asynchronous execution logic of the job.
205/// - `id`: A stable unique identifier used for cancellation and deduplication.
206/// - `name`: A human-readable name for identification and logging.
207/// - `schedule`: A description of when the job runs.
208///
209/// ## Optional Methods
210/// - `description`: An optional human-friendly description of the job's purpose.
211/// - `on_start`: Lifecycle hook called just before `run`.
212/// - `on_complete`: Lifecycle hook called after a successful `run`.
213/// - `on_error`: Lifecycle hook called when `run` returns an error.
214#[async_trait::async_trait]
215pub trait JobContract: Send + Sync {
216 /// The asynchronous logic to run when the job is triggered.
217 ///
218 /// Called every time the scheduler reaches the job's scheduled time.
219 ///
220 /// # Returns
221 /// - `Ok(())` if the job completed successfully.
222 /// - `Err(anyhow::Error)` if an error occurred during execution.
223 async fn run(&self) -> CronResult<()>;
224
225 /// A stable unique identifier for this job.
226 ///
227 /// Used for cancellation, deduplication, and internal tracking.
228 /// Should remain consistent across restarts (e.g. a static string or UUID).
229 fn id(&self) -> Cow<'_, str>;
230
231 /// A human-readable name for the job.
232 ///
233 /// Used in logs and debug output. Does not need to be globally unique.
234 fn name(&self) -> Cow<'_, str>;
235
236 /// The schedule describing when this job should run.
237 fn schedule(&self) -> &dyn Schedule;
238
239 /// The type of job.
240 fn job_type(&self) -> JobType {
241 JobType::Recurring
242 }
243
244 /// The time at which the job should run if it is a one-time job.
245 ///
246 /// If `job_type()` returns `JobType::Once`, this must return `Some`.
247 fn run_at(&self) -> Option<DateTime<Utc>> {
248 None
249 }
250
251 /// The time after which the job should start running.
252 fn start_after(&self) -> Option<DateTime<Utc>> {
253 None
254 }
255
256 /// The timezone in which the cron schedule should be evaluated.
257 ///
258 /// Defaults to `UTC`.
259 fn timezone(&self) -> Tz {
260 chrono_tz::UTC
261 }
262
263 /// A brief optional description of what this job does.
264 ///
265 /// Defaults to `None`.
266 fn description(&self) -> Option<Cow<'_, str>> {
267 None
268 }
269
270 /// The maximum duration a single job run should take.
271 ///
272 /// If `None`, the job has no timeout.
273 fn timeout(&self) -> Option<Duration> {
274 None
275 }
276
277 /// The priority of the job. Higher values represent higher priority.
278 ///
279 /// When multiple jobs are scheduled at the same time, higher priority
280 /// jobs will be triggered first.
281 fn priority(&self) -> i32 {
282 0
283 }
284
285 /// The maximum number of concurrent executions for this job.
286 ///
287 /// If `None`, there's no per-job concurrency limit.
288 fn concurrency_limit(&self) -> Option<usize> {
289 None
290 }
291
292 /// Defines how the scheduler behaves if a scheduled execution is missed.
293 fn misfire_policy(&self) -> MisfirePolicy {
294 MisfirePolicy::default()
295 }
296
297 /// Defines how the scheduler behaves if an execution fails.
298 fn retry_policy(&self) -> RetryPolicy {
299 RetryPolicy::default()
300 }
301
302 /// Called just before [`run`](Self::run) is invoked.
303 ///
304 /// Useful for metrics, logging, or pre-flight checks.
305 /// Defaults to a no-op.
306 async fn on_start(&self) {}
307
308 /// Called after [`run`](Self::run) completes successfully.
309 ///
310 /// Useful for metrics or post-processing.
311 /// Defaults to a no-op.
312 async fn on_complete(&self) {}
313
314 /// Called if [`run`](Self::run) returns an `Err`.
315 ///
316 /// Useful for alerting, retry logic, or error reporting.
317 /// Defaults to a no-op.
318 async fn on_error(&self, _error: &CronError) {}
319}