Skip to main content

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::GroupedInterval`] spreads known group members evenly across
15//!   each interval.
16//! - [`Schedule::GroupedCron`] spreads known group members evenly inside a
17//!   stable post-cron window for each cron anchor.
18//! - [`Schedule::WindowedInterval`] selects an interval by local time windows;
19//!   `None` intervals disable triggers for the matching period.
20//! - [`Schedule::Cron`] evaluates a standard 5-field cron expression in
21//!   [`SchedulerConfig::timezone`].
22//! - [`JobTimeWindow`] can restrict execution by local weekday and time
23//!   segments; outside-window occurrences are skipped with
24//!   [`RunSkipReason::OutsideTimeWindow`].
25//! - [`Job::with_max_runs`] applies to every schedule kind; `0` exits without
26//!   running.
27//! - [`SchedulerConfig::timezone`] is forwarded through [`RunContext`], drives
28//!   [`Schedule::Cron`] evaluation, and does not rewrite absolute
29//!   [`Schedule::AtTimes`] values.
30//! - Restarts resume by `job_id` from the saved [`JobState::next_run_at`].
31//! - [`SchedulerHandle::pause`] pauses future scheduling without interrupting
32//!   the current run. [`SchedulerHandle::resume`] recomputes immediately and
33//!   applies the existing missed-run policy.
34//! - Pause scope is backend-specific: legacy schedulers pause locally, while
35//!   coordinated schedulers persist a shared pause state per `job_id`.
36//! - Dependency injection here means passing an explicit `deps` value when the
37//!   job is constructed. The scheduler does not auto-resolve parameters.
38//!
39//! ```rust
40//! use std::time::Duration;
41//!
42//! use scheduler::{InMemoryStateStore, Job, Schedule, Scheduler, SchedulerConfig, Task};
43//!
44//! let runtime = tokio::runtime::Runtime::new().unwrap();
45//! runtime.block_on(async {
46//!     let scheduler = Scheduler::new(SchedulerConfig::default(), InMemoryStateStore::new());
47//!     let job = Job::without_deps(
48//!         "doc-simple",
49//!         Schedule::Interval(Duration::from_millis(1)),
50//!         Task::from_async(|_| async { Ok(()) }),
51//!     )
52//!     .with_max_runs(1);
53//!
54//!     let report = scheduler.run(job).await.unwrap();
55//!     assert_eq!(report.history.len(), 1);
56//! });
57//! ```
58
59mod coordinated_store;
60mod error;
61mod execution_guard;
62mod guarded_runner;
63mod model;
64mod observer;
65mod scheduler;
66mod store;
67#[cfg(feature = "valkey-store")]
68mod valkey_coordinated_store;
69#[cfg(any(feature = "valkey-guard", feature = "valkey-store"))]
70mod valkey_execution_support;
71#[cfg(feature = "valkey-guard")]
72mod valkey_guard;
73#[cfg(any(feature = "valkey-guard", feature = "valkey-store"))]
74mod valkey_runtime;
75#[cfg(any(feature = "valkey-guard", feature = "valkey-store"))]
76mod valkey_scripts;
77#[cfg(feature = "valkey-store")]
78mod valkey_store;
79
80pub use coordinated_store::{
81    CoordinatedClaim, CoordinatedCompletion, CoordinatedLeaseConfig, CoordinatedPendingTrigger,
82    CoordinatedRuntimeState, CoordinatedStateStore, NoopCoordinatedStateStore,
83};
84pub use error::{
85    ExecutionGuardError, ExecutionGuardErrorKind, InvalidJobError, InvalidJobKind, SchedulerError,
86    StoreError, StoreErrorKind, TaskJoinError, TaskJoinErrorKind,
87};
88pub use execution_guard::{
89    ExecutionGuard, ExecutionGuardAcquire, ExecutionGuardEvent, ExecutionGuardRenewal,
90    ExecutionGuardScope, ExecutionLease, ExecutionSlot, NoopExecutionGuard,
91};
92pub use guarded_runner::{GuardedRunResult, GuardedRunner};
93pub use model::{
94    CronSchedule, GroupedCronSchedule, GroupedIntervalSchedule, IntervalWindow, Job, JobFuture,
95    JobResult, JobState, JobTimeWindow, MissedRunPolicy, OverlapPolicy, RunContext, RunRecord,
96    RunSkipReason, RunStatus, Schedule, SchedulerConfig, SchedulerReport,
97    StaggeredIntervalSchedule, Task, TaskContext, TerminalStatePolicy, TimeWindowAlignment,
98    TimeWindowSegment, TriggerSource, TriggeredTaskContext, WindowedIntervalSchedule,
99};
100pub use observer::{
101    LogObserver, NoopObserver, PauseScope, SchedulerEvent, SchedulerObserver, SchedulerStopReason,
102    StateLoadSource,
103};
104pub use scheduler::{Scheduler, SchedulerHandle};
105pub use store::{
106    InMemoryStateStore, ResilientStateStore, ResilientStoreError, StateStore, StoreEvent,
107    StoreOperation,
108};
109#[cfg(feature = "valkey-store")]
110pub use valkey_coordinated_store::ValkeyCoordinatedStateStore;
111#[cfg(feature = "valkey-guard")]
112pub use valkey_guard::{ValkeyExecutionGuard, ValkeyLeaseConfig};
113#[cfg(any(feature = "valkey-guard", feature = "valkey-store"))]
114pub use valkey_runtime::ValkeyRecoveryConfig;
115#[cfg(feature = "valkey-store")]
116pub use valkey_store::ValkeyStateStore;