job_scheduler_ng
A lightweight, cron-like job scheduling library for Rust with pluggable cron parsers and caller-controlled tick loops.
Repository Health
Technical Analysis
job_scheduler_ng is a maintained fork of the original job_scheduler crate, providing a small in-process scheduler for running closures on a cron-like schedule. Jobs are defined with a standard six/seven-field cron expression (seconds through an optional year) and driven by repeatedly calling tick() from your own loop and thread, so the library never spawns background threads or owns a runtime on your behalf.
Schedules can be parsed with either of two pluggable backends selected via mutually exclusive Cargo features: the well-known cron crate (default) or croner, which adds support for extended syntax like the L (last day of month) token. Both are re-exported so callers never need to depend on the backend crate directly.
The scheduler tracks missed runs per job and caps replay at 1,000 executions per tick (configurable via limit_missed_runs) so a single backlogged job can’t starve the others sharing the same tick loop — a common footgun in naive cron implementations. Timezones default to UTC but can be set globally or per-scheduler via chrono’s FixedOffset.
Actively maintained since being forked in 2022, with regular dependency updates, an extensive clippy/rustfmt lint configuration, and CI covering MSRV, stable, and nightly Rust across both cron backends.
What You Get
- A
JobSchedulerthat owns a list ofJobs and executes due ones each time you calltick() - Choice of two cron parsers via Cargo features:
cron(default) for standard expressions orcronerfor extended syntax likeL(last day of month) - Per-job backlog control with
limit_missed_runs, capped globally at 1,000 executions per tick so one job can’t starve the others - Custom timezone support via chrono’s
FixedOffset, settable on the whole scheduler or overridden per run time_till_next_job()to sleep precisely until the next scheduled run instead of polling on a fixed interval- Runnable examples covering single-threaded, background-thread, MPSC-channel, and Tokio-based execution patterns
Common Use Cases
- Running periodic maintenance tasks (cleanup, cache eviction, health checks) inside a long-running Rust service without pulling in a full job-queue system
- Scheduling recurring notifications or reports at specific times of day in a chosen timezone
- Driving lightweight polling loops (e.g. checking an external API every N seconds) with precise cron semantics instead of a hand-rolled sleep loop
- Embedding cron-style scheduling inside a CLI daemon or embedded application where adding a full async scheduler crate would be overkill
Under The Hood
Architecture
The crate is a single ~430-line file (src/lib.rs) exposing two public types: Job (a cron schedule paired with a boxed FnMut closure) and JobScheduler (a Vec<Job> driven by tick()). The cron backend is abstracted behind a private CronSchedule type alias and a feature-gated item_after() free function with two implementations — one for the cron crate, one for croner — so the two backends present a single iterator-based interface to the rest of the code and swapping between them touches only these two blocks. Execution is pull-based and single-threaded: the caller owns the loop, calling tick() and sleeping for MIN_DURATION between calls; each Job::tick() walks cron occurrences since last_tick, capped at MAX_MISSED_PER_TICK (1,000) per call, so a job with a large backlog can’t monopolize a shared tick loop. There is no internal timer thread or synchronization primitive, since jobs run inline on whichever thread calls tick().
Tech Stack
Rust 2024 edition, MSRV 1.87.0. Runtime dependencies are minimal: chrono 0.4.45 (clock feature only) for DateTime/FixedOffset handling and uuid 1.24 (v4 feature) for job identifiers, plus exactly one of two optional, mutually exclusive cron parsers — cron 0.17.0 (default) or croner 3.0.1 — selected through Cargo features. tokio (>=1.53) appears only as a dev-dependency for one example; the library itself has zero async-runtime dependency. CI is a GitHub Actions matrix across three Rust channels (MSRV, stable, nightly) and both cron features, run with RUSTFLAGS=-Dwarnings, plus a separate typo-checking workflow.
Code Quality
There are no #[test]-annotated unit tests in src/ or examples/; correctness instead relies on doc-tests embedded throughout the public API’s doc comments (covering Job::new, limit_missed_runs, last_tick, JobScheduler::new, set_timezone) which cargo test compiles and runs, together with six standalone runnable examples exercising threading, MPSC channels, Tokio integration, and timezone handling. The workspace forbids unsafe_code outright and denies whole clippy lint groups (complexity, pedantic, perf, style, suspicious) plus an extensive explicit deny list (redundant_clone, mem_forget, float_cmp_const, and more), enforced in CI across all channel/feature combinations alongside rustfmt and a pre-commit config. Error handling avoids panics in the hot path; the two new() constructors are explicitly documented as infallible via #[expect(clippy::missing_panics_doc)].
API Design
The public surface is deliberately small — two structs and one constant (MIN_DURATION) — and every public method carries a runnable doc-test showing exact usage. The crate re-exports the active backend’s schedule type (Schedule from cron, or Cron from croner) so callers parse schedule strings without ever importing the backend crate directly, keeping the feature choice mostly transparent at the call site. Getting started requires only adding the dependency, parsing a cron string, calling .add(), and looping tick() plus a sleep — a handful of lines. The trade-off, stated explicitly rather than hidden, is that job closures must be Send + 'a and boxed, and execution is synchronous on whichever thread calls tick(); the shipped threading and MPSC examples show the documented pattern for offloading long-running work.