use crate::record::{
Lsn, Name, PAUSED, Payload, Record, RecordBuf, RunId, Seq, Status, WaitOutcome, stream,
value_kind,
};
use rustc_hash::{FxHashMap, FxHashSet};
use std::collections::VecDeque;
use std::fmt;
use std::sync::Arc;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OpKind {
Step,
Sleep,
Wait,
Child,
Value,
Send,
Await,
UpdateDone,
Attributes,
}
impl OpKind {
pub fn as_str(self) -> &'static str {
match self {
Self::Step => "step",
Self::Sleep => "sleep",
Self::Wait => "wait",
Self::Child => "child",
Self::Value => "value",
Self::Send => "send",
Self::Await => "awaitCompletion",
Self::UpdateDone => "update result",
Self::Attributes => "setAttributes",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DeterminismError {
pub seq: Option<Seq>,
pub message: String,
}
impl fmt::Display for DeterminismError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.seq {
Some(seq) => write!(
f,
"determinism error at journal position {seq}: {}",
self.message
),
None => write!(f, "determinism error: {}", self.message),
}
}
}
impl std::error::Error for DeterminismError {}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Outcome {
pub status: Status,
pub payload: Payload,
}
impl Outcome {
pub fn ok(payload: Payload) -> Self {
Self {
status: Status::Ok,
payload,
}
}
pub fn empty(status: Status) -> Self {
Self {
status,
payload: empty(),
}
}
}
pub fn empty() -> Payload {
EMPTY.with(Payload::clone)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Completion {
Step {
status: Status,
payload: Payload,
},
Fired,
Signal {
ordinal: u64,
payload: Payload,
},
WaitTimedOut,
Child {
status: Status,
payload: Payload,
},
Completed {
attempt: u32,
status: Status,
payload: Payload,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum StepStart {
Replay,
Run { attempt: u32, not_before_ms: u64 },
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AwaitStart {
Replay,
Waiting { attempt: u32 },
Run { attempt: u32, not_before_ms: u64 },
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Arm {
Replay,
Armed,
Append(RecordBuf),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Next {
Deliver {
seq: Seq,
outcome: Outcome,
record: Option<RecordBuf>,
},
DeliverLive {
seq: Seq,
outcome: Outcome,
record: LiveRecord,
},
CancelAll {
seqs: Vec<Seq>,
reason: Payload,
record: Option<RecordBuf>,
},
Update {
ordinal: u64,
name: Name,
id: Name,
payload: Payload,
live: Option<u64>,
},
Stalled,
Idle,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LiveRecord {
Step { name: Name, attempt: u32 },
Completed { attempt: u32 },
}
impl LiveRecord {
pub fn record<'a>(
&'a self,
run: RunId,
seq: Seq,
outcome: &'a Outcome,
time: Option<crate::record::StepTime>,
) -> crate::record::RecordRef<'a> {
match self {
Self::Step { name, attempt } => Record::Step {
run,
seq,
name,
attempt: *attempt,
status: outcome.status,
payload: &outcome.payload,
time,
},
Self::Completed { attempt } => Record::Completed {
run,
seq,
attempt: *attempt,
status: outcome.status,
payload: &outcome.payload,
},
}
}
}
#[derive(Clone, Debug)]
struct Recorded {
kind: OpKind,
name: Name,
attempt: u32,
retry_at_ms: u64,
base: u32,
marked: bool,
value: Option<Payload>,
delivered: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Source {
Replay,
Live,
}
#[derive(Clone, Debug)]
struct PendingOp {
kind: OpKind,
name: Name,
attempt: u32,
source: Source,
waiting: bool,
queued: bool,
paused: bool,
}
struct SeqMap<V, const N: usize> {
small: Vec<(Seq, V)>,
large: FxHashMap<Seq, V>,
}
impl<V, const N: usize> Default for SeqMap<V, N> {
fn default() -> Self {
Self {
small: Vec::new(),
large: FxHashMap::default(),
}
}
}
impl<V, const N: usize> SeqMap<V, N> {
fn insert(&mut self, seq: Seq, v: V) {
if self.large.is_empty() {
if let Some(e) = self.small.iter_mut().find(|(s, _)| *s == seq) {
e.1 = v;
return;
}
if self.small.len() < N {
self.small.push((seq, v));
return;
}
self.large.extend(self.small.drain(..));
}
self.large.insert(seq, v);
}
fn get_or_insert_with(&mut self, seq: Seq, f: impl FnOnce() -> V) -> &mut V {
if self.large.is_empty() {
if let Some(i) = self.small.iter().position(|(s, _)| *s == seq) {
return &mut self.small[i].1;
}
if self.small.len() < N {
self.small.push((seq, f()));
let last = self.small.len() - 1;
return &mut self.small[last].1;
}
self.large.extend(self.small.drain(..));
}
self.large.entry(seq).or_insert_with(f)
}
fn get(&self, seq: &Seq) -> Option<&V> {
if self.large.is_empty() {
self.small.iter().find(|(s, _)| s == seq).map(|(_, v)| v)
} else {
self.large.get(seq)
}
}
fn get_mut(&mut self, seq: &Seq) -> Option<&mut V> {
if self.large.is_empty() {
self.small
.iter_mut()
.find(|(s, _)| s == seq)
.map(|(_, v)| v)
} else {
self.large.get_mut(seq)
}
}
fn remove(&mut self, seq: &Seq) -> Option<V> {
if self.large.is_empty() {
let i = self.small.iter().position(|(s, _)| s == seq)?;
Some(self.small.swap_remove(i).1)
} else {
self.large.remove(seq)
}
}
fn clear(&mut self) {
self.small.clear();
self.large.clear();
}
fn len(&self) -> usize {
self.small.len() + self.large.len()
}
fn is_empty(&self) -> bool {
self.small.is_empty() && self.large.is_empty()
}
fn capacity(&self) -> usize {
self.small.capacity().max(self.large.capacity())
}
fn values(&self) -> impl Iterator<Item = &V> {
self.small.iter().map(|(_, v)| v).chain(self.large.values())
}
fn keys(&self) -> impl Iterator<Item = &Seq> {
self.small.iter().map(|(s, _)| s).chain(self.large.keys())
}
fn iter(&self) -> impl Iterator<Item = (Seq, &V)> {
self.small
.iter()
.map(|(s, v)| (*s, v))
.chain(self.large.iter().map(|(s, v)| (*s, v)))
}
}
#[derive(Clone, Debug)]
enum ReplayItem {
Op {
seq: Seq,
outcome: Outcome,
},
Cancel {
reason: Payload,
},
Update {
ordinal: u64,
name: Name,
id: Name,
payload: Payload,
},
}
#[derive(Clone, Debug)]
enum LiveItem {
Complete {
seq: Seq,
completion: Completion,
},
Cancel {
reason: Payload,
by: Option<Payload>,
},
Update {
token: u64,
name: Name,
id: Name,
payload: Payload,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum StreamOwner {
None,
Workflow,
Step(Seq),
}
#[derive(Clone, Debug)]
struct StreamState {
name: Name,
len: u64,
closed: Option<Seq>,
replay_left: u64,
by_step: Vec<(Seq, u64)>,
owner: StreamOwner,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct StreamOpen {
pub recorded: u64,
pub closed: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RunInfo {
pub run: RunId,
pub id: Name,
pub wtype: Name,
pub version: u32,
pub codec: u8,
pub started_ms: u64,
pub deadline_ms: u64,
pub policy: u8,
pub input: Payload,
pub trace: Option<Payload>,
}
pub struct RunMachine {
info: RunInfo,
next_seq: Seq,
recorded: SeqMap<Recorded, 16>,
max_recorded: Option<Seq>,
replay: VecDeque<ReplayItem>,
pending: SeqMap<PendingOp, 4>,
live: VecDeque<LiveItem>,
early: FxHashMap<Seq, Completion>,
signals: Vec<Payload>,
progress: FxHashMap<Seq, Payload>,
patches: FxHashSet<(Seq, Name)>,
updates: u64,
abandoned: FxHashSet<Seq>,
cancel_queued: bool,
last_cancel_position: Option<Seq>,
done: Option<(Status, Payload)>,
names: FxHashSet<Name>,
last_name: Option<Name>,
streams: Vec<StreamState>,
pub gate: Lsn,
}
const INTERNED_NAMES: usize = 256;
thread_local! {
static BLANK: Name = Arc::from("");
static EMPTY: Payload = Arc::from(&[][..]);
}
fn not_started() -> DeterminismError {
DeterminismError {
seq: None,
message: "journal does not begin with a start record".into(),
}
}
fn blank_name() -> Name {
BLANK.with(Name::clone)
}
fn mismatch(seq: Seq, rec: &Recorded, kind: OpKind, name: &str) -> DeterminismError {
DeterminismError {
seq: Some(seq),
message: format!(
"the journal recorded {} '{}' here but the code called {} '{}'",
rec.kind.as_str(),
rec.name,
kind.as_str(),
name
),
}
}
fn compatible(recorded: &Recorded, kind: OpKind) -> bool {
recorded.kind == kind
|| (recorded.kind == OpKind::Step
&& kind == OpKind::Await
&& !recorded.delivered
&& !recorded.marked)
}
fn add_chunk(by_step: &mut Vec<(Seq, u64)>, seq: Seq) {
match by_step.iter_mut().find(|(s, _)| *s == seq) {
Some((_, n)) => *n += 1,
None => by_step.push((seq, 1)),
}
}
fn chunks_of(by_step: &[(Seq, u64)], seq: Seq) -> u64 {
by_step
.iter()
.find(|(s, _)| *s == seq)
.map_or(0, |(_, n)| *n)
}
impl RunMachine {
pub fn new(journal: &[RecordBuf], gate: Lsn) -> Result<Self, DeterminismError> {
let Some(Record::Start {
run,
id,
wtype,
version,
codec,
started_ms,
deadline_ms,
policy,
update,
input,
meta,
..
}) = journal.first()
else {
return Err(DeterminismError {
seq: None,
message: "journal does not begin with a start record".into(),
});
};
let mut m = Self {
info: RunInfo {
run: *run,
id: id.clone(),
wtype: wtype.clone(),
version: *version,
codec: *codec,
started_ms: *started_ms,
deadline_ms: *deadline_ms,
policy: *policy,
input: input.clone(),
trace: meta.as_ref().and_then(|m| m.trace.clone()),
},
next_seq: 0,
recorded: SeqMap::default(),
max_recorded: None,
replay: VecDeque::new(),
pending: SeqMap::default(),
live: VecDeque::new(),
early: FxHashMap::default(),
signals: Vec::new(),
progress: FxHashMap::default(),
patches: FxHashSet::default(),
updates: 0,
abandoned: FxHashSet::default(),
cancel_queued: false,
last_cancel_position: None,
done: None,
names: FxHashSet::default(),
last_name: None,
streams: Vec::new(),
gate,
};
if let Some(u) = update {
m.replay.push_back(ReplayItem::Update {
ordinal: 0,
name: u.name.clone(),
id: u.id.clone(),
payload: u.payload.clone(),
});
m.updates = 1;
}
for rec in &journal[1..] {
m.load(rec)?;
}
Ok(m)
}
pub fn rebuild(&mut self, journal: &[RecordBuf], gate: Lsn) -> Result<(), DeterminismError> {
let Some(Record::Start {
run,
id,
wtype,
version,
codec,
started_ms,
deadline_ms,
policy,
update,
input,
meta,
..
}) = journal.first()
else {
return Err(not_started());
};
let info = RunInfo {
run: *run,
id: id.clone(),
wtype: wtype.clone(),
version: *version,
codec: *codec,
started_ms: *started_ms,
deadline_ms: *deadline_ms,
policy: *policy,
input: input.clone(),
trace: meta.as_ref().and_then(|m| m.trace.clone()),
};
let update = update
.as_ref()
.map(|u| (u.name.clone(), u.id.clone(), u.payload.clone()));
self.reset(info, update, gate);
for rec in &journal[1..] {
self.load(rec)?;
}
Ok(())
}
pub fn rebuild_drain(
&mut self,
journal: &mut Vec<RecordBuf>,
gate: Lsn,
) -> Result<(), DeterminismError> {
let mut records = journal.drain(..);
let Some(Record::Start {
run,
id,
wtype,
version,
codec,
started_ms,
deadline_ms,
policy,
update,
input,
meta,
..
}) = records.next()
else {
return Err(not_started());
};
let info = RunInfo {
run,
id,
wtype,
version,
codec,
started_ms,
deadline_ms,
policy,
input,
trace: meta.and_then(|m| m.trace),
};
self.reset(info, update.map(|u| (u.name, u.id, u.payload)), gate);
for rec in records {
self.load(&rec)?;
}
Ok(())
}
fn reset(&mut self, new_info: RunInfo, update: Option<(Name, Name, Payload)>, gate: Lsn) {
let Self {
info,
next_seq,
recorded,
max_recorded,
replay,
pending,
live,
early,
signals,
progress,
patches,
updates,
abandoned,
cancel_queued,
last_cancel_position,
done,
names: _,
last_name: _,
streams,
gate: machine_gate,
} = self;
*info = new_info;
*next_seq = 0;
recorded.clear();
*max_recorded = None;
replay.clear();
pending.clear();
live.clear();
early.clear();
signals.clear();
progress.clear();
patches.clear();
*updates = 0;
abandoned.clear();
*cancel_queued = false;
*last_cancel_position = None;
*done = None;
streams.clear();
*machine_gate = gate;
if let Some((name, id, payload)) = update {
self.replay.push_back(ReplayItem::Update {
ordinal: 0,
name,
id,
payload,
});
self.updates = 1;
}
}
pub fn release(&mut self) {
let blank = blank_name();
self.info.id = blank.clone();
self.info.wtype = blank;
self.info.input = EMPTY.with(Payload::clone);
self.recorded.clear();
self.replay.clear();
self.pending.clear();
self.live.clear();
self.early.clear();
self.signals.clear();
self.progress.clear();
self.patches.clear();
self.abandoned.clear();
self.done = None;
self.streams.clear();
}
pub fn reusable(&self) -> bool {
const MAX: usize = 64;
self.recorded.capacity() <= MAX
&& self.replay.capacity() <= MAX
&& self.pending.capacity() <= MAX
&& self.live.capacity() <= MAX
&& self.early.capacity() <= MAX
&& self.signals.capacity() <= MAX
&& self.progress.capacity() <= MAX
&& self.patches.capacity() <= MAX
&& self.abandoned.capacity() <= MAX
&& self.streams.capacity() <= MAX
}
fn mark(
&mut self,
seq: Seq,
kind: OpKind,
name: &Name,
) -> Result<&mut Recorded, DeterminismError> {
self.max_recorded = Some(self.max_recorded.map_or(seq, |m| m.max(seq)));
let entry = self.recorded.get_or_insert_with(seq, || Recorded {
kind,
name: name.clone(),
attempt: 0,
retry_at_ms: 0,
base: 0,
marked: false,
value: None,
delivered: false,
});
if entry.kind == OpKind::Step && kind == OpKind::Await && !entry.delivered && !entry.marked
{
entry.kind = OpKind::Await;
}
if !compatible(entry, kind) || (!name.is_empty() && *entry.name != **name) {
return Err(DeterminismError {
seq: Some(seq),
message: format!(
"journal is inconsistent: {} '{}' and {} '{}' share a position",
entry.kind.as_str(),
entry.name,
kind.as_str(),
name
),
});
}
if !name.is_empty() && entry.name.is_empty() {
entry.name = name.clone();
}
Ok(entry)
}
fn load(&mut self, rec: &RecordBuf) -> Result<(), DeterminismError> {
match rec {
Record::Step {
seq,
name,
status,
payload,
..
} => {
self.mark(*seq, OpKind::Step, name)?.delivered = true;
self.replay.push_back(ReplayItem::Op {
seq: *seq,
outcome: Outcome {
status: *status,
payload: payload.clone(),
},
});
}
Record::Attempt {
seq,
name,
attempt,
retry_at_ms,
..
} => {
let r = self.mark(*seq, OpKind::Step, name)?;
r.attempt = *attempt;
r.retry_at_ms = *retry_at_ms;
}
Record::Resume { seq, attempt, .. } => {
let r = self.mark(*seq, OpKind::Step, &blank_name())?;
r.retry_at_ms = 0;
r.base = *attempt;
}
Record::Timer { seq, name, .. } => self.mark(*seq, OpKind::Sleep, name)?.marked = true,
Record::Fired { seq, .. } => {
self.mark(*seq, OpKind::Sleep, &blank_name())?.delivered = true;
self.replay.push_back(ReplayItem::Op {
seq: *seq,
outcome: Outcome::empty(Status::Ok),
});
}
Record::Signal { payload, .. } => self.signals.push(payload.clone()),
Record::Wait { seq, name, .. } => self.mark(*seq, OpKind::Wait, name)?.marked = true,
Record::Waited {
seq,
outcome: WaitOutcome::Abandoned,
..
} => {
self.mark(*seq, OpKind::Wait, &blank_name())?;
self.abandoned.insert(*seq);
}
Record::Waited { seq, outcome, .. } => {
self.mark(*seq, OpKind::Wait, &blank_name())?.delivered = true;
let outcome = match outcome {
WaitOutcome::Signal(ordinal) => Outcome::ok(
usize::try_from(*ordinal)
.ok()
.and_then(|i| self.signals.get(i))
.cloned()
.ok_or_else(|| DeterminismError {
seq: Some(*seq),
message: "wait consumed a signal the journal does not hold".into(),
})?,
),
WaitOutcome::TimedOut | WaitOutcome::Abandoned => {
Outcome::empty(Status::TimedOut)
}
};
self.replay.push_back(ReplayItem::Op { seq: *seq, outcome });
}
Record::Value {
seq, kind, value, ..
} => {
let r = self.mark(*seq, OpKind::Value, &blank_name())?;
r.marked = true;
r.value = Some(value.clone());
r.attempt = u32::from(*kind);
}
Record::Child { seq, wtype, .. } => {
self.mark(*seq, OpKind::Child, wtype)?.marked = true
}
Record::ChildDone {
seq,
status,
payload,
..
} => {
self.mark(*seq, OpKind::Child, &blank_name())?.delivered = true;
self.replay.push_back(ReplayItem::Op {
seq: *seq,
outcome: Outcome {
status: *status,
payload: payload.clone(),
},
});
}
Record::Send { seq, name, .. } => {
let r = self.mark(*seq, OpKind::Send, name)?;
r.marked = true;
r.value = Some(empty());
}
Record::Cancel { reason, .. } => self.replay.push_back(ReplayItem::Cancel {
reason: reason.clone(),
}),
Record::Done {
status, payload, ..
} => self.done = Some((*status, payload.clone())),
Record::Update {
name, id, payload, ..
} => {
let ordinal = self.updates;
self.updates += 1;
self.replay.push_back(ReplayItem::Update {
ordinal,
name: name.clone(),
id: id.clone(),
payload: payload.clone(),
});
}
Record::UpdateDone { seq, ordinal, .. } => {
let r = self.mark(*seq, OpKind::UpdateDone, &blank_name())?;
r.marked = true;
r.value = Some(Arc::from(&ordinal.to_le_bytes()[..]));
}
Record::Heartbeat { seq, payload, .. } => {
self.progress.insert(*seq, payload.clone());
}
Record::Awaiting {
seq, name, attempt, ..
} => {
let r = self.mark(*seq, OpKind::Await, name)?;
r.marked = true;
r.attempt = *attempt;
}
Record::Completed {
seq,
status,
payload,
..
} => {
self.mark(*seq, OpKind::Await, &blank_name())?.delivered = true;
self.replay.push_back(ReplayItem::Op {
seq: *seq,
outcome: Outcome {
status: *status,
payload: payload.clone(),
},
});
}
Record::Patch { at, id, .. } => {
self.patches.insert((*at, id.clone()));
}
Record::Attributes { seq, .. } => {
let r = self.mark(*seq, OpKind::Attributes, &blank_name())?;
r.marked = true;
r.value = Some(empty());
}
Record::Chunk {
seq,
stream: name,
offset,
..
} => {
let st = self.stream_state(name);
st.len = st.len.max(offset.saturating_add(1));
if *seq == stream::WORKFLOW {
st.replay_left += 1;
} else {
add_chunk(&mut st.by_step, *seq);
}
}
Record::StreamEnd {
seq, stream: name, ..
} => {
let st = self.stream_state(name);
st.closed.get_or_insert(*seq);
}
Record::Start { .. } => {
return Err(DeterminismError {
seq: None,
message: "journal holds a second start record".into(),
});
}
}
Ok(())
}
pub fn fresh(start: &RecordBuf, lsn: Lsn) -> Result<Self, DeterminismError> {
Self::new(std::slice::from_ref(start), lsn)
}
pub fn info(&self) -> &RunInfo {
&self.info
}
pub fn run(&self) -> RunId {
self.info.run
}
pub fn recorded_done(&self) -> Option<&(Status, Payload)> {
self.done.as_ref()
}
pub fn is_replaying(&self) -> bool {
!self.replay.is_empty()
}
pub fn pending_len(&self) -> usize {
self.pending.len()
}
pub fn has_queued_deliveries(&self) -> bool {
!self.live.is_empty()
}
pub fn delivers_only(&self, seq: Seq) -> bool {
self.replay.is_empty()
&& !self.cancel_queued
&& self.pending.len() == 1
&& self.live.len() == 1
&& matches!(
self.live.front(),
Some(LiveItem::Complete {
seq: s,
completion: Completion::Step { .. },
}) if *s == seq
)
&& self
.pending
.get(&seq)
.is_some_and(|p| p.source == Source::Live && p.kind == OpKind::Step)
}
pub fn return_name(&mut self, name: Name) {
if self.last_name.is_none() {
self.last_name = Some(name);
}
}
pub fn take_live_step(&mut self, seq: Seq) -> Option<LiveRecord> {
if !self.replay.is_empty()
|| self.cancel_queued
|| self.pending.len() != 1
|| !self.live.is_empty()
|| self.owns_streams(seq)
{
return None;
}
match self.pending.get(&seq) {
Some(p) if p.source == Source::Live && p.kind == OpKind::Step && !p.queued => {}
_ => return None,
}
let p = self.pending.remove(&seq)?;
Some(LiveRecord::Step {
name: p.name,
attempt: p.attempt,
})
}
pub fn has_live_pending(&self) -> bool {
self.pending.values().any(|p| p.source == Source::Live) || !self.live.is_empty()
}
pub fn is_idle(&self) -> bool {
self.replay.is_empty() && self.live.is_empty() && self.pending.is_empty()
}
pub fn is_suspended(&self) -> bool {
self.replay.is_empty()
&& self.live.is_empty()
&& !self.pending.is_empty()
&& self.pending.values().all(|p| {
matches!(p.kind, OpKind::Sleep | OpKind::Wait | OpKind::Child)
|| (p.kind == OpKind::Await && p.waiting)
|| p.paused
})
}
pub fn next_seq(&self) -> Seq {
self.next_seq
}
pub fn journal_size(&self) -> u32 {
self.next_seq
}
pub fn key(&self, seq: Seq) -> String {
format!("{}/{}/{}", self.info.id, self.info.run, seq)
}
pub fn pending_name(&self, seq: Seq) -> Option<&str> {
self.pending.get(&seq).map(|p| &*p.name)
}
fn take_seq(&mut self) -> Seq {
let s = self.next_seq;
self.next_seq += 1;
s
}
fn check(
&self,
seq: Seq,
kind: OpKind,
name: &str,
) -> Result<Option<&Recorded>, DeterminismError> {
if self.recorded.is_empty() {
return Ok(None);
}
match self.recorded.get(&seq) {
Some(r)
if !compatible(r, kind)
|| (!matches!(
kind,
OpKind::Value | OpKind::UpdateDone | OpKind::Attributes
) && *r.name != *name) =>
{
Err(mismatch(seq, r, kind, name))
}
other => Ok(other),
}
}
fn intern(&mut self, name: &str) -> Name {
if self.last_name.as_deref() == Some(name) {
return self.last_name.take().expect("checked");
}
let n = match self.names.get(name) {
Some(n) => n.clone(),
None => {
let n: Name = Arc::from(name);
if self.names.len() < INTERNED_NAMES {
self.names.insert(n.clone());
}
n
}
};
self.last_name = Some(n.clone());
n
}
fn add_pending(&mut self, seq: Seq, kind: OpKind, name: &str, attempt: u32, source: Source) {
let name = self.intern(name);
self.pending.insert(
seq,
PendingOp {
kind,
name,
attempt,
source,
waiting: false,
queued: false,
paused: false,
},
);
}
pub fn begin_step(&mut self, name: &str) -> Result<(Seq, StepStart), DeterminismError> {
let seq = self.take_seq();
let (start, source, attempt) = match self.check(seq, OpKind::Step, name)? {
Some(r) if r.delivered => (StepStart::Replay, Source::Replay, 0),
Some(r) => (
StepStart::Run {
attempt: r.attempt + 1 - r.base,
not_before_ms: r.retry_at_ms,
},
Source::Live,
r.attempt + 1,
),
None => (
StepStart::Run {
attempt: 1,
not_before_ms: 0,
},
Source::Live,
1,
),
};
self.add_pending(seq, OpKind::Step, name, attempt, source);
if let StepStart::Run {
not_before_ms: PAUSED,
..
} = start
&& let Some(p) = self.pending.get_mut(&seq)
{
p.paused = true;
}
self.take_early(seq);
Ok((seq, start))
}
pub fn begin_await(&mut self, name: &str) -> Result<(Seq, AwaitStart), DeterminismError> {
let seq = self.take_seq();
let start = match self.check(seq, OpKind::Await, name)? {
Some(r) if r.delivered => AwaitStart::Replay,
Some(r) if r.marked && r.kind == OpKind::Await => {
AwaitStart::Waiting { attempt: r.attempt }
}
Some(r) => AwaitStart::Run {
attempt: r.attempt + 1,
not_before_ms: r.retry_at_ms,
},
None => AwaitStart::Run {
attempt: 1,
not_before_ms: 0,
},
};
let (attempt, source, waiting) = match start {
AwaitStart::Replay => (0, Source::Replay, false),
AwaitStart::Waiting { attempt } => (attempt, Source::Live, true),
AwaitStart::Run { attempt, .. } => (attempt, Source::Live, false),
};
self.add_pending(seq, OpKind::Await, name, attempt, source);
if let Some(p) = self.pending.get_mut(&seq) {
p.waiting = waiting;
}
self.take_early(seq);
Ok((seq, start))
}
pub fn awaiting(&mut self, seq: Seq) -> Option<RecordBuf> {
let run = self.info.run;
let p = self.pending.get_mut(&seq)?;
if p.source != Source::Live || p.kind != OpKind::Await || p.waiting {
return None;
}
p.waiting = true;
Some(Record::Awaiting {
run,
seq,
name: p.name.clone(),
attempt: p.attempt,
})
}
pub fn await_attempt(&self, seq: Seq) -> Option<u32> {
self.pending
.get(&seq)
.filter(|p| p.kind == OpKind::Await && p.source == Source::Live)
.map(|p| p.attempt)
}
pub fn step_retry(&mut self, seq: Seq, retry_at_ms: u64, error: Payload) -> Option<RecordBuf> {
let p = self.pending.get_mut(&seq)?;
if p.source != Source::Live {
return None;
}
let rec = Record::Attempt {
run: self.info.run,
seq,
name: p.name.clone(),
attempt: p.attempt,
retry_at_ms,
error,
};
p.attempt += 1;
p.waiting = false;
p.paused = retry_at_ms == PAUSED;
Some(rec)
}
pub fn step_paused(&self, seq: Seq) -> bool {
self.pending.get(&seq).is_some_and(|p| p.paused)
}
pub fn step_pending(&self, seq: Seq) -> bool {
self.pending
.get(&seq)
.is_some_and(|p| p.source == Source::Live && !p.queued)
}
pub fn is_paused(&self) -> bool {
self.pending.values().any(|p| p.paused) || self.recorded_paused().next().is_some()
}
fn recorded_paused(&self) -> impl Iterator<Item = (Seq, &Recorded)> {
self.recorded.iter().filter(|(seq, r)| {
r.kind == OpKind::Step
&& r.retry_at_ms == PAUSED
&& !r.delivered
&& self.pending.get(seq).is_none()
})
}
pub fn resume(&mut self, by: Option<Payload>) -> Vec<RecordBuf> {
let run = self.info.run;
let mut seqs: Vec<(Seq, u32)> = self
.pending
.iter()
.filter(|(_, p)| p.paused)
.map(|(seq, p)| (seq, p.attempt - 1))
.collect();
let waiting: Vec<(Seq, u32)> = self
.recorded_paused()
.map(|(seq, r)| (seq, r.attempt))
.collect();
for (seq, _) in &seqs {
if let Some(p) = self.pending.get_mut(seq) {
p.paused = false;
}
}
for &(seq, attempt) in &waiting {
if let Some(r) = self.recorded.get_mut(&seq) {
r.retry_at_ms = 0;
r.base = attempt;
}
}
seqs.extend(waiting);
seqs.sort_unstable();
seqs.into_iter()
.map(|(seq, attempt)| Record::Resume {
run,
seq,
attempt,
by: by.clone(),
})
.collect()
}
pub fn step_attempt(&self, seq: Seq) -> Option<u32> {
self.pending.get(&seq).map(|p| p.attempt)
}
pub fn heartbeat(&mut self, seq: Seq, payload: Payload) -> Option<RecordBuf> {
let p = self.pending.get(&seq)?;
if p.source != Source::Live || !matches!(p.kind, OpKind::Step | OpKind::Await) {
return None;
}
let attempt = p.attempt;
self.progress.insert(seq, payload.clone());
Some(Record::Heartbeat {
run: self.info.run,
seq,
attempt,
payload,
})
}
pub fn progress(&self, seq: Seq) -> Option<Payload> {
self.progress.get(&seq).cloned()
}
fn stream_state(&mut self, name: &str) -> &mut StreamState {
match self.streams.iter().position(|s| &*s.name == name) {
Some(i) => &mut self.streams[i],
None => {
let name = self.intern(name);
self.streams.push(StreamState {
name,
len: 0,
closed: None,
replay_left: 0,
by_step: Vec::new(),
owner: StreamOwner::None,
});
let last = self.streams.len() - 1;
&mut self.streams[last]
}
}
}
fn stream_step(&self, seq: Seq, what: &str) -> Result<u32, String> {
match self.pending.get(&seq) {
Some(p)
if p.source == Source::Live
&& matches!(p.kind, OpKind::Step | OpKind::Await)
&& !p.waiting =>
{
Ok(p.attempt)
}
_ => Err(format!(
"{what} is called from a step that is no longer running"
)),
}
}
pub fn stream_open(&mut self, name: &str, step: Option<Seq>) -> Result<StreamOpen, String> {
if let Some(s) = step {
self.stream_step(s, "ctx.stream")?;
}
let st = self.stream_state(name);
match (step, st.owner) {
(None, StreamOwner::Step(t)) => {
return Err(format!(
"stream '{name}' is written by the step at journal position {t}"
));
}
(None, _) => st.owner = StreamOwner::Workflow,
(Some(s), StreamOwner::Step(t)) if t != s => {
return Err(format!(
"stream '{name}' is written by the step at journal position {t}"
));
}
(Some(s), StreamOwner::None) if st.closed.is_none() => st.owner = StreamOwner::Step(s),
_ => {}
}
let recorded = match step {
Some(s) => chunks_of(&st.by_step, s),
None => 0,
};
Ok(StreamOpen {
recorded,
closed: st.closed.is_some(),
})
}
pub fn stream_append(
&mut self,
name: &str,
step: Option<Seq>,
payload: Payload,
) -> Result<Option<RecordBuf>, String> {
let attempt = match step {
Some(s) => self.stream_step(s, "stream append")?,
None => 0,
};
let run = self.info.run;
let st = self.stream_state(name);
let seq = match step {
None => {
if st.replay_left > 0 {
st.replay_left -= 1;
return Ok(None);
}
if let StreamOwner::Step(t) = st.owner {
return Err(format!(
"stream '{name}' is written by the step at journal position {t}"
));
}
stream::WORKFLOW
}
Some(s) => {
if st.closed == Some(s) {
return Ok(None);
}
if let StreamOwner::Step(t) = st.owner
&& t != s
{
return Err(format!(
"stream '{name}' is written by the step at journal position {t}"
));
}
s
}
};
if st.closed.is_some() {
return Err(format!("stream '{name}' is closed"));
}
match (seq, st.owner) {
(stream::WORKFLOW, _) => st.owner = StreamOwner::Workflow,
(s, StreamOwner::None) => st.owner = StreamOwner::Step(s),
_ => {}
}
if seq != stream::WORKFLOW {
add_chunk(&mut st.by_step, seq);
}
let offset = st.len;
st.len += 1;
Ok(Some(Record::Chunk {
run,
seq,
attempt,
stream: st.name.clone(),
offset,
payload,
}))
}
pub fn stream_close(
&mut self,
name: &str,
step: Option<Seq>,
) -> Result<Option<RecordBuf>, String> {
if let Some(s) = step {
self.stream_step(s, "stream close")?;
}
let run = self.info.run;
let st = self.stream_state(name);
if st.closed.is_some() {
return Ok(None);
}
let seq = step.unwrap_or(stream::WORKFLOW);
match (step, st.owner) {
(None, StreamOwner::Step(t)) => {
return Err(format!(
"stream '{name}' is written by the step at journal position {t}"
));
}
(Some(s), StreamOwner::Step(t)) if t != s => {
return Err(format!(
"stream '{name}' is written by the step at journal position {t}"
));
}
_ => {}
}
st.closed = Some(seq);
if st.owner == StreamOwner::Step(seq) {
st.owner = StreamOwner::None;
}
Ok(Some(Record::StreamEnd {
run,
seq,
stream: st.name.clone(),
chunks: st.len,
status: Status::Ok,
}))
}
pub fn stream_ends(&mut self, seq: Seq, status: Status) -> Vec<RecordBuf> {
let run = self.info.run;
let mut out = Vec::new();
for st in &mut self.streams {
if st.owner != StreamOwner::Step(seq) {
continue;
}
st.owner = StreamOwner::None;
if st.closed.is_none() {
st.closed = Some(seq);
out.push(Record::StreamEnd {
run,
seq,
stream: st.name.clone(),
chunks: st.len,
status,
});
}
}
out
}
pub fn owns_streams(&self, seq: Seq) -> bool {
self.streams
.iter()
.any(|s| s.owner == StreamOwner::Step(seq))
}
pub fn stream_len(&self, name: &str) -> Option<u64> {
self.streams
.iter()
.find(|s| &*s.name == name)
.map(|s| s.len)
}
pub fn begin_sleep(
&mut self,
name: &str,
fire_at_ms: u64,
) -> Result<(Seq, Arm), DeterminismError> {
let seq = self.take_seq();
let arm = match self.check(seq, OpKind::Sleep, name)? {
Some(r) if r.delivered => Arm::Replay,
Some(r) if r.marked => Arm::Armed,
_ => Arm::Append(Record::Timer {
run: self.info.run,
seq,
name: self.intern(name),
fire_at_ms,
}),
};
self.register(seq, OpKind::Sleep, name, &arm);
Ok((seq, arm))
}
pub fn begin_wait(
&mut self,
name: &str,
deadline_ms: u64,
) -> Result<(Seq, Arm), DeterminismError> {
let seq = self.take_seq();
let arm = match self.check(seq, OpKind::Wait, name)? {
Some(r) if r.delivered => Arm::Replay,
Some(r) if r.marked => Arm::Armed,
_ => Arm::Append(Record::Wait {
run: self.info.run,
seq,
name: self.intern(name),
deadline_ms,
}),
};
self.register(seq, OpKind::Wait, name, &arm);
Ok((seq, arm))
}
pub fn begin_child(
&mut self,
wtype: &str,
id: &str,
flags: u8,
deadline_ms: u64,
input: Payload,
) -> Result<(Seq, Arm), DeterminismError> {
let seq = self.take_seq();
let arm = match self.check(seq, OpKind::Child, wtype)? {
Some(r) if r.delivered => Arm::Replay,
Some(r) if r.marked => Arm::Armed,
_ => Arm::Append(Record::Child {
run: self.info.run,
seq,
wtype: Arc::from(wtype),
id: Arc::from(id),
flags,
deadline_ms,
input,
}),
};
if flags & crate::record::policy::DETACHED != 0 {
return Ok((seq, arm));
}
self.register(seq, OpKind::Child, wtype, &arm);
Ok((seq, arm))
}
fn register(&mut self, seq: Seq, kind: OpKind, name: &str, arm: &Arm) {
let source = if matches!(arm, Arm::Replay) {
Source::Replay
} else {
Source::Live
};
self.add_pending(seq, kind, name, 0, source);
self.take_early(seq);
}
fn take_early(&mut self, seq: Seq) {
if self.early.is_empty() {
return;
}
if let Some(c) = self.early.remove(&seq)
&& self.is_live_pending(seq)
{
if let Completion::Completed { attempt, .. } = &c
&& self.await_attempt(seq) != Some(*attempt)
{
return;
}
self.push_complete(seq, c);
}
}
fn push_complete(&mut self, seq: Seq, completion: Completion) {
if let Some(p) = self.pending.get_mut(&seq) {
p.queued = true;
}
self.live.push_back(LiveItem::Complete { seq, completion });
}
pub fn value(
&mut self,
kind: u8,
make: impl FnOnce() -> Payload,
) -> Result<(Seq, Payload, Option<RecordBuf>), DeterminismError> {
let seq = self.take_seq();
if let Some(r) = self.check(seq, OpKind::Value, "")? {
if r.attempt != u32::from(kind) {
return Err(DeterminismError {
seq: Some(seq),
message: format!(
"the journal recorded value kind {} here but the code asked for kind {kind}",
r.attempt
),
});
}
return Ok((seq, r.value.clone().unwrap_or_else(empty), None));
}
let value = make();
let rec = Record::Value {
run: self.info.run,
seq,
kind,
value: value.clone(),
};
Ok((seq, value, Some(rec)))
}
pub fn now(&mut self, now_ms: u64) -> Result<(u64, Option<RecordBuf>), DeterminismError> {
let (_, v, rec) = self.value(value_kind::NOW, || Arc::from(&now_ms.to_le_bytes()[..]))?;
let mut b = [0u8; 8];
if v.len() == 8 {
b.copy_from_slice(&v);
}
Ok((u64::from_le_bytes(b), rec))
}
pub fn send(
&mut self,
target: &str,
name: &str,
payload: Payload,
) -> Result<(Seq, Option<RecordBuf>), DeterminismError> {
let seq = self.take_seq();
if self.check(seq, OpKind::Send, name)?.is_some() {
return Ok((seq, None));
}
Ok((
seq,
Some(Record::Send {
run: self.info.run,
seq,
target: Arc::from(target),
name: Arc::from(name),
payload,
}),
))
}
pub fn set_attributes(
&mut self,
payload: Payload,
) -> Result<(Seq, Option<RecordBuf>), DeterminismError> {
let seq = self.take_seq();
if self.check(seq, OpKind::Attributes, "")?.is_some() {
return Ok((seq, None));
}
Ok((
seq,
Some(Record::Attributes {
run: self.info.run,
seq,
payload,
}),
))
}
fn beyond_journal(&self) -> bool {
self.max_recorded.is_none_or(|m| m < self.next_seq)
}
pub fn patched(&mut self, id: &str) -> (bool, Option<RecordBuf>) {
let at = self.next_seq;
let key = (at, Name::from(id));
if self.patches.contains(&key) {
return (true, None);
}
if self.beyond_journal() {
self.patches.insert(key);
return (
true,
Some(Record::Patch {
run: self.info.run,
at,
id: Arc::from(id),
}),
);
}
(false, None)
}
pub fn update_done(
&mut self,
ordinal: u64,
status: Status,
payload: Payload,
) -> Result<(Seq, Option<RecordBuf>), DeterminismError> {
let seq = self.take_seq();
if let Some(r) = self.check(seq, OpKind::UpdateDone, "")? {
let recorded = r
.value
.as_ref()
.and_then(|v| <[u8; 8]>::try_from(&v[..]).ok())
.map(u64::from_le_bytes);
if recorded != Some(ordinal) {
return Err(DeterminismError {
seq: Some(seq),
message: format!(
"the journal recorded the result of update #{} here but the code returned update #{ordinal}",
recorded.unwrap_or(u64::MAX)
),
});
}
return Ok((seq, None));
}
Ok((
seq,
Some(Record::UpdateDone {
run: self.info.run,
seq,
ordinal,
status,
payload,
state: empty(),
}),
))
}
pub fn offer_update(&mut self, token: u64, name: Name, id: Name, payload: Payload) {
self.live.push_back(LiveItem::Update {
token,
name,
id,
payload,
});
}
pub fn accept_update(&mut self, name: Name, id: Name, payload: Payload) -> (u64, RecordBuf) {
let ordinal = self.updates;
self.updates += 1;
(
ordinal,
Record::Update {
run: self.info.run,
name,
id,
payload,
caller: None,
},
)
}
pub fn complete(&mut self, seq: Seq, completion: Completion) -> bool {
if seq >= self.next_seq {
self.early.insert(seq, completion);
return false;
}
match self.pending.get_mut(&seq) {
Some(p) if p.source == Source::Live => {
if let Completion::Completed { attempt, .. } = &completion
&& (p.kind != OpKind::Await || p.attempt != *attempt)
{
return false;
}
if p.queued {
return false;
}
p.queued = true;
self.live.push_back(LiveItem::Complete { seq, completion });
true
}
_ => false,
}
}
pub fn is_live_pending(&self, seq: Seq) -> bool {
self.pending
.get(&seq)
.is_some_and(|p| p.source == Source::Live)
}
pub fn pending_kind(&self, seq: Seq) -> Option<OpKind> {
self.pending.get(&seq).map(|p| p.kind)
}
pub fn cancellation_covers_current_operations(&self) -> bool {
self.pending.is_empty() && self.last_cancel_position == Some(self.next_seq)
}
pub fn request_cancel(&mut self, reason: Payload) {
self.request_cancel_by(reason, None);
}
pub fn request_cancel_by(&mut self, reason: Payload, by: Option<Payload>) {
if !self.cancel_queued && !self.cancellation_covers_current_operations() {
self.cancel_queued = true;
self.live.push_back(LiveItem::Cancel { reason, by });
}
}
pub fn forget(&mut self, seq: Seq) -> Option<RecordBuf> {
let p = self.pending.get(&seq)?;
if p.source != Source::Live {
return None;
}
let kind = p.kind;
self.pending.remove(&seq);
if kind != OpKind::Wait || !self.abandoned.insert(seq) {
return None;
}
Some(Record::Waited {
run: self.info.run,
seq,
outcome: WaitOutcome::Abandoned,
})
}
pub fn next(&mut self) -> Next {
if let Some(front) = self.replay.front() {
return match front {
ReplayItem::Op { seq, .. } => {
let seq = *seq;
match self.pending.get(&seq) {
Some(p) if p.source == Source::Replay => {
self.pending.remove(&seq);
let Some(ReplayItem::Op { outcome, .. }) = self.replay.pop_front()
else {
return Next::Stalled;
};
Next::Deliver {
seq,
outcome,
record: None,
}
}
_ => Next::Stalled,
}
}
ReplayItem::Cancel { .. } => {
let Some(ReplayItem::Cancel { reason }) = self.replay.pop_front() else {
return Next::Stalled;
};
self.last_cancel_position = Some(self.next_seq);
let seqs = self.drain_pending();
Next::CancelAll {
seqs,
reason,
record: None,
}
}
ReplayItem::Update { .. } => {
let Some(ReplayItem::Update {
ordinal,
name,
id,
payload,
}) = self.replay.pop_front()
else {
return Next::Stalled;
};
Next::Update {
ordinal,
name,
id,
payload,
live: None,
}
}
};
}
while let Some(item) = self.live.pop_front() {
match item {
LiveItem::Complete { seq, completion } => {
if !self
.pending
.get(&seq)
.is_some_and(|p| p.source == Source::Live)
{
continue;
}
let Some(p) = self.pending.remove(&seq) else {
continue;
};
let run = self.info.run;
let (outcome, record) = match completion {
Completion::Step { status, payload } => {
let record = if p.kind == OpKind::Await {
LiveRecord::Completed { attempt: p.attempt }
} else {
LiveRecord::Step {
name: p.name,
attempt: p.attempt,
}
};
return Next::DeliverLive {
seq,
outcome: Outcome { status, payload },
record,
};
}
Completion::Fired => {
(Outcome::empty(Status::Ok), Record::Fired { run, seq })
}
Completion::Signal { ordinal, payload } => (
Outcome::ok(payload),
Record::Waited {
run,
seq,
outcome: WaitOutcome::Signal(ordinal),
},
),
Completion::WaitTimedOut => (
Outcome::empty(Status::TimedOut),
Record::Waited {
run,
seq,
outcome: WaitOutcome::TimedOut,
},
),
Completion::Child { status, payload } => (
Outcome {
status,
payload: payload.clone(),
},
Record::ChildDone {
run,
seq,
status,
payload,
},
),
Completion::Completed {
attempt,
status,
payload,
} => {
return Next::DeliverLive {
seq,
outcome: Outcome { status, payload },
record: LiveRecord::Completed { attempt },
};
}
};
return Next::Deliver {
seq,
outcome,
record: Some(record),
};
}
LiveItem::Cancel { reason, by } => {
self.cancel_queued = false;
self.last_cancel_position = Some(self.next_seq);
let seqs = self.drain_pending();
return Next::CancelAll {
seqs,
record: Some(Record::Cancel {
run: self.info.run,
reason: reason.clone(),
by,
}),
reason,
};
}
LiveItem::Update {
token,
name,
id,
payload,
} => {
return Next::Update {
ordinal: self.updates,
name,
id,
payload,
live: Some(token),
};
}
}
}
Next::Idle
}
pub fn take_offered_updates(&mut self) -> Vec<u64> {
let mut out = Vec::new();
self.live.retain(|l| match l {
LiveItem::Update { token, .. } => {
out.push(*token);
false
}
_ => true,
});
out
}
fn drain_pending(&mut self) -> Vec<Seq> {
let mut seqs: Vec<Seq> = self.pending.keys().copied().collect();
seqs.sort_unstable();
self.pending.clear();
seqs
}
pub fn stall_error(&self) -> Option<DeterminismError> {
if self.has_live_pending() {
return None;
}
match self.replay.front() {
Some(ReplayItem::Op { seq, .. }) => {
let what = self
.recorded
.get(seq)
.map(|r| format!("{} '{}'", r.kind.as_str(), r.name))
.unwrap_or_else(|| "an operation".into());
Some(DeterminismError {
seq: Some(*seq),
message: format!(
"the journal delivers {what} next, but the code is waiting without having started it"
),
})
}
_ => None,
}
}
pub fn check_end(&self) -> Result<(), DeterminismError> {
if self.cancellation_covers_current_operations()
&& self
.replay
.iter()
.all(|r| matches!(r, ReplayItem::Cancel { .. }))
{
return Ok(());
}
if let Some(item) = self.replay.front() {
let seq = match item {
ReplayItem::Op { seq, .. } => Some(*seq),
_ => None,
};
return Err(DeterminismError {
seq,
message: format!(
"the workflow returned but its journal holds {} more deliveries",
self.replay.len()
),
});
}
Ok(())
}
pub fn finish(
&mut self,
status: Status,
payload: Payload,
finished_ms: u64,
) -> Result<RecordBuf, DeterminismError> {
self.check_end()?;
Ok(Record::Done {
run: self.info.run,
status,
finished_ms,
payload,
by: None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn p(s: &str) -> Payload {
Arc::from(s.as_bytes())
}
fn start() -> RecordBuf {
crate::record::start_record(
1,
Arc::from("wf"),
Arc::from("t"),
1,
10,
0,
None,
0,
p("in"),
)
}
fn drain(m: &mut RunMachine, journal: &mut Vec<RecordBuf>) -> Vec<(Seq, Outcome)> {
let mut out = Vec::new();
loop {
match m.next() {
Next::Deliver {
seq,
outcome,
record,
} => {
if let Some(r) = record {
journal.push(r);
}
out.push((seq, outcome));
}
Next::DeliverLive {
seq,
outcome,
record,
} => {
journal.push(record.record(m.info().run, seq, &outcome, None).to_buf());
out.push((seq, outcome));
}
Next::CancelAll {
seqs,
reason,
record,
} => {
if let Some(r) = record {
journal.push(r);
}
for s in seqs {
out.push((
s,
Outcome {
status: Status::Cancelled,
payload: reason.clone(),
},
));
}
}
Next::Update { .. } => {}
Next::Stalled | Next::Idle => return out,
}
}
}
#[test]
fn live_then_replay_in_journal_order() {
let mut journal = vec![start()];
let mut m = RunMachine::fresh(&journal[0], 1).unwrap();
let (a, sa) = m.begin_step("a").unwrap();
let (b, _) = m.begin_step("b").unwrap();
assert_eq!(
sa,
StepStart::Run {
attempt: 1,
not_before_ms: 0
}
);
m.complete(
b,
Completion::Step {
status: Status::Ok,
payload: p("B"),
},
);
m.complete(
a,
Completion::Step {
status: Status::Ok,
payload: p("A"),
},
);
let got = drain(&mut m, &mut journal);
assert_eq!(got.iter().map(|g| g.0).collect::<Vec<_>>(), vec![b, a]);
let mut r = RunMachine::new(&journal, 3).unwrap();
assert_eq!(r.begin_step("a").unwrap().1, StepStart::Replay);
assert_eq!(r.begin_step("b").unwrap().1, StepStart::Replay);
let mut scratch = Vec::new();
let got = drain(&mut r, &mut scratch);
assert!(scratch.is_empty());
assert_eq!(got[0], (b, Outcome::ok(p("B"))));
assert_eq!(got[1], (a, Outcome::ok(p("A"))));
assert!(!r.is_replaying());
}
#[test]
fn mismatched_name_is_a_determinism_error() {
let mut journal = vec![start()];
let mut m = RunMachine::fresh(&journal[0], 1).unwrap();
let (s, _) = m.begin_step("charge").unwrap();
m.complete(
s,
Completion::Step {
status: Status::Ok,
payload: p("1"),
},
);
drain(&mut m, &mut journal);
let mut r = RunMachine::new(&journal, 2).unwrap();
let err = r.begin_step("refund").unwrap_err();
assert!(
err.message.contains("'charge'") && err.message.contains("'refund'"),
"{err}"
);
}
#[test]
fn live_completions_wait_for_replay() {
let mut journal = vec![start()];
let mut m = RunMachine::fresh(&journal[0], 1).unwrap();
let (a, _) = m.begin_step("a").unwrap();
let (_b, _) = m.begin_step("b").unwrap();
m.complete(
a,
Completion::Step {
status: Status::Ok,
payload: p("A"),
},
);
drain(&mut m, &mut journal);
let mut r = RunMachine::new(&journal, 2).unwrap();
let (b2, sb) = {
let (a2, sa) = r.begin_step("a").unwrap();
assert_eq!((a2, sa), (a, StepStart::Replay));
r.begin_step("b").unwrap()
};
assert!(matches!(sb, StepStart::Run { .. }));
r.complete(
b2,
Completion::Step {
status: Status::Ok,
payload: p("B"),
},
);
let mut more = Vec::new();
let got = drain(&mut r, &mut more);
assert_eq!(got[0].0, a);
assert_eq!(got[1].0, b2);
assert_eq!(more.len(), 1);
}
#[test]
fn retries_resume_from_the_journal() {
let mut journal = vec![start()];
let mut m = RunMachine::fresh(&journal[0], 1).unwrap();
let (s, _) = m.begin_step("flaky").unwrap();
journal.push(m.step_retry(s, 500, p("e1")).unwrap());
journal.push(m.step_retry(s, 900, p("e2")).unwrap());
let mut r = RunMachine::new(&journal, 3).unwrap();
assert_eq!(
r.begin_step("flaky").unwrap().1,
StepStart::Run {
attempt: 3,
not_before_ms: 900
}
);
}
#[test]
fn a_paused_step_resumes_with_a_fresh_budget() {
let mut journal = vec![start()];
let mut m = RunMachine::fresh(&journal[0], 1).unwrap();
let (s, _) = m.begin_step("charge").unwrap();
journal.push(m.step_retry(s, 500, p("e1")).unwrap());
assert!(!m.is_suspended());
journal.push(m.step_retry(s, PAUSED, p("e2")).unwrap());
assert!(m.step_paused(s) && m.is_paused() && m.is_suspended());
let mut r = RunMachine::new(&journal, 3).unwrap();
assert!(r.is_paused());
assert_eq!(
r.begin_step("charge").unwrap().1,
StepStart::Run {
attempt: 3,
not_before_ms: PAUSED
}
);
assert!(r.step_paused(s) && r.is_suspended());
let resumed = m.resume(Some(p("cli:ana")));
assert_eq!(
resumed,
vec![Record::Resume {
run: 1,
seq: s,
attempt: 2,
by: Some(p("cli:ana")),
}]
);
assert!(!m.step_paused(s) && !m.is_paused() && m.resume(None).is_empty());
journal.extend(resumed);
journal.push(m.step_retry(s, 900, p("e3")).unwrap());
let mut r = RunMachine::new(&journal, 4).unwrap();
assert_eq!(
r.begin_step("charge").unwrap().1,
StepStart::Run {
attempt: 2,
not_before_ms: 900
}
);
assert_eq!(
r.step_retry(s, 950, p("e4")),
Some(Record::Attempt {
run: 1,
seq: s,
name: Arc::from("charge"),
attempt: 4,
retry_at_ms: 950,
error: p("e4"),
})
);
}
#[test]
fn a_resume_before_the_code_reaches_the_step() {
let mut journal = vec![start()];
let mut m = RunMachine::fresh(&journal[0], 1).unwrap();
let (s, _) = m.begin_step("charge").unwrap();
journal.push(m.step_retry(s, PAUSED, p("e1")).unwrap());
let mut r = RunMachine::new(&journal, 2).unwrap();
assert!(r.is_paused());
let resumed = r.resume(None);
assert_eq!(resumed.len(), 1);
assert!(!r.is_paused());
assert_eq!(
r.begin_step("charge").unwrap().1,
StepStart::Run {
attempt: 1,
not_before_ms: 0
}
);
assert!(!r.step_paused(s));
}
#[test]
fn cancellation_replays_at_the_same_point() {
let mut journal = vec![start()];
let mut m = RunMachine::fresh(&journal[0], 1).unwrap();
let (a, _) = m.begin_step("a").unwrap();
let (w, arm) = m.begin_wait("approve", 0).unwrap();
if let Arm::Append(rec) = arm {
journal.push(rec);
}
m.request_cancel(p("stop"));
let got = drain(&mut m, &mut journal);
assert_eq!(got.len(), 2);
assert!(got.iter().all(|g| g.1.status == Status::Cancelled));
let (c, _) = m.begin_step("undo").unwrap();
m.complete(
c,
Completion::Step {
status: Status::Ok,
payload: p("u"),
},
);
drain(&mut m, &mut journal);
journal.push(m.finish(Status::Cancelled, p("stop"), 99).unwrap());
let mut r = RunMachine::new(&journal, 9).unwrap();
assert_eq!(
r.begin_step("a").unwrap(),
(
a,
StepStart::Run {
attempt: 1,
not_before_ms: 0
}
)
);
assert_eq!(r.begin_wait("approve", 0).unwrap(), (w, Arm::Armed));
let mut none = Vec::new();
let got = drain(&mut r, &mut none);
assert_eq!(got.len(), 2);
assert!(none.is_empty());
assert_eq!(r.begin_step("undo").unwrap().1, StepStart::Replay);
assert_eq!(drain(&mut r, &mut none)[0].1, Outcome::ok(p("u")));
assert!(r.recorded_done().is_some());
}
#[test]
fn repeated_terminal_cancel_replays_without_hiding_other_deliveries() {
let mut journal = vec![start()];
let mut m = RunMachine::fresh(&journal[0], 1).unwrap();
m.begin_step("charge").unwrap();
m.request_cancel(p("stop"));
assert_eq!(drain(&mut m, &mut journal).len(), 1);
m.request_cancel(p("retry"));
assert!(drain(&mut m, &mut journal).is_empty());
assert_eq!(journal.len(), 2);
journal.push(Record::Cancel {
run: 1,
reason: p("retry"),
by: None,
});
let mut replay = RunMachine::new(&journal, 3).unwrap();
replay.begin_step("charge").unwrap();
assert!(matches!(replay.next(), Next::CancelAll { .. }));
replay.check_end().unwrap();
replay.begin_step("undo").unwrap();
assert!(replay.check_end().is_err());
assert!(matches!(replay.next(), Next::CancelAll { .. }));
replay.check_end().unwrap();
}
#[test]
fn values_and_waits_replay() {
let mut journal = vec![start()];
journal.push(Record::Signal {
run: 1,
name: Arc::from("go"),
dedup: 0,
payload: p("yes"),
by: None,
});
let mut m = RunMachine::new(&journal, 2).unwrap();
let (_, now, rec) = m.value(value_kind::NOW, || p("12345678")).unwrap();
journal.push(rec.unwrap());
assert_eq!(&*now, b"12345678");
let (w, arm) = m.begin_wait("go", 0).unwrap();
let Arm::Append(rec) = arm else { panic!() };
journal.push(rec);
m.complete(
w,
Completion::Signal {
ordinal: 0,
payload: p("yes"),
},
);
drain(&mut m, &mut journal);
let mut r = RunMachine::new(&journal, 5).unwrap();
let (_, v, rec) = r.value(value_kind::NOW, || p("other")).unwrap();
assert_eq!((&*v, rec), (&b"12345678"[..], None));
assert_eq!(r.begin_wait("go", 0).unwrap().1, Arm::Replay);
assert_eq!(
r.next(),
Next::Deliver {
seq: w,
outcome: Outcome::ok(p("yes")),
record: None
}
);
}
#[test]
fn stall_is_reported_only_when_nothing_live_is_pending() {
let mut journal = vec![start()];
let mut m = RunMachine::fresh(&journal[0], 1).unwrap();
let _ = m.begin_step("a").unwrap();
let (b, _) = m.begin_step("b").unwrap();
m.complete(
b,
Completion::Step {
status: Status::Ok,
payload: p("B"),
},
);
drain(&mut m, &mut journal);
let mut r = RunMachine::new(&journal, 2).unwrap();
let (a, _) = r.begin_step("a").unwrap();
assert_eq!(r.next(), Next::Stalled);
assert!(r.stall_error().is_none(), "step a is live work");
r.forget(a);
let err = r.stall_error().unwrap();
assert!(err.message.contains("step 'b'"), "{err}");
}
#[test]
fn updates_are_deliveries_in_journal_order() {
let mut journal = vec![start()];
let mut m = RunMachine::fresh(&journal[0], 1).unwrap();
let (a, _) = m.begin_step("a").unwrap();
m.offer_update(77, Arc::from("add"), Arc::from("u1"), p("5"));
let Next::Update {
ordinal,
live: Some(77),
name,
id,
payload,
} = m.next()
else {
panic!("expected a live update");
};
assert_eq!(ordinal, 0);
let (ord, rec) = m.accept_update(name, id, payload);
journal.push(rec);
let (h, _) = m.begin_step("handler-step").unwrap();
m.complete(
h,
Completion::Step {
status: Status::Ok,
payload: p("H"),
},
);
drain(&mut m, &mut journal);
let (_, done) = m.update_done(ord, Status::Ok, p("6")).unwrap();
journal.push(done.unwrap());
m.complete(
a,
Completion::Step {
status: Status::Ok,
payload: p("A"),
},
);
drain(&mut m, &mut journal);
let mut r = RunMachine::new(&journal, 9).unwrap();
assert_eq!(r.begin_step("a").unwrap().1, StepStart::Replay);
let Next::Update {
ordinal: 0,
live: None,
..
} = r.next()
else {
panic!("expected the recorded update");
};
assert_eq!(r.begin_step("handler-step").unwrap().1, StepStart::Replay);
assert!(matches!(r.next(), Next::Deliver { seq, .. } if seq == h));
assert_eq!(r.update_done(0, Status::Ok, p("6")).unwrap().1, None);
assert!(matches!(r.next(), Next::Deliver { seq, .. } if seq == a));
assert!(!r.is_replaying());
}
#[test]
fn async_completion_checks_the_attempt() {
let mut journal = vec![start()];
let mut m = RunMachine::fresh(&journal[0], 1).unwrap();
let (s, st) = m.begin_await("approval").unwrap();
assert_eq!(
st,
AwaitStart::Run {
attempt: 1,
not_before_ms: 0
}
);
journal.push(m.step_retry(s, 5, p("smtp down")).unwrap());
journal.push(m.awaiting(s).unwrap());
assert!(m.is_suspended());
assert!(!m.complete(
s,
Completion::Completed {
attempt: 1,
status: Status::Ok,
payload: p("x")
}
));
assert!(m.complete(
s,
Completion::Completed {
attempt: 2,
status: Status::Ok,
payload: p("yes")
}
));
let got = drain(&mut m, &mut journal);
assert_eq!(got, vec![(s, Outcome::ok(p("yes")))]);
let mut r = RunMachine::new(&journal[..3], 3).unwrap();
assert_eq!(
r.begin_await("approval").unwrap().1,
AwaitStart::Waiting { attempt: 2 }
);
let mut r = RunMachine::new(&journal, 4).unwrap();
assert_eq!(r.begin_await("approval").unwrap().1, AwaitStart::Replay);
}
#[test]
fn patches_take_no_position() {
let mut journal = vec![start()];
let mut m = RunMachine::fresh(&journal[0], 1).unwrap();
let (a, _) = m.begin_step("a").unwrap();
m.complete(
a,
Completion::Step {
status: Status::Ok,
payload: p("A"),
},
);
drain(&mut m, &mut journal);
let (b, _) = m.begin_step("b").unwrap();
m.complete(
b,
Completion::Step {
status: Status::Ok,
payload: p("B"),
},
);
drain(&mut m, &mut journal);
let mut r = RunMachine::new(&journal, 3).unwrap();
r.begin_step("a").unwrap();
assert_eq!(r.patched("fix"), (false, None));
assert_eq!(r.begin_step("b").unwrap(), (1, StepStart::Replay));
let (on, rec) = r.patched("later");
assert!(on);
let mut j2 = journal.clone();
j2.push(rec.unwrap());
let mut r2 = RunMachine::new(&j2, 4).unwrap();
r2.begin_step("a").unwrap();
r2.begin_step("b").unwrap();
assert_eq!(r2.patched("later"), (true, None));
}
#[test]
fn heartbeat_progress_survives_into_the_retry() {
let mut journal = vec![start()];
let mut m = RunMachine::fresh(&journal[0], 1).unwrap();
let (s, _) = m.begin_step("export").unwrap();
journal.push(m.heartbeat(s, p("page-7")).unwrap());
let r = {
let mut r = RunMachine::new(&journal, 2).unwrap();
r.begin_step("export").unwrap();
r
};
assert_eq!(r.progress(s), Some(p("page-7")));
}
#[test]
fn a_queued_completion_is_taken_once_and_in_order() {
let mut journal = vec![start()];
let mut m = RunMachine::fresh(&journal[0], 1).unwrap();
let n: Seq = 20_000;
let seqs: Vec<Seq> = (0..n).map(|_| m.begin_step("s").unwrap().0).collect();
let done = |i: Seq| Completion::Step {
status: Status::Ok,
payload: p(&i.to_string()),
};
for &s in seqs.iter().rev() {
assert!(m.complete(s, done(s)));
assert!(!m.complete(s, done(s + 1)));
}
let got = drain(&mut m, &mut journal);
assert_eq!(
got.iter().map(|g| g.0).collect::<Vec<_>>(),
seqs.iter().rev().copied().collect::<Vec<_>>()
);
assert!(got.iter().all(|(s, o)| o.payload == p(&s.to_string())));
assert!(!m.complete(seqs[0], done(0)));
assert!(matches!(m.next(), Next::Idle));
}
}