Resource Lifecycle and Concurrency
When setup/teardown fire, and how resources behave under concurrency.
See Resources for defining and retrieving resources. This page covers their lifecycle and concurrency guarantees.
Lifecycle Guarantees
setup_all() in FIFO order; teardown in LIFO order
| Guarantee | Detail |
|---|---|
| Setup order | FIFO — insert dependencies before dependents. |
| Teardown order | LIFO — each teardown can rely on its dependencies still being live. |
| Sequentiality | Setup and teardown calls are sequential, never concurrent. |
| Partial rollback | If setup fails at position N, teardown runs LIFO from N−1 to 0. Resources at positions ≥ N never set up, never torn down. |
| Teardown errors | Logged, never aborts the sequence — every remaining resource still gets torn down. |
use cano::prelude::*;
#[derive(Resource)] struct ServiceA;
#[derive(Resource)] struct ServiceB;
#[derive(Resource)] struct ServiceD;
struct ServiceC;
impl ServiceA { fn new() -> Self { Self } }
impl ServiceB { fn new() -> Self { Self } }
impl ServiceC { fn new() -> Self { Self } }
impl ServiceD { fn new() -> Self { Self } }
#[resource]
impl Resource for ServiceC {
async fn setup(&self) -> Result<(), CanoError> {
Err(CanoError::Configuration("ServiceC failed".into()))
}
}
#[tokio::main]
async fn main() {
let resources: Resources = Resources::new()
.insert("a", ServiceA::new()) // position 0 — setup OK
.insert("b", ServiceB::new()) // position 1 — setup OK
.insert("c", ServiceC::new()) // position 2 — setup FAILS
.insert("d", ServiceD::new()); // position 3 — never reached
// setup_all() returns Err from C; teardown runs for B then A (LIFO)
let result = resources.setup_all().await;
assert!(result.is_err());
}
Concurrency and Interior Mutability
get() returns an Arc<R>. Split tasks each get their own clone —
a refcount bump, no data copy. Read-only resources need no synchronization. Resources with
mutable state must use interior mutability.
| Primitive | When to use |
|---|---|
tokio::sync::RwLock<T> | Many readers, rare writers. Best default. |
tokio::sync::Mutex<T> | Exclusive access. Serializes split tasks — watch throughput. |
std::sync::atomic::* | Counters and flags. Lock-free. |
DashMap<K, V> | Concurrent map writes. Sharded locking. |
A single Mutex<T> guarding an entire resource serializes split tasks that
write to it. If your splits all hit the same resource, parallelism gives no throughput gain —
partition by task index or use a concurrent structure.
Standalone vs Scheduler Lifecycle
Standalone orchestrate() runs the full lifecycle on every call:
setup_all() before the FSM, teardown_range(all) after, even on
error. Resources are scoped to one workflow run. This is the per-request model — HTTP
handlers, request-bound jobs.
Scheduler runs setup_all() exactly once on
scheduler.start() and teardown_range(all) once on
scheduler.stop(). Each scheduled firing calls execute_workflow()
directly and reuses the same resource instances — open a DB pool once, reuse across runs.
Resources persist across runs. Anything accumulated in a resource (counters, cached state,
the contents of a MemoryStore) carries forward. For per-run reset, clear it at
the start of the workflow — for example, store.clear()? in the initial task.