cloudiful-scheduler 0.4.12

Single-job async scheduling library for background work with optional Valkey-backed state.
Documentation
#[allow(dead_code)]
#[path = "support/guarded_and_coordinated_fixtures.rs"]
mod fixtures;
#[path = "support/time.rs"]
mod time_support;

use chrono::Utc;
use fixtures::{FakeCoordinatedStore, RecordingObserver};
use scheduler::{
    CoordinatedLeaseConfig, InMemoryStateStore, Job, JobState, OverlapPolicy, Schedule, Scheduler,
    SchedulerConfig, SchedulerEvent, Task,
};
use std::time::Duration;
use time_support::shanghai_after;

fn core_event_kinds(events: &[SchedulerEvent]) -> Vec<&'static str> {
    events
        .iter()
        .filter_map(|event| match event {
            SchedulerEvent::StateLoaded { .. } => Some("state_loaded"),
            SchedulerEvent::TriggerEmitted { .. } => Some("trigger_emitted"),
            SchedulerEvent::RunCompleted { .. } => Some("run_completed"),
            SchedulerEvent::SchedulerStopped { .. } => Some("scheduler_stopped"),
            _ => None,
        })
        .collect()
}

#[tokio::test]
async fn legacy_and_coordinated_share_core_event_sequence() {
    let when = shanghai_after(20);

    let legacy_observer = RecordingObserver::default();
    let legacy_scheduler = Scheduler::with_observer(
        SchedulerConfig::default(),
        InMemoryStateStore::new(),
        legacy_observer.clone(),
    );
    let legacy_report = legacy_scheduler
        .run(
            Job::without_deps(
                "runtime-reuse-legacy",
                Schedule::AtTimes(vec![when]),
                Task::from_async(|_| async { Ok(()) }),
            )
            .with_overlap_policy(OverlapPolicy::Forbid)
            .with_max_runs(1),
        )
        .await
        .unwrap();

    let coordinated_observer = RecordingObserver::default();
    let coordinated_scheduler = Scheduler::with_observer_and_coordinated_state_store(
        SchedulerConfig::default(),
        FakeCoordinatedStore::new(JobState::new(
            "runtime-reuse-coordinated",
            Some(when.with_timezone(&Utc)),
        )),
        coordinated_observer.clone(),
        CoordinatedLeaseConfig {
            ttl: Duration::from_secs(1),
            renew_interval: Duration::from_millis(50),
        },
    );
    let coordinated_report = coordinated_scheduler
        .run(
            Job::without_deps(
                "runtime-reuse-coordinated",
                Schedule::AtTimes(vec![when]),
                Task::from_async(|_| async { Ok(()) }),
            )
            .with_overlap_policy(OverlapPolicy::Forbid)
            .with_max_runs(1),
        )
        .await
        .unwrap();

    assert_eq!(legacy_report.history.len(), 1);
    assert_eq!(coordinated_report.history.len(), 1);
    assert_eq!(
        core_event_kinds(&legacy_observer.snapshot()),
        vec![
            "state_loaded",
            "trigger_emitted",
            "run_completed",
            "scheduler_stopped"
        ]
    );
    assert_eq!(
        core_event_kinds(&coordinated_observer.snapshot()),
        vec![
            "state_loaded",
            "trigger_emitted",
            "run_completed",
            "scheduler_stopped"
        ]
    );
    let coordinated_events = coordinated_observer.snapshot();
    assert!(
        coordinated_events
            .iter()
            .any(|event| matches!(event, SchedulerEvent::ExecutionGuardAcquired { .. }))
    );
    assert!(
        coordinated_events
            .iter()
            .any(|event| matches!(event, SchedulerEvent::ExecutionGuardReleased { .. }))
    );
}