Skip to main content

Crate chronon

Crate chronon 

Source
Expand description

Chronon is a Rust cron and run-once scheduler for services: typed script handlers, durable job/run history, and an optional coordinator–worker split behind a thin SchedulerStore port.

Wire storage once with ChrononBuilder, register scripts with script, schedule Jobs, then call Chronon::run. Swap mem, sqlite, Postgres, or Postgres+Redis without changing script code.

§Features

  • Typed scripts#[chronon::script] registers handlers with inventory; params stay typed.
  • Durable jobs and runs — schedule config, revisions, and execution history on SchedulerStore.
  • Composable storage — in-memory, SQLite, PostgreSQL, or Postgres + Redis claim overlay.
  • Embedded or split topology — one process, or coordinator / worker / remote HTTP client (see Choose a topology).
  • Host identityContextFactory rebuilds run-time context from stored JSON.
  • Optional HTTP API — mount chronon_router (axum feature).

Cron and run-once scheduling without locking you into one database or a full workflow engine.

This crate ships with no default features (default = []). Enable explicitly: mem, sqlite, postgres, redis (requires postgres), axum, telemetry-console.

§Getting started

You always define scripts with #[chronon::script] and schedule via the generated ScriptHandle (nightly_cleanup().job_with_params(...), then CoordinatorService::upsert_job / CoordinatorService::run_now). What changes is which process ticks the schedule and which process executes scripts.

§Choose a topology

TopologyBuilderStore fitWhen to use
Embedded.embedded()mem / sqlite / postgres / postgres+redisLocal, single host, or simple production
Coordinator.coordinator_only()Shared durable (postgres ± redis)Scale-out: tick only
Worker.worker(pool)Same shared storeScale-out: claim + execute
Remote client.remote_coordinator(url)None locallySchedule via HTTP (Mode 3)

Topology is DeploymentShape on ChrononBuilder. After you pick a mode, continue with define a script (shared by every mode).

§Mode 1 — Embedded (one binary)

This process runs the scheduler tick and the worker. There is no second binary.

Your app ──ScriptHandle / upsert_job──► Chronon ──tick + claim──► script handlers
                                           │
                                           └──► mem / SQLite / Postgres / Postgres+Redis
BackendTypeFeatureTopologyMode 1 boot
In-memoryInMemorySchedulerStorememembedded onlyBelow
SQLiteSqliteSchedulerStoresqliteembeddedsqlite crate
PostgreSQLPostgresSchedulerStorepostgresembedded or Mode 2postgres crate
Postgres + RedisPostgresRedisSchedulerStorepostgres,redisembedded or Mode 2redis crate

In-memory first run#[chronon::script] generates a handle factory and NightlyCleanupParams; prefer that over stringly Job::new:

use std::sync::Arc;
use chronon::prelude::*;
use chronon::InMemorySchedulerStore;

#[chronon::script(name = "nightly_cleanup")]
async fn nightly_cleanup(
    ctx: Box<dyn ScriptContext>,
    retention_days: u32,
) -> chronon::Result<()> {
    let _ = (ctx.label(), retention_days);
    Ok(())
}

let chronon = ChrononBuilder::new()
    .scheduler_store(Arc::new(InMemorySchedulerStore::new()))
    .context_factory(Arc::new(JsonScriptContextFactory))
    .embedded()
    .auto_registry()
    .build()?;

let mut job = nightly_cleanup().job_with_params(
    "nightly-schedule",
    &NightlyCleanupParams { retention_days: 7 },
)?;
job.schedule_kind = ScheduleKind::Cron;
job.cron_expr = Some("0 2 * * *".into());
job.timezone = Some("UTC".into());
chronon.coordinator_service().upsert_job(job).await?;
// chronon.scheduler.init_partitions().await;
// chronon.run().await?;

Runnable: script_handle_job, script_macro, embedded_tick, run_now (--features mem). Other stores: follow the Mode 1 links in the table above. Then continue with define a script.

§Mode 2 — Coordinator + worker (two binaries)

Use this when you want scale-out execution or to keep scheduling separate from script work. Both processes share the same durable store; they do not share memory. InMemorySchedulerStore cannot cross process boundaries — Mode 2 needs SQLite (same-host file), Postgres, or Postgres+Redis.

Coordinator binary ──tick──► shared store ──claim──► Worker binary(ies)
       │                                              │
       └── ScriptHandle / upsert_job           script handlers

§What you create

PiecePurpose
Shared scriptsSame #[chronon::script] names linked into workers
Coordinator binary.coordinator_only() — tick + partitions; no worker slots
Worker binary(ies).worker(pool) — claim + execute; unique .instance_id()
Shared storePostgres (add Redis for production claim throughput)

§Pick a shared store

Wire coordinator and worker from the adapter pages (production default: Postgres + Redis):

BackendFeatureMode 2 coordinatorMode 2 worker
Postgres + Redispostgres,redisCoordinatorWorker
PostgreSQLpostgresCoordinatorWorker
SQLite (same host)sqliteCoordinatorWorker

§Run both

  1. Start Postgres (and Redis). Set CHRONON_POSTGRES_URL / CHRONON_REDIS_URL.
  2. Start the coordinator (init_partitions then Chronon::run).
  3. Start one or more workers with unique CHRONON_INSTANCE_ID values.
  4. Upsert jobs (via ScriptHandle) from the coordinator, an Axum host, or Mode 3.
export CHRONON_POSTGRES_URL=postgres://user:pass@localhost/chronon
export CHRONON_REDIS_URL=redis://127.0.0.1:6379
cargo run -p uf-chronon --example coordinator_daemon --features postgres,redis &
CHRONON_INSTANCE_ID=worker-a cargo run -p uf-chronon --example worker_daemon --features postgres,redis

§Mode 3 — Remote HTTP client (optional)

Use this when an application process should schedule or trigger jobs but must not run Chronon loops locally. Pair it with a host that mounts chronon_router on a Mode 1 or Mode 2 coordinator process.

App binary ──RemoteCoordinatorClient──HTTP──► API host (chronon_router)
                                                   │
                                                   └── Mode 1 or Mode 2 coordinator + store

API host — nest the router under API_PREFIX (/api/chronon). Sketch: axum_host (mem,axum).

App binary — build a Job from your ScriptHandle, then call RemoteCoordinatorClient (do not call Chronon::run):

use chronon::prelude::*;

let base = resolve_remote_base_url()
    .unwrap_or_else(|| "http://127.0.0.1:8080".into());
let client = RemoteCoordinatorClient::new(base);

let mut job = nightly_cleanup().job_with_params(
    "nightly-schedule",
    &NightlyCleanupParams { retention_days: 7 },
)?;
job.schedule_kind = ScheduleKind::Manual;
client.upsert_job(job.clone()).await?;
let _run_id = client.run_now(&job.job_id).await?;

Set CHRONON_REMOTE_BASE_URL for resolve_remote_base_url. Timeout: CHRONON_REMOTE_HTTP_TIMEOUT_MS (default 3000).

§4. Define a script

#[chronon::script] registers the handler and turns the function into a ScriptHandle factory. Parameter types become a generated *Params struct (for example NightlyCleanupParams).

use chronon::prelude::*;

#[chronon::script(name = "nightly_cleanup")]
async fn nightly_cleanup(
    ctx: Box<dyn ScriptContext>,
    retention_days: u32,
) -> chronon::Result<()> {
    println!("{}: retaining {retention_days} days", ctx.label());
    Ok(())
}

// nightly_cleanup() -> ScriptHandle<NightlyCleanupParams>
// NightlyCleanupParams { retention_days: u32 }

Use .auto_registry() so inventory picks up every #[chronon::script] linked into the binary. In Mode 2, scripts must be linked into worker binaries (that is where they run).

See script, ScriptHandle, and ScriptContext. Runnable: script_handle_job, script_macro.

§5. Schedule and trigger jobs

Prefer the generated handle over stringly Job::new("…", "script_name"). Set ScheduleKind on the returned Job:

ScheduleKindBehavior
CronRecurring; set cron_expr (+ optional timezone)
RunOnceFires when next_run_at is due
ManualNever due for tick — only CoordinatorService::run_now
use chronon::prelude::*;

let mut nightly = nightly_cleanup().job_with_params(
    "nightly-schedule",
    &NightlyCleanupParams { retention_days: 7 },
)?;
nightly.schedule_kind = ScheduleKind::Cron;
nightly.cron_expr = Some("0 2 * * *".into());
chronon.coordinator_service().upsert_job(nightly).await?;

let mut manual = nightly_cleanup().job_with_params(
    "cleanup-now",
    &NightlyCleanupParams { retention_days: 30 },
)?;
manual.schedule_kind = ScheduleKind::Manual;
let id = manual.job_id.clone();
chronon.coordinator_service().upsert_job(manual).await?;
chronon.coordinator_service().run_now(&id).await?;

Cron uses standard five-field syntax (optional sixth field for seconds). Parse helpers: CronExpr. Runnable: script_handle_job, run_now, embedded_tick.

Storage wiring: Mode 1 (mem below; other backends on adapter crates) and Mode 2 (link table).

§Notes

  • No default Cargo features — enable mem, sqlite, postgres, redis, and/or axum explicitly. Document the facade with --all-features so rustdoc links resolve.
  • Mode 2 scripts live on workers — inventory must be linked into the binary that calls .worker(...); the coordinator ticks but does not execute handlers.
  • Call scheduler.init_partitions().await before Chronon::run on embedded and coordinator-only shapes.
  • RemoteClient must not call Chronon::run — that shape returns an error; use RemoteCoordinatorClient.
  • mem is Mode 1 only — it does not cross process boundaries.

§Architecture

Your application owns identity policy and business logic. Chronon owns scheduling semantics: due queries, claiming, cron evaluation, and script dispatch.

Your app / worker binary
        │
        ▼
 ChrononBuilder ──► SchedulerStore port ──► mem | sqlite | postgres | postgres+redis | custom
        │
        ├──► Scheduler (tick / partitions)
        └──► Executor + ScriptRegistry  ◄── ContextFactory / #[chronon::script]

Mode 2 splits the loops across processes that share the store:

Coordinator ──.coordinator_only()──► tick + partitions ──► SchedulerStore
Worker(s)   ──.worker(pool)────────► claim + execute   ──► same SchedulerStore

§Configuration

Settings merge in this order: explicit ChrononBuilder values override environment defaults where both exist.

SettingBuilder APIEnvironmentDefault
Store.scheduler_store() / .scheduler_store_from_global()required
Context factory.context_factory()NoOpContextFactory
Telemetry.telemetry_sink()NoOpSink
Script registry.script_registry() / .auto_registry()empty or inventory
Tick interval.tick_interval_ms()CHRONON_TICK_INTERVAL_MS250 ms
Instance id.instance_id()random UUID
Partition count— (env only)CHRONON_NUM_PARTITIONS64
Worker pool.worker(pool) / envCHRONON_WORKER_POOL"general"
Worker concurrencyCHRONON_WORKER_CONCURRENCY4
Remote base URL.remote_coordinator(url)CHRONON_REMOTE_BASE_URL

Lease TTLs and tick batch limits are environment-only. See chronon-scheduler crate documentation for the full table.

§Cargo features

FeatureTypeStatus
memInMemorySchedulerStoreReady — tests and local Mode 1
sqliteSqliteSchedulerStoreReady — embedded file-backed
postgresPostgresSchedulerStoreReady — shared durable
redisPostgresRedisSchedulerStoreReady — Postgres + Redis claim overlay (requires postgres)
axumchronon_router, HTTP DTOsReady — mount on host Axum server
telemetry-consoleDocuments ConsoleSink usageOptional marker (ConsoleSink always re-exported)

§Runnable examples

ExampleTopologyFeatures
script_macroMode 1mem
script_handle_jobMode 1mem
run_nowMode 1mem
embedded_tickMode 1mem
store_router_bootMode 1mem
sqlite_bootMode 1sqlite
postgres_bootMode 1postgres
postgres_redis_bootMode 1postgres,redis
axum_hostMode 1 + HTTPmem,axum
coordinator_daemonMode 2 coordinatorpostgres,redis
worker_daemonMode 2 workerpostgres,redis
cargo run -p uf-chronon --example script_handle_job --features mem
cargo run -p uf-chronon --example coordinator_daemon --features postgres,redis
cargo run -p uf-chronon --example worker_daemon --features postgres,redis

Re-exports§

pub use quark::inventory;
pub use chronon_core as core;

Modules§

prelude
Curated re-exports for application developers building Chronon worker binaries.

Structs§

ApiResponse
Standard API wrapper: success, optional data, optional error.
Chronon
Assembled Chronon runtime: store, scheduler, executor, and deployment loops.
ChrononBuilder
Builds a crate::Chronon runtime with explicit adapter injection.
ChrononState
Shared state for Chronon API handlers.
ConsoleSink
Writes telemetry to stderr (development and bench).
CoordinatorService
Job and run CRUD backed by SchedulerStore — no background loops.
CronExpr
A parsed cron expression ready for next-run calculations.
InMemorySchedulerStore
Thread-safe in-memory persistence for jobs, runs, and coordinator metadata.
NoOpSink
Discards all telemetry (default for tests and minimal hosts).
PostgresRedisSchedulerStore
SQL persistence with Redis-backed run claim ordering.
PostgresSchedulerStore
PostgreSQL-backed scheduler store.
RedisQueueLayer
Redis ZSET layer for queued runs ({prefix}:ready:{pool} or hash-tagged for cluster).
RemoteCoordinatorClient
HTTP client for Mode 3 — schedule/trigger jobs without local Chronon loops.
ScriptDescriptor
Descriptor for a registered script.
ScriptHandle
A typed handle for scheduling a script with specific parameters.
ScriptRegistry
In-memory script registry with optional link-time inventory discovery.
SqliteSchedulerStore
SQLite-backed scheduler store.

Enums§

ChrononError
Errors that can occur in Chronon operations.
DeploymentShape
Named deployment assembly — not a global mode enum.

Constants§

API_PREFIX
API mount prefix for host routers (e.g. nest(API_PREFIX, chronon_router())).

Traits§

TelemetrySink
Host-injectable telemetry sink for scheduler and executor metrics/events.

Functions§

builder
Shorthand for ChrononBuilder::new.
chronon_router
Create the Chronon API router with job, run, and script routes.
install_default_mem_store
Registers a new in-memory store as the global default.
postgres_test_url
Resolve a PostgreSQL URL for tests.
resolve_remote_base_url
Resolve remote API base URL from CHRONON_REMOTE_BASE_URL.

Type Aliases§

Result
Result type alias for Chronon operations.

Attribute Macros§

script
Marks an async function as a Chronon script, enabling automatic registration and typed parameter handling.