use std::sync::Arc;
use crate::activity::bridge::{ActivityDispatch, ActivityDispatcher};
use crate::durability::Recorder;
pub(super) fn spawn_completion_task(
tokio_handle: &tokio::runtime::Handle,
runtime: Arc<crate::RuntimeHandle>,
dispatcher: Arc<dyn ActivityDispatcher>,
seam: RetryRecorderSeam,
workflow_pid: u64,
correlation_id: String,
request: ActivityDispatch,
) {
let future = async move {
let outcome = dispatch_with_retries(&dispatcher, &seam, &request).await;
let attempt = outcome.attempt;
match outcome.terminal {
RetryLoopTerminal::Completed(payload) => {
if let Err(error) = runtime.deliver_activity_completion_message_with_attempt(
workflow_pid,
&correlation_id,
payload,
Some(attempt),
) {
tracing::warn!(%error, workflow_pid, correlation_id, "activity completion delivery failed");
}
}
RetryLoopTerminal::Failed(reason) => {
if let Err(error) = runtime.deliver_activity_failure_message_with_attempt(
workflow_pid,
&correlation_id,
reason,
Some(attempt),
) {
tracing::warn!(%error, workflow_pid, correlation_id, "activity failure delivery failed");
}
}
RetryLoopTerminal::SettledElsewhere => {
tracing::debug!(
workflow_id = %request.workflow_id,
activity_id = %request.activity_id,
attempt,
"activity retry loop stopped: the activity settled through another path"
);
}
RetryLoopTerminal::Parked => {
tracing::debug!(
workflow_id = %request.workflow_id,
activity_id = %request.activity_id,
attempt,
"activity dispatch parked for restart recovery; retry loop stood down"
);
}
}
};
tokio_handle.spawn(future);
}
pub(super) struct RetryRecorderSeam {
pub(super) recorder: Arc<tokio::sync::Mutex<Recorder>>,
pub(super) run_id: aion_core::RunId,
}
pub(super) struct RetryLoopOutcome {
pub(super) attempt: u32,
pub(super) terminal: RetryLoopTerminal,
}
pub(super) enum RetryLoopTerminal {
Completed(String),
Failed(String),
SettledElsewhere,
Parked,
}
fn failure_stand_down(
policy: Option<&super::nif_activity_retry::RetryPolicy>,
reason: &str,
attempt: u32,
) -> Option<RetryLoopTerminal> {
use super::nif_activity_retry::{is_parked_reason, is_retryable_reason};
if is_parked_reason(reason) {
return Some(RetryLoopTerminal::Parked);
}
match policy {
Some(policy) if is_retryable_reason(reason) && attempt < policy.max_attempts => None,
_ => Some(RetryLoopTerminal::Failed(reason.to_owned())),
}
}
pub(super) async fn dispatch_with_retries(
dispatcher: &Arc<dyn ActivityDispatcher>,
seam: &RetryRecorderSeam,
request: &ActivityDispatch,
) -> RetryLoopOutcome {
use super::nif_activity_retry::retry_policy_from_config;
let policy = retry_policy_from_config(&request.config);
let mut attempt = request.attempt;
loop {
let mut delivery = request.clone();
delivery.attempt = attempt;
let reason = match Arc::clone(dispatcher).dispatch_async(delivery).await {
Ok(payload) => {
return RetryLoopOutcome {
attempt,
terminal: RetryLoopTerminal::Completed(payload),
};
}
Err(reason) => reason,
};
if let Some(terminal) = failure_stand_down(policy.as_ref(), &reason, attempt) {
return RetryLoopOutcome { attempt, terminal };
}
let Some(policy) = policy.as_ref() else {
return RetryLoopOutcome {
attempt,
terminal: RetryLoopTerminal::Failed(reason),
};
};
match record_retry_event(
seam,
request,
RetryRecord::AttemptFailed {
attempt,
reason: reason.clone(),
},
)
.await
{
RetryRecordOutcome::Recorded => {}
RetryRecordOutcome::Settled => {
return RetryLoopOutcome {
attempt,
terminal: RetryLoopTerminal::SettledElsewhere,
};
}
RetryRecordOutcome::RecordFailed(record_error) => {
tracing::warn!(
workflow_id = %request.workflow_id,
activity_id = %request.activity_id,
attempt,
error = %record_error,
"failed to record a retryable activity failure; failing the activity instead \
of retrying unrecorded"
);
return RetryLoopOutcome {
attempt,
terminal: RetryLoopTerminal::Failed(reason),
};
}
}
let delay = policy.backoff.delay_after(attempt);
tracing::warn!(
workflow_id = %request.workflow_id,
activity_id = %request.activity_id,
activity_type = %request.name,
attempt,
max_attempts = policy.max_attempts,
retry_in_ms = u64::try_from(delay.as_millis()).unwrap_or(u64::MAX),
reason = %reason,
"activity attempt failed with a retryable error; re-dispatching"
);
tokio::time::sleep(delay).await;
attempt += 1;
match record_retry_event(seam, request, RetryRecord::AttemptStarted { attempt }).await {
RetryRecordOutcome::Recorded => {}
RetryRecordOutcome::Settled => {
return RetryLoopOutcome {
attempt,
terminal: RetryLoopTerminal::SettledElsewhere,
};
}
RetryRecordOutcome::RecordFailed(record_error) => {
tracing::warn!(
workflow_id = %request.workflow_id,
activity_id = %request.activity_id,
attempt,
error = %record_error,
"failed to record a retry attempt start; failing the activity instead of \
dispatching unrecorded"
);
return RetryLoopOutcome {
attempt,
terminal: RetryLoopTerminal::Failed(reason),
};
}
}
}
}
enum RetryRecord {
AttemptFailed { attempt: u32, reason: String },
AttemptStarted { attempt: u32 },
}
enum RetryRecordOutcome {
Recorded,
Settled,
RecordFailed(crate::durability::DurabilityError),
}
async fn record_retry_event(
seam: &RetryRecorderSeam,
request: &ActivityDispatch,
record: RetryRecord,
) -> RetryRecordOutcome {
let mut recorder = seam.recorder.lock().await;
let history = match recorder.read_history().await {
Ok(history) => history,
Err(error) => return RetryRecordOutcome::RecordFailed(error),
};
let history = match crate::durability::current_run_segment(history, &seam.run_id) {
Ok(history) => history,
Err(error) => return RetryRecordOutcome::RecordFailed(error),
};
if super::nif_activity_retry::activity_settled(&history, &request.activity_id) {
return RetryRecordOutcome::Settled;
}
let append_result = match record {
RetryRecord::AttemptFailed { attempt, reason } => {
recorder
.record_activity_failed(
chrono::Utc::now(),
request.activity_id.clone(),
aion_core::ActivityError {
kind: aion_core::ActivityErrorKind::Retryable,
message: reason,
details: None,
},
attempt,
)
.await
}
RetryRecord::AttemptStarted { attempt } => {
recorder
.record_activity_started(chrono::Utc::now(), request.activity_id.clone(), attempt)
.await
}
};
match append_result {
Ok(()) => RetryRecordOutcome::Recorded,
Err(error) => RetryRecordOutcome::RecordFailed(error),
}
}