use std::sync::Weak;
use std::time::Duration;
use aion_core::{Event, TimerCancelCause, TimerId, WorkflowId};
use aion_store::StoreError;
use chrono::{DateTime, Utc};
use crate::engine_seam::EngineSeamError;
use crate::runtime::nif_state::EngineNifState;
use crate::runtime::nif_timer_bridge::{TimerNifBridge, timer_bridge};
use crate::time::TimerServiceError;
pub(super) fn wheel_torn_down_before_arming(timer_id: &TimerId) -> EngineSeamError {
EngineSeamError::TimerWheel {
reason: format!(
"timer `{timer_id}` was not armed: this engine's timer wheel has been torn down, so \
the fire belongs to whichever engine owns the run now"
),
}
}
pub(super) fn wheel_torn_down_after_firing(timer_id: &TimerId) -> EngineSeamError {
EngineSeamError::TimerWheel {
reason: format!(
"timer `{timer_id}` fired, but its append was refused: this engine's timer wheel has \
been torn down, so the fire belongs to whichever engine owns the run now. The timer \
is still live in durable history and the owning engine re-arms it from there"
),
}
}
pub(super) fn wheel_torn_down_before_cancelling(timer_id: &TimerId) -> EngineSeamError {
EngineSeamError::TimerWheel {
reason: format!(
"cancellation of timer `{timer_id}` was not recorded: this engine's timer wheel has \
been torn down, so the run belongs to whichever engine owns it now. The timer stays \
live in durable history and that engine re-arms it; the cancellation takes effect \
only once the run reissues it there"
),
}
}
pub(super) fn wheel_torn_down_before_teardown_cancel(timer_id: &TimerId) -> EngineSeamError {
EngineSeamError::TimerWheel {
reason: format!(
"teardown cancellation of timer `{timer_id}` was not recorded: this engine's timer \
wheel has been torn down, so the run belongs to whichever engine owns it now. The \
timer stays live in durable history. The cancel transition that issued this runs \
immediately after it, and once that run's terminal lands the timer is inert — the \
owning engine's fire is refused as post-terminal, recording nothing. No operator \
action in that case. If the cancel itself then failed, the run is still live with \
this timer armed and it will fire: check the run's status before assuming it is gone"
),
}
}
pub(super) enum RefusedAppend {
Fire,
Cancel(TimerCancelCause),
}
pub(super) enum TimerAppendError {
WheelTornDown {
timer_id: TimerId,
refused: RefusedAppend,
},
Append(Box<dyn std::error::Error + Send + Sync>),
}
impl TimerAppendError {
pub(super) fn into_seam_error(self) -> EngineSeamError {
match self {
Self::WheelTornDown {
timer_id,
refused: RefusedAppend::Fire,
} => wheel_torn_down_after_firing(&timer_id),
Self::WheelTornDown {
timer_id,
refused: RefusedAppend::Cancel(TimerCancelCause::WorkflowIntent),
} => wheel_torn_down_before_cancelling(&timer_id),
Self::WheelTornDown {
timer_id,
refused: RefusedAppend::Cancel(TimerCancelCause::CancelTeardown),
} => wheel_torn_down_before_teardown_cancel(&timer_id),
Self::Append(error) => EngineSeamError::Recorder {
reason: error.to_string(),
},
}
}
pub(super) fn append(error: impl std::error::Error + Send + Sync + 'static) -> Self {
Self::Append(Box::new(error))
}
}
pub(super) fn is_wheel_teardown(error: &TimerServiceError) -> bool {
matches!(
error,
TimerServiceError::Engine(EngineSeamError::TimerWheel { .. })
)
}
pub(super) async fn fire_wheel_timer(
nif_state: &Weak<EngineNifState>,
workflow_id: &WorkflowId,
timer_id: &TimerId,
fire_at: DateTime<Utc>,
) {
const MAX_ATTEMPTS: u32 = 6;
const INITIAL_BACKOFF: Duration = Duration::from_millis(200);
const MAX_BACKOFF: Duration = Duration::from_secs(30);
let mut backoff = INITIAL_BACKOFF;
for attempt in 1..=MAX_ATTEMPTS {
let Some(bridge) = nif_state
.upgrade()
.and_then(|state| timer_bridge(&state).ok())
else {
return;
};
let result = bridge
.service()
.fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
.await;
let Err(error) = result else {
return;
};
if is_wheel_teardown(&error) {
tracing::debug!(
%workflow_id,
%timer_id,
"timer fire abandoned: this engine's wheel has been torn down and the timer stays live for its owner"
);
return;
}
if crate::time::is_deadline_timer(timer_id) {
match deadline_remains_live(&bridge, workflow_id, timer_id).await {
Ok(false) => return,
Ok(true) => tracing::warn!(
error = %error,
attempt,
"workflow deadline fire failed while its timer is still live; retrying with backoff"
),
Err(read_error) => tracing::warn!(
error = %error,
%read_error,
attempt,
"workflow deadline fire failed and its liveness could not be read (store outage?); treating as still-eligible and retrying with backoff"
),
}
} else {
tracing::warn!(
error = %error,
%workflow_id,
%timer_id,
attempt,
"timer wheel fire callback failed; retrying with backoff"
);
}
if attempt == MAX_ATTEMPTS {
break;
}
tokio::time::sleep(backoff).await;
backoff = backoff.saturating_mul(2).min(MAX_BACKOFF);
}
if crate::time::is_deadline_timer(timer_id) {
tracing::error!(
%workflow_id,
%timer_id,
"workflow deadline fire exhausted same-epoch retries; the durable timer stays live for restart recovery"
);
} else {
tracing::error!(
%workflow_id,
%timer_id,
"timer fire exhausted same-epoch retries; if its TimerFired append never landed, the \
durable TimerStarted stays live for restart recovery, and if an append landed \
unacknowledged, the recorded fire is consumed by replay on restart"
);
}
}
async fn deadline_remains_live(
bridge: &TimerNifBridge,
workflow_id: &WorkflowId,
timer_id: &TimerId,
) -> Result<bool, StoreError> {
let Some(run_id) = crate::time::deadline_run_id(timer_id) else {
return Ok(false);
};
let history = bridge.store.read_history(workflow_id).await?;
Ok(crate::time::outstanding_deadline_timer(&history, &run_id).is_some())
}
pub(super) fn active_run_has_terminal(history: &[Event]) -> bool {
let Some(run_id) = history.iter().rev().find_map(|event| match event {
Event::WorkflowStarted { run_id, .. } => Some(run_id.clone()),
_ => None,
}) else {
return false;
};
crate::lifecycle::completion::terminal_outcome_from_history(history, &run_id).is_some()
}
pub(super) fn event_kind(event: &Event) -> &'static str {
match event {
Event::TimerFired { .. } => "TimerFired",
Event::TimerCancelled { .. } => "TimerCancelled",
Event::WithTimeoutCompleted { .. } => "WithTimeoutCompleted",
_ => "non-timer",
}
}