use anyhow::{anyhow, Result};
use chrono::{DateTime, Utc};
use cron::Schedule as CronSchedule;
use std::str::FromStr;
pub struct Schedule {
pub name: String,
cron: CronSchedule,
next_run: DateTime<Utc>,
}
impl Schedule {
pub fn new(name: impl Into<String>, expr: &str) -> Result<Self> {
let cron = CronSchedule::from_str(expr)
.map_err(|e| anyhow!("invalid cron `{}`: {}", expr, e))?;
let next_run = Self::compute_next(&cron, Utc::now())?;
Ok(Self { name: name.into(), cron, next_run })
}
fn compute_next(cron: &CronSchedule, after: DateTime<Utc>) -> Result<DateTime<Utc>> {
cron.after(&after)
.next()
.ok_or_else(|| anyhow!("cron schedule has no upcoming occurrences"))
}
pub fn is_due(&self, now: DateTime<Utc>) -> bool {
now >= self.next_run
}
pub fn advance(&mut self) -> Result<()> {
// Anchor to the fired time, not `now`, to avoid drift and skipped ticks.
self.next_run = Self::compute_next(&self.cron, self.next_run)?;
Ok(())
}
}
use futures::future::BoxFuture;
use std::sync::Arc;
pub type JobFn = Arc<dyn Fn() -> BoxFuture<'static, ()> + Send + Sync>;
#[derive(Clone)]
pub struct Job {
pub name: String,
run: JobFn,
}
impl Job {
pub fn new<F>(name: impl Into<String>, run: F) -> Self
where
F: Fn() -> BoxFuture<'static, ()> + Send + Sync + 'static,
{
Self { name: name.into(), run: Arc::new(run) }
}
pub fn spawn(&self) {
let run = self.run.clone();
let name = self.name.clone();
tokio::spawn(async move {
tracing::info!(job = %name, "job started");
run().await;
tracing::info!(job = %name, "job finished");
});
}
}
use crate::job::Job;
use crate::schedule::Schedule;
use anyhow::Result;
use chrono::Utc;
use std::time::Duration;
use tokio::sync::Mutex;
struct Entry {
schedule: Schedule,
job: Job,
}
#[derive(Default)]
pub struct Scheduler {
entries: Mutex<Vec<Entry>>,
}
impl Scheduler {
pub fn new() -> Self {
Self { entries: Mutex::new(Vec::new()) }
}
pub async fn register(&self, expr: &str, job: Job) -> Result<()> {
let schedule = Schedule::new(job.name.clone(), expr)?;
self.entries.lock().await.push(Entry { schedule, job });
Ok(())
}
pub async fn run(&self, tick: Duration) {
let mut ticker = tokio::time::interval(tick);
loop {
ticker.tick().await;
let now = Utc::now();
let mut entries = self.entries.lock().await;
for entry in entries.iter_mut() {
if entry.schedule.is_due(now) {
entry.job.spawn();
if let Err(e) = entry.schedule.advance() {
tracing::error!(job = %entry.job.name, error = %e, "advance failed");
}
}
}
}
}
}
This snippet builds a small recurring-job scheduler in Rust that turns cron expressions into concrete next_run timestamps, stores them per job, and executes only the jobs that are actually due. The pattern separates when a job should fire (schedule computation) from what it does (the task), which keeps the run loop simple and makes each job independently testable.
In schedule.rs, a Schedule wraps a parsed cron::Schedule alongside the job's identity and a cached next_run. The key idea is that a cron expression is not a timer — it is a rule that must be re-evaluated against the current clock. Schedule::new parses the expression once (so a malformed cron fails fast at registration, not mid-loop) and immediately computes the first fire time via compute_next. The is_due check compares next_run against Utc::now, and advance rolls the cached time forward using after, which asks the cron iterator for the first occurrence strictly after a given instant. Anchoring advance to the just-fired next_run rather than to now prevents drift and avoids skipping ticks when the loop wakes up slightly late.
In job.rs, Job is a trait object model: a boxed async closure stored behind JobFn, with name used for logging and correlation. Modeling the work as Arc<dyn Fn -> BoxFuture> lets the scheduler own heterogeneous jobs in one collection and spawn them without knowing their concrete types. The Arc clone per invocation means a slow job never blocks the tick loop, since each due job is handed to tokio::spawn.
In scheduler.rs, Scheduler holds the registered schedules and drives them. register builds a Schedule and pairs it with its Job. The run loop ticks on a fixed interval (a coarse granularity that bounds wakeups regardless of how many jobs exist), scans for due entries, spawns each due job, then calls advance so the same job is not re-run on the next tick. A subtle correctness detail: advance happens right after dispatch, inside the loop that holds the lock, so overlapping ticks cannot double-fire the same occurrence.
The trade-off is resolution — jobs fire on the next interval boundary, not to the millisecond — which is exactly right for cron-style recurring work. For sub-second precision or persistence across restarts, next_run would need to be stored in a database and reloaded, but the same compute_next/advance logic applies unchanged.
Related snips
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
struct Config<'a> {
name: &'a str,
value: &'a str,
}
fn parse_config(line: &str) -> Config {
Lifetime annotations for flexible borrowing in structs
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
Share this code
Here's the card — post it anywhere.