Skip to main content

resonate-basic-durable-world-usage-rust

Core patterns for writing Resonate durable functions in Rust using the

الانتقال إلى التثبيت

معلومات المصدر

المستودع
resonatehq/resonate-skills
آخر نشاط في المصدر
٦ أغسطس ٢٠٢٦ في ١١:٥٩
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٦
التفرعات
٠

خيارات التثبيت

يُحدَّد 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