use cano::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Step {
Reserve,
PanicTask,
Done,
}
struct Panicker;
#[task(state = Step)]
impl Panicker {
fn config(&self) -> TaskConfig {
TaskConfig::minimal()
}
async fn run_bare(&self) -> Result<TaskResult<Step>, CanoError> {
panic!("something went terribly wrong");
}
}
#[derive(Debug, Serialize, Deserialize)]
struct Reservation {
slot_id: u32,
held_by: String,
}
struct ReserveSlot;
#[saga::task(state = Step)]
impl ReserveSlot {
type Output = Reservation;
async fn run(&self, _res: &Resources) -> Result<(TaskResult<Step>, Reservation), CanoError> {
let r = Reservation {
slot_id: 42,
held_by: "order-7001".into(),
};
println!(" reserve : holding slot {} for {}", r.slot_id, r.held_by);
Ok((TaskResult::Single(Step::PanicTask), r))
}
async fn compensate(&self, _res: &Resources, r: Reservation) -> Result<(), CanoError> {
println!(
" reserve : releasing slot {} for {} (compensation run after panic)",
r.slot_id, r.held_by
);
Ok(())
}
}
struct PanicAfterReserve;
#[task(state = Step)]
impl PanicAfterReserve {
fn config(&self) -> TaskConfig {
TaskConfig::minimal()
}
async fn run_bare(&self) -> Result<TaskResult<Step>, CanoError> {
panic!("panic after reservation — should trigger compensation");
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== Panic Safety Demo ===\n");
println!("-- Scenario 1: task panics, engine catches it --");
{
let workflow = Workflow::bare()
.register(Step::PanicTask, Panicker)
.add_exit_state(Step::Done);
match workflow.orchestrate(Step::PanicTask).await {
Ok(s) => println!(" outcome: Ok({s:?}) (unexpected)"),
Err(e) => {
println!(" outcome: Err(\"{e}\")");
assert!(
matches!(&e, CanoError::TaskExecution(msg) if msg.contains("panic")),
"expected TaskExecution with 'panic' in message, got: {e:?}"
);
println!(" confirmed: CanoError::TaskExecution carrying the panic message");
}
}
println!(" program continues normally after the panic\n");
}
println!("-- Scenario 2: compensation runs after a downstream panic --");
{
let workflow = Workflow::bare()
.register_with_compensation(Step::Reserve, ReserveSlot)
.register(Step::PanicTask, PanicAfterReserve)
.add_exit_state(Step::Done);
match workflow.orchestrate(Step::Reserve).await {
Ok(s) => println!(" outcome: Ok({s:?}) (unexpected)"),
Err(e) => {
println!(" outcome: Err(\"{e}\")");
assert!(
matches!(&e, CanoError::TaskExecution(msg) if msg.contains("panic")),
"expected TaskExecution, got: {e:?}"
);
println!(" confirmed: original panic error returned after clean compensation");
}
}
}
println!("\n=== Done ===");
Ok(())
}