Skip to main content

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}