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.
  • Fluent job construction — preferred JobBuilder for cron / run-once / manual schedules (seed helpers on ScriptHandle remain as a low-level alternate).
  • Durable jobs and runs — schedule config, revisions, and execution history on SchedulerStore.
  • Upsert-by-name — HTTP/job upsert preserves job_id when job_name already exists and bumps revision (see Remote HTTP client / axum handlers).
  • Security bounds — list pagination and policy knobs clamp to MAX_LIST_LIMIT / related ceilings.
  • Revision redaction — HTTP revision responses omit actor/params; store keeps full snapshots.
  • Schema allowlist — isolated Postgres schema names must match validate_postgres_schema_name in chronon-backend-sql-common.
  • 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 the run actor_json snapshot.
  • Optional HTTP API — mount chronon_router (axum feature) with AdminAuth / RequireAdmin and CHRONON_REQUIRE_ADMIN_AUTH; external upsert rejects System-shaped actor_json.

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 with JobBuilder (preferred), 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

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

§Embedded (one process)

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
BackendTypeFeatureTopologyEmbedded boot
In-memoryInMemorySchedulerStorememembedded onlyBelow
SQLiteSqliteSchedulerStoresqliteembeddedsqlite crate
PostgreSQLPostgresSchedulerStorepostgresembedded or coordinator–workerpostgres crate
Postgres + RedisPostgresRedisSchedulerStorepostgres,redisembedded or coordinator–workerredis 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 job = JobBuilder::new(&nightly_cleanup())
    .name("nightly-schedule")
    .cron("0 2 * * *")?
    .timezone("UTC")
    .params(NightlyCleanupParams { retention_days: 7 })
    .build()?;
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 Embedded links in the table above. Then continue with define a script.

§Coordinator–worker (split processes)

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 — coordinator–worker 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):

BackendFeatureCoordinatorWorker
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 a remote HTTP client.
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

Same-host SQLite split (shared file path):

export CHRONON_SQLITE_PATH=/tmp/chronon-split.db
cargo run -p uf-chronon --example sqlite_coordinator_daemon --features sqlite &
CHRONON_INSTANCE_ID=worker-a cargo run -p uf-chronon --example sqlite_worker_daemon --features sqlite

§Remote HTTP client

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 an embedded or coordinator–worker coordinator process.

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

API host — nest the router under API_PREFIX (/api/chronon) behind host authentication (Chronon does not authenticate these routes). Sketches: axum_host (mem,axum), axum_auth_wrap (Tower Bearer demo). See repository SECURITY.md.

App binary — prefer JobBuilder 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 job = JobBuilder::new(&nightly_cleanup())
    .name("nightly-schedule")
    .manual()
    .params(NightlyCleanupParams { retention_days: 7 })
    .build()?;
client.upsert_job(job.clone()).await?;
let _run_id = client.run_now(&job.job_id).await?;

Runnable end-to-end demo (short-lived mem host + client): cargo run -p uf-chronon --example remote_http_client --features mem,axum.

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 a coordinator–worker split, 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

Preferred: build a Job with JobBuilder from the generated ScriptHandle, then upsert. This validates cron and sets next_run_at for you.

ScheduleKindBuilder methodBehavior
Cron.cron (+ optional .timezone)Recurring
RunOnce.run_once_atFires when next_run_at is due
Manual.manualNever due for tick — only CoordinatorService::run_now
use chronon::prelude::*;

let nightly = JobBuilder::new(&nightly_cleanup())
    .name("nightly-schedule")
    .cron("0 2 * * *")?
    .params(NightlyCleanupParams { retention_days: 7 })
    .build()?;
chronon.coordinator_service().upsert_job(nightly).await?;

let manual = JobBuilder::new(&nightly_cleanup())
    .name("cleanup-now")
    .manual()
    .params(NightlyCleanupParams { retention_days: 30 })
    .build()?;
let id = manual.job_id.clone();
chronon.coordinator_service().upsert_job(manual).await?;
chronon.coordinator_service().run_now(&id).await?;

Low-level alternate: ScriptHandle::job_with_params then mutate schedule fields on the Job — prefer JobBuilder in new code.

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

Storage wiring: Embedded (mem below; other backends on adapter crates) and Coordinator–worker (link table).

§Notes

  • No default Cargo features — enable mem, sqlite, postgres, redis, and/or axum explicitly. Document the public crate with --all-features so rustdoc links resolve.
  • Coordinator–worker 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 embedded-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. Production trust boundaries (HTTP auth, store credentials, fail-closed ContextFactory, list/policy clamps, revision redaction, schema allowlisting) are documented in the repository SECURITY.md.

ConcernWhere
Upsert-by-nameAxum upsert + get_job_by_name
AdminAuth / require flagchronon-axum RequireAdmin + CHRONON_REQUIRE_ADMIN_AUTH
External System actorRejectExternalSystemActor on HTTP upsert
Actor snapshot at executeRuntime worker / Executor::spawn_run use run actor_json
List / policy boundsMAX_* + Job::clamp_security_bounds / handler .min(MAX_LIST_LIMIT)
Revision HTTP redactionAxum revision handlers
Error sanitize / URL redactsanitize_error_message / redact_endpoint
Postgres schema allowlistvalidate_postgres_schema_name in sql-common
Your app / worker binary
        │
        ▼
 ChrononBuilder ──► SchedulerStore port ──► mem | sqlite | postgres | postgres+redis | custom
        │
        ├──► Scheduler (tick / partitions)
        └──► Executor + ScriptRegistry  ◄── ContextFactory / #[chronon::script]

Coordinator–worker 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 embedded
sqliteSqliteSchedulerStoreReady — embedded file-backed
postgresPostgresSchedulerStoreReady — shared durable
redisPostgresRedisSchedulerStoreReady — Postgres + Redis claim overlay (requires postgres)
axumchronon_router, HTTP DTOsReady — mount on host Axum server (host must authenticate)
telemetry-consoleDocuments ConsoleSink usageOptional marker (ConsoleSink always re-exported)

§Runnable examples

Canonical path (see crate README How to run examples for multi-worker recipes):

ExampleTopologyFeatures
sqlite_bootEmbeddedsqlite
sqlite_coordinator_daemon / sqlite_worker_daemonCoordinator–worker (local)sqlite
coordinator_daemon / worker_daemonCoordinator–worker (Postgres+Redis)postgres,redis
remote_http_clientRemote HTTP clientmem,axum

Other examples: script_macro, script_handle_job, run_now, embedded_tick, store_router_boot, postgres_boot, postgres_redis_boot, axum_host, axum_auth_wrap, postgres_coordinator_daemon, postgres_worker_daemon.

cargo run -p uf-chronon --example sqlite_boot --features sqlite
cargo run -p uf-chronon --example remote_http_client --features mem,axum

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§

AdminAuthError
Rejection from AdminAuth::authorize.
AllowAllAdminAuth
Shared always-allow verifier for local tests (not for production).
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.
ChrononStateBuilder
Build ChrononState with admin auth and actor overrides.
ConsoleSink
Writes telemetry via tracing (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.
JobBuilder
Fluent builder for a scheduled Job.
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 the remote HTTP client topology — schedule/trigger jobs without local Chronon loops.
RequireAdmin
Extractor that enforces ChrononState::admin_auth / require-flag before the handler runs.
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.
StaticTokenAdminAuth
Header-based verifier: require x-chronon-admin-token equal to the configured secret.

Enums§

ChrononError
Errors that can occur in Chronon operations.
DeploymentShape
Named deployment assembly — process topology shape.

Constants§

API_PREFIX
API mount prefix for host routers (e.g. nest(API_PREFIX, chronon_router())).
REQUIRE_ADMIN_AUTH_ENV
Environment variable: when 1/true/yes, admin routes require a configured AdminAuth.

Traits§

AdminAuth
Host-supplied verifier for Chronon admin HTTP.
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.
require_admin_auth_from_env
Read REQUIRE_ADMIN_AUTH_ENV: 1, true, or yes (case-insensitive) ⇒ required.
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.