cloudiful-scheduler 0.4.6

Single-job async scheduling library for background work with optional Valkey-backed state.
Documentation
use chrono::{TimeDelta, Utc};
use scheduler::{
    InMemoryStateStore, Job, JobState, Schedule, Scheduler, SchedulerConfig, StateStore, Task,
};
use std::sync::{
    Arc,
    atomic::{AtomicUsize, Ordering},
};
use tokio::sync::mpsc;

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn grouped_interval_persists_state_after_the_first_run() {
    let store = Arc::new(InMemoryStateStore::new());
    let initial_due = Utc::now() + TimeDelta::milliseconds(30);
    store
        .save(&JobState::new("scrape-grouped", Some(initial_due)))
        .await
        .unwrap();

    let scheduler = Scheduler::new(SchedulerConfig::default(), store.clone());
    let handle = scheduler.handle();
    let (tx, mut rx) = mpsc::channel::<()>(1);
    let invocations = Arc::new(AtomicUsize::new(0));
    let seen = invocations.clone();

    let job = Job::without_deps(
        "scrape-grouped",
        Schedule::grouped_interval_with_seed(
            TimeDelta::days(1).to_std().unwrap(),
            3,
            1,
            "example.com",
        ),
        Task::from_async(move |_| {
            let tx = tx.clone();
            let seen = seen.clone();
            async move {
                seen.fetch_add(1, Ordering::SeqCst);
                let _ = tx.send(()).await;
                Ok(())
            }
        }),
    )
    .with_max_runs(2);

    let shutdown_handle = handle.clone();
    let task = tokio::spawn(async move {
        tokio::spawn(async move {
            let _ = rx.recv().await;
            shutdown_handle.shutdown();
        });
        scheduler.run(job).await.unwrap()
    });

    let report = task.await.unwrap();

    assert_eq!(invocations.load(Ordering::SeqCst), 1);
    assert_eq!(report.history.len(), 1);
    let first_scheduled_at = report.history[0].scheduled_at;
    let saved_state = store.load("scrape-grouped").await.unwrap().unwrap();
    assert_eq!(
        saved_state.next_run_at,
        Some(first_scheduled_at + TimeDelta::days(1))
    );
    assert_eq!(report.state.next_run_at, saved_state.next_run_at);
}