use crate::rng::Rng;
use crate::world::{Failure, Report, SimConfig, World, erase};
use fusee::ctx::Ctx;
use fusee::error::Error;
use fusee::runtime::SharedRegistry;
use fusee_core::record::Status;
use serde::Serialize;
use serde::de::DeserializeOwned;
use std::collections::BTreeMap;
use std::future::Future;
use std::sync::Arc;
use std::time::Duration;
pub type Check = Box<dyn FnOnce(&Outcome) -> Result<(), String>>;
pub(crate) enum UserAction {
Start {
id: String,
wtype: String,
input: Vec<u8>,
},
Signal {
id: String,
name: String,
payload: Vec<u8>,
},
Cancel {
id: String,
},
Terminate {
id: String,
},
ReadStream {
id: String,
name: String,
},
Call {
wtype: String,
key: String,
handler: String,
call_id: String,
arg: Vec<u8>,
},
Enqueue {
workflow: String,
key: String,
event_id: String,
input: serde_json::Value,
rule: fusee::queue::Rule,
},
}
pub struct Setup {
seed: u64,
rng: Rng,
registry: SharedRegistry,
pub(crate) actions: Vec<(u64, UserAction)>,
pub(crate) checks: Vec<Check>,
}
impl Setup {
pub(crate) fn new(seed: u64, registry: SharedRegistry) -> Self {
Self {
seed,
rng: Rng::new(seed ^ 0x5ce7_a210_0f1e_5eed),
registry,
actions: Vec::new(),
checks: Vec::new(),
}
}
pub fn seed(&self) -> u64 {
self.seed
}
pub fn random(&mut self, n: u64) -> u64 {
if n == 0 { 0 } else { self.rng.below(n) }
}
pub fn register<I, O, F, Fut>(
&mut self,
workflow: impl fusee::AsWorkflow<I, O>,
f: F,
) -> &mut Self
where
I: Serialize + DeserializeOwned + 'static,
O: Serialize + DeserializeOwned + 'static,
F: Fn(Ctx, I) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<O, Error>> + 'static,
{
let mut r = self.registry.write().unwrap_or_else(|e| e.into_inner());
r.register(workflow.name(), workflow.version().unwrap_or(1), erase(f));
drop(r);
self
}
pub fn object(&mut self, name: &str, handlers: fusee::object::ObjectHandlers) -> &mut Self {
let (f, def) = handlers.into_code();
let mut r = self.registry.write().unwrap_or_else(|e| e.into_inner());
r.register(name, 1, f);
r.register_object(name, def);
drop(r);
self
}
pub fn call(
&mut self,
at: Duration,
wtype: &str,
key: &str,
handler: &str,
call_id: &str,
arg: &impl Serialize,
) -> &mut Self {
assert!(!call_id.is_empty(), "a simulated call needs an ID");
self.actions.push((
at.as_millis() as u64,
UserAction::Call {
wtype: wtype.into(),
key: key.into(),
handler: handler.into(),
call_id: call_id.into(),
arg: serde_json::to_vec(arg).expect("the argument encodes as JSON"),
},
));
self
}
pub fn start(
&mut self,
at: Duration,
id: &str,
workflow: &str,
input: &impl Serialize,
) -> &mut Self {
self.actions.push((
at.as_millis() as u64,
UserAction::Start {
id: id.into(),
wtype: workflow.into(),
input: serde_json::to_vec(input).expect("the input encodes as JSON"),
},
));
self
}
pub fn signal(
&mut self,
at: Duration,
id: &str,
name: &str,
payload: &impl Serialize,
) -> &mut Self {
self.actions.push((
at.as_millis() as u64,
UserAction::Signal {
id: id.into(),
name: name.into(),
payload: serde_json::to_vec(payload).expect("the payload encodes as JSON"),
},
));
self
}
pub fn read_stream(&mut self, at: Duration, id: &str, name: &str) -> &mut Self {
self.actions.push((
at.as_millis() as u64,
UserAction::ReadStream {
id: id.into(),
name: name.into(),
},
));
self
}
pub fn cancel(&mut self, at: Duration, id: &str) -> &mut Self {
self.actions
.push((at.as_millis() as u64, UserAction::Cancel { id: id.into() }));
self
}
pub fn terminate(&mut self, at: Duration, id: &str) -> &mut Self {
self.actions.push((
at.as_millis() as u64,
UserAction::Terminate { id: id.into() },
));
self
}
pub fn debounce(
&mut self,
at: Duration,
workflow: &str,
key: &str,
event_id: &str,
input: &impl Serialize,
rule: &fusee::Debounce,
) -> &mut Self {
self.enqueue(
at,
workflow,
key,
event_id,
input,
fusee::queue::Rule::Debounce(rule.clone()),
)
}
pub fn batch(
&mut self,
at: Duration,
workflow: &str,
key: &str,
event_id: &str,
event: &impl Serialize,
rule: &fusee::Batch,
) -> &mut Self {
self.enqueue(
at,
workflow,
key,
event_id,
event,
fusee::queue::Rule::Batch(rule.clone()),
)
}
fn enqueue(
&mut self,
at: Duration,
workflow: &str,
key: &str,
event_id: &str,
input: &impl Serialize,
rule: fusee::queue::Rule,
) -> &mut Self {
assert!(!event_id.is_empty(), "a simulated event needs an ID");
self.actions.push((
at.as_millis() as u64,
UserAction::Enqueue {
workflow: workflow.into(),
key: key.into(),
event_id: event_id.into(),
input: serde_json::to_value(input).expect("the input encodes as JSON"),
rule,
},
));
self
}
pub fn check(&mut self, f: impl FnOnce(&Outcome) -> Result<(), String> + 'static) -> &mut Self {
self.checks.push(Box::new(f));
self
}
}
#[derive(Debug)]
pub struct Outcome {
pub seed: u64,
pub crashes: u32,
pub(crate) results: BTreeMap<String, (Status, Vec<u8>)>,
pub(crate) receipts: BTreeMap<String, String>,
pub(crate) streams: BTreeMap<(String, String), (Vec<serde_json::Value>, Option<String>)>,
pub(crate) calls: BTreeMap<String, CallReply>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CallReply {
pub status: String,
pub payload: Vec<u8>,
pub sent_ms: u64,
pub answered_ms: u64,
}
impl Outcome {
pub fn call<T: DeserializeOwned>(&self, call_id: &str) -> Option<Result<T, String>> {
let c = self.calls.get(call_id)?;
Some(if c.status == "ok" {
serde_json::from_slice(&c.payload).map_err(|e| e.to_string())
} else {
Err(String::from_utf8_lossy(&c.payload).into_owned())
})
}
pub fn calls(&self) -> impl Iterator<Item = (&str, &CallReply)> {
self.calls.iter().map(|(k, v)| (k.as_str(), v))
}
pub fn receipt(&self, event_id: &str) -> Option<&str> {
self.receipts.get(event_id).map(String::as_str)
}
pub fn receipts(&self) -> impl Iterator<Item = (&str, &str)> {
self.receipts.iter().map(|(e, r)| (e.as_str(), r.as_str()))
}
pub fn stream(&self, id: &str, name: &str) -> Option<&[serde_json::Value]> {
self.streams
.get(&(id.to_owned(), name.to_owned()))
.map(|s| s.0.as_slice())
}
pub fn stream_end(&self, id: &str, name: &str) -> Option<&str> {
self.streams
.get(&(id.to_owned(), name.to_owned()))
.and_then(|s| s.1.as_deref())
}
pub fn ids(&self) -> impl Iterator<Item = &str> {
self.results.keys().map(String::as_str)
}
pub fn status(&self, id: &str) -> Option<&'static str> {
self.results.get(id).map(|r| r.0.as_str())
}
pub fn result<T: DeserializeOwned>(&self, id: &str) -> Option<Result<T, String>> {
let (status, payload) = self.results.get(id)?;
Some(match status {
Status::Ok => serde_json::from_slice(payload).map_err(|e| e.to_string()),
_ => Err(String::from_utf8_lossy(payload).into_owned()),
})
}
}
type SetupFn = dyn Fn(&mut Setup) + Send + Sync;
#[derive(Clone)]
pub struct UserSetup(pub(crate) Arc<SetupFn>);
impl std::fmt::Debug for UserSetup {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("UserSetup")
}
}
#[derive(Clone)]
pub struct Scenario {
setup: UserSetup,
shards: Option<u32>,
fault_window_ms: u64,
crash_every_ms: Option<u64>,
flush_fail_per_mille: Option<u32>,
tear: bool,
settle_ms: u64,
}
#[derive(Clone, Debug, Default)]
pub struct Summary {
pub seeds: u64,
pub crashes: u64,
pub workflows: u64,
pub records: u64,
pub flush_failures: u64,
pub simulated_ms: u64,
}
impl Scenario {
pub fn new(setup: impl Fn(&mut Setup) + Send + Sync + 'static) -> Self {
Self {
setup: UserSetup(Arc::new(setup)),
shards: None,
fault_window_ms: 2_000,
crash_every_ms: None,
flush_fail_per_mille: None,
tear: true,
settle_ms: 3_600_000,
}
}
pub fn shards(mut self, n: u32) -> Self {
self.shards = Some(n.max(1));
self
}
pub fn fault_window(mut self, d: Duration) -> Self {
self.fault_window_ms = d.as_millis() as u64;
self
}
pub fn crash_every(mut self, d: Duration) -> Self {
self.crash_every_ms = Some(d.as_millis() as u64);
self
}
pub fn flush_failures_per_mille(mut self, n: u32) -> Self {
self.flush_fail_per_mille = Some(n);
self
}
pub fn torn_writes(mut self, on: bool) -> Self {
self.tear = on;
self
}
pub fn no_faults(self) -> Self {
self.crash_every(Duration::ZERO).flush_failures_per_mille(0)
}
pub fn settle_within(mut self, d: Duration) -> Self {
self.settle_ms = d.as_millis() as u64;
self
}
pub fn config(&self, seed: u64) -> SimConfig {
let mut cfg = SimConfig::for_seed(seed);
if let Some(n) = self.shards {
cfg.shards = n;
}
cfg.duration_ms = self.fault_window_ms;
if let Some(c) = self.crash_every_ms {
cfg.crash_every_ms = c;
}
if let Some(f) = self.flush_fail_per_mille {
cfg.flush_fail_per_mille = f;
}
cfg.tear = self.tear;
cfg.settle_ms = self.settle_ms;
cfg.user = Some(self.setup.clone());
cfg
}
pub fn run(&self, seed: u64) -> Result<Report, Failure> {
World::new(self.config(seed)).run()
}
pub fn run_seeds(&self, seeds: impl IntoIterator<Item = u64>) -> Result<Summary, Failure> {
let mut s = Summary::default();
for seed in seeds {
let r = self.run(seed)?;
s.seeds += 1;
s.crashes += u64::from(r.crashes);
s.workflows += u64::from(r.workflows);
s.records += r.records;
s.flush_failures += u64::from(r.flush_failures);
s.simulated_ms += r.sim_ms;
}
Ok(s)
}
}