Skip to main content

resonate-recursive-fan-out-pattern-rust

Implement recursive fan-out in Rust with Resonate — spawn N sub-workflows via .spawn(), optionally recurse deeper, collect results with per-future .await. Use when processing a batch, tree, or crawl where each child is independently durable. SDK in active development; API surface may change between releases.

Aller à l'installation

Informations de source

Dépôt
resonatehq/resonate-skills
Dernière activité de la source
6 août 2026 à 11:59
Langue détectée de SKILL.md
anglais
Étoiles
6
Forks
0

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
resonate-recursive-fan-out-pattern-rust
description
Implement recursive fan-out in Rust with Resonate — spawn N sub-workflows via .spawn(), optionally recurse deeper, collect results with per-future .await. Use when processing a batch, tree, or crawl where each child is independently durable. SDK in active development; API surface may change between releases.
license
Apache-2.0
# Resonate Recursive Fan-Out Pattern — Rust > **SDK note.** The Rust SDK is in active development (v0.6.0, on crates.io). This pattern uses `ctx.run().spawn()` and `ctx.rpc().spawn()` — both synchronous as of 0.5.0+. Verify against current SDK source before shipping. ## Overview Recursive fan-out spawns multiple child invocations in parallel, awaits them individually, and (optionally) recurses deeper. The Rust expression uses `.spawn()` on a Context execution builder to get a `DurableFuture`, collects those futures into a `Vec`, then awaits each in turn. For the language-agnostic mental model, see `resonate-recursive-fan-out-pattern-typescript`. ## When to use - Batch processing where items are independent - Web crawling / tree traversal with dynamic depth - Map-reduce shaped workflows - Any fan-out where each leaf is a discrete, retryable unit ## Parallel fan-out in the same process ```rust use resonate::prelude::*; #[resonate::function] async fn enrich_batch(ctx: &Context, order_ids: Vec<String>) -> Result<Vec<String>> { let mut futures = Vec::with_capacity(order_ids.len()); // fire off N children in parallel for id in order_ids { let fut = ctx.run(enrich_one, id).spawn()?; futures.push(fut); } // await all; order preserved let mut results = Vec::with_capacity(futures.len()); for f in futures { results.push(f.await?); } Ok(results) } #[resonate::function] async fn enrich_one(order_id: String) -> Result<String> { Ok(format!("enriched-{}", order_id)) } ``` Each `enrich_one` is a durable promise. If the parent crashes mid-loop, the children are stored server-side; on parent replay, `f.await?` lookups hit the stored results. ## Parallel fan-out across workers ```rust #[resonate::function] async fn parallel_enrich(ctx: &Context, ids: Vec<String>) -> Result<Vec<String>> { let mut futures = Vec::with_capacity(ids.len()); for id in ids { let fut = ctx .rpc::<String>("enrich_one", id) .target("poll://any@enrichment-workers") .spawn()?; futures.push(fut); } let mut results = Vec::with_capacity(futures.len()); for f in futures { results.push(f.await?); } Ok(results) } ``` The Resonate server handles fair dispatch across any process polling `enrichment-workers`. ## Recursive fan-out A durable function calling itself for tree traversal: ```rust #[derive(serde::Serialize, serde::Deserialize)] struct CrawlInput { url: String, depth: u32, } #[derive(serde::Serialize, serde::Deserialize)] struct CrawlOutput { url: String, depth: u32, links: Vec<String>, children: Vec<CrawlOutput>, } #[resonate::function] async fn crawl(ctx: &Context, input: CrawlInput) -> Result<CrawlOutput> { let links = ctx.run(fetch_links, input.url.clone()).await?; if input.depth == 0 { return Ok(CrawlOutput { url: input.url, depth: 0, links, children: vec![], }); } // fan out to each link, one depth deeper let mut futures = Vec::with_capacity(links.len()); for link in &links { let sub = CrawlInput { url: link.clone(), depth: input.depth - 1 }; let fut = ctx.rpc::<CrawlOutput>("crawl", sub).spawn()?; futures.push(fut); } let mut children = Vec::with_capacity(futures.len()); for f in futures { children.push(f.await?); } Ok(CrawlOutput { url: input.url, depth: input.depth, links, children }) } #[resonate::function] async fn fetch_links(url: String) -> Result<Vec<String>> { // ... Ok(vec![]) } ``` ## Bounded parallelism Process in chunks of K to cap in-flight invocations: ```rust #[resonate::function] async fn process_bounded( ctx: &Context, (items, concurrency): (Vec<String>, usize), ) -> Result<Vec<String>> { let mut results = Vec::with_capacity(items.len()); for chunk in items.chunks(concurrency) { let mut futures = Vec::with_capacity(chunk.len()); for item in chunk { let fut = ctx.run(process_one, item.clone()).spawn()?; futures.push(fut); } for f in futures { results.push(f.await?); } } Ok(results) } #[resonate::function] async fn process_one(item: String) -> Result<String> { Ok(format!("processed-{}", item)) } ``` Each chunk of up to `concurrency` runs in parallel; the next chunk only starts after the previous fully settles. ## Partial-failure tolerance By default, a `?` on `.await?` bubbles the first child error up. To tolerate individual failures: ```rust #[resonate::function] async fn enrich_tolerant(ctx: &Context, ids: Vec<String>) -> Result<Vec<std::result::Result<String, String>>> { let mut futures = Vec::with_capacity(ids.len()); for id in ids { let fut = ctx.run(enrich_one, id).spawn()?; futures.push(fut); } let mut out = Vec::with_capacity(futures.len()); for f in futures { match f.await { Ok(v) => out.push(Ok(v)), Err(e) => out.push(Err(e.to_string())), } } Ok(out) } ``` (Note the return type uses `std::result::Result` to disambiguate from Resonate's aliased `Result`.) ## Distinct Rust idioms - **`.spawn()?`** — synchronous: returns `Result<DurableFuture<T>>` immediately (no await needed); unwrap with `?`, then await the `DurableFuture` later to get the actual `T` - **`Vec::with_capacity(n)`** — idiomatic capacity pre-allocation for known-size collections - **`.chunks(n)` slice method** — bounded parallelism cleaner than iterator fiddling - **Turbofish `ctx.rpc::<T>(...)`** — needed when the return type isn't inferable from context - **`.map_err(|e| e.to_string())`** for error-type narrowing in tolerant-fan-out cases - **`Clone`** on input types — borrowed slices (`&[String]`) can't cross `ctx.run` boundaries; own the data ## Avoid - Unbounded recursion without a depth counter — the promise tree grows; Rust's default stack is fine but the server-side promise graph isn't free - Holding borrowed references across `.await?` — `.spawn()` and `.await?` mean owned data only ## Related skills - `resonate-basic-durable-world-usage-rust` — `ctx.run`, `ctx.rpc`, `.spawn()`, builder options - `resonate-saga-pattern-rust` — fan-out inside or alongside sagas - `durable-execution` — foundational replay semantics - `resonate-recursive-fan-out-pattern-typescript` / `-python` — sibling SDKs
Voir sur GitHub