use std::sync::Arc;
use chrono::Utc;
use tokio::spawn;
use tracing::{error, info, warn};
use uuid::Uuid;
use ironflow_store::error::StoreError;
use ironflow_store::models::{Run, RunStatus, RunUpdate};
use crate::engine::{Engine, ExecutionMode, chain_root};
use crate::error::EngineError;
use crate::notify::{Event, RunStatusChangedEvent};
pub const RUN_CANCELLED_ERROR: &str = "run cancelled";
#[derive(Debug, Clone)]
pub struct RunCancellation {
pub run: Run,
pub cancelled_descendants: Vec<Uuid>,
}
impl Engine {
pub async fn cancel_run(
self: &Arc<Self>,
run_id: Uuid,
) -> Result<RunCancellation, EngineError> {
let run = self.load_run(run_id).await?;
let from = run.status.state;
if !from.can_transition_to(&RunStatus::Cancelled) {
return Err(EngineError::Store(StoreError::InvalidTransition {
from,
to: RunStatus::Cancelled,
}));
}
if from != RunStatus::Cancelled {
self.store()
.update_run(
run_id,
RunUpdate {
status: Some(RunStatus::Cancelled),
completed_at: Some(Utc::now()),
..RunUpdate::default()
},
)
.await?;
self.publish_cancelled(&run, None);
info!(run_id = %run_id, from = %from, "run cancelled");
}
self.fail_orphaned_steps(run_id, RUN_CANCELLED_ERROR)
.await?;
let reason = format!("ancestor run {run_id} cancelled");
let cancelled_descendants = self.cancel_descendants(run_id, &reason).await?;
self.wake_root_of_cancelled_child(&run).await;
Ok(RunCancellation {
run: self.load_run(run_id).await?,
cancelled_descendants,
})
}
pub async fn cancel_descendants(
&self,
run_id: Uuid,
reason: &str,
) -> Result<Vec<Uuid>, EngineError> {
let descendants = self.store().list_active_descendants(run_id).await?;
let mut cancelled = Vec::with_capacity(descendants.len());
for descendant in descendants {
let update = RunUpdate {
status: Some(RunStatus::Cancelled),
error: Some(reason.to_string()),
completed_at: Some(Utc::now()),
..RunUpdate::default()
};
match self.store().update_run(descendant.id, update).await {
Ok(()) => {}
Err(StoreError::InvalidTransition { from, .. }) => {
info!(run_id = %descendant.id, status = %from, "descendant already finished");
continue;
}
Err(err) => return Err(err.into()),
}
self.fail_orphaned_steps(descendant.id, reason).await?;
self.publish_cancelled(&descendant, Some(reason));
cancelled.push(descendant.id);
}
if !cancelled.is_empty() {
info!(
run_id = %run_id,
count = cancelled.len(),
reason = %reason,
"descendant runs cancelled"
);
}
Ok(cancelled)
}
pub(crate) async fn cancel_descendants_of_stopped_run(&self, run_id: Uuid, error: &str) {
let reason = format!("parent run {run_id} stopped: {error}");
if let Err(err) = self.cancel_descendants(run_id, &reason).await {
error!(run_id = %run_id, error = %err, "failed to cancel the descendants of a stopped run");
}
}
async fn wake_root_of_cancelled_child(self: &Arc<Self>, run: &Run) {
let Some(root_id) = chain_root(run) else {
return;
};
let root = match self.store().get_run(root_id).await {
Ok(Some(root)) => root,
Ok(None) => return,
Err(err) => {
warn!(run_id = %run.id, root_run_id = %root_id, error = %err, "cannot read the root of a cancelled child");
return;
}
};
let woken = match (root.status.state, self.execution_mode()) {
(RunStatus::AwaitingApproval | RunStatus::Sleeping, ExecutionMode::Workers) => {
self.store()
.update_run_status(root_id, RunStatus::Pending)
.await
}
(RunStatus::AwaitingApproval, ExecutionMode::Local) => {
self.store()
.update_run_status(root_id, RunStatus::Running)
.await
}
(RunStatus::Sleeping, ExecutionMode::Local) => {
match self
.store()
.update_run_status(root_id, RunStatus::Pending)
.await
{
Ok(()) => {
self.store()
.update_run_status(root_id, RunStatus::Running)
.await
}
Err(err) => Err(err),
}
}
_ => return,
};
if let Err(err) = woken {
warn!(run_id = %run.id, root_run_id = %root_id, error = %err, "cannot wake the root of a cancelled child");
return;
}
info!(run_id = %run.id, root_run_id = %root_id, "root run woken to observe its cancelled child");
if self.execution_mode() == ExecutionMode::Local {
let engine = Arc::clone(self);
spawn(async move {
if let Err(err) = engine.resume_run(root_id).await {
error!(root_run_id = %root_id, error = %err, "root run stopped after its child was cancelled");
}
});
}
}
fn publish_cancelled(&self, run: &Run, error: Option<&str>) {
self.event_publisher()
.publish(Event::RunStatusChanged(RunStatusChangedEvent {
run_id: run.id,
workflow_name: run.workflow_name.clone(),
from: run.status.state,
to: RunStatus::Cancelled,
error: error.map(str::to_string),
cost_usd: run.cost_usd,
duration_ms: run.duration_ms,
labels: run.labels.clone(),
at: Utc::now(),
}));
}
async fn load_run(&self, run_id: Uuid) -> Result<Run, EngineError> {
self.store()
.get_run(run_id)
.await?
.ok_or(EngineError::Store(StoreError::RunNotFound(run_id)))
}
}