use super::{support::*, *};
const WORKER_LOST: &str = "lost:worker WorkerId(2) lost before reporting activity result";
const TRANSPORT_EXHAUSTED: &str = "transport-exhausted:the transport failed to deliver this activity for 121000ms across 5 \
worker losses, past its 120000ms budget (4 heartbeat windows); the infrastructure is \
flapping, the activity never ran.";
#[tokio::test]
async fn a_worker_loss_redispatches_even_with_no_authored_retry_policy() -> TestResult {
let harness = RetryLoopHarness::seeded(r#"{"retry":null}"#).await?;
let dispatcher = ScriptedRetryDispatcher::new(vec![
Err(WORKER_LOST.to_owned()),
Ok(r#""served-by-the-replacement-worker""#.to_owned()),
]);
let outcome = super::dispatch_with_retries(
&(Arc::clone(&dispatcher) as Arc<dyn ActivityDispatcher>),
&harness.seam,
&harness.request,
)
.await;
match outcome.terminal {
RetryLoopTerminal::Completed(payload) => {
assert_eq!(payload, r#""served-by-the-replacement-worker""#);
}
other => {
return Err(format!(
"a lost worker must never terminate the activity; got {}",
terminal_name(&other)
)
.into());
}
}
Ok(())
}
#[tokio::test]
async fn a_worker_loss_consumes_no_authored_budget_and_records_nothing() -> TestResult {
let harness = RetryLoopHarness::seeded(FIXED_RETRY_CONFIG).await?;
let seeded = harness.history().await?.len();
let dispatcher = ScriptedRetryDispatcher::new(vec![
Err(WORKER_LOST.to_owned()),
Err(WORKER_LOST.to_owned()),
Ok(r#""finally""#.to_owned()),
]);
let outcome = super::dispatch_with_retries(
&(Arc::clone(&dispatcher) as Arc<dyn ActivityDispatcher>),
&harness.seam,
&harness.request,
)
.await;
assert!(
matches!(outcome.terminal, RetryLoopTerminal::Completed(_)),
"two transport deaths under a 3-attempt policy must still complete"
);
assert_eq!(
dispatcher.seen_attempts(),
vec![1, 1, 1],
"every re-dispatch after a worker loss must carry the SAME attempt: the \
action never ran, so it consumed no authored budget"
);
assert_eq!(
outcome.attempt, 1,
"the surviving attempt is still the first"
);
assert_eq!(
harness.history().await?.len(),
seeded,
"an attempt-neutral re-dispatch records NOTHING — no ActivityFailed, no \
second ActivityStarted"
);
Ok(())
}
#[tokio::test]
async fn an_action_failure_still_consumes_its_authored_budget() -> TestResult {
let harness = RetryLoopHarness::seeded(FIXED_RETRY_CONFIG).await?;
let dispatcher = ScriptedRetryDispatcher::new(vec![
Err("retryable:the test was red".to_owned()),
Ok(r#""green-on-retry""#.to_owned()),
]);
let outcome = super::dispatch_with_retries(
&(Arc::clone(&dispatcher) as Arc<dyn ActivityDispatcher>),
&harness.seam,
&harness.request,
)
.await;
assert!(matches!(outcome.terminal, RetryLoopTerminal::Completed(_)));
assert_eq!(
dispatcher.seen_attempts(),
vec![1, 2],
"a real action failure still increments the attempt and spends budget"
);
Ok(())
}
#[tokio::test]
async fn a_flapping_link_terminates_naming_the_transport_not_the_action() -> TestResult {
let harness = RetryLoopHarness::seeded(r#"{"retry":null}"#).await?;
let dispatcher = ScriptedRetryDispatcher::new(vec![
Err(WORKER_LOST.to_owned()),
Err(WORKER_LOST.to_owned()),
Err(WORKER_LOST.to_owned()),
Err(WORKER_LOST.to_owned()),
Err(TRANSPORT_EXHAUSTED.to_owned()),
]);
let outcome = super::dispatch_with_retries(
&(Arc::clone(&dispatcher) as Arc<dyn ActivityDispatcher>),
&harness.seam,
&harness.request,
)
.await;
match outcome.terminal {
RetryLoopTerminal::Failed(reason) => {
assert!(
reason.starts_with("transport-exhausted:"),
"the terminal must name the transport domain: {reason}"
);
assert!(
reason.contains("the infrastructure is flapping"),
"the operator must be able to tell infra from a red action: {reason}"
);
}
other => {
return Err(format!(
"a flapping link must terminate, not loop for ever; got {}",
terminal_name(&other)
)
.into());
}
}
assert_eq!(
dispatcher.seen_attempts().len(),
5,
"the loop stops the moment the transport says its budget is spent"
);
Ok(())
}
#[tokio::test]
async fn a_settled_ordinal_stops_the_worker_loss_redispatch() -> TestResult {
let harness = RetryLoopHarness::seeded(r#"{"retry":null}"#).await?;
{
let mut recorder = harness.seam.recorder.lock().await;
recorder
.record_activity_failed(
chrono::Utc::now(),
ActivityId::from_sequence_position(0),
aion_core::ActivityError {
kind: aion_core::ActivityErrorKind::Terminal,
message: "timeout:the workflow gave up on this ordinal".to_owned(),
details: None,
},
1,
)
.await?;
}
let settled = harness.history().await?.len();
let dispatcher = ScriptedRetryDispatcher::new(vec![Err(WORKER_LOST.to_owned())]);
let outcome = super::dispatch_with_retries(
&(Arc::clone(&dispatcher) as Arc<dyn ActivityDispatcher>),
&harness.seam,
&harness.request,
)
.await;
assert!(
matches!(outcome.terminal, RetryLoopTerminal::SettledElsewhere),
"an ordinal already settled elsewhere must stand the loop down"
);
assert_eq!(
dispatcher.seen_attempts(),
vec![1],
"the settled ordinal is never re-dispatched"
);
assert_eq!(
harness.history().await?.len(),
settled,
"the stand-down records nothing"
);
Ok(())
}
fn terminal_name(terminal: &RetryLoopTerminal) -> &'static str {
match terminal {
RetryLoopTerminal::Completed(_) => "Completed",
RetryLoopTerminal::Failed(_) => "Failed",
RetryLoopTerminal::SettledElsewhere => "SettledElsewhere",
RetryLoopTerminal::Parked => "Parked",
}
}