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