Skip to main content

durable-execution

Conceptual introduction to durable execution — what it is, why you'd want it, and the tradeoffs between rolling your own (just your DB) versus using a framework like Resonate. Read this BEFORE picking an implementation. For the Resonate SDK's concrete Context API (ctx.run, ctx.sleep, ctx.promise, structured concurrency), use the per-SDK skill for your language — resonate-basic-durable-world-usage-{typescript,python,rust,go}.

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

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

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

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

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

مستكشف الملفات
8 ملفات

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
durable-execution
description
Conceptual introduction to durable execution — what it is, why you'd want it, and the tradeoffs between rolling your own (just your DB) versus using a framework like Resonate. Read this BEFORE picking an implementation. For the Resonate SDK's concrete Context API (ctx.run, ctx.sleep, ctx.promise, structured concurrency), use the per-SDK skill for your language — resonate-basic-durable-world-usage-{typescript,python,rust,go}.
license
Apache-2.0
# Durable Execution Your code will crash. Durable execution means: when it restarts, it picks up where it left off — not from the beginning. ``` Traditional: start → step1 → step2 → crash → lost Queue-based: start → step1 → step2 → crash → retry from top → duplicates Durable: start → step1 → step2 → crash → replay to step2 → continue from step3 ``` Everything else — frameworks, protocols, infrastructure — is implementation detail. --- ## Do You Need This? Score 1–5 on each dimension: | Dimension | 1 (low) | 5 (high) | |-----------|---------|----------| | **Failure cost** | Retry is inexpensive, no side effects | Retry causes duplicates, data loss, or revenue loss | | **Duration** | Milliseconds, single request | Hours/days/weeks, spans process lifetimes | | **Coordination** | Single service, single step | Multiple services, human gates, external callbacks | | **State complexity** | Stateless or simple key-value | Branching workflows, conditional logic, fan-out/fan-in | | Total | Recommendation | |-------|---------------| | 4–8 | Use a task queue with idempotent handlers | | 9–12 | Durabilize the critical path only | | 13–20 | Durable execution is the right primitive | **Quick check — you need this if any are true:** - A crash mid-workflow means a customer gets charged but never receives the product - Your workflow spans multiple HTTP requests or process lifetimes - You need human approval gates that pause for hours or days - Duplicate execution of a step causes real-world harm (double charges, duplicate emails) - You have multi-step processes across multiple services that must all succeed or all roll back --- ## Choose Your Path ``` Does your workflow need... ├─ Just crash recovery + idempotency? │ └─ BAKED IN: add checkpointing to your existing DB. Zero infra cost. │ See: references/BAKED-IN.md │ ├─ Fan-out parallelism, durable sleep, human-in-the-loop, │ or cross-service coordination? │ └─ RESONATE: single-binary server + tiny SDK. ~$5/mo on a VPS. │ See: references/RESONATE-QUICKSTART.md │ └─ Enterprise-scale with managed infrastructure? └─ Temporal Cloud ($520/mo+ at scale) or AWS Step Functions. But consider: do you actually need that complexity? ``` | | Baked In | Resonate | Temporal | |---|---|---|---| | **Infra cost** | $0 (your existing DB) | ~$5/mo (VPS) to ~$170/mo (1M tasks/day) | ~$520/mo (1M tasks/day) | | **Dependencies** | None | Single binary + SQLite | Cluster + multiple services | | **Serverless** | Yes (any runtime) | Yes (Lambda, Edge Functions) | No (requires always-on cluster) | | **Setup time** | Minutes | 5 minutes | Hours to days | | **Patterns** | Checkpoint, idempotency, outbox | All 5 patterns + distributed coordination | All patterns + enterprise features | | **Learning curve** | Low (just SQL + your code) | Low (sequential code — generators in TS/Py, or async/await in TS/Rust/Go) | High (proprietary DSL + concepts) | | **When it fits** | Single-service, sequential workflows | Multi-service, any complexity | Large teams with dedicated infra staff | --- ## Baked In — Framework-Free Durability Three building blocks. No framework. Just your database. ### 1. Idempotency Keys Every operation gets a deterministic ID. Before executing, check if it already ran. ```typescript async function runOnce<T>(db: Database, key: string, fn: () => Promise<T>): Promise<T> { const existing = db.query("SELECT result FROM completed_steps WHERE key = ?").get(key); if (existing) return JSON.parse(existing.result); const result = await fn(); db.run("INSERT INTO completed_steps (key, result) VALUES (?, ?)", [key, JSON.stringify(result)]); return result; } ``` ### 2. Step-Level Checkpointing Wrap each step. On crash and restart, completed steps return cached results. Execution resumes from the first incomplete step. ```typescript async function durableCheckout(db: Database, orderId: string) { const inventory = await runOnce(db, `${orderId}:reserve`, () => reserveInventory(orderId)); const payment = await runOnce(db, `${orderId}:charge`, () => chargeCard(orderId, inventory)); const shipment = await runOnce(db, `${orderId}:ship`, () => createShipment(orderId, payment)); const email = await runOnce(db, `${orderId}:notify`, () => sendConfirmation(orderId, shipment)); return { inventory, payment, shipment, email }; } ``` ### 3. Outbox Pattern Side effects (emails, webhooks, API calls) go to a table first. A separate process delivers them exactly once. ```typescript // Inside your workflow — write to outbox, don't send directly db.run("INSERT OR IGNORE INTO outbox (id, payload) VALUES (?, ?)", [`${orderId}:confirmation-email`, JSON.stringify({ to: email, subject: "Order confirmed" })]); // Separate delivery loop — idempotent, retryable const pending = db.query("SELECT * FROM outbox WHERE delivered_at IS NULL").all(); for (const msg of pending) { await deliver(msg); // your send logic db.run("UPDATE outbox SET delivered_at = ? WHERE id = ?", [Date.now(), msg.id]); } ``` **When baked-in hits its limits:** - You need a workflow to sleep for days without a process staying alive → need durable timers - You need to suspend and wait for a human to approve something → need external promise resolution - You need to fan out work across multiple services in parallel → need distributed coordination - You want the framework to handle retries, timeouts, and replay for you → use Resonate **Full implementation with templates:** See `references/BAKED-IN.md` and `assets/baked-in-checkpoint.ts`. --- ## With Resonate — Durable Execution Platform Resonate's open-source server is a single binary (Rust + SQLite, zero external deps) paired with a tiny SDK. Runs anywhere — VPS, serverless, edge functions. Costs ~$5/mo on a small VPS. Your code is an ordinary function with durable steps — a generator in TypeScript/Python, an `async fn`/func in Rust/Go. TypeScript also ships a second engine that writes the same thing as `async function`/`await` instead of `function*`/`yield*` (see below). Each durable step is a checkpoint. If the process crashes, the server re-dispatches the work to any available worker, which replays from the last checkpoint. > **Language note.** The examples below (and in this skill's references) are shown in **TypeScript**. The concepts are identical across all four Resonate SDKs; only the syntax differs. For concrete, idiomatic syntax in your language, see the per-SDK skills — `resonate-basic-durable-world-usage-{typescript,python,rust,go}` for the Context API, `resonate-async-await-engine-typescript` for TypeScript's `async`/`await` engine, and the matching `resonate-saga-pattern-*` / `resonate-recursive-fan-out-pattern-*` / `resonate-human-in-the-loop-pattern-*` skills (and `resonate-durable-sleep-scheduled-work-{typescript,rust,go}`) for the patterns shown here. ```typescript import { Resonate, type Context } from "@resonatehq/sdk"; const resonate = new Resonate({ url: "http://localhost:8001" }); resonate.register("processOrder", function* (ctx: Context, orderId: string) { const order = yield* ctx.run(fetchOrder, orderId); const payment = yield* ctx.run(chargeCard, order); const shipment = yield* ctx.run(createShipment, order); yield* ctx.run(sendConfirmation, order.email); return { payment, shipment }; }); await resonate.run("order-42", "processOrder", "order-42"); ``` Prefer `async`/`await` over generators? TypeScript's other engine (`@resonatehq/sdk/async`, v0.11+) writes the same workflow without `function*`: ```typescript import { Resonate } from "@resonatehq/sdk/async"; const resonate = new Resonate({ url: "http://localhost:8001" }); resonate.register("processOrder", async (ctx, orderId: string) => { const order = await ctx.run(fetchOrder, orderId); const payment = await ctx.run(chargeCard, order); const shipment = await ctx.run(createShipment, order); await ctx.run(sendConfirmation, order.email); return { payment, shipment }; }); const handle = await resonate.run("order-42", "processOrder", "order-42"); await handle.result(); ``` Both engines talk to the same server and give the same guarantees — pick whichever reads better in your codebase. Each step is checkpointed; crash recovery, retries, and replay are automatic. **Get running in 5 minutes:** See `references/RESONATE-QUICKSTART.md`. **All patterns with full code:** See `references/RESONATE-PATTERNS.md`. **SDK API reference (TypeScript):** See `references/RESONATE-SDK.md` (for Python/Rust/Go, see `resonate-basic-durable-world-usage-{python,rust,go}`). **Starter template (TypeScript):** Copy `assets/resonate-worker.ts` (the `assets/` templates are TypeScript; for other languages start from the per-SDK skill). --- ## The 5 Patterns Shown here in the generator engine; every pattern translates directly to the async/await engine — swap `function*`/`yield*` for `async function`/`await` (see `resonate-async-await-engine-typescript`). ### 1. Saga — Multi-step with compensating rollbacks Each step is checkpointed. On failure, compensate in reverse order. ```typescript function* orderSaga(ctx: Context, orderId: string) { const completed: string[] = []; try { yield* ctx.run(reserveInventory, orderId); completed.push("inventory"); yield* ctx.run(chargePayment, orderId); completed.push("payment"); yield* ctx.run(createShipment, orderId); completed.push("shipment"); return { status: "success" }; } catch (error) { for (const step of completed.reverse()) { yield* ctx.run(compensate, step, orderId); } return { status: "rolled-back", compensated: completed }; } } ``` **Use when:** Multiple services must all succeed or all roll back (payments, bookings, provisioning). ### 2. Fan-Out / Fan-In — Parallel work with aggregation Dispatch work to parallel workers via RPC. Each branch is independently durable. ```typescript function* batchProcess(ctx: Context, items: string[]) { const results: string[] = []; for (const item of items) { const result = yield* ctx.rpc( "processItem", item, ctx.options({ target: "poll://any@item-workers" }) ); results.push(result); } return results; } ``` **Use when:** Batch processing, parallel API calls, map-reduce workloads. ### 3. Human-in-the-Loop — Suspend for external signal Workflow suspends without holding resources. Resumes when a human (or webhook) resolves the promise. ```typescript function* approvalFlow(ctx: Context, orderId: string) { // No id argument — ctx.promise() derives a deterministic id from the call // tree. Read it back off the returned future to hand to the approver. const approval = yield* ctx.promise<string>({ timeout: 48 * 60 * 60 * 1000 // 48 hours }); yield* ctx.run(sendApprovalEmail, orderId, approval.id); const decision = yield* approval; if (decision === "approved") yield* ctx.run(processOrder, orderId); return decision; } ``` **Use when:** Approval workflows, manual review gates, external callbacks, payment confirmation. ### 4. Scheduled / Cron — Durable timers Sleep is durable. Process can die and restart — the timer still fires. ```typescript function* onboarding(ctx: Context, userId: string) { yield* ctx.run(sendEmail, userId, "Welcome!"); yield* ctx.sleep(24 * 60 * 60 * 1000); // 1 day — survives crashes yield* ctx.run(sendEmail, userId, "Getting started tips"); yield* ctx.sleep(6 * 24 * 60 * 60 * 1000); // 6 days yield* ctx.run(sendEmail, userId, "How are we doing?"); } ``` **Use when:** Drip campaigns, SLA reminders, retry delays, polling loops, recurring jobs. ### 5. Entity — Long-lived mutable state Each method call is a durable step on a persistent entity. ```typescript function* orderLifecycle(ctx: Context, orderId: string) { const order = yield* ctx.run(createOrder, orderId); yield* ctx.run(validateOrder, order); const payment = yield* ctx.run(processPayment, order); yield* ctx.run(fulfillOrder, order, payment); yield* ctx.run(notifyCustomer, order); return { orderId, status: "fulfilled" }; } ``` **Use when:** Lifecycle management, state machines, long-lived domain objects. --- ## The Hard Problems ### Versioning You deploy new code. Old executions are mid-flight. The replay now hits different code paths than what was recorded. **Solutions:** Version-tag your workflows. Drain in-flight executions before deploying breaking changes. Or design steps to be additive (new steps at the end, never remove or reorder existing ones). ### Side Effects at the Boundary You sent an email in step 3. Step 4 crashes. On replay, step 3 returns the stored result — but the email was already sent. **Solutions:** - **Outbox pattern** — write to a durable outbox; a separate process delivers exactly once - **Idempotency keys** — pass a deterministic key to external APIs so duplicates are no-ops - **Accept-and-compensate** — accept that duplicates happen; send a correction if needed ### Testing Durability How do you test that replay actually works? That your workflow survives a crash at every possible step? **Approaches:** - **Kill-and-resume test** — start workflow, kill the process mid-step, restart, verify it completes correctly - **Replay unit test** — capture an execution log, replay against new code, assert same result - **Transition tests** — enumerate all valid state transitions, verify each one produces correct output - **Deterministic simulation testing (DST)** — inject controlled randomness across thousands of runs, verify invariants hold **Full testing guide:** See `references/TESTING.md`. ### Observability You cannot `console.log` your way through replay. You need to see: what step am I on, what's pending, what failed, what's the state of each promise. **What to monitor:** Execution list (ID, status, duration), step timeline/waterfall, promise state graph, worker health, queue depth, error rates, retry storms. --- ## Efficiency — Why This Matters Durable execution has a reputation for being expensive and complex. It doesn't have to be.
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub