Skip to main content

resonate-basic-durable-world-usage-rust

Core patterns for writing Resonate durable functions in Rust using the

설치로 이동

소스 정보

저장소
resonatehq/resonate-skills
최근 소스 활동
2026년 8월 6일 11:59
감지된 SKILL.md 언어
영어
스타
6
포크
0

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
resonate-basic-durable-world-usage-rust
description
Core patterns for writing Resonate durable functions in Rust using the
license
Apache-2.0
# Resonate Basic Durable World Usage — Rust > **SDK note.** The Resonate Rust SDK (`resonate-sdk` v0.6.0, published on crates.io) is in active development; APIs may change between releases. Verify against the current SDK source before relying on any specific shape. ## Overview Durable functions in Rust are async functions decorated with `#[resonate::function]`. The macro wraps your function in Resonate's durable-execution machinery — every successful `ctx.run` / `ctx.rpc` / `ctx.sleep` is a checkpoint, and the function resumes from the last checkpoint on process restart. This skill covers the Context API surface used inside those functions. The ephemeral-world counterpart (registration, top-level invocation, promises) lives in `resonate-basic-ephemeral-world-usage-rust`. ## The contract - **Attribute macro:** `#[resonate::function]` (or `#[resonate::function(name = "alias")]`) - **Async function:** `async fn` — tokio is the default runtime - **Return type:** `Result<T>` (aliased from `resonate::error::Result`) — use `?` for propagation - **First parameter inferred kind:** | First param | Kind | Capabilities | |---|---|---| | `&Context` | Workflow | `ctx.run`, `ctx.rpc`, `ctx.sleep`, `.spawn()` parallelism, context accessors | | `&Info` | Leaf with metadata | Read-only access to `info.id()`, `info.parent_id()`, etc. | | value types (`String`, `MyStruct`) | Pure leaf | No context; stateless computation | Minimal workflow shape: ```rust use resonate::prelude::*; #[resonate::function] async fn process_order(ctx: &Context, order_id: String) -> Result<String> { let order = ctx.run(load_order, order_id.clone()).await?; let charge = ctx.rpc::<String>("charge_card", order.clone()).await?; Ok(format!("order={} charge={}", order, charge)) } #[resonate::function] async fn load_order(order_id: String) -> Result<String> { Ok(format!("order-{}", order_id)) } ``` If this workflow crashes after `load_order` but before `rpc("charge_card")`, it resumes at the `rpc` call on restart — `load_order` is NOT re-executed; its stored result is returned. ## `ctx.run` — same-process invocation ### Sequential (await directly) ```rust #[resonate::function] async fn foo(ctx: &Context, input: String) -> Result<String> { let a = ctx.run(step_a, input.clone()).await?; let b = ctx.run(step_b, a).await?; Ok(b) } ``` ### Parallel (`.spawn()` returns a DurableFuture) ```rust #[resonate::function] async fn foo(ctx: &Context, input: String) -> Result<(String, String)> { let fut_a = ctx.run(step_a, input.clone()).spawn()?; let fut_b = ctx.run(step_b, input).spawn()?; let a = fut_a.await?; let b = fut_b.await?; Ok((a, b)) } ``` `.spawn()` starts the sub-task without blocking; the returned `DurableFuture` is awaited later. This is the Rust analog of TS's `begin_run` / Python's `begin_run` — different shape, same semantics. ## `ctx.rpc` — remote-process invocation The registered name is a string; the result type needs a turbofish: ```rust #[resonate::function] async fn orchestrator(ctx: &Context, batch_id: String) -> Result<()> { let _result = ctx.rpc::<String>("worker_fn", batch_id).await?; Ok(()) } ``` With target group + parallelism: ```rust #[resonate::function] async fn parallel_remote(ctx: &Context) -> Result<()> { let f1 = ctx.rpc::<String>("worker-a", "data") .target("poll://any@group-a") .spawn()?; let f2 = ctx.rpc::<String>("worker-b", "data") .target("poll://any@group-b") .spawn()?; f1.await?; f2.await?; Ok(()) } ``` ## `ctx.sleep` — durable sleep `ctx.sleep` takes a `std::time::Duration`. There is no upper limit; sleeps survive process restarts because the continuation lives in the Resonate server: ```rust use std::time::Duration; #[resonate::function] async fn daily_digest(ctx: &Context, user_id: String) -> Result<()> { loop { ctx.sleep(Duration::from_secs(24 * 60 * 60)).await?; ctx.rpc::<()>("send_digest", user_id.clone()).await?; } } ``` ## Builder options All Context execution methods return a builder that accepts options before `.await` or `.spawn()`: ```rust use std::time::Duration; #[resonate::function] async fn foo(ctx: &Context) -> Result<String> { let result: String = ctx.run(expensive_leaf, "input".into()) .timeout(Duration::from_secs(30)) .await?; Ok(result) } ``` Documented Context builder options: | Method | Applies to | Purpose | |---|---|---| | `.timeout(Duration)` | `ctx.run`, `ctx.rpc` | Execution timeout | | `.target(&str)` | `ctx.rpc` only | Worker group routing (`poll://any@group-name`) | Note: `.version(u32)` and `.tags(HashMap<String, String>)` are **ephemeral-world only** (on `resonate.run()` / `resonate.rpc()`) — they are NOT available on Context builders. ## Context accessors Inside a workflow, `&Context` provides read-only metadata: ```rust #[resonate::function] async fn foo(ctx: &Context) -> Result<()> { println!("id={}", ctx.id()); println!("parent={}", ctx.parent_id()); println!("origin={}", ctx.origin_id()); println!("func={}", ctx.func_name()); println!("timeout_at={}", ctx.timeout_at()); Ok(()) } ``` `origin_id` is the top-level invocation that kicked off this call graph; useful for tracing across RPC hops. `ctx.info()` returns a snapshot `Info` struct with a superset of these accessors, including `branch_id()` and `tags()` (a `&HashMap<String, String>` of tags set at invocation time). Useful when you want to hand execution metadata to a helper function without passing the whole `Context`. ## Dependency access: `ctx.get_dependency::<T>()` Dependencies set on the `Resonate` instance with `.with_dependency<T>(value)` (see `resonate-basic-ephemeral-world-usage-rust`) are retrieved inside a durable function by type: ```rust use std::sync::Arc; use sqlx::PgPool; #[resonate::function] async fn write_order(ctx: &Context, order_id: String) -> Result<()> { let pool: Arc<PgPool> = ctx.get_dependency::<PgPool>(); // wrap I/O in a leaf so the effect is checkpointed ctx.run(insert_order_row, (pool.clone(), order_id)).await?; Ok(()) } async fn insert_order_row((pool, order_id): (Arc<PgPool>, String)) -> Result<()> { sqlx::query("INSERT INTO orders (id) VALUES ($1) ON CONFLICT DO NOTHING") .bind(&order_id) .execute(&*pool) .await?; Ok(()) } ``` Type-dispatched: there is one dependency per type per `Resonate` instance. For multiple values of the same logical type (e.g. two Postgres pools pointing at different databases), wrap them in newtypes and register each separately. `ctx.get_dependency::<T>()` panics if no dependency of that type was registered — wire your DI at startup and keep it static, not conditional. `&Info` also exposes `get_dependency::<T>()`, so a leaf that takes `info: &Info` as its first parameter can access dependencies without needing the full Context. > **Note:** `ctx.get_dependency` + `Info::get_dependency` are in the v0.6.0 SDK source (`resonate-sdk-rs:resonate/src/context.rs:120`, `info.rs:43`) but not yet covered in `docs/develop/rust.mdx`. Rust-skill review discovered the docs lag; the API is real. ## Human-in-the-loop: `ctx.promise::<T>()` Context-side promises let a durable function block until an external actor (webhook, UI, CLI, operator) resolves or rejects. The returned `PromiseTask<T>` is a lazy builder: you can attach `.timeout(Duration)` and `.data(&impl Serialize)`, fetch the generated ID via `.id().await?`, eagerly `create()` a handle for later awaiting, or `.await` it directly to block. ```rust use serde::{Serialize, Deserialize}; use std::time::Duration; #[derive(Serialize, Deserialize)] struct Decision { approved: bool, reviewer: String, } #[resonate::function] async fn expense_approval(ctx: &Context, expense_id: String) -> Result<String> { // create a promise the reviewer will resolve from outside let decision: Decision = ctx .promise::<Decision>() .timeout(Duration::from_secs(24 * 60 * 60)) // 24-hour SLA .data(&serde_json::json!({ "expense_id": expense_id }))? .await?; // blocks until resolved if decision.approved { ctx.run(process_reimbursement, expense_id.clone()).await?; Ok(format!("approved by {}", decision.reviewer)) } else { Ok(format!("rejected by {}", decision.reviewer)) } } ``` From outside the worker, resolve it with the ephemeral-world `resonate.promises.resolve(...)` API using the same ID. Since the ID is SDK-generated per-invocation, the typical pattern is: fetch `task.id().await?` before awaiting, stash it somewhere the reviewer can read (DB, webhook payload, UI), then `await` the promise. For the deep HITL pattern (multi-approver, webhooks, SLAs), see `resonate-human-in-the-loop-pattern-rust`. > **Note:** `ctx.promise::<T>()` is in the v0.6.0 SDK source (`resonate-sdk-rs:resonate/src/context.rs:335`) with a full `PromiseTask<T>` builder (`.timeout`, `.data`, `.create`) but not yet covered in `docs/develop/rust.mdx`. ## Leaf with `&Info` A pure leaf that still needs execution metadata uses `&Info` as its first parameter: ```rust #[resonate::function] async fn stamped_log(info: &Info, message: String) -> Result<()> { println!("[{}] {}", info.id(), message); Ok(()) } ``` `&Info` is read-only — no sub-task invocation, no sleep — which lets the SDK optimize its execution differently from workflows. ## Common shapes **Sequential pipeline with typed `?` propagation:** ```rust #[resonate::function] async fn ingest(ctx: &Context, url: String) -> Result<usize> { let raw = ctx.run(fetch, url).await?; let parsed = ctx.run(parse, raw).await?; let count = ctx.run(persist, parsed).await?; Ok(count) } ``` **Parallel fan-out over a known set:** ```rust #[resonate::function] async fn enrich_batch(ctx: &Context, ids: Vec<String>) -> Result<Vec<String>> { let mut futures = Vec::with_capacity(ids.len()); for id in ids { futures.push(ctx.run(enrich_one, id).spawn()?); } let mut results = Vec::with_capacity(futures.len()); for f in futures { results.push(f.await?); }
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기