Resources
Lifecycle-managed, typed dependency injection for Cano workflows.
Resources<TResourceKey> is the typed dictionary every workflow carries. Database
pools, HTTP clients, config structs, MemoryStore, and per-run parameters
are all resources — registered once, injected everywhere. Each lookup returns an
Arc<R>, cheap to clone and safe to share across concurrent split tasks.
The engine calls setup() on each registered resource in insertion order before
the FSM runs, and teardown() in reverse order after — even on failure. Tasks
never receive resources directly; they retrieve typed handles from the injected
&Resources<TResourceKey> via res.get::<R, _>(key)?.
Quick Start
Define a resource, register it, retrieve it by typed key in any task. The engine wires
setup / teardown automatically.
use cano::prelude::*;
#[derive(Debug, Hash, Eq, PartialEq)]
enum Key { Store, Config }
// Stateless resource — derive Resource for a no-op setup/teardown impl
#[derive(Resource)]
struct AppConfig { multiplier: u32 }
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Step { Init, Done }
struct InitTask;
#[task(state = Step, key = Key)]
impl InitTask {
async fn run(&self, res: &Resources<Key>) -> Result<TaskResult<Step>, CanoError> {
let store = res.get::<MemoryStore, _>(&Key::Store)?;
let config = res.get::<AppConfig, _>(&Key::Config)?;
store.put("value", 10u32 * config.multiplier)?;
Ok(TaskResult::Single(Step::Done))
}
}
#[tokio::main]
async fn main() -> Result<(), CanoError> {
let resources = Resources::<Key>::new()
.insert(Key::Store, MemoryStore::new())
.insert(Key::Config, AppConfig { multiplier: 3 });
let workflow = Workflow::new(resources)
.register(Step::Init, InitTask)
.add_exit_state(Step::Done);
workflow.orchestrate(Step::Init).await?;
Ok(())
}
Runnable example: cargo run --example workflow_resources — a stateless config resource,
a stateful counter resource (interior mutability), and a resource with real setup /
teardown lifecycle work, all retrieved by type inside tasks.
Defining a Resource
The Resource trait gives every dependency three hooks — two lifecycle, one
observability. All default to no-ops (and health() defaults to
Healthy), so most resources need no manual impl. health() is
opt-in observability — it is never called during normal workflow execution; see
Observers & Health Probes.
pub trait Resource: Send + Sync + 'static {
async fn setup(&self) -> Result<(), CanoError> { Ok(()) }
async fn teardown(&self) -> Result<(), CanoError> { Ok(()) }
async fn health(&self) -> HealthStatus { HealthStatus::Healthy }
}
Stateless — #[derive(Resource)]
Generates an empty impl Resource for T {}. Use it for config, parameter bags,
read-only data — anything without lifecycle work.
#[derive(Resource)]
struct WorkflowParams {
batch_size: usize,
timeout_ms: u64,
}
Stateful — #[resource] on the impl block
Override setup / teardown for resources that open connections,
allocate buffers, or flush state. Both hooks take &self: resources are shared
via Arc, so mutation requires interior mutability —
tokio::sync::Mutex, RwLock, atomics, etc.
use cano::prelude::*;
use std::sync::{Arc, Mutex};
struct CounterResource {
setup_count: Arc<Mutex<u32>>,
}
#[resource]
impl Resource for CounterResource {
async fn setup(&self) -> Result<(), CanoError> {
*self.setup_count.lock().unwrap() += 1;
Ok(())
}
async fn teardown(&self) -> Result<(), CanoError> {
// flush, close handles, etc.
Ok(())
}
}
Building a Resources Map
Resources is a typed builder. insert consumes self and
panics on duplicate keys (programmer error). try_insert returns
Result<Self, CanoError> so dynamic input can handle collisions explicitly.
The two compose in a single chain.
// Static keys — duplicates indicate buggy wiring; let it panic
let resources = Resources::new()
.insert("store", MemoryStore::new())
.insert("config", AppConfig::default())
// Dynamic key — handle collision as data
.try_insert(user_supplied_key, plugin)?;
Retrieving Resources in Tasks
get::<R, Q>(key) returns Result<Arc<R>, CanoError>.
R names the resource type; Q is the key query type — use
_ to let the compiler infer it from the key argument.
// String keys
let store = res.get::<MemoryStore, _>("store")?;
let params = res.get::<WorkflowParams, _>("params")?;
// Enum keys
let store = res.get::<MemoryStore, _>(&Key::Store)?;
let params = res.get::<WorkflowParams, _>(&Key::Params)?;
| Error variant | Meaning |
|---|---|
ResourceNotFound | No entry for the key. The resource was not inserted. |
ResourceTypeMismatch | Key present, but stored under a different type than R. |
ResourceDuplicateKey | Returned by try_insert when the key is already present. |
Declarative Loading with #[derive(FromResources)]
A struct of Arc dependencies can declare its lookups instead of calling
res.get field-by-field. The derive generates
fn from_resources(res: &Resources<K>) -> CanoResult<Self>.
Field-level #[res(...)] declares the lookup key — string literal for string-keyed
maps, enum path for enum-keyed maps. Container-level
#[from_resources(key = MyKey)] sets the key type when fields use enum keys.
use cano::prelude::*;
use std::sync::Arc;
#[derive(Hash, Eq, PartialEq)]
enum Key { Store, Config }
#[derive(Resource)]
struct AppConfig { multiplier: u32 }
#[derive(FromResources)]
#[from_resources(key = Key)]
struct InitDeps {
#[res(Key::Store)]
store: Arc<MemoryStore>,
#[res(Key::Config)]
config: Arc<AppConfig>,
}
fn lookup(res: &Resources<Key>) -> Result<(), CanoError> {
// In a task:
let InitDeps { store, config } = InitDeps::from_resources(res)?;
let _ = (store, config);
Ok(())
}
Key Types: String vs Enum
TResourceKey defaults to Cow<'static, str>:
&'static str literals stay borrowed (no allocation), owned Strings
are accepted, lookups go through &str. Any type satisfying
Hash + Eq + Send + Sync + 'static works — an enum is recommended for non-trivial
workflows.
| String keys (default) | Enum keys | |
|---|---|---|
| Construction | Resources::new() | Resources::<Key>::new() |
| Typo detection | Runtime — ResourceNotFound | Compile time |
| Allocation on lookup | None for &str literals | None |
| Best for | Prototypes, small workflows | Production, larger workflows |
Runnable example: cargo run --example resources_advanced — enum-typed keys,
try_insert returning CanoError::ResourceDuplicateKey on a repeated key, and the
partial-LIFO rollback when a later resource's setup fails.
Lifecycle & Concurrency
When setup() and teardown() fire, how resources behave across concurrent splits, and standalone vs scheduler lifecycle are covered on a dedicated page:
Workflows Without Resources
When every task is self-contained, skip the resources map. Implement run_bare()
instead of run(), and build the workflow with Workflow::bare()
(equivalent to Workflow::new(Resources::empty())).
use cano::prelude::*;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Step { Compute, Done }
#[derive(Clone)]
struct PureTask;
#[task(state = Step)]
impl PureTask {
async fn run_bare(&self) -> Result<TaskResult<Step>, CanoError> {
let answer = 40 + 2;
println!("Computed: {answer}");
Ok(TaskResult::Single(Step::Done))
}
}
fn build() -> Workflow<Step> {
Workflow::bare()
.register(Step::Compute, PureTask)
.add_exit_state(Step::Done)
}
Runnable example: cargo run --example workflow_bare — a workflow with no resources at all.
API Reference
| Method | Signature | Notes |
|---|---|---|
new() | Resources::new() -> Self | Empty map. |
empty() | Resources::empty() -> Self | Alias for new(). |
with_capacity(n) | Resources::with_capacity(n) -> Self | Pre-allocates the underlying HashMap. |
insert | insert<R: Resource>(self, key, resource) -> Self | Builder; panics on duplicate. |
try_insert | try_insert<R>(self, key, resource) -> Result<Self, CanoError> | Returns ResourceDuplicateKey on duplicate. |
get | get<R, Q>(&self, key: &Q) -> Result<Arc<R>, CanoError> | ResourceNotFound / ResourceTypeMismatch. |
setup_all() | async fn setup_all(&self) -> Result<(), CanoError> | FIFO setup with partial LIFO rollback. Engine calls this. |
teardown_all() | async fn teardown_all(&self) | LIFO teardown. Errors logged, never propagated. |