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