use durare::{DurableContext, DurableEngine, Error, InMemoryProvider, Result, WorkflowOptions};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
#[tokio::test]
async fn step_runs_once_across_replays() -> Result<()> {
static CHARGES: AtomicUsize = AtomicUsize::new(0);
let provider = Arc::new(InMemoryProvider::new());
let mut engine = DurableEngine::new(provider).await?;
engine.register("charge", |ctx: DurableContext, _: ()| async move {
let amount = ctx
.step("charge_card", || async {
CHARGES.fetch_add(1, Ordering::SeqCst);
Ok::<_, Error>(4999_i64)
})
.await?;
Ok::<_, Error>(amount)
});
let a: i64 = engine
.start("charge", (), WorkflowOptions::with_id("wf-1"))
.await?
.result()
.await?;
let b: i64 = engine
.start("charge", (), WorkflowOptions::with_id("wf-1"))
.await?
.result()
.await?;
assert_eq!(a, 4999);
assert_eq!(b, 4999);
assert_eq!(
CHARGES.load(Ordering::SeqCst),
1,
"the charge side effect must execute exactly once across replays"
);
Ok(())
}
#[tokio::test]
async fn caught_step_failure_replays_without_rerunning() -> Result<()> {
static RUNS: AtomicUsize = AtomicUsize::new(0);
let provider = Arc::new(InMemoryProvider::new());
let mut engine = DurableEngine::new(provider).await?;
engine.register("flaky_caught", |ctx: DurableContext, _: ()| async move {
let r: Result<i64> = ctx
.step("maybe", || async {
let n = RUNS.fetch_add(1, Ordering::SeqCst);
if n == 0 {
Err(Error::app("transient"))
} else {
Ok(7)
}
})
.await;
Ok::<_, Error>(if r.is_ok() { "ok" } else { "caught-error" }.to_string())
});
let a: String = engine
.start("flaky_caught", (), WorkflowOptions::with_id("wf-step-err"))
.await?
.result()
.await?;
let b: String = engine
.start("flaky_caught", (), WorkflowOptions::with_id("wf-step-err"))
.await?
.result()
.await?;
assert_eq!(a, "caught-error");
assert_eq!(b, "caught-error", "replay observes the recorded step error");
assert_eq!(
RUNS.load(Ordering::SeqCst),
1,
"a checkpointed failed step is not re-run on replay"
);
let steps = engine.get_workflow_steps("wf-step-err").await?;
let maybe = steps
.iter()
.find(|s| s.name == "maybe")
.expect("step recorded");
assert_eq!(maybe.error.as_deref(), Some("transient"));
Ok(())
}
#[tokio::test]
async fn multi_step_results_are_stable() -> Result<()> {
let provider = Arc::new(InMemoryProvider::new());
let mut engine = DurableEngine::new(provider).await?;
engine.register("pipeline", |ctx: DurableContext, start: i64| async move {
let a = ctx
.step("double", || async { Ok::<_, Error>(start * 2) })
.await?;
let b = ctx
.step("plus_one", || async { Ok::<_, Error>(a + 1) })
.await?;
Ok::<_, Error>(b)
});
let out: i64 = engine
.start("pipeline", 10_i64, WorkflowOptions::with_id("wf-2"))
.await?
.result()
.await?;
assert_eq!(out, 21);
let out2: i64 = engine
.start("pipeline", 10_i64, WorkflowOptions::with_id("wf-2"))
.await?
.result()
.await?;
assert_eq!(out2, 21);
Ok(())
}
#[tokio::test]
async fn durable_sleep_is_not_repeated_on_replay() -> Result<()> {
static AFTER_SLEEP: AtomicUsize = AtomicUsize::new(0);
let nap = Duration::from_millis(500);
let provider = Arc::new(InMemoryProvider::new());
let mut engine = DurableEngine::new(provider).await?;
engine.register("napper", move |ctx: DurableContext, _: ()| async move {
ctx.sleep(nap).await?;
ctx.step("after_nap", || async {
AFTER_SLEEP.fetch_add(1, Ordering::SeqCst);
Ok::<_, Error>(1_i64)
})
.await?;
Ok::<_, Error>(())
});
let t0 = Instant::now();
engine
.start::<_, ()>("napper", (), WorkflowOptions::with_id("wf-nap"))
.await?
.result()
.await?;
let first = t0.elapsed();
assert!(
first >= Duration::from_millis(400),
"first run should really sleep (~500ms), took {first:?}"
);
let t1 = Instant::now();
engine
.start::<_, ()>("napper", (), WorkflowOptions::with_id("wf-nap"))
.await?
.result()
.await?;
let resubmit = t1.elapsed();
assert!(
resubmit < Duration::from_millis(200),
"a completed workflow must not re-sleep on resubmit, took {resubmit:?}"
);
assert_eq!(
AFTER_SLEEP.load(Ordering::SeqCst),
1,
"the step after the sleep runs once; the resubmit does not re-run the body"
);
Ok(())
}