Skip to main content

chronon/
lib.rs

1//! Chronon is a Rust cron and run-once scheduler for services: typed script handlers,
2//! durable job/run history, and an optional coordinator–worker split behind a thin
3//! [`SchedulerStore`](chronon_core::SchedulerStore) port.
4//!
5//! Wire storage once with [`ChrononBuilder`], register scripts with [`script`], schedule
6//! [`Job`](chronon_core::Job)s, then call [`Chronon::run`]. Swap `mem`, `sqlite`, Postgres,
7//! or Postgres+Redis without changing script code.
8//!
9//! ## Features
10//!
11//! - **Typed scripts** — `#[chronon::script]` registers handlers with inventory; params stay typed.
12//! - **Durable jobs and runs** — schedule config, revisions, and execution history on
13//!   [`SchedulerStore`](chronon_core::SchedulerStore).
14//! - **Composable storage** — in-memory, SQLite, PostgreSQL, or Postgres + Redis claim overlay.
15//! - **Embedded or split topology** — one process, or coordinator / worker / remote HTTP client
16//!   (see [Choose a topology](#choose-a-topology)).
17//! - **Host identity** — [`ContextFactory`](chronon_core::ContextFactory) rebuilds run-time
18//!   context from stored JSON.
19//! - **Optional HTTP API** — mount [`chronon_router`] (`axum` feature).
20//!
21//! *Cron and run-once scheduling without locking you into one database or a full workflow engine.*
22//!
23//! This crate ships with **no default features** (`default = []`). Enable explicitly:
24//! `mem`, `sqlite`, `postgres`, `redis` (requires `postgres`), `axum`, `telemetry-console`.
25//!
26//! # Getting started
27//!
28//! You always define scripts with `#[chronon::script]` and schedule via the generated
29//! [`ScriptHandle`] (`nightly_cleanup().job_with_params(...)`, then
30//! [`CoordinatorService::upsert_job`] / [`CoordinatorService::run_now`]). What changes is
31//! **which process ticks the schedule and which process executes scripts**.
32//!
33//! ## Choose a topology
34//!
35//! - **[Mode 1 — Embedded](#mode-1--embedded-one-binary)** — one binary schedules **and**
36//!   executes. Start here.
37//! - **[Mode 2 — Coordinator + worker](#mode-2--coordinator--worker-two-binaries)** — one
38//!   process ticks / enqueues; one or more **worker** binaries claim and run scripts.
39//! - **[Mode 3 — Remote HTTP client](#mode-3--remote-http-client-optional)** — your app has
40//!   **no** local Chronon loops; it talks to a coordinator HTTP API via
41//!   [`RemoteCoordinatorClient`]. Optional.
42//!
43//! | Topology | Builder | Store fit | When to use |
44//! |----------|---------|-----------|-------------|
45//! | Embedded | [`.embedded()`](ChrononBuilder::embedded) | mem / sqlite / postgres / postgres+redis | Local, single host, or simple production |
46//! | Coordinator | [`.coordinator_only()`](ChrononBuilder::coordinator_only) | Shared durable (postgres ± redis) | Scale-out: tick only |
47//! | Worker | [`.worker(pool)`](ChrononBuilder::worker) | Same shared store | Scale-out: claim + execute |
48//! | Remote client | [`.remote_coordinator(url)`](ChrononBuilder::remote_coordinator) | None locally | Schedule via HTTP (Mode 3) |
49//!
50//! Topology is [`DeploymentShape`] on [`ChrononBuilder`]. After you pick a mode, continue with
51//! [define a script](#4-define-a-script) (shared by every mode).
52//!
53//! ## Mode 1 — Embedded (one binary)
54//!
55//! This process runs the scheduler tick **and** the worker. There is no second binary.
56//!
57//! ```text
58//! Your app ──ScriptHandle / upsert_job──► Chronon ──tick + claim──► script handlers
59//!                                            │
60//!                                            └──► mem / SQLite / Postgres / Postgres+Redis
61//! ```
62//!
63//! | Backend | Type | Feature | Topology | Mode 1 boot |
64//! |---------|------|---------|----------|-------------|
65//! | In-memory | [`InMemorySchedulerStore`] | `mem` | embedded only | Below |
66//! | SQLite | [`SqliteSchedulerStore`] | `sqlite` | embedded | [sqlite crate](../chronon_backend_sqlite/index.html#mode-1--embedded) |
67//! | PostgreSQL | [`PostgresSchedulerStore`] | `postgres` | embedded or Mode 2 | [postgres crate](../chronon_backend_postgres/index.html#mode-1--embedded) |
68//! | Postgres + Redis | [`PostgresRedisSchedulerStore`] | `postgres,redis` | embedded or Mode 2 | [redis crate](../chronon_backend_redis/index.html#mode-1--embedded) |
69//!
70//! **In-memory first run** — `#[chronon::script]` generates a handle factory and
71//! `NightlyCleanupParams`; prefer that over stringly `Job::new`:
72//!
73//! ```ignore
74//! use std::sync::Arc;
75//! use chronon::prelude::*;
76//! use chronon::InMemorySchedulerStore;
77//!
78//! #[chronon::script(name = "nightly_cleanup")]
79//! async fn nightly_cleanup(
80//!     ctx: Box<dyn ScriptContext>,
81//!     retention_days: u32,
82//! ) -> chronon::Result<()> {
83//!     let _ = (ctx.label(), retention_days);
84//!     Ok(())
85//! }
86//!
87//! # async fn main() -> chronon::Result<()> {
88//! let chronon = ChrononBuilder::new()
89//!     .scheduler_store(Arc::new(InMemorySchedulerStore::new()))
90//!     .context_factory(Arc::new(JsonScriptContextFactory))
91//!     .embedded()
92//!     .auto_registry()
93//!     .build()?;
94//!
95//! let mut job = nightly_cleanup().job_with_params(
96//!     "nightly-schedule",
97//!     &NightlyCleanupParams { retention_days: 7 },
98//! )?;
99//! job.schedule_kind = ScheduleKind::Cron;
100//! job.cron_expr = Some("0 2 * * *".into());
101//! job.timezone = Some("UTC".into());
102//! chronon.coordinator_service().upsert_job(job).await?;
103//! // chronon.scheduler.init_partitions().await;
104//! // chronon.run().await?;
105//! # Ok(())
106//! # }
107//! ```
108//!
109//! Runnable: `script_handle_job`, `script_macro`, `embedded_tick`, `run_now` (`--features mem`).
110//! Other stores: follow the Mode 1 links in the table above. Then continue with
111//! [define a script](#4-define-a-script).
112//!
113//! ## Mode 2 — Coordinator + worker (two binaries)
114//!
115//! Use this when you want **scale-out execution** or to keep scheduling separate from script
116//! work. Both processes share the same durable store; they do **not** share memory.
117//! [`InMemorySchedulerStore`] cannot cross process boundaries — Mode 2 needs SQLite
118//! (same-host file), Postgres, or Postgres+Redis.
119//!
120//! ```text
121//! Coordinator binary ──tick──► shared store ──claim──► Worker binary(ies)
122//!        │                                              │
123//!        └── ScriptHandle / upsert_job           script handlers
124//! ```
125//!
126//! ### What you create
127//!
128//! | Piece | Purpose |
129//! |-------|---------|
130//! | Shared scripts | Same `#[chronon::script]` names linked into **workers** |
131//! | Coordinator binary | [`.coordinator_only()`](ChrononBuilder::coordinator_only) — tick + partitions; **no** worker slots |
132//! | Worker binary(ies) | [`.worker(pool)`](ChrononBuilder::worker) — claim + execute; unique [`.instance_id()`](ChrononBuilder::instance_id) |
133//! | Shared store | Postgres (add Redis for production claim throughput) |
134//!
135//! ### Pick a shared store
136//!
137//! Wire coordinator and worker from the adapter pages (production default: Postgres + Redis):
138//!
139//! | Backend | Feature | Mode 2 coordinator | Mode 2 worker |
140//! |---------|---------|--------------------|---------------|
141//! | Postgres + Redis | `postgres,redis` | [Coordinator](../chronon_backend_redis/index.html#mode-2--coordinator-binary) | [Worker](../chronon_backend_redis/index.html#mode-2--worker-binary) |
142//! | PostgreSQL | `postgres` | [Coordinator](../chronon_backend_postgres/index.html#mode-2--coordinator-binary) | [Worker](../chronon_backend_postgres/index.html#mode-2--worker-binary) |
143//! | SQLite (same host) | `sqlite` | [Coordinator](../chronon_backend_sqlite/index.html#mode-2--coordinator-binary) | [Worker](../chronon_backend_sqlite/index.html#mode-2--worker-binary) |
144//!
145//! ### Run both
146//!
147//! 1. Start Postgres (and Redis). Set `CHRONON_POSTGRES_URL` / `CHRONON_REDIS_URL`.
148//! 2. Start the **coordinator** (`init_partitions` then [`Chronon::run`]).
149//! 3. Start one or more **workers** with unique `CHRONON_INSTANCE_ID` values.
150//! 4. Upsert jobs (via [`ScriptHandle`]) from the coordinator, an Axum host, or
151//!    [Mode 3](#mode-3--remote-http-client-optional).
152//!
153//! ```bash
154//! export CHRONON_POSTGRES_URL=postgres://user:pass@localhost/chronon
155//! export CHRONON_REDIS_URL=redis://127.0.0.1:6379
156//! cargo run -p uf-chronon --example coordinator_daemon --features postgres,redis &
157//! CHRONON_INSTANCE_ID=worker-a cargo run -p uf-chronon --example worker_daemon --features postgres,redis
158//! ```
159//!
160//! ## Mode 3 — Remote HTTP client (optional)
161//!
162//! Use this when an application process should **schedule or trigger jobs** but must not run
163//! Chronon loops locally. Pair it with a host that mounts [`chronon_router`] on a Mode 1 or
164//! Mode 2 coordinator process.
165//!
166//! ```text
167//! App binary ──RemoteCoordinatorClient──HTTP──► API host (chronon_router)
168//!                                                    │
169//!                                                    └── Mode 1 or Mode 2 coordinator + store
170//! ```
171//!
172//! **API host** — nest the router under [`API_PREFIX`] (`/api/chronon`). Sketch:
173//! `axum_host` (`mem,axum`).
174//!
175//! **App binary** — build a [`Job`](chronon_core::Job) from your [`ScriptHandle`], then call
176//! [`RemoteCoordinatorClient`] (do not call [`Chronon::run`]):
177//!
178//! ```ignore
179//! use chronon::prelude::*;
180//!
181//! let base = resolve_remote_base_url()
182//!     .unwrap_or_else(|| "http://127.0.0.1:8080".into());
183//! let client = RemoteCoordinatorClient::new(base);
184//!
185//! let mut job = nightly_cleanup().job_with_params(
186//!     "nightly-schedule",
187//!     &NightlyCleanupParams { retention_days: 7 },
188//! )?;
189//! job.schedule_kind = ScheduleKind::Manual;
190//! client.upsert_job(job.clone()).await?;
191//! let _run_id = client.run_now(&job.job_id).await?;
192//! ```
193//!
194//! Set `CHRONON_REMOTE_BASE_URL` for [`resolve_remote_base_url`]. Timeout:
195//! `CHRONON_REMOTE_HTTP_TIMEOUT_MS` (default 3000).
196//!
197//! ## 4. Define a script
198//!
199//! `#[chronon::script]` registers the handler **and** turns the function into a
200//! [`ScriptHandle`] factory. Parameter types become a generated `*Params` struct
201//! (for example `NightlyCleanupParams`).
202//!
203//! ```ignore
204//! use chronon::prelude::*;
205//!
206//! #[chronon::script(name = "nightly_cleanup")]
207//! async fn nightly_cleanup(
208//!     ctx: Box<dyn ScriptContext>,
209//!     retention_days: u32,
210//! ) -> chronon::Result<()> {
211//!     println!("{}: retaining {retention_days} days", ctx.label());
212//!     Ok(())
213//! }
214//!
215//! // nightly_cleanup() -> ScriptHandle<NightlyCleanupParams>
216//! // NightlyCleanupParams { retention_days: u32 }
217//! ```
218//!
219//! Use [`.auto_registry()`](ChrononBuilder::auto_registry) so inventory picks up every
220//! `#[chronon::script]` linked into the binary. In Mode 2, scripts must be linked into
221//! **worker** binaries (that is where they run).
222//!
223//! See [`script`], [`ScriptHandle`], and [`ScriptContext`](chronon_core::ScriptContext).
224//! Runnable: `script_handle_job`, `script_macro`.
225//!
226//! ## 5. Schedule and trigger jobs
227//!
228//! Prefer the generated handle over stringly `Job::new("…", "script_name")`. Set
229//! [`ScheduleKind`](chronon_core::ScheduleKind) on the returned [`Job`](chronon_core::Job):
230//!
231//! | [`ScheduleKind`](chronon_core::ScheduleKind) | Behavior |
232//! |----------------------------------------------|----------|
233//! | `Cron` | Recurring; set `cron_expr` (+ optional `timezone`) |
234//! | `RunOnce` | Fires when `next_run_at` is due |
235//! | `Manual` | Never due for tick — only [`CoordinatorService::run_now`] |
236//!
237//! ```ignore
238//! use chronon::prelude::*;
239//!
240//! let mut nightly = nightly_cleanup().job_with_params(
241//!     "nightly-schedule",
242//!     &NightlyCleanupParams { retention_days: 7 },
243//! )?;
244//! nightly.schedule_kind = ScheduleKind::Cron;
245//! nightly.cron_expr = Some("0 2 * * *".into());
246//! chronon.coordinator_service().upsert_job(nightly).await?;
247//!
248//! let mut manual = nightly_cleanup().job_with_params(
249//!     "cleanup-now",
250//!     &NightlyCleanupParams { retention_days: 30 },
251//! )?;
252//! manual.schedule_kind = ScheduleKind::Manual;
253//! let id = manual.job_id.clone();
254//! chronon.coordinator_service().upsert_job(manual).await?;
255//! chronon.coordinator_service().run_now(&id).await?;
256//! ```
257//!
258//! Cron uses standard five-field syntax (optional sixth field for seconds). Parse helpers:
259//! [`CronExpr`]. Runnable: `script_handle_job`, `run_now`, `embedded_tick`.
260//!
261//! Storage wiring: [Mode 1](#mode-1--embedded-one-binary) (mem below; other backends on adapter
262//! crates) and [Mode 2](#mode-2--coordinator--worker-two-binaries) (link table).
263//!
264//! # Notes
265//!
266//! - **No default Cargo features** — enable `mem`, `sqlite`, `postgres`, `redis`, and/or `axum`
267//!   explicitly. Document the facade with `--all-features` so rustdoc links resolve.
268//! - **Mode 2 scripts live on workers** — inventory must be linked into the binary that calls
269//!   `.worker(...)`; the coordinator ticks but does not execute handlers.
270//! - **Call `scheduler.init_partitions().await` before [`Chronon::run`]** on embedded and
271//!   coordinator-only shapes.
272//! - **RemoteClient must not call [`Chronon::run`]** — that shape returns an error; use
273//!   [`RemoteCoordinatorClient`].
274//! - **`mem` is Mode 1 only** — it does not cross process boundaries.
275//!
276//! # Architecture
277//!
278//! Your application owns identity policy and business logic. Chronon owns scheduling semantics:
279//! due queries, claiming, cron evaluation, and script dispatch.
280//!
281//! ```text
282//! Your app / worker binary
283//!         │
284//!         ▼
285//!  ChrononBuilder ──► SchedulerStore port ──► mem | sqlite | postgres | postgres+redis | custom
286//!         │
287//!         ├──► Scheduler (tick / partitions)
288//!         └──► Executor + ScriptRegistry  ◄── ContextFactory / #[chronon::script]
289//! ```
290//!
291//! Mode 2 splits the loops across processes that share the store:
292//!
293//! ```text
294//! Coordinator ──.coordinator_only()──► tick + partitions ──► SchedulerStore
295//! Worker(s)   ──.worker(pool)────────► claim + execute   ──► same SchedulerStore
296//! ```
297//!
298//! # Configuration
299//!
300//! Settings merge in this order: explicit [`ChrononBuilder`] values override environment
301//! defaults where both exist.
302//!
303//! | Setting | Builder API | Environment | Default |
304//! |---------|-------------|-------------|---------|
305//! | Store | `.scheduler_store()` / `.scheduler_store_from_global()` | — | required |
306//! | Context factory | `.context_factory()` | — | `NoOpContextFactory` |
307//! | Telemetry | `.telemetry_sink()` | — | `NoOpSink` |
308//! | Script registry | `.script_registry()` / `.auto_registry()` | — | empty or inventory |
309//! | Tick interval | `.tick_interval_ms()` | `CHRONON_TICK_INTERVAL_MS` | 250 ms |
310//! | Instance id | `.instance_id()` | — | random UUID |
311//! | Partition count | — (env only) | `CHRONON_NUM_PARTITIONS` | 64 |
312//! | Worker pool | `.worker(pool)` / env | `CHRONON_WORKER_POOL` | `"general"` |
313//! | Worker concurrency | — | `CHRONON_WORKER_CONCURRENCY` | 4 |
314//! | Remote base URL | `.remote_coordinator(url)` | `CHRONON_REMOTE_BASE_URL` | — |
315//!
316//! Lease TTLs and tick batch limits are environment-only. See `chronon-scheduler` crate
317//! documentation for the full table.
318//!
319//! # Cargo features
320//!
321//! | Feature | Type | Status |
322//! |---------|------|--------|
323//! | `mem` | [`InMemorySchedulerStore`] | Ready — tests and local Mode 1 |
324//! | `sqlite` | [`SqliteSchedulerStore`] | Ready — embedded file-backed |
325//! | `postgres` | [`PostgresSchedulerStore`] | Ready — shared durable |
326//! | `redis` | [`PostgresRedisSchedulerStore`] | Ready — Postgres + Redis claim overlay (**requires `postgres`**) |
327//! | `axum` | [`chronon_router`], HTTP DTOs | Ready — mount on host Axum server |
328//! | `telemetry-console` | Documents `ConsoleSink` usage | Optional marker (`ConsoleSink` always re-exported) |
329//!
330//! # Runnable examples
331//!
332//! | Example | Topology | Features |
333//! |---------|----------|----------|
334//! | `script_macro` | Mode 1 | `mem` |
335//! | `script_handle_job` | Mode 1 | `mem` |
336//! | `run_now` | Mode 1 | `mem` |
337//! | `embedded_tick` | Mode 1 | `mem` |
338//! | `store_router_boot` | Mode 1 | `mem` |
339//! | `sqlite_boot` | Mode 1 | `sqlite` |
340//! | `postgres_boot` | Mode 1 | `postgres` |
341//! | `postgres_redis_boot` | Mode 1 | `postgres,redis` |
342//! | `axum_host` | Mode 1 + HTTP | `mem,axum` |
343//! | `coordinator_daemon` | Mode 2 coordinator | `postgres,redis` |
344//! | `worker_daemon` | Mode 2 worker | `postgres,redis` |
345//!
346//! ```bash
347//! cargo run -p uf-chronon --example script_handle_job --features mem
348//! cargo run -p uf-chronon --example coordinator_daemon --features postgres,redis
349//! cargo run -p uf-chronon --example worker_daemon --features postgres,redis
350//! ```
351
352pub use chronon_macros::script;
353pub use quark::inventory;
354
355pub mod prelude {
356    //! Curated re-exports for **application developers** building Chronon worker binaries.
357    //!
358    //! One-import surface for models, runtime boot, scheduler, executor, and the [`script`] macro.
359    //! Prefer `use chronon::prelude::*;` in worker binaries and integration tests rather than
360    //! importing internal crates directly. For durable storage wiring, also enable facade features
361    //! (`sqlite`, `postgres`, `redis`) and construct the matching [`SchedulerStore`] adapter.
362
363    pub use chronon_core::{
364        ContextFactory, Job, JobRevision, JsonScriptContextFactory, NoOpContextFactory,
365        NoOpScriptContext, Run, RunStatus, ScheduleKind, SchedulerStore, Script, ScriptContext,
366        ScriptHandle, StoreRouter, ChrononError, Result, DEFAULT_STORE_NAME,
367    };
368    pub use chronon_executor::{Executor, ExecutorEvent, ScriptDescriptor, ScriptRegistry};
369    pub use chronon_runtime::{
370        builder, Chronon, ChrononBuilder, CoordinatorService, DeploymentShape, JobSummary,
371        RemoteCoordinatorClient, resolve_remote_base_url,
372    };
373    pub use chronon_scheduler::{CronExpr, Scheduler, SchedulerConfig};
374    pub use crate::script;
375}
376
377pub use chronon_core as core;
378pub use chronon_runtime::{
379    builder, resolve_remote_base_url, Chronon, ChrononBuilder, CoordinatorService, DeploymentShape,
380    RemoteCoordinatorClient,
381};
382pub use chronon_core::{ChrononError, Result, ScriptHandle};
383pub use chronon_executor::{ScriptDescriptor, ScriptRegistry};
384pub use chronon_scheduler::CronExpr;
385
386#[cfg(feature = "axum")]
387pub use chronon_axum::{chronon_router, ApiResponse, ChrononState, API_PREFIX};
388
389#[cfg(feature = "mem")]
390pub use chronon_backend_mem::{install_default_mem_store, InMemorySchedulerStore};
391
392#[cfg(feature = "sqlite")]
393pub use chronon_backend_sqlite::SqliteSchedulerStore;
394
395#[cfg(feature = "postgres")]
396pub use chronon_backend_postgres::{postgres_test_url, PostgresSchedulerStore};
397
398#[cfg(feature = "redis")]
399pub use chronon_backend_redis::{PostgresRedisSchedulerStore, RedisQueueLayer};
400
401pub use chronon_telemetry::{ConsoleSink, NoOpSink, TelemetrySink};