resonate-recursive-fan-out-pattern-go — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited resonate-recursive-fan-out-pattern-go (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Version note. The Go SDK's first tagged release is0.1.0. The tag is0.1.0, notv0.1.0, sogo get …@latestdoes not resolve to it — pin the tag explicitly:go get github.com/resonatehq/[email protected]. APIs may still change before a1.0. Every code block here is verified against the0.1.0tag source and theexample-fan-out-fan-in-go/example-recursive-factorial-gorepos.
Recursive fan-out dispatches multiple child invocations in parallel, awaits them individually, and optionally recurses deeper. The Go expression is a two-loop pattern: a dispatch loop builds a []*resonate.Future slice, then a separate await loop reads each result. Mixing dispatch and await serializes the children — that is the single most common mistake.
For the language-agnostic mental model (promise deduplication, load-balancing, detached vs. result-gathering), see resonate-recursive-fan-out-pattern-typescript.
The canonical fan-out from example-fan-out-fan-in-go — dispatch all children first, await all second:
type FanoutArgs struct {
Channels []string `json:"channels"`
Message string `json:"message"`
}
type FanoutResult struct {
Delivered []Delivery `json:"delivered"`
}
type SendArgs struct {
Channel string `json:"channel"`
Message string `json:"message"`
}
type Delivery struct {
Channel string `json:"channel"`
OK bool `json:"ok"`
Reason string `json:"reason,omitempty"`
}
func fanout(ctx *resonate.Context, args FanoutArgs) (FanoutResult, error) {
// Dispatch loop — create all durable promises before awaiting any.
futures := make([]*resonate.Future, 0, len(args.Channels))
for _, ch := range args.Channels {
f, err := ctx.RPC("send", SendArgs{Channel: ch, Message: args.Message})
if err != nil {
return FanoutResult{}, err
}
futures = append(futures, f)
}
// Await loop — read results in order; record per-child failures.
out := FanoutResult{Delivered: make([]Delivery, 0, len(futures))}
for i, f := range futures {
var d Delivery
if err := f.Await(&d); err != nil {
out.Delivered = append(out.Delivered, Delivery{
Channel: args.Channels[i],
OK: false,
Reason: err.Error(),
})
continue
}
out.Delivered = append(out.Delivered, d)
}
return out, nil
}The dispatch loop runs serially in Go code, but each ctx.RPC creates an independent durable promise on the server — all children execute concurrently. The await loop only reads results; it does not gate execution of siblings.
A workflow can call ctx.RPC(SameName, smallerArgs) to recurse. Each recursive call is its own durable promise; with multiple workers running the same registered name, the promise tree fans out across them automatically. From example-recursive-factorial-go:
// factorial/factorial.go
const Name = "Factorial"
const WorkerGroup = "factorial-workers"
type Args struct {
N int `json:"n"`
}
func Workflow(ctx *resonate.Context, args Args) (int, error) {
// Base case — must always be present to terminate the recursion.
if args.N <= 1 {
return 1, nil
}
// Recurse via ctx.RPC so the sub-call is dispatched as a separate
// durable promise; with multiple workers it may run on a different process.
f, err := ctx.RPC(Name, Args{N: args.N - 1})
if err != nil {
return 0, err
}
var sub int
if err := f.Await(&sub); err != nil {
return 0, err
}
return args.N * sub, nil
}Worker binary — joins the named group so only registered workers receive dispatches:
// cmd/worker/main.go
import "github.com/resonatehq/resonate-sdk-go/httpnet"
r, err := resonate.New(resonate.Config{
Network: httpnet.NewHTTP(url, httpnet.HTTPOptions{
PID: pid,
Group: factorial.WorkerGroup, // "factorial-workers"
}),
})
if _, err := resonate.Register(r, factorial.Name, factorial.Workflow); err != nil {
log.Fatalf("Register: %v", err)
}
// block on SIGINT / SIGTERM; do NOT call r.Stop() on long-running workersClient binary — uses a distinct group so it cannot be mistakenly assigned worker tasks:
// cmd/client/main.go
r, err := resonate.New(resonate.Config{
Network: httpnet.NewHTTP(url, httpnet.HTTPOptions{
PID: pid,
Group: "factorial-client", // distinct — not in WorkerGroup
}),
})
target := fmt.Sprintf("poll://any@%s", factorial.WorkerGroup)
h, err := r.RPC(ctx, "factorial-10", factorial.Name, factorial.Args{N: 10},
resonate.RPCOptions{Target: target})
var result int
if err := h.Result(ctx, &result); err != nil { ... }The example-fan-out-fan-in-go pattern records per-child failures rather than aborting the parent at the first error. Use this when you want a summary of all deliveries rather than a short-circuit:
for i, f := range futures {
var d Delivery
if err := f.Await(&d); err != nil {
// Record the failure and continue to the next child.
out.Delivered = append(out.Delivered, Delivery{
Channel: channels[i],
OK: false,
Reason: err.Error(),
})
continue
}
out.Delivered = append(out.Delivered, d)
}To fail-fast on the first child error, replace the continue block with return ..., err.
ctx.Run vs ctx.RPC for fan-outctx.Run | ctx.RPC | |
|---|---|---|
| Execution target | Same process | Remote process (by registered name) |
| Fan-out across workers | No | Yes — server distributes to the named group |
| Recursion across workers | No | Yes — each ctx.RPC(SameName, ...) is re-dispatched |
| Best for | In-process leaf tasks | Distributed branches, cross-worker recursion |
Both methods return *resonate.Future immediately. Both support the two-loop fan-out pattern. ctx.Run functions must return promptly — long-running or blocking work belongs in ctx.RPC.
slice with make(..., 0, len(items))`** — pre-allocate to the known capacity; the two-loop pattern iterates this slice twice (dispatch, then await).json tags on every type that crosses ctx.RPC or ctx.Run.factorial.WorkerGroup). This prevents string drift between the httpnet.HTTPOptions{Group: ...} in the worker and the poll://any@... target in the client.ctx.RPC / ctx.Run calls, and already-settled children short-circuit. No replay guard code is needed.ctx.RPC followed immediately by f.Await in the same loop body runs children sequentially. Collect all futures first, then await.ctx.RPC(SameName, ...) with a termination condition (if args.N <= 1, if depth == 0, etc.).Config.URL shorthand builds a transport in the "default" group. If both the worker and the client use Config.URL, the client joins the worker pool and steals dispatches it cannot fulfill.resonate-basic-durable-world-usage-go — ctx.Run, ctx.RPC, Future.Await, options structsresonate-saga-pattern-go — fan-out alongside compensating transactionsdurable-execution — foundational replay semanticsresonate-recursive-fan-out-pattern-typescript — canonical mental model, deduplication strategyresonate-recursive-fan-out-pattern-rust — .spawn() / Vec<DurableFuture> sibling~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.