use fusee::{Config, Ctx, Engine, Error, RetryPolicy, StepOptions};
use serde_json::Value;
use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll};
use std::time::{Duration, Instant};
struct Counted<F> {
polls: Arc<AtomicU64>,
fut: Pin<Box<F>>,
}
impl<F: Future> Future for Counted<F> {
type Output = F::Output;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<F::Output> {
self.polls.fetch_add(1, Ordering::Relaxed);
self.fut.as_mut().poll(cx)
}
}
fn counted<F: Future>(polls: &Arc<AtomicU64>, fut: F) -> Counted<F> {
Counted {
polls: polls.clone(),
fut: Box::pin(fut),
}
}
struct YieldOnce(bool);
impl Future for YieldOnce {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
if self.0 {
return Poll::Ready(());
}
self.0 = true;
cx.waker().wake_by_ref();
Poll::Pending
}
}
fn step_payloads(journal: &Value, name: &str) -> Vec<u64> {
journal
.as_array()
.unwrap()
.iter()
.filter(|r| r["kind"] == "step" && r["name"] == name)
.map(|r| r["payload"].as_u64().unwrap())
.collect()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_fan_out_of_ten_thousand_steps_polls_each_branch_a_bounded_number_of_times() {
let engine = Engine::open(Config::memory().shards(1).keep_history(true))
.await
.unwrap();
let polls = Arc::new(AtomicU64::new(0));
let p = polls.clone();
let wf = engine.register("fan", move |ctx: Ctx, n: u64| {
let p = p.clone();
async move {
let legs = (0..n).map(|i| {
let ctx = ctx.clone();
counted(&p, async move {
ctx.step("leg", || async move { Ok::<_, Error>(i) }).await
})
});
let mut sum = 0;
for r in ctx.all(legs).await {
sum += r?;
}
Ok(sum)
}
});
let n = 10_000u64;
let sum: u64 = engine
.start(&wf, "f", &n)
.await
.unwrap()
.result()
.await
.unwrap();
assert_eq!(sum, n * (n - 1) / 2);
let polls = polls.load(Ordering::Relaxed);
eprintln!("{polls} branch polls for {n} branches");
assert!(polls <= 5 * n, "{polls} branch polls for {n} branches");
let d = engine.describe("f").await.unwrap().unwrap();
let journal = fusee::journal::normalize(&d.journal);
assert_eq!(step_payloads(&journal, "leg"), (0..n).collect::<Vec<_>>());
engine.shutdown().await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn nested_fan_out_and_a_wide_race_stay_linear() {
let engine = Engine::open(Config::memory().shards(1).keep_history(true))
.await
.unwrap();
let polls = Arc::new(AtomicU64::new(0));
let p = polls.clone();
let wf = engine.register("nested", move |ctx: Ctx, n: u64| {
let p = p.clone();
async move {
let groups = (0..n).map(|g| {
let (ctx, p) = (ctx.clone(), p.clone());
let outer = p.clone();
counted(&outer, async move {
let legs = (0..n).map(|i| {
let ctx = ctx.clone();
counted(&p, async move {
let a = ctx
.step("a", || async move { Ok::<_, Error>(g * n + i) })
.await?;
ctx.step("b", || async move { Ok::<_, Error>(a) }).await
})
});
let mut sum = 0;
for r in ctx.all(legs).await {
sum += r?;
}
Ok::<_, Error>(sum)
})
});
let mut sum = 0;
for r in ctx.all(groups).await {
sum += r?;
}
let runners = (0..n * n).map(|i| {
let ctx = ctx.clone();
counted(&p, async move {
ctx.step("runner", || async move { Ok::<_, Error>(i) })
.await
})
});
let (winner, v) = ctx.race(runners).await;
Ok((sum, winner as u64, v?))
}
});
let n = 100u64;
let (sum, winner, v): (u64, u64, u64) = engine
.start(&wf, "n", &n)
.await
.unwrap()
.result()
.await
.unwrap();
let steps = n * n;
assert_eq!(sum, steps * (steps - 1) / 2);
assert_eq!((winner, v), (0, 0));
let polls = polls.load(Ordering::Relaxed);
eprintln!("{polls} branch polls for {steps} steps in {n} groups");
assert!(
polls <= 20 * (n + steps),
"{polls} branch polls for {steps} steps"
);
let d = engine.describe("n").await.unwrap().unwrap();
let journal = fusee::journal::normalize(&d.journal);
let order: Vec<u64> = (0..steps).collect();
assert_eq!(step_payloads(&journal, "a"), order);
assert_eq!(step_payloads(&journal, "b"), order);
assert_eq!(step_payloads(&journal, "runner"), vec![0]);
engine.shutdown().await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn finished_steps_leave_no_volatile_timers_behind() {
let engine = Engine::open(Config::memory().shards(1)).await.unwrap();
let hour = Duration::from_secs(3600);
type Release = Arc<Mutex<Option<tokio::sync::oneshot::Receiver<()>>>>;
let (tx, rx) = tokio::sync::oneshot::channel::<()>();
let release: Release = Arc::new(Mutex::new(Some(rx)));
let r = release.clone();
let wf = engine.register("timeouts", move |ctx: Ctx, n: u64| {
let r = r.clone();
async move {
let long = StepOptions::default()
.timeout(2 * hour)
.heartbeat_timeout(2 * hour);
let mut sum = 0;
for i in 0..n {
let c = ctx.clone();
sum += ctx
.step("seq", || {
let c = c.clone();
async move {
YieldOnce(false).await;
c.heartbeat(&i)?;
YieldOnce(false).await;
Ok::<_, Error>(i)
}
})
.options(long.clone())
.await?;
}
let legs = (0..=n).map(|i| {
let (ctx, r, long) = (ctx.clone(), r.clone(), long.clone());
Box::pin(async move {
if i < n {
return ctx
.step("leg", || async move {
YieldOnce(false).await;
Ok::<_, Error>(i)
})
.options(long)
.await;
}
ctx.step("last", || {
let rx = r.lock().unwrap().take();
async move {
if let Some(rx) = rx {
let _ = rx.await;
}
Ok::<_, Error>(0)
}
})
.options(StepOptions::default().timeout(hour))
.await
}) as Pin<Box<dyn Future<Output = Result<u64, Error>>>>
});
for r in ctx.all(legs).await {
sum += r?;
}
let slow = RetryPolicy::exponential(2, hour, hour);
let (c1, c2, long) = (ctx.clone(), ctx.clone(), long.clone());
let (winner, v) = ctx
.race(vec![
Box::pin(async move {
c1.step("flaky", || async { Err::<u64, _>("not yet") })
.options(StepOptions::default().retry(slow))
.await
}) as Pin<Box<dyn Future<Output = Result<u64, Error>>>>,
Box::pin(async move {
c2.step("quick", || async {
YieldOnce(false).await;
Ok::<_, Error>(7)
})
.options(long)
.await
}),
])
.await;
Ok((sum + v?, winner as u64))
}
});
let n = 2_000u64;
let run = engine.start(&wf, "t", &n).await.unwrap();
let began = Instant::now();
let mid = loop {
let m = engine.metrics().await.unwrap();
if m.steps_run >= 2 * n {
break m;
}
assert!(began.elapsed() < Duration::from_secs(120), "{m:?}");
tokio::time::sleep(Duration::from_millis(5)).await;
};
eprintln!(
"{} volatile timers with one step running",
mid.volatile_timers
);
assert!(
(1..=130).contains(&mid.volatile_timers),
"{} volatile timers with one step running",
mid.volatile_timers
);
tx.send(()).unwrap();
let (sum, winner): (u64, u64) = run.result().await.unwrap();
assert_eq!((sum, winner), (n * (n - 1) + 7, 1));
assert_eq!(engine.metrics().await.unwrap().volatile_timers, 0);
engine.shutdown().await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_fan_out_takes_a_few_flushes_whatever_its_width() {
let dir = std::env::temp_dir().join(format!(
"fusee-fanout-flushes-{}-{}",
std::process::id(),
fusee::local::unix_ms()
));
let _ = std::fs::remove_dir_all(&dir);
let mut config = Config::local(&dir).shards(1);
config.local.flush = fusee::FlushMethod::Fsync;
let engine = Engine::open(config).await.unwrap();
let wf = engine.register("fan", |ctx: Ctx, n: u32| async move {
let legs: Vec<_> = (0..n)
.map(|i| {
let c = ctx.clone();
async move { c.step("leg", || async move { Ok::<_, Error>(i) }).await }
})
.collect();
let sum: u32 = ctx
.all(legs)
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()?
.iter()
.sum();
ctx.step("join", || async move { Ok::<_, Error>(sum) })
.await
});
let width = 100u32;
let mut before = 0;
for run in 0..6 {
let sum = engine
.execute(&wf, &format!("f{run}"), &width)
.await
.unwrap();
assert_eq!(sum, width * (width - 1) / 2);
let flushes = engine.metrics().await.unwrap().flushes;
assert!(
flushes - before <= 6,
"fan-out {run} of {width} took {} flushes",
flushes - before
);
before = flushes;
}
engine.shutdown().await.unwrap();
let _ = std::fs::remove_dir_all(&dir);
}