use super::*;
use crate::runtime::nif_collect::OrdinalState;
use crate::runtime::nif_collect_settlement::recorded_terminal;
#[test]
fn a_retryable_attempt_record_leaves_the_member_unresolved() -> TestResult {
let mut history = scheduled_started(0, "a");
history.push(retryable_failed(0, "retryable:boom", 1));
assert_eq!(
recorded_terminal(&history, 0)
.map_err(|reason| -> Box<dyn std::error::Error> { reason.into() })?,
None,
"a retryable attempt record must leave the ordinal unresolved, so recovery re-dispatches it"
);
Ok(())
}
#[test]
fn a_terminal_ending_a_retry_trail_resolves_the_member_over_the_retry_record() -> TestResult {
let mut history = scheduled_started(0, "a");
history.push(retryable_failed(0, "retryable:boom", 1));
history.push(started(0, 2));
history.push(failed_at_attempt(0, "boom-final", 2));
assert_eq!(
recorded_terminal(&history, 0)
.map_err(|reason| -> Box<dyn std::error::Error> { reason.into() })?,
Some(OrdinalState::Failed("boom-final".to_owned())),
"the terminal that ends a retry trail resolves the ordinal, and its message — not the \
retry record's — is what the settlement reports"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_retryable_attempt_record_does_not_fail_fast_a_collect_all() -> TestResult {
let mut events = pending_batch(&["a", "b"]);
events.push(retryable_failed(0, "retryable:boom", 1));
events.push(started(0, 2));
events.push(completed(1, r#""done-b""#));
let harness = CollectHarness::over_events(&events).await?;
harness
.deps
.runtime
.deliver_activity_completion_message_with_attempt(
harness.pid,
"activity:0",
r#""done-a""#.to_owned(),
Some(2),
)?;
let step = harness.step(CollectKind::All, &specs(&["a", "b"]));
assert_eq!(
step,
Ok(CollectStep::AllCompleted(vec![
r#""done-a""#.to_owned(),
r#""done-b""#.to_owned(),
])),
"a retried member's success must settle the batch, not its retry record"
);
assert_eq!(
harness.cancelled_ordinals().await?,
Vec::<u64>::new(),
"nothing may be cancelled: no member failed terminally"
);
harness.shutdown()
}
#[tokio::test(flavor = "multi_thread")]
async fn a_retryable_attempt_record_does_not_win_a_race() -> TestResult {
let mut events = pending_batch(&["a", "b"]);
events.push(retryable_failed(0, "retryable:boom", 1));
events.push(started(0, 2));
let harness = CollectHarness::over_events(&events).await?;
let step = harness.step(CollectKind::Race, &specs(&["a", "b"]));
assert_eq!(
step,
Ok(CollectStep::Suspend),
"an open retry trail settles nothing, so the race must stay suspended"
);
assert_eq!(
harness.cancelled_ordinals().await?,
Vec::<u64>::new(),
"no winner means no loser cancellations"
);
harness.shutdown()
}
#[tokio::test(flavor = "multi_thread")]
async fn a_retry_record_landing_mid_sweep_leaves_the_replayed_settlement_unchanged() -> TestResult {
let mut events = pending_batch(&["a", "b"]);
events.push(completed(1, r#""done-b""#));
let live = CollectHarness::over_events(&events).await?;
live.deps
.runtime
.deliver_activity_completion_message_with_attempt(
live.pid,
"activity:0",
r#""done-a""#.to_owned(),
Some(2),
)?;
let live_answer = live.step(CollectKind::All, &specs(&["a", "b"]));
assert_eq!(
live_answer,
Ok(CollectStep::AllCompleted(vec![
r#""done-a""#.to_owned(),
r#""done-b""#.to_owned(),
])),
"the live sweep settles ordinal 0 from the runtime map and records its completion"
);
let workflow_id = live.workflow_id.clone();
let run_id = live.handle.run_id().clone();
let recorded = live.store.read_history(&workflow_id).await?;
let completion_index = recorded
.iter()
.position(|event| {
matches!(event, Event::ActivityCompleted { activity_id, .. }
if activity_id.sequence_position() == 0)
})
.ok_or("the live sweep must have recorded ordinal 0's completion")?;
let mut interleaved = recorded.clone();
interleaved.insert(completion_index, retryable_failed(0, "retryable:boom", 1));
live.shutdown()?;
let replay = CollectHarness::over_store(
restored_store(&workflow_id, &interleaved).await?,
workflow_id,
run_id,
)
.await?;
let replayed_length = replay.store.read_history(&replay.workflow_id).await?.len();
assert_eq!(
replay.step(CollectKind::All, &specs(&["a", "b"])),
live_answer,
"the replayed settlement must answer exactly what the live run answered"
);
assert_eq!(
replay.store.read_history(&replay.workflow_id).await?.len(),
replayed_length,
"replay must append nothing"
);
replay.shutdown()
}
#[tokio::test(flavor = "multi_thread")]
async fn a_terminal_ending_a_retry_trail_fails_fast_with_the_terminals_message() -> TestResult {
let mut events = pending_batch(&["a", "b"]);
events.push(retryable_failed(0, "retryable:boom", 1));
events.push(started(0, 2));
events.push(failed_at_attempt(0, "exhausted: boom", 2));
let harness = CollectHarness::over_events(&events).await?;
let step = harness.step(CollectKind::All, &specs(&["a", "b"]));
assert_eq!(
step,
Ok(CollectStep::FailFast("exhausted: boom".to_owned()))
);
assert_eq!(harness.cancelled_ordinals().await?, vec![1]);
assert_eq!(harness.pinned(), None);
harness.shutdown()
}
#[tokio::test(flavor = "multi_thread")]
async fn a_terminal_ending_a_retry_trail_wins_a_race_with_the_terminals_message() -> TestResult {
let mut events = pending_batch(&["a", "b"]);
events.push(retryable_failed(1, "retryable:boom", 1));
events.push(started(1, 2));
events.push(failed_at_attempt(1, "exhausted: boom", 2));
let harness = CollectHarness::over_events(&events).await?;
let step = harness.step(CollectKind::Race, &specs(&["a", "b"]));
assert_eq!(
step,
Ok(CollectStep::RaceWon(Err("exhausted: boom".to_owned())))
);
assert_eq!(harness.cancelled_ordinals().await?, vec![0]);
harness.shutdown()
}
#[tokio::test(flavor = "multi_thread")]
async fn a_cancellation_over_an_open_retry_trail_resolves_the_member_as_cancelled() -> TestResult {
let mut history = scheduled_started(0, "a");
history.push(retryable_failed(0, "retryable:boom", 1));
history.push(Event::ActivityCancelled {
envelope: placeholder_envelope(),
activity_id: ActivityId::from_sequence_position(0),
attempt: 1,
});
assert_eq!(
recorded_terminal(&history, 0)
.map_err(|reason| -> Box<dyn std::error::Error> { reason.into() })?,
Some(OrdinalState::Cancelled),
"a cancellation still resolves an ordinal whose retry trail was open"
);
Ok(())
}