scheduler/lib.rs
1//! Async scheduling for a single logical job at a time.
2//!
3//! The scheduler decides when to trigger work and persists the resulting job
4//! state through a [`StateStore`]. Domain-specific retry, idempotency, and
5//! cursor management remain in the caller.
6//!
7//! Key semantics:
8//!
9//! - [`Schedule::AtTimes`] waits until each planned timestamp and treats an
10//! empty list as a no-op schedule.
11//! - [`Schedule::Interval`] schedules the first run at `now + interval`.
12//! - [`Schedule::StaggeredInterval`] spreads interval jobs by a stable phase
13//! derived from the job id or an explicit seed.
14//! - [`Schedule::Cron`] evaluates a standard 5-field cron expression in
15//! [`SchedulerConfig::timezone`].
16//! - [`JobTimeWindow`] can restrict execution by local weekday and time
17//! segments; outside-window occurrences are skipped with
18//! [`RunSkipReason::OutsideTimeWindow`].
19//! - [`Job::with_max_runs`] applies to every schedule kind; `0` exits without
20//! running.
21//! - [`SchedulerConfig::timezone`] is forwarded through [`RunContext`], drives
22//! [`Schedule::Cron`] evaluation, and does not rewrite absolute
23//! [`Schedule::AtTimes`] values.
24//! - Restarts resume by `job_id` from the saved [`JobState::next_run_at`].
25//! - [`SchedulerHandle::pause`] pauses future scheduling without interrupting
26//! the current run. [`SchedulerHandle::resume`] recomputes immediately and
27//! applies the existing missed-run policy.
28//! - Pause scope is backend-specific: legacy schedulers pause locally, while
29//! coordinated schedulers persist a shared pause state per `job_id`.
30//! - Dependency injection here means passing an explicit `deps` value when the
31//! job is constructed. The scheduler does not auto-resolve parameters.
32//!
33//! ```rust
34//! use std::time::Duration;
35//!
36//! use scheduler::{InMemoryStateStore, Job, Schedule, Scheduler, SchedulerConfig, Task};
37//!
38//! let runtime = tokio::runtime::Runtime::new().unwrap();
39//! runtime.block_on(async {
40//! let scheduler = Scheduler::new(SchedulerConfig::default(), InMemoryStateStore::new());
41//! let job = Job::without_deps(
42//! "doc-simple",
43//! Schedule::Interval(Duration::from_millis(1)),
44//! Task::from_async(|_| async { Ok(()) }),
45//! )
46//! .with_max_runs(1);
47//!
48//! let report = scheduler.run(job).await.unwrap();
49//! assert_eq!(report.history.len(), 1);
50//! });
51//! ```
52
53mod coordinated_store;
54mod error;
55mod execution_guard;
56mod guarded_runner;
57mod model;
58mod observer;
59mod scheduler;
60mod store;
61#[cfg(feature = "valkey-store")]
62mod valkey_coordinated_store;
63#[cfg(any(feature = "valkey-guard", feature = "valkey-store"))]
64mod valkey_execution_support;
65#[cfg(feature = "valkey-guard")]
66mod valkey_guard;
67#[cfg(any(feature = "valkey-guard", feature = "valkey-store"))]
68mod valkey_scripts;
69#[cfg(feature = "valkey-store")]
70mod valkey_store;
71
72pub use coordinated_store::{
73 CoordinatedClaim, CoordinatedLeaseConfig, CoordinatedPendingTrigger, CoordinatedRuntimeState,
74 CoordinatedStateStore, NoopCoordinatedStateStore,
75};
76pub use error::{
77 ExecutionGuardError, ExecutionGuardErrorKind, InvalidJobError, InvalidJobKind, SchedulerError,
78 StoreError, StoreErrorKind, TaskJoinError, TaskJoinErrorKind,
79};
80pub use execution_guard::{
81 ExecutionGuard, ExecutionGuardAcquire, ExecutionGuardRenewal, ExecutionGuardScope,
82 ExecutionLease, ExecutionSlot, NoopExecutionGuard,
83};
84pub use guarded_runner::{GuardedRunResult, GuardedRunner};
85pub use model::{
86 CronSchedule, Job, JobFuture, JobResult, JobState, JobTimeWindow, MissedRunPolicy,
87 OverlapPolicy, RunContext, RunRecord, RunSkipReason, RunStatus, Schedule, SchedulerConfig,
88 SchedulerReport, StaggeredIntervalSchedule, Task, TaskContext, TerminalStatePolicy,
89 TimeWindowSegment,
90};
91pub use observer::{
92 LogObserver, NoopObserver, PauseScope, SchedulerEvent, SchedulerObserver, SchedulerStopReason,
93 StateLoadSource,
94};
95pub use scheduler::{Scheduler, SchedulerHandle};
96pub use store::{
97 InMemoryStateStore, ResilientStateStore, ResilientStoreError, StateStore, StoreEvent,
98 StoreOperation,
99};
100#[cfg(feature = "valkey-store")]
101pub use valkey_coordinated_store::ValkeyCoordinatedStateStore;
102#[cfg(feature = "valkey-guard")]
103pub use valkey_guard::{ValkeyExecutionGuard, ValkeyLeaseConfig};
104#[cfg(feature = "valkey-store")]
105pub use valkey_store::ValkeyStateStore;