Skip to main content

resonate-basic-durable-world-usage-python

Core patterns for writing Resonate durable functions in Python — async/await syntax, Context API reference (run, rpc, detached, promise, sleep, dependency injection), and the determinism rules that make recovery work. Use when writing any function body registered with r.register().

跳到安装

来源信息

仓库
resonatehq/resonate-skills
最近来源活动
2026年6月24日 15:56
检测到的 SKILL.md 语言
英语
星标
6
分支
0

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
resonate-basic-durable-world-usage-python
description
Core patterns for writing Resonate durable functions in Python — async/await syntax, Context API reference (run, rpc, detached, promise, sleep, dependency injection), and the determinism rules that make recovery work. Use when writing any function body registered with r.register().
license
Apache-2.0
# Resonate Basic Durable World Usage — Python ## Overview Durable functions are the payload of Resonate. They are `async def` coroutines that the SDK treats as a sequence of durable checkpoints. Every `await ctx.run(...)` or `await ctx.rpc(...)` is a save point: if the process crashes, the function resumes from the last successful checkpoint on another process or a restart. This skill covers the Context API (`ctx.*`) that you use inside those functions. The ephemeral-world counterpart (`Resonate` class, registration, top-level invocation) lives in `resonate-basic-ephemeral-world-usage-python`. ## The contract Every durable function: 1. Is registered with `r.register(fn)` in the ephemeral world. 2. Takes `ctx` (the Context) as its first parameter; any other parameters after that are the invocation args. 3. Uses `await` to interact with `ctx`. Each awaited `ctx.*` call is a durable checkpoint. 4. Is deterministic when replayed: no direct `time.time()`, no `random.random()`, no raw network I/O outside `ctx.run`. Minimal shape: ```python from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from resonate.context import Context async def process_order(ctx: Context, order_id: str) -> dict: # step 1 — durable: its result is persisted order = await ctx.run(load_order, order_id) # step 2 — durable: runs locally in the same worker charge = await ctx.run(charge_card, order) # step 3 — ordinary Python control flow; free to branch, loop, raise if not charge["ok"]: raise RuntimeError(f"payment failed: {charge['reason']}") return {"order_id": order_id, "status": "paid"} ``` If this function crashes after step 1 but before step 2, it resumes at step 2 on restart — `load_order` is NOT re-run. Its result is read from the promise store. ## Same-process invocation: `ctx.run` `ctx.run` invokes another function in the same process durably. The durable function awaits it and the result is checkpointed: ```python async def foo(ctx: Context, arg: str) -> str: result = await ctx.run(bar, arg) return result async def bar(ctx: Context, arg: str) -> str: # bar can be any async def; it runs as a durable leaf return arg.upper() ``` To fire off multiple children and await them in parallel, launch them before awaiting: ```python async def enrich_batch(ctx: Context, order_ids: list[str]) -> list[dict]: # Launch all children (returns futures) futures = [ctx.run(enrich_one, oid) for oid in order_ids] # Await them all results = [await f for f in futures] return results ``` **Do NOT use `asyncio.gather` here.** `ctx.run(...)` returns a Resonate future, not an asyncio future; `asyncio.gather(ctx.run(...), ctx.run(...))` will not work correctly. The pattern above — collect futures in a list, then `await` each — is the correct idiom. Note: unawaited `ctx.run(...)` futures are still tracked by the runtime via structured concurrency — the parent cannot settle until all spawned children complete. But explicitly awaiting them gives you the results. ## Remote invocation: `ctx.rpc` and `ctx.detached` `ctx.rpc` dispatches a registered function to a remote worker process by name: ```python async def orchestrator(ctx: Context, batch_id: str) -> dict: # "worker_fn" must be registered in a reachable worker group result = await ctx.rpc("worker_fn", batch_id) return result ``` `ctx.detached` is fire-and-forget — the parent does NOT wait for the result: ```python async def place_order( ctx: Context, customer: str, sku: str, qty: int, amount: int ) -> tuple[str, str, str]: stock_ref = await ctx.run(reserve_stock, sku, qty) charge_ref = await ctx.run(charge_card, customer, amount) # Dispatch audit as a detached workflow; get back the promise id only audit_future = ctx.detached( "write_audit_log", customer=customer, sku=sku, amount=amount, stock_ref=stock_ref, charge_ref=charge_ref, ) audit_id = await audit_future.id() # await the id; do NOT await the result return stock_ref, charge_ref, audit_id ``` Use `detached` when the side effect should survive independently of the parent's success — post-commit hooks, notifications, audit logs. ## Options Options configure an individual call. For Context invocations, options come first via `ctx.options(...)`: ```python from resonate.retry import Exponential, Never async def foo(ctx: Context, arg: str) -> str: result = await ctx.options( retry_policy=Exponential(), version=2, ).run(bar, arg) return result ``` To target a specific worker group for `rpc`: ```python result = await ctx.options(target="db-workers").rpc("query_orders", user_id) ``` ## External promises: `ctx.promise` `ctx.promise` creates a durable promise the function suspends on until externally resolved. This is the primitive for human-in-the-loop workflows. ```python async def fulfill_order(ctx: Context, order_id: str, amount: int) -> str: # Create the promise; its id is deterministic (derived from the workflow id) approval = ctx.promise() approval_id = await approval.id() # Notify whoever needs to resolve (via a leaf — side effects in leaves only) await ctx.run(notify_reviewer, order_id, amount, approval_id) # Suspend here until an external party resolves or rejects decision_raw = await approval if decision_raw.get("approve"): return await ctx.run(ship_order, order_id) return await ctx.run(cancel_order, order_id) ``` The external resolver calls `await r.promises.resolve(approval_id, Value(data=decision))` from outside the worker (a webhook handler, a CLI, a separate process). ## `ctx.sleep` `ctx.sleep` suspends for a `timedelta`. The Resonate server holds the continuation; no process needs to stay alive during the sleep: ```python from datetime import timedelta async def daily_digest(ctx: Context, user_id: str) -> None: while True: await ctx.sleep(timedelta(hours=24)) await ctx.rpc("send_digest", user_id) ``` Pass a `timedelta`, not a raw float. For fake async work inside a leaf, use `await asyncio.sleep(...)` directly instead. ## Dependencies `ctx.get_dependency(MyType)` retrieves a dependency registered via `r.with_dependency(obj)`: ```python import psycopg async def write_to_db(ctx: Context, record: dict) -> None: db = ctx.get_dependency(psycopg.Connection) # Wrap the actual I/O in a helper via ctx.run so the side effect is checkpointed await ctx.run(_insert_row, db, record) async def _insert_row(ctx: Context, db: psycopg.Connection, record: dict) -> None: db.execute("INSERT INTO orders VALUES (...)", record) ``` Never instantiate DB clients or HTTP sessions inside a durable function body — they would be recreated on every replay. ## Side effects and the `ctx.run` envelope Any side effect — HTTP call, DB write, file write, print — belongs inside a function that the durable function reaches via `ctx.run`. This ensures the effect is executed exactly once: the orchestrator awaits the checkpoint, the leaf runs, the result is persisted, and on replay the stored result is returned instead of re-executing. Bad (replays re-execute the side effect): ```python async def foo(ctx: Context, arg: str) -> str: requests.post(WEBHOOK_URL, json=arg) # runs on every replay return "done" ``` Good (side effect is checkpointed exactly once): ```python async def send_webhook(ctx: Context, arg: str) -> str: requests.post(WEBHOOK_URL, json=arg) return "sent" async def foo(ctx: Context, arg: str) -> str: await ctx.run(send_webhook, arg) return "done" ``` ## Determinism: time and randomness Never call `time.time()` or `random.random()` directly in a durable function — on replay, the values change and the function branches differently. Wrap them in a leaf via `ctx.run`. **v0.7.0 note:** `ctx.time.time()` and `ctx.random.random()` were removed in v0.7.0. If you are migrating from v0.6.x, replace any `ctx.time.time()` / `ctx.random.random()` calls with a leaf function dispatched via `ctx.run`, as shown below. ```python import time, random async def _get_time(ctx: Context) -> float: return time.time() async def _get_random(ctx: Context) -> float: return random.random() async def foo(ctx: Context) -> None: t = await ctx.run(_get_time) r = await ctx.run(_get_random) ``` The leaf settles once and its value is persisted; replay returns the stored value. ## Common shapes **Sequential pipeline with retries:** ```python from resonate.retry import Exponential async def ingest(ctx: Context, source_url: str) -> dict: data = await ctx.options(retry_policy=Exponential()).run(fetch, source_url) parsed = await ctx.run(parse, data) await ctx.run(persist, parsed) return {"count": len(parsed)} ``` **Parallel fan-out within a single function:** ```python async def enrich_batch(ctx: Context, ids: list[str]) -> list[dict]: futures = [ctx.run(enrich_one, i) for i in ids] return [await f for f in futures] ``` **Recursive workflow (dispatching itself via rpc):** ```python async def crawl(ctx: Context, url: str, depth: int) -> dict: page = await ctx.run(fetch, url) if depth > 0: for link in page["links"]: ctx.detached("crawl", link, depth - 1) return {"url": url, "id": page["id"]} ``` ## Related skills - `resonate-basic-ephemeral-world-usage-python` — the Client API companion - `resonate-basic-debugging-python` — diagnosing stuck workflows, missing registrations, non-determinism regressions - `durable-execution` — the foundational concept - `resonate-philosophy` — "do not build these things" mindset
在 GitHub 查看