From 4aa9fa01288837f001ab1d9abfd93e8dfb978d0d Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Wed, 13 May 2026 13:09:55 -0400 Subject: [PATCH 1/3] ## implementing batch patterns - batch iterator - fanout - mapreduce tree - sliding window --- docs/.vitepress/config.mts | 10 + docs/batch-iterator.md | 233 +++++++++ docs/batch-processing-patterns.md | 143 ++++++ docs/fanout-child-workflows.md | 278 +++++++++++ docs/index.md | 40 ++ docs/mapreduce-tree.md | 412 ++++++++++++++++ docs/sliding-window.md | 462 ++++++++++++++++++ .../patterns/batch-iterator/go/activities.go | 24 + .../patterns/batch-iterator/go/go.mod | 33 ++ .../patterns/batch-iterator/go/go.sum | 179 +++++++ .../patterns/batch-iterator/go/shared.go | 8 + .../patterns/batch-iterator/go/starter.go | 45 ++ .../patterns/batch-iterator/go/warmup.go | 13 + .../patterns/batch-iterator/go/worker.go | 26 + .../patterns/batch-iterator/go/workflows.go | 39 ++ .../batch-iterator/java/_warmup/Warmup.java | 23 + .../patterns/batch-iterator/java/pom.xml | 40 ++ .../java/src/main/java/Activities.java | 31 ++ .../src/main/java/BatchIteratorWorkflow.java | 44 ++ .../java/src/main/java/Shared.java | 8 + .../java/src/main/java/Starter.java | 26 + .../java/src/main/java/Worker.java | 24 + .../patterns/batch-iterator/pattern.json | 42 ++ .../batch-iterator/python/activities.py | 17 + .../batch-iterator/python/pyproject.toml | 8 + .../patterns/batch-iterator/python/shared.py | 4 + .../patterns/batch-iterator/python/starter.py | 30 ++ .../patterns/batch-iterator/python/worker.py | 24 + .../batch-iterator/python/workflows.py | 40 ++ .../batch-iterator/typescript/activities.ts | 15 + .../batch-iterator/typescript/package.json | 14 + .../batch-iterator/typescript/shared.ts | 4 + .../batch-iterator/typescript/starter.ts | 32 ++ .../batch-iterator/typescript/worker.ts | 19 + .../batch-iterator/typescript/workflows.ts | 34 ++ .../fanout-child-workflows/go/activities.go | 12 + .../patterns/fanout-child-workflows/go/go.mod | 33 ++ .../patterns/fanout-child-workflows/go/go.sum | 179 +++++++ .../fanout-child-workflows/go/shared.go | 8 + .../fanout-child-workflows/go/starter.go | 45 ++ .../fanout-child-workflows/go/warmup.go | 13 + .../fanout-child-workflows/go/worker.go | 26 + .../fanout-child-workflows/go/workflows.go | 64 +++ .../java/_warmup/Warmup.java | 26 + .../fanout-child-workflows/java/pom.xml | 40 ++ .../java/src/main/java/Activities.java | 17 + .../java/src/main/java/FanOutWorkflow.java | 72 +++ .../java/src/main/java/Shared.java | 8 + .../java/src/main/java/Starter.java | 26 + .../java/src/main/java/Worker.java | 26 + .../fanout-child-workflows/pattern.json | 42 ++ .../python/activities.py | 9 + .../python/pyproject.toml | 8 + .../fanout-child-workflows/python/shared.py | 4 + .../fanout-child-workflows/python/starter.py | 30 ++ .../fanout-child-workflows/python/worker.py | 24 + .../python/workflows.py | 56 +++ .../typescript/activities.ts | 6 + .../typescript/package.json | 14 + .../typescript/shared.ts | 4 + .../typescript/starter.ts | 32 ++ .../typescript/worker.ts | 19 + .../typescript/workflows.ts | 57 +++ .../patterns/mapreduce-tree/go/activities.go | 13 + .../patterns/mapreduce-tree/go/go.mod | 33 ++ .../patterns/mapreduce-tree/go/go.sum | 179 +++++++ .../patterns/mapreduce-tree/go/shared.go | 26 + .../patterns/mapreduce-tree/go/starter.go | 48 ++ .../patterns/mapreduce-tree/go/warmup.go | 13 + .../patterns/mapreduce-tree/go/worker.go | 26 + .../patterns/mapreduce-tree/go/workflows.go | 84 ++++ .../mapreduce-tree/java/_warmup/Warmup.java | 23 + .../patterns/mapreduce-tree/java/pom.xml | 40 ++ .../java/src/main/java/Activities.java | 18 + .../java/src/main/java/NodeWorkflow.java | 108 ++++ .../java/src/main/java/Shared.java | 21 + .../java/src/main/java/Starter.java | 29 ++ .../java/src/main/java/Worker.java | 26 + .../patterns/mapreduce-tree/pattern.json | 42 ++ .../mapreduce-tree/python/activities.py | 10 + .../mapreduce-tree/python/pyproject.toml | 8 + .../patterns/mapreduce-tree/python/shared.py | 6 + .../patterns/mapreduce-tree/python/starter.py | 31 ++ .../patterns/mapreduce-tree/python/worker.py | 24 + .../mapreduce-tree/python/workflows.py | 96 ++++ .../mapreduce-tree/typescript/activities.ts | 5 + .../mapreduce-tree/typescript/package.json | 14 + .../mapreduce-tree/typescript/shared.ts | 10 + .../mapreduce-tree/typescript/starter.ts | 33 ++ .../mapreduce-tree/typescript/worker.ts | 19 + .../mapreduce-tree/typescript/workflows.ts | 98 ++++ .../patterns/sliding-window/go/activities.go | 12 + .../patterns/sliding-window/go/go.mod | 33 ++ .../patterns/sliding-window/go/go.sum | 179 +++++++ .../patterns/sliding-window/go/shared.go | 29 ++ .../patterns/sliding-window/go/starter.go | 47 ++ .../patterns/sliding-window/go/warmup.go | 13 + .../patterns/sliding-window/go/worker.go | 26 + .../patterns/sliding-window/go/workflows.go | 121 +++++ .../sliding-window/java/_warmup/Warmup.java | 23 + .../patterns/sliding-window/java/pom.xml | 40 ++ .../java/src/main/java/Activities.java | 18 + .../java/src/main/java/Shared.java | 44 ++ .../src/main/java/SlidingWindowWorkflow.java | 137 ++++++ .../java/src/main/java/Starter.java | 30 ++ .../java/src/main/java/Worker.java | 26 + .../patterns/sliding-window/pattern.json | 42 ++ .../sliding-window/python/activities.py | 9 + .../sliding-window/python/pyproject.toml | 8 + .../patterns/sliding-window/python/shared.py | 18 + .../patterns/sliding-window/python/starter.py | 30 ++ .../patterns/sliding-window/python/worker.py | 24 + .../sliding-window/python/workflows.py | 123 +++++ .../sliding-window/typescript/activities.ts | 4 + .../sliding-window/typescript/package.json | 14 + .../sliding-window/typescript/shared.ts | 16 + .../sliding-window/typescript/starter.ts | 32 ++ .../sliding-window/typescript/worker.ts | 19 + .../sliding-window/typescript/workflows.ts | 125 +++++ 119 files changed, 5649 insertions(+) create mode 100644 docs/batch-iterator.md create mode 100644 docs/batch-processing-patterns.md create mode 100644 docs/fanout-child-workflows.md create mode 100644 docs/mapreduce-tree.md create mode 100644 docs/sliding-window.md create mode 100644 sandbox-runner/patterns/batch-iterator/go/activities.go create mode 100644 sandbox-runner/patterns/batch-iterator/go/go.mod create mode 100644 sandbox-runner/patterns/batch-iterator/go/go.sum create mode 100644 sandbox-runner/patterns/batch-iterator/go/shared.go create mode 100644 sandbox-runner/patterns/batch-iterator/go/starter.go create mode 100644 sandbox-runner/patterns/batch-iterator/go/warmup.go create mode 100644 sandbox-runner/patterns/batch-iterator/go/worker.go create mode 100644 sandbox-runner/patterns/batch-iterator/go/workflows.go create mode 100644 sandbox-runner/patterns/batch-iterator/java/_warmup/Warmup.java create mode 100644 sandbox-runner/patterns/batch-iterator/java/pom.xml create mode 100644 sandbox-runner/patterns/batch-iterator/java/src/main/java/Activities.java create mode 100644 sandbox-runner/patterns/batch-iterator/java/src/main/java/BatchIteratorWorkflow.java create mode 100644 sandbox-runner/patterns/batch-iterator/java/src/main/java/Shared.java create mode 100644 sandbox-runner/patterns/batch-iterator/java/src/main/java/Starter.java create mode 100644 sandbox-runner/patterns/batch-iterator/java/src/main/java/Worker.java create mode 100644 sandbox-runner/patterns/batch-iterator/pattern.json create mode 100644 sandbox-runner/patterns/batch-iterator/python/activities.py create mode 100644 sandbox-runner/patterns/batch-iterator/python/pyproject.toml create mode 100644 sandbox-runner/patterns/batch-iterator/python/shared.py create mode 100644 sandbox-runner/patterns/batch-iterator/python/starter.py create mode 100644 sandbox-runner/patterns/batch-iterator/python/worker.py create mode 100644 sandbox-runner/patterns/batch-iterator/python/workflows.py create mode 100644 sandbox-runner/patterns/batch-iterator/typescript/activities.ts create mode 100644 sandbox-runner/patterns/batch-iterator/typescript/package.json create mode 100644 sandbox-runner/patterns/batch-iterator/typescript/shared.ts create mode 100644 sandbox-runner/patterns/batch-iterator/typescript/starter.ts create mode 100644 sandbox-runner/patterns/batch-iterator/typescript/worker.ts create mode 100644 sandbox-runner/patterns/batch-iterator/typescript/workflows.ts create mode 100644 sandbox-runner/patterns/fanout-child-workflows/go/activities.go create mode 100644 sandbox-runner/patterns/fanout-child-workflows/go/go.mod create mode 100644 sandbox-runner/patterns/fanout-child-workflows/go/go.sum create mode 100644 sandbox-runner/patterns/fanout-child-workflows/go/shared.go create mode 100644 sandbox-runner/patterns/fanout-child-workflows/go/starter.go create mode 100644 sandbox-runner/patterns/fanout-child-workflows/go/warmup.go create mode 100644 sandbox-runner/patterns/fanout-child-workflows/go/worker.go create mode 100644 sandbox-runner/patterns/fanout-child-workflows/go/workflows.go create mode 100644 sandbox-runner/patterns/fanout-child-workflows/java/_warmup/Warmup.java create mode 100644 sandbox-runner/patterns/fanout-child-workflows/java/pom.xml create mode 100644 sandbox-runner/patterns/fanout-child-workflows/java/src/main/java/Activities.java create mode 100644 sandbox-runner/patterns/fanout-child-workflows/java/src/main/java/FanOutWorkflow.java create mode 100644 sandbox-runner/patterns/fanout-child-workflows/java/src/main/java/Shared.java create mode 100644 sandbox-runner/patterns/fanout-child-workflows/java/src/main/java/Starter.java create mode 100644 sandbox-runner/patterns/fanout-child-workflows/java/src/main/java/Worker.java create mode 100644 sandbox-runner/patterns/fanout-child-workflows/pattern.json create mode 100644 sandbox-runner/patterns/fanout-child-workflows/python/activities.py create mode 100644 sandbox-runner/patterns/fanout-child-workflows/python/pyproject.toml create mode 100644 sandbox-runner/patterns/fanout-child-workflows/python/shared.py create mode 100644 sandbox-runner/patterns/fanout-child-workflows/python/starter.py create mode 100644 sandbox-runner/patterns/fanout-child-workflows/python/worker.py create mode 100644 sandbox-runner/patterns/fanout-child-workflows/python/workflows.py create mode 100644 sandbox-runner/patterns/fanout-child-workflows/typescript/activities.ts create mode 100644 sandbox-runner/patterns/fanout-child-workflows/typescript/package.json create mode 100644 sandbox-runner/patterns/fanout-child-workflows/typescript/shared.ts create mode 100644 sandbox-runner/patterns/fanout-child-workflows/typescript/starter.ts create mode 100644 sandbox-runner/patterns/fanout-child-workflows/typescript/worker.ts create mode 100644 sandbox-runner/patterns/fanout-child-workflows/typescript/workflows.ts create mode 100644 sandbox-runner/patterns/mapreduce-tree/go/activities.go create mode 100644 sandbox-runner/patterns/mapreduce-tree/go/go.mod create mode 100644 sandbox-runner/patterns/mapreduce-tree/go/go.sum create mode 100644 sandbox-runner/patterns/mapreduce-tree/go/shared.go create mode 100644 sandbox-runner/patterns/mapreduce-tree/go/starter.go create mode 100644 sandbox-runner/patterns/mapreduce-tree/go/warmup.go create mode 100644 sandbox-runner/patterns/mapreduce-tree/go/worker.go create mode 100644 sandbox-runner/patterns/mapreduce-tree/go/workflows.go create mode 100644 sandbox-runner/patterns/mapreduce-tree/java/_warmup/Warmup.java create mode 100644 sandbox-runner/patterns/mapreduce-tree/java/pom.xml create mode 100644 sandbox-runner/patterns/mapreduce-tree/java/src/main/java/Activities.java create mode 100644 sandbox-runner/patterns/mapreduce-tree/java/src/main/java/NodeWorkflow.java create mode 100644 sandbox-runner/patterns/mapreduce-tree/java/src/main/java/Shared.java create mode 100644 sandbox-runner/patterns/mapreduce-tree/java/src/main/java/Starter.java create mode 100644 sandbox-runner/patterns/mapreduce-tree/java/src/main/java/Worker.java create mode 100644 sandbox-runner/patterns/mapreduce-tree/pattern.json create mode 100644 sandbox-runner/patterns/mapreduce-tree/python/activities.py create mode 100644 sandbox-runner/patterns/mapreduce-tree/python/pyproject.toml create mode 100644 sandbox-runner/patterns/mapreduce-tree/python/shared.py create mode 100644 sandbox-runner/patterns/mapreduce-tree/python/starter.py create mode 100644 sandbox-runner/patterns/mapreduce-tree/python/worker.py create mode 100644 sandbox-runner/patterns/mapreduce-tree/python/workflows.py create mode 100644 sandbox-runner/patterns/mapreduce-tree/typescript/activities.ts create mode 100644 sandbox-runner/patterns/mapreduce-tree/typescript/package.json create mode 100644 sandbox-runner/patterns/mapreduce-tree/typescript/shared.ts create mode 100644 sandbox-runner/patterns/mapreduce-tree/typescript/starter.ts create mode 100644 sandbox-runner/patterns/mapreduce-tree/typescript/worker.ts create mode 100644 sandbox-runner/patterns/mapreduce-tree/typescript/workflows.ts create mode 100644 sandbox-runner/patterns/sliding-window/go/activities.go create mode 100644 sandbox-runner/patterns/sliding-window/go/go.mod create mode 100644 sandbox-runner/patterns/sliding-window/go/go.sum create mode 100644 sandbox-runner/patterns/sliding-window/go/shared.go create mode 100644 sandbox-runner/patterns/sliding-window/go/starter.go create mode 100644 sandbox-runner/patterns/sliding-window/go/warmup.go create mode 100644 sandbox-runner/patterns/sliding-window/go/worker.go create mode 100644 sandbox-runner/patterns/sliding-window/go/workflows.go create mode 100644 sandbox-runner/patterns/sliding-window/java/_warmup/Warmup.java create mode 100644 sandbox-runner/patterns/sliding-window/java/pom.xml create mode 100644 sandbox-runner/patterns/sliding-window/java/src/main/java/Activities.java create mode 100644 sandbox-runner/patterns/sliding-window/java/src/main/java/Shared.java create mode 100644 sandbox-runner/patterns/sliding-window/java/src/main/java/SlidingWindowWorkflow.java create mode 100644 sandbox-runner/patterns/sliding-window/java/src/main/java/Starter.java create mode 100644 sandbox-runner/patterns/sliding-window/java/src/main/java/Worker.java create mode 100644 sandbox-runner/patterns/sliding-window/pattern.json create mode 100644 sandbox-runner/patterns/sliding-window/python/activities.py create mode 100644 sandbox-runner/patterns/sliding-window/python/pyproject.toml create mode 100644 sandbox-runner/patterns/sliding-window/python/shared.py create mode 100644 sandbox-runner/patterns/sliding-window/python/starter.py create mode 100644 sandbox-runner/patterns/sliding-window/python/worker.py create mode 100644 sandbox-runner/patterns/sliding-window/python/workflows.py create mode 100644 sandbox-runner/patterns/sliding-window/typescript/activities.ts create mode 100644 sandbox-runner/patterns/sliding-window/typescript/package.json create mode 100644 sandbox-runner/patterns/sliding-window/typescript/shared.ts create mode 100644 sandbox-runner/patterns/sliding-window/typescript/starter.ts create mode 100644 sandbox-runner/patterns/sliding-window/typescript/worker.ts create mode 100644 sandbox-runner/patterns/sliding-window/typescript/workflows.ts diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index aa18ed9..c9b355b 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -73,6 +73,16 @@ export default withMermaid(defineConfig({ { text: 'Fairness', link: '/fairness' } ] }, + { + text: 'Batch Processing Patterns', + items: [ + { text: 'Overview', link: '/batch-processing-patterns' }, + { text: 'Fan-Out with Child Workflows', link: '/fanout-child-workflows' }, + { text: 'Batch Iterator', link: '/batch-iterator' }, + { text: 'Sliding Window', link: '/sliding-window' }, + { text: 'MapReduce Tree', link: '/mapreduce-tree' } + ] + }, ], socialLinks: [ { icon: 'github', link: 'https://github.com/taonic/temporal-design-patterns' } diff --git a/docs/batch-iterator.md b/docs/batch-iterator.md new file mode 100644 index 0000000..6e54651 --- /dev/null +++ b/docs/batch-iterator.md @@ -0,0 +1,233 @@ + +# Batch Iterator + +:::info TLDR +**Process one page at a time** and call Continue-as-New with the next offset after each page so the Workflow's event history never grows without bound. With this method you can process infinite pages. Use this when your record set is arbitrarily large, you need a durable checkpoint after every page, and sequential page-by-page throughput is acceptable. +::: + +## Overview + +The Batch Iterator pattern processes a large record set one page at a time. Each Workflow run processes a single page and then calls Continue-as-New with the next offset, producing a chain of short-lived runs that together cover the entire record set without accumulating unbounded event history. + +## Problem + +A single Workflow run is limited to 50,000 history events (aim for 2,000) and 2,000 in-flight Activities. Processing millions of records in one run is not possible within these bounds. + +You need a way to process an arbitrarily large record set reliably, with the ability to resume from a checkpoint if the Workflow is interrupted, and without overwhelming downstream systems with a burst of concurrent requests. + +## Solution + +Each Workflow run fetches one page of records using a persistent `offset` parameter, processes each record sequentially, and then calls `continueAsNew` with the incremented offset. The next run picks up exactly where the previous one left off. + +Because each run processes only a bounded number of records, history stays well within limits. The offset acts as a durable checkpoint: if the Workflow is interrupted mid-page, the next run replays only from the start of the current page. + +```mermaid +flowchart TD + DB[("Data Source\n(paginated)")] + WF1["Workflow Run 1\n(offset=0)"] + WF2["Workflow Run 2\n(offset=PAGE_SIZE)"] + WF3["Workflow Run N\n(offset=N×PAGE_SIZE)"] + Done(["Complete"]) + + DB -->|"fetch page 1"| WF1 + WF1 -->|"processRecord ×PAGE_SIZE"| Acts1["Activities"] + WF1 -->|"continueAsNew\n(offset=PAGE_SIZE)"| WF2 + + DB -->|"fetch page 2"| WF2 + WF2 -->|"processRecord ×PAGE_SIZE"| Acts2["Activities"] + WF2 -->|"continueAsNew\n(offset=N×PAGE_SIZE)"| WF3 + + DB -->|"fetch page N"| WF3 + WF3 -->|"processRecord ×PAGE_SIZE"| Acts3["Activities"] + WF3 -->|"last page → return"| Done +``` + +The following describes each step in the diagram: + +1. The Workflow starts with `offset=0` and calls `fetchPage(offset, pageSize)` to retrieve the first page of records. +2. It processes each record in the page by executing the `processRecord` Activity. +3. After the page is fully processed, it calls `continueAsNew` with `offset + pageSize`, passing the updated offset to the next run. +4. The next run begins with a clean history and repeats the same steps for the next page. +5. When `fetchPage` returns fewer records than `pageSize`, the Workflow knows it has reached the last page and returns normally. + +## Implementation + + + +The following examples show how each SDK implements the Batch Iterator pattern. + +::: code-group +```typescript [TypeScript] +// workflows.ts +import { continueAsNew, log, proxyActivities } from "@temporalio/workflow"; +import type * as activities from "./activities"; +import { PAGE_SIZE } from "./shared"; + +const { fetchPage, processRecord } = proxyActivities({ + startToCloseTimeout: "10 seconds", +}); + +export async function batchIteratorWorkflow( + offset: number = 0, + totalProcessed: number = 0 +): Promise { + const page = await fetchPage(offset, PAGE_SIZE); + + for (const record of page) { + await processRecord(record); + totalProcessed++; + } + + log.info(`Processed page at offset ${offset} (${page.length} records, running total: ${totalProcessed})`); + + if (page.length === PAGE_SIZE) { + await continueAsNew(offset + PAGE_SIZE, totalProcessed); + } + + return totalProcessed; +} +``` + +```python [Python] +# workflows.py +from temporalio import workflow +from temporalio.workflow import continue_as_new +from datetime import timedelta +from activities import fetch_page, process_record +from shared import PAGE_SIZE + + +@workflow.defn +class BatchIteratorWorkflow: + @workflow.run + async def run(self, offset: int = 0, total_processed: int = 0) -> int: + page = await workflow.execute_activity( + fetch_page, + args=[offset, PAGE_SIZE], + start_to_close_timeout=timedelta(seconds=10), + ) + + for record in page: + await workflow.execute_activity( + process_record, + record, + start_to_close_timeout=timedelta(seconds=10), + ) + total_processed += 1 + + workflow.logger.info( + f"Processed page at offset {offset} ({len(page)} records, running total: {total_processed})" + ) + + if len(page) == PAGE_SIZE: + continue_as_new(offset + PAGE_SIZE, total_processed) + + return total_processed +``` + +```go [Go] +// workflows.go +package main + +import ( + "go.temporal.io/sdk/workflow" +) + +func BatchIteratorWorkflow(ctx workflow.Context, offset int, totalProcessed int) (int, error) { + ao := workflow.ActivityOptions{ + StartToCloseTimeout: 10 * time.Second, + } + ctx = workflow.WithActivityOptions(ctx, ao) + + var page []Record + if err := workflow.ExecuteActivity(ctx, FetchPage, offset, PageSize).Get(ctx, &page); err != nil { + return totalProcessed, err + } + + for _, record := range page { + if err := workflow.ExecuteActivity(ctx, ProcessRecord, record).Get(ctx, nil); err != nil { + return totalProcessed, err + } + totalProcessed++ + } + + workflow.GetLogger(ctx).Info("Processed page", + "offset", offset, + "pageSize", len(page), + "totalProcessed", totalProcessed) + + if len(page) == PageSize { + return totalProcessed, workflow.NewContinueAsNewError(ctx, BatchIteratorWorkflow, offset+PageSize, totalProcessed) + } + + return totalProcessed, nil +} +``` + +```java [Java] +// BatchIteratorWorkflow.java +import io.temporal.activity.ActivityOptions; +import io.temporal.workflow.*; +import java.time.Duration; +import java.util.List; + +@WorkflowInterface +public interface BatchIteratorWorkflow { + @WorkflowMethod + int run(int offset, int totalProcessed); +} + +// BatchIteratorWorkflowImpl.java +public class BatchIteratorWorkflowImpl implements BatchIteratorWorkflow { + private final Activities activities = Workflow.newActivityStub( + Activities.class, + ActivityOptions.newBuilder() + .setStartToCloseTimeout(Duration.ofSeconds(10)) + .build() + ); + + @Override + public int run(int offset, int totalProcessed) { + List page = activities.fetchPage(offset, Shared.PAGE_SIZE); + + for (Record record : page) { + activities.processRecord(record); + totalProcessed++; + } + + Workflow.getLogger(BatchIteratorWorkflowImpl.class).info( + "Processed page at offset " + offset + " (" + page.size() + " records, total: " + totalProcessed + ")" + ); + + if (page.size() == Shared.PAGE_SIZE) { + throw Workflow.newContinueAsNewStub(BatchIteratorWorkflow.class) + .run(offset + Shared.PAGE_SIZE, totalProcessed); + } + + return totalProcessed; + } +} +``` +::: + +## Best Practices + +- **Choose a page size that keeps history under 2,000 events.** Each page produces roughly `2 × pageSize` history events (Activity scheduled + completed). A page size of 500–800 records is a safe target. +- **Include `totalProcessed` (or a similar counter) in the `continueAsNew` args.** This lets you observe overall progress via the Workflow input visible in the UI without querying internal state. +- **Fetch inside an Activity, not the Workflow.** The `fetchPage` call must be an Activity — not inline Workflow code — so it can interact with external systems and be retried independently. +- **Make `processRecord` idempotent.** If the Workflow is interrupted after some records in a page are processed but before `continueAsNew`, the next run replays the full page. Activities that have already completed are skipped by the replay, but your downstream system must tolerate duplicate calls in failure-recovery scenarios. +- **Avoid accumulating large local state between pages.** `continueAsNew` does not carry over in-memory state; only the arguments you pass are available in the next run. + +## Common Pitfalls + +- **Forgetting `continueAsNew` on the last page.** If you call `continueAsNew` unconditionally, the Workflow loops forever even when the data source is exhausted. Check whether the returned page is shorter than `pageSize` before continuing. +- **Passing mutable objects into `continueAsNew`.** All arguments are serialized. Pass only the minimal state needed (offset, counters) — not accumulated results or large data structures. +- **Sequential processing bottlenecks.** The Batch Iterator processes one record at a time per page. If throughput matters more than rate limiting, consider [Sliding Window](sliding-window) or [MapReduce Tree](mapreduce-tree). + +## Related Resources + +- [Continue-as-New pattern](continue-as-new) — core concepts for history management via `continueAsNew` +- [Sliding Window](sliding-window) — bounded concurrency that progresses at the rate of the fastest processor +- [MapReduce Tree](mapreduce-tree) — fully parallel processing for maximum speed +- [Temporal limits reference](https://docs.temporal.io/cloud/limits) +- [Batch samples (Java)](https://github.com/temporalio/samples-java/tree/main/core/src/main/java/io/temporal/samples/batch/iterator) diff --git a/docs/batch-processing-patterns.md b/docs/batch-processing-patterns.md new file mode 100644 index 0000000..0d93e9d --- /dev/null +++ b/docs/batch-processing-patterns.md @@ -0,0 +1,143 @@ + +# Batch Processing Patterns + +Patterns for processing large volumes of records reliably, at scale, and without overwhelming downstream systems. + +Choose based on your throughput requirements, record set size, and whether you need rate limiting or maximum parallelism. + +## When to use which pattern + +| Pattern | Record set size | Parallelism model | Workflow-based rate control | +|---|---|---|---| +| [Basic Workflow](#basic-workflow-single-tier-fan-out) | Small (up to a few hundred records) | Sequential or parallel activities in one Workflow | No | +| [Fan-Out with Child Workflows](fanout-child-workflows) | Up to ~4M records | Fixed concurrency (one child per chunk) | No | +| [Batch Iterator](batch-iterator) | Unlimited | Limited (activities per page) | Yes — fixed page rate | +| [Sliding Window](sliding-window) | Unlimited | Bounded window of concurrent children | Yes — configurable window | +| [MapReduce Tree](mapreduce-tree) | Unlimited | Fully parallel recursive tree | No — maximum speed | + + + +--- + +## Schedules + +Schedules allow Workflows to be executed on a recurring basis — think of them as a more powerful cron. + +- Supports `start` / `pause` / `stop` / `update` / `backfill` of scheduled Workflow executions +- Configurable **Overlap Policies** control what happens when the previous run is still running +- Full execution history visibility in the Temporal UI +- Schedules can be created via the UI, CLI, or SDK + +```bash +temporal schedule create \ + --schedule-id 'your-schedule-id' \ + --workflow-id 'your-workflow-id' \ + --task-queue 'your-task-queue' \ + --workflow-type 'YourWorkflowType' +``` + +**References:** +- [Temporal Schedules](https://docs.temporal.io/workflows#schedule) +- [CLI schedule commands](https://docs.temporal.io/cli/schedule) + +--- + +## Basic Workflow (single-tier fan-out) + +The simplest form of batch processing: the Workflow fetches or receives record IDs and executes one Activity per record. + +- Activities can be executed sequentially or concurrently (using the SDK's async primitives) +- **Limit: 2,000 in-flight Activities per Workflow run** (aim for 500) +- If total event count is likely to exceed 2,000 (hard limit: 50,000), use the [Batch Iterator](batch-iterator) instead + +**Pros:** Simple +**Cons:** Hard cap on concurrent Activities; all-or-nothing failure model; can overwhelm downstream systems + +```mermaid +flowchart TD + Records["📋 Record IDs\n(fetched or passed in)"] + WF["Workflow"] + A1["Activity"] + A2["Activity"] + AN["Activity ..."] + + Records --> WF + WF --> A1 + WF --> A2 + WF --> AN +``` + +--- + +## Batch Signalling + +The Temporal CLI lets you signal, reset, cancel, or terminate multiple Workflows with a single command using a visibility query. + +- 1 running batch job per namespace +- 50 Workflows per second per batch + +```bash +# Signal all running Workflows of a given type +temporal workflow signal \ + --name MySignal \ + --input '{"Input": "As-JSON"}' \ + --query 'ExecutionStatus = "Running" AND WorkflowType="YourWorkflow"' \ + --reason "Testing" + +# Terminate all running Workflows of a given type +temporal workflow terminate \ + --query 'ExecutionStatus = "Running" AND WorkflowType="SomeWorkflowType"' \ + --reason "Terminate Test Workflows" +``` + +**Reference:** [CLI batch commands](https://docs.temporal.io/cli/batch) + +--- + +## Key Limits + +Full reference: [Temporal Cloud limits](https://docs.temporal.io/cloud/limits) + +| Limit | Value | +|---|---| +| Unfinished actions per Workflow | 2,000 max (aim for 500). Includes Activities, Signals, Child Workflows, cancellation requests | +| Events per Workflow history | 50,000 events max (aim for 2,000) **or** 50 MB total history size | +| Signals per Workflow | 10,000 | +| Updates per Workflow | 10 in-flight, 2,000 total | +| Batch Signalling | 1 batch job per namespace; 50 Workflows/sec per batch | diff --git a/docs/fanout-child-workflows.md b/docs/fanout-child-workflows.md new file mode 100644 index 0000000..ce6e0ed --- /dev/null +++ b/docs/fanout-child-workflows.md @@ -0,0 +1,278 @@ + +# Fan-Out with Child Workflows + +:::info TLDR +Split your record set into fixed-size chunks and start **one child Workflow per chunk** so that each chunk's history stays within Temporal's limits. Use this when your record set fits within ~4 million items, you want maximum concurrency with no rate control, and you can pre-compute how many chunks you need before the job starts. +::: + +## Overview + +The Fan-Out pattern distributes a large record set across multiple independent child Workflows, each responsible for processing a fixed-size chunk. The parent Workflow assigns work by offset and length so that no record IDs need to be passed over the wire — only two integers per child. + +## Problem + +A single Workflow run can have at most 2,000 in-flight Activities (aim for 500) and at most 50,000 history events. Processing millions of records in a single Workflow run is therefore not possible. + +You need a way to partition a large record set, process each partition independently, and coordinate the overall job while keeping each Workflow's history within safe bounds. + +## Solution + +You split the total record count into fixed-size chunks and start one child Workflow per chunk. Each child is given an `offset` and a `length` so it knows which slice of the record set to fetch and process independently. + +The parent Workflow starts all children concurrently and waits for them all to complete. If a child fails the parent can retry that child without re-processing the records handled by other children. + +```mermaid +flowchart TD + Records["📋 Total record set\n(N records)"] + Parent["Parent Workflow\n(fanOutWorkflow)"] + C1["Child Workflow\n(offset=0, length=chunk)"] + C2["Child Workflow\n(offset=chunk, length=chunk)"] + C3["Child Workflow\n(offset=2×chunk, length=chunk)"] + + Records --> Parent + Parent -->|"start child 1"| C1 + Parent -->|"start child 2"| C2 + Parent -->|"start child 3"| C3 + + C1 --> A1["processRecord ×chunk"] + C2 --> A2["processRecord ×chunk"] + C3 --> A3["processRecord ×chunk"] + + A1 -->|"done"| Parent + A2 -->|"done"| Parent + A3 -->|"done"| Parent +``` + +The following describes each step in the diagram: + +1. The parent Workflow receives the total record count and a configured chunk size. +2. It divides the total into chunks and starts one child Workflow per chunk, passing only `offset` and `length`. +3. Each child independently fetches its slice of records (using the offset and length) and calls `processRecord` for each one. +4. Each child completes and returns its result to the parent. +5. The parent blocks until all children have completed, then returns the aggregated result. + +## Implementation + + + +The following examples show how each SDK implements the Fan-Out pattern. + +::: code-group +```typescript [TypeScript] +// workflows.ts +import { + executeChild, + proxyActivities, + workflowInfo, +} from "@temporalio/workflow"; +import type * as activities from "./activities"; +import { TASK_QUEUE, CHUNK_SIZE } from "./shared"; + +const { processRecord } = proxyActivities({ + startToCloseTimeout: "10 seconds", +}); + +export async function fanOutWorkflow( + totalRecords: number, + chunkSize: number = CHUNK_SIZE +): Promise { + const children: Promise[] = []; + + for (let offset = 0; offset < totalRecords; offset += chunkSize) { + const length = Math.min(chunkSize, totalRecords - offset); + children.push( + executeChild(recordBatchWorkflow, { + args: [offset, length], + taskQueue: TASK_QUEUE, + workflowId: `${workflowInfo().workflowId}/batch-${offset}`, + }) + ); + } + + const results = await Promise.all(children); + return results.reduce((sum, n) => sum + n, 0); +} + +export async function recordBatchWorkflow( + offset: number, + length: number +): Promise { + let processed = 0; + for (let i = offset; i < offset + length; i++) { + await processRecord(i); + processed++; + } + return processed; +} +``` + +```python [Python] +# workflows.py +from datetime import timedelta +from temporalio import workflow +from temporalio.workflow import ChildWorkflowHandle +import asyncio +from activities import process_record +from shared import TASK_QUEUE, CHUNK_SIZE + + +@workflow.defn +class RecordBatchWorkflow: + @workflow.run + async def run(self, offset: int, length: int) -> int: + processed = 0 + for i in range(offset, offset + length): + await workflow.execute_activity( + process_record, + i, + start_to_close_timeout=timedelta(seconds=10), + ) + processed += 1 + return processed + + +@workflow.defn +class FanOutWorkflow: + @workflow.run + async def run(self, total_records: int, chunk_size: int = CHUNK_SIZE) -> int: + handles: list[ChildWorkflowHandle] = [] + parent_id = workflow.info().workflow_id + + offset = 0 + while offset < total_records: + length = min(chunk_size, total_records - offset) + handle = await workflow.start_child_workflow( + RecordBatchWorkflow.run, + args=[offset, length], + id=f"{parent_id}/batch-{offset}", + task_queue=TASK_QUEUE, + ) + handles.append(handle) + offset += chunk_size + + results = await asyncio.gather(*[h.result() for h in handles]) + return sum(results) +``` + +```go [Go] +// workflows.go +package main + +import ( + "go.temporal.io/sdk/temporal" + "go.temporal.io/sdk/workflow" +) + +func FanOutWorkflow(ctx workflow.Context, totalRecords int, chunkSize int) (int, error) { + if chunkSize <= 0 { + chunkSize = ChunkSize + } + + var futures []workflow.Future + parentID := workflow.GetInfo(ctx).WorkflowExecution.ID + + for offset := 0; offset < totalRecords; offset += chunkSize { + length := chunkSize + if offset+chunkSize > totalRecords { + length = totalRecords - offset + } + off := offset // capture loop variable + cwo := workflow.ChildWorkflowOptions{ + WorkflowID: parentID + "/batch-" + fmt.Sprintf("%d", off), + TaskQueue: TaskQueue, + } + cctx := workflow.WithChildOptions(ctx, cwo) + futures = append(futures, workflow.ExecuteChildWorkflow(cctx, RecordBatchWorkflow, off, length)) + } + + total := 0 + for _, f := range futures { + var n int + if err := f.Get(ctx, &n); err != nil { + return total, temporal.NewApplicationError("child failed", "ChildFailed", err) + } + total += n + } + return total, nil +} + +func RecordBatchWorkflow(ctx workflow.Context, offset int, length int) (int, error) { + ao := workflow.ActivityOptions{ + StartToCloseTimeout: 10 * time.Second, + } + ctx = workflow.WithActivityOptions(ctx, ao) + + processed := 0 + for i := offset; i < offset+length; i++ { + if err := workflow.ExecuteActivity(ctx, ProcessRecord, i).Get(ctx, nil); err != nil { + return processed, err + } + processed++ + } + return processed, nil +} +``` + +```java [Java] +// FanOutWorkflow.java +import io.temporal.activity.ActivityOptions; +import io.temporal.workflow.*; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +@WorkflowInterface +public interface FanOutWorkflow { + @WorkflowMethod + int run(int totalRecords, int chunkSize); +} + +// FanOutWorkflowImpl.java +public class FanOutWorkflowImpl implements FanOutWorkflow { + @Override + public int run(int totalRecords, int chunkSize) { + if (chunkSize <= 0) chunkSize = Shared.CHUNK_SIZE; + + List> promises = new ArrayList<>(); + String parentId = Workflow.getInfo().getWorkflowId(); + + for (int offset = 0; offset < totalRecords; offset += chunkSize) { + int length = Math.min(chunkSize, totalRecords - offset); + ChildWorkflowOptions opts = ChildWorkflowOptions.newBuilder() + .setWorkflowId(parentId + "/batch-" + offset) + .setTaskQueue(Shared.TASK_QUEUE) + .build(); + RecordBatchWorkflow child = Workflow.newChildWorkflowStub(RecordBatchWorkflow.class, opts); + promises.add(Async.function(child::run, offset, length)); + } + + int total = 0; + for (Promise p : promises) { + total += p.get(); + } + return total; + } +} +``` +::: + +## Best Practices + +- **Use offset and length, not explicit IDs.** Pass only two integers to each child rather than a full slice of IDs. The child fetches its own records. This keeps history events small. +- **Size chunks to stay under the Activity limit.** Each child Workflow can have at most 2,000 in-flight Activities. Aim for chunks of 500 records or fewer if each record maps to one Activity. +- **Cap concurrent children in the parent.** Starting thousands of child Workflows simultaneously puts pressure on the namespace. Consider batching child starts or using [Sliding Window](sliding-window) if you need tighter concurrency control. +- **Set `PARENT_CLOSE_POLICY_ABANDON`** if you do not need the parent to collect results. This lets children complete independently even if the parent is cancelled or timed out. +- **Give each child a deterministic Workflow ID** (`parentId/batch-`). This makes it safe to re-run the parent: Temporal deduplicates child starts by Workflow ID, so already-completed children are not re-executed. + +## Common Pitfalls + +- **Starting too many children at once.** Each child start adds to the parent's history. If you have thousands of chunks, consider paging through them or switching to the [Batch Iterator](batch-iterator) or [Sliding Window](sliding-window). +- **Passing large lists of IDs.** Workflow inputs are stored in event history. Passing millions of record IDs as a list will blow the history size limit. Use offset + length instead. +- **Ignoring child failures.** A failed child does not automatically fail the parent unless you await all results. Always await child handles and handle errors explicitly. + +## Related Resources + +- [Child Workflows pattern](child-workflows) — core concepts for parent/child Workflow coordination +- [Batch Iterator](batch-iterator) — unbounded record sets with Continue-as-New pagination +- [Sliding Window](sliding-window) — bounded concurrency with maximum throughput +- [Temporal limits reference](https://docs.temporal.io/cloud/limits) diff --git a/docs/index.md b/docs/index.md index 09df9f4..10226a9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -190,3 +190,43 @@ Having these patterns in your toolbox helps you solve recurring problems in a ba + +## Batch processing patterns {.pattern-section-title} + + diff --git a/docs/mapreduce-tree.md b/docs/mapreduce-tree.md new file mode 100644 index 0000000..c4ae411 --- /dev/null +++ b/docs/mapreduce-tree.md @@ -0,0 +1,412 @@ + +# MapReduce Tree + +:::info TLDR +Recursively split a record set into a binary tree of child Workflows, process every leaf in parallel, and signal results back up the tree to the root. Use this when you need **maximum throughput** for an embarrassingly parallel workload and downstream systems can absorb an unbounded burst of concurrent requests. +::: + +## Overview + +The MapReduce Tree pattern processes a large record set with maximum parallelism by recursively splitting it into smaller chunks and distributing each chunk to a child Workflow. Results are signalled back up the tree to the parent. It is best suited for embarrassingly parallel workloads where speed matters more than rate limiting. + +## Problem + +Both the [Batch Iterator](batch-iterator) and [Sliding Window](sliding-window) patterns bound concurrency, which limits throughput. When you need to process a large record set as fast as possible and downstream systems can handle the load, you want to fan out work across as many concurrent processors as possible without a fixed window. + +You also need a way to handle record sets larger than what a single Workflow's concurrency limits allow, without pre-partitioning data into fixed chunks before the job starts. + +## Solution + +A Node Workflow receives a slice of records. If the slice is small enough (at or below a configurable `leafThreshold`), it starts one Leaf Workflow per record. Otherwise it splits the slice into `n` sub-slices and starts `n` Node child Workflows recursively. + +Each Leaf Workflow runs the actual processing Activity and signals its result back to its parent Node. Each Node aggregates the results it receives and signals them up to its own parent. The Root Node returns the final aggregated result. + +```mermaid +flowchart TD + Records["📋 Full record set"] + Root["Root Node Workflow\n(depth=0)"] + Node1["Node Workflow\n(depth=1, chunk 1)"] + Node2["Node Workflow\n(depth=1, chunk 2)"] + L1["Leaf Workflow\n(record A)"] + L2["Leaf Workflow\n(record B)"] + L3["Leaf Workflow\n(record C)"] + L4["Leaf Workflow\n(record D)"] + L5["Leaf Workflow\n(record E)"] + L6["Leaf Workflow\n(record F)"] + + Records --> Root + Root -->|"split → chunk 1"| Node1 + Root -->|"split → chunk 2"| Node2 + + Node1 --> L1 + Node1 --> L2 + Node1 --> L3 + + Node2 --> L4 + Node2 --> L5 + Node2 --> L6 + + L1 -->|"Signal result"| Node1 + L2 -->|"Signal result"| Node1 + L3 -->|"Signal result"| Node1 + + L4 -->|"Signal result"| Node2 + L5 -->|"Signal result"| Node2 + L6 -->|"Signal result"| Node2 + + Node1 -->|"Signal result"| Root + Node2 -->|"Signal result"| Root +``` + +The following describes each step in the diagram: + +1. The Root Node Workflow receives the full record set and `depth=0`. +2. Because the record set is larger than `leafThreshold`, the Root splits it into two chunks and starts two child Node Workflows. +3. Each Node Workflow receives its chunk and checks its size against `leafThreshold`. In this example, each chunk is small enough, so each Node starts one Leaf Workflow per record. +4. Each Leaf Workflow calls the `processRecord` Activity and, when complete, signals its result back to its parent Node using `signalExternalWorkflow`. +5. Each Node collects all leaf results via signal handlers, aggregates them, and signals the aggregated result back to the Root. +6. The Root collects both node results and returns the final aggregate. + +## Implementation + + + +The following examples show how each SDK implements the MapReduce Tree pattern. + +::: code-group +```typescript [TypeScript] +// workflows.ts +import { + condition, + defineSignal, + executeChild, + proxyActivities, + setHandler, + workflowInfo, +} from "@temporalio/workflow"; +import type * as activities from "./activities"; +import { TASK_QUEUE, LEAF_THRESHOLD, MAX_DEPTH } from "./shared"; + +const { processRecord } = proxyActivities({ + startToCloseTimeout: "30 seconds", +}); + +export const resultSignal = defineSignal<[string, string]>("leafResult"); + +export async function leafWorkflow( + record: string, + parentWorkflowId: string +): Promise { + const result = await processRecord(record); + // Signal result back to parent node. + await executeChild(signalProxy, { + workflowId: parentWorkflowId, + args: [record, result], + }); +} + +// Placeholder — in real usage call signalExternalWorkflow / getExternalWorkflowHandle. +async function signalProxy(_record: string, _result: string): Promise {} + +export async function nodeWorkflow( + records: string[], + depth: number = 0, + parentWorkflowId: string = "" +): Promise { + if (depth > MAX_DEPTH) { + throw new Error(`Tree depth exceeded ${MAX_DEPTH}`); + } + + const myId = workflowInfo().workflowId; + const results: string[] = []; + let received = 0; + + setHandler(resultSignal, (_record: string, result: string) => { + results.push(result); + received++; + }); + + if (records.length <= LEAF_THRESHOLD) { + // Start one leaf per record. + for (const record of records) { + executeChild(leafWorkflow, { + args: [record, myId], + workflowId: `${myId}/leaf-${record}`, + taskQueue: TASK_QUEUE, + }); + } + await condition(() => received === records.length); + } else { + // Split and recurse. + const mid = Math.floor(records.length / 2); + const chunks = [records.slice(0, mid), records.slice(mid)]; + + for (let i = 0; i < chunks.length; i++) { + executeChild(nodeWorkflow, { + args: [chunks[i], depth + 1, myId], + workflowId: `${myId}/node-d${depth + 1}-${i}`, + taskQueue: TASK_QUEUE, + }); + } + await condition(() => received === chunks.length); + } + + // Signal aggregated result up to parent. + if (parentWorkflowId) { + await executeChild(signalProxy, { + workflowId: parentWorkflowId, + args: [myId, results.join(",")], + }); + } + + return results; +} +``` + +```python [Python] +# workflows.py +import asyncio +from datetime import timedelta +from temporalio import workflow +from activities import process_record +from shared import TASK_QUEUE, LEAF_THRESHOLD, MAX_DEPTH + +RESULT_SIGNAL = "leafResult" + + +@workflow.defn +class LeafWorkflow: + @workflow.run + async def run(self, record: str, parent_workflow_id: str) -> None: + result = await workflow.execute_activity( + process_record, + record, + start_to_close_timeout=timedelta(seconds=30), + ) + handle = workflow.get_external_workflow_handle(parent_workflow_id) + await handle.signal(RESULT_SIGNAL, [record, result]) + + +@workflow.defn +class NodeWorkflow: + def __init__(self) -> None: + self._results: list[str] = [] + + @workflow.signal(name=RESULT_SIGNAL) + def leaf_result(self, record: str, result: str) -> None: + self._results.append(result) + + @workflow.run + async def run( + self, + records: list[str], + depth: int = 0, + parent_workflow_id: str = "", + ) -> list[str]: + if depth > MAX_DEPTH: + raise RuntimeError(f"Tree depth exceeded {MAX_DEPTH}") + + my_id = workflow.info().workflow_id + expected = 0 + + if len(records) <= LEAF_THRESHOLD: + for record in records: + await workflow.start_child_workflow( + LeafWorkflow.run, + args=[record, my_id], + id=f"{my_id}/leaf-{record}", + task_queue=TASK_QUEUE, + ) + expected = len(records) + else: + mid = len(records) // 2 + chunks = [records[:mid], records[mid:]] + for i, chunk in enumerate(chunks): + await workflow.start_child_workflow( + NodeWorkflow.run, + args=[chunk, depth + 1, my_id], + id=f"{my_id}/node-d{depth+1}-{i}", + task_queue=TASK_QUEUE, + ) + expected = len(chunks) + + await workflow.wait_condition(lambda: len(self._results) >= expected) + + if parent_workflow_id: + handle = workflow.get_external_workflow_handle(parent_workflow_id) + await handle.signal(RESULT_SIGNAL, [my_id, ",".join(self._results)]) + + return self._results +``` + +```go [Go] +// workflows.go +package main + +import ( + "fmt" + "strings" + "go.temporal.io/sdk/workflow" +) + +const ResultSignal = "leafResult" + +type ResultPayload struct { + ID string + Result string +} + +func LeafWorkflow(ctx workflow.Context, record string, parentWorkflowID string) error { + ao := workflow.ActivityOptions{StartToCloseTimeout: 30 * time.Second} + ctx = workflow.WithActivityOptions(ctx, ao) + + var result string + if err := workflow.ExecuteActivity(ctx, ProcessRecord, record).Get(ctx, &result); err != nil { + return err + } + + return workflow.SignalExternalWorkflow(ctx, parentWorkflowID, "", ResultSignal, + ResultPayload{ID: record, Result: result}).Get(ctx, nil) +} + +func NodeWorkflow(ctx workflow.Context, records []string, depth int, parentWorkflowID string) ([]string, error) { + if depth > MaxDepth { + return nil, fmt.Errorf("tree depth exceeded %d", MaxDepth) + } + + myID := workflow.GetInfo(ctx).WorkflowExecution.ID + resultCh := workflow.GetSignalChannel(ctx, ResultSignal) + + var results []string + expected := 0 + + if len(records) <= LeafThreshold { + for _, record := range records { + cwo := workflow.ChildWorkflowOptions{ + WorkflowID: myID + "/leaf-" + record, + TaskQueue: TaskQueue, + } + workflow.ExecuteChildWorkflow(workflow.WithChildOptions(ctx, cwo), LeafWorkflow, record, myID) + expected++ + } + } else { + mid := len(records) / 2 + chunks := [][]string{records[:mid], records[mid:]} + for i, chunk := range chunks { + cwo := workflow.ChildWorkflowOptions{ + WorkflowID: fmt.Sprintf("%s/node-d%d-%d", myID, depth+1, i), + TaskQueue: TaskQueue, + } + workflow.ExecuteChildWorkflow(workflow.WithChildOptions(ctx, cwo), NodeWorkflow, chunk, depth+1, myID) + expected++ + } + } + + for i := 0; i < expected; i++ { + var payload ResultPayload + resultCh.Receive(ctx, &payload) + results = append(results, payload.Result) + } + + if parentWorkflowID != "" { + err := workflow.SignalExternalWorkflow(ctx, parentWorkflowID, "", ResultSignal, + ResultPayload{ID: myID, Result: strings.Join(results, ",")}).Get(ctx, nil) + if err != nil { + return results, err + } + } + + return results, nil +} +``` + +```java [Java] +// NodeWorkflow.java +import io.temporal.workflow.*; +import java.util.*; + +@WorkflowInterface +public interface NodeWorkflow { + @WorkflowMethod + List run(List records, int depth, String parentWorkflowId); + + @SignalMethod + void leafResult(String id, String result); +} + +// NodeWorkflowImpl.java +public class NodeWorkflowImpl implements NodeWorkflow { + private final List results = new ArrayList<>(); + + @Override + public void leafResult(String id, String result) { + results.add(result); + } + + @Override + public List run(List records, int depth, String parentWorkflowId) { + if (depth > Shared.MAX_DEPTH) { + throw new RuntimeException("Tree depth exceeded " + Shared.MAX_DEPTH); + } + + String myId = Workflow.getInfo().getWorkflowId(); + int expected; + + if (records.size() <= Shared.LEAF_THRESHOLD) { + for (String record : records) { + ChildWorkflowOptions opts = ChildWorkflowOptions.newBuilder() + .setWorkflowId(myId + "/leaf-" + record) + .setTaskQueue(Shared.TASK_QUEUE) + .build(); + LeafWorkflow leaf = Workflow.newChildWorkflowStub(LeafWorkflow.class, opts); + Async.procedure(leaf::run, record, myId); + } + expected = records.size(); + } else { + int mid = records.size() / 2; + List> chunks = List.of(records.subList(0, mid), records.subList(mid, records.size())); + for (int i = 0; i < chunks.size(); i++) { + ChildWorkflowOptions opts = ChildWorkflowOptions.newBuilder() + .setWorkflowId(String.format("%s/node-d%d-%d", myId, depth + 1, i)) + .setTaskQueue(Shared.TASK_QUEUE) + .build(); + NodeWorkflow child = Workflow.newChildWorkflowStub(NodeWorkflow.class, opts); + Async.function(child::run, chunks.get(i), depth + 1, myId); + } + expected = chunks.size(); + } + + Workflow.await(() -> results.size() >= expected); + + if (parentWorkflowId != null && !parentWorkflowId.isEmpty()) { + ExternalWorkflowStub parent = Workflow.newUntypedExternalWorkflowStub(parentWorkflowId, ""); + parent.signal("leafResult", myId, String.join(",", results)); + } + + return results; + } +} +``` +::: + +## Best Practices + +- **Set a `leafThreshold` to control tree depth.** A threshold of 3–10 records per leaf is typical. Too small a threshold creates excessive Workflow overhead; too large prevents full parallelism. +- **Set a `MAX_DEPTH` guard.** Recursive fan-out without a depth limit can produce extremely deep trees for large record sets. Fail fast if depth exceeds your expected maximum (e.g. `log2(totalRecords / leafThreshold) + 2`). +- **Avoid external writes in Node Workflows.** Node Workflows only aggregate results from children. Leaf Workflows perform the actual work. Keeping the roles separate prevents duplicate external writes if a Node is retried. +- **Use signals for result aggregation, not return values.** A parent cannot directly await a child started in a previous Workflow run. Signals decouple the result delivery from the parent-child lifetime, making the pattern resilient to replays. +- **Skip the reduce phase if results are not needed.** If you only need the side effects of processing each record (writes to a database, messages sent), omit the signal-back entirely and set `PARENT_CLOSE_POLICY_ABANDON` on all children. + +## Common Pitfalls + +- **Thundering herd.** The MapReduce Tree fans out exponentially. For large record sets, all leaf Activities start nearly simultaneously. Ensure your downstream system can absorb the burst, or switch to [Sliding Window](sliding-window) for rate limiting. +- **Signal storms.** If thousands of leaves all signal a single Node at the same time, the Node's signal queue can become a bottleneck. A two-level tree (Root → Nodes → Leaves) distributes this load; a deeper tree helps even more. +- **History bloat in the Root Workflow.** Each child start and signal received adds events to the Root's history. For very large record sets, consider adding an extra tree level to keep the Root from receiving too many direct signals. +- **Attempting external/downstream writes from Node Workflows.** Nodes may be retried. Any external write in a Node Workflow will be executed multiple times. Keep all side effects in Leaf Workflows (or Activities called by Leaves). + +## Related Resources + +- [Fan-Out with Child Workflows](fanout-child-workflows) — simpler flat fan-out for smaller record sets +- [Sliding Window](sliding-window) — bounded concurrency with rate limiting +- [Child Workflows pattern](child-workflows) — core concepts for parent/child coordination +- [Temporal limits reference](https://docs.temporal.io/cloud/limits) diff --git a/docs/sliding-window.md b/docs/sliding-window.md new file mode 100644 index 0000000..0c1f22c --- /dev/null +++ b/docs/sliding-window.md @@ -0,0 +1,462 @@ + +# Sliding Window + +:::info TLDR +Keep exactly `windowSize` child Workflows running at all times — each completion signal triggers the next record to start immediately. Use this when your record set is arbitrarily large, you need **bounded concurrency** to protect downstream systems, and you want higher throughput than a sequential Batch Iterator provides. +::: + +## Overview + +The Sliding Window pattern maintains a fixed-size pool of concurrently running child Workflows. As each child completes it signals the parent, which immediately starts a replacement — keeping the concurrency level constant and progressing at the rate of the fastest processor. Continue-as-New prevents the parent's history from growing without bound. + +## Problem + +The [Batch Iterator](batch-iterator) processes records sequentially — the overall throughput is limited by the slowest record in each page. The [Fan-Out](fanout-child-workflows) pattern starts all children at once, which can overwhelm downstream systems when the record set is large. + +You need a way to process an arbitrarily large record set with bounded concurrency, maximum throughput within that bound, and protection against history bloat. + +## Solution + +The parent Workflow starts exactly `windowSize` child Workflows simultaneously. Each child processes one record and, when finished, signals the parent with a completion notification. The parent maintains a count of completed children and starts a new child for the next record as soon as a slot becomes free. + +Continue-as-New is called after the parent has started `windowSize` children. Because child Workflows have stable Workflow IDs and Continue-as-New preserves the parent's Workflow ID, children started by a previous run can still signal the current run. + +```mermaid +flowchart TD + Records["📋 Record IDs\n[r0, r1, r2, ...]"] + Parent["Parent Workflow\n(window size = W)"] + C1["Child r0\n✅ done"] + C2["Child r1\n⏳ running"] + C3["Child r2\n⏳ running"] + C4["Child r3\n🆕 started"] + CAN["continueAsNew\n(startIndex + W)"] + + Records --> Parent + Parent -->|"start W children"| C1 + Parent --> C2 + Parent --> C3 + + C1 -->|"Signal: complete"| Parent + Parent -->|"slot free → start next"| C4 + + Parent -->|"after W children started"| CAN +``` + +The following describes each step in the diagram: + +1. The parent Workflow starts with a list of record IDs and a configured `windowSize`. +2. It starts the first `windowSize` children concurrently, one per record, each receiving the parent's Workflow ID so they know where to signal. +3. As each child completes, it sends a completion signal to the parent. +4. The parent receives the signal, increments its completion counter, and starts the next child (the next record in the list). +5. After starting `windowSize` children in total, the parent calls `continueAsNew` with the updated start index. The window slides forward without gaps because the parent's Workflow ID is preserved across runs. +6. Children from previous runs that have not yet signalled will find the new run when they send the signal, because the parent Workflow ID remains the same. + +## Implementation + + + +The following examples show how each SDK implements the Sliding Window pattern. + +::: code-group +```typescript [TypeScript] +// workflows.ts +import { + ApplicationFailure, + ParentClosePolicy, + condition, + continueAsNew, + defineSignal, + executeChild, + getExternalWorkflowHandle, + proxyActivities, + setHandler, + workflowInfo, +} from "@temporalio/workflow"; +import type * as activities from "./activities"; +import { TASK_QUEUE, WINDOW_SIZE } from "./shared"; + +const { processRecord } = proxyActivities({ + startToCloseTimeout: "30 seconds", +}); + +export const completionSignal = defineSignal<[string]>("recordCompleted"); + +export async function recordProcessorWorkflow( + recordId: string, + parentWorkflowId: string +): Promise { + await processRecord(recordId); + // Ignore NOT_FOUND — the parent's final run may have already completed. + try { + const parent = getExternalWorkflowHandle(parentWorkflowId); + await parent.signal(completionSignal, recordId); + } catch (err) { + if (!(err instanceof ApplicationFailure && err.type === 'NOT_FOUND')) throw err; + } +} + +export async function slidingWindowWorkflow(input: SlidingWindowInput): Promise { + const { + recordIds, + windowSize = WINDOW_SIZE, + startIndex = 0, + inFlight = 0, + } = input; + let totalProcessed = input.totalProcessed ?? 0; + const parentId = workflowInfo().workflowId; + let pendingSignals = 0; + let nextIndex = startIndex; + let dispatched = 0; + let active = inFlight; + + // Signal handler: each completion frees a slot and increments the total. + setHandler(completionSignal, (_recordId: string) => { + pendingSignals++; + totalProcessed++; + }); + + // Only start (windowSize - inFlight) new children. Carried-over in-flight + // children from the previous run will signal us when they complete. + const newFill = Math.min(windowSize - inFlight, recordIds.length - startIndex); + for (let i = 0; i < newFill; i++) { + executeChild(recordProcessorWorkflow, { + args: [recordIds[nextIndex], parentId], + workflowId: `${parentId}/record-${recordIds[nextIndex]}`, + taskQueue: TASK_QUEUE, + parentClosePolicy: ParentClosePolicy.ABANDON, + }); + nextIndex++; + dispatched++; + active++; + } + + // As slots free up, start the next child. + while (nextIndex < recordIds.length) { + await condition(() => pendingSignals > 0); + pendingSignals--; + active--; + executeChild(recordProcessorWorkflow, { + args: [recordIds[nextIndex], parentId], + workflowId: `${parentId}/record-${recordIds[nextIndex]}`, + taskQueue: TASK_QUEUE, + parentClosePolicy: ParentClosePolicy.ABANDON, + }); + nextIndex++; + dispatched++; + active++; + + // Continue-as-New after starting windowSize children to keep history short. + // Pass nextIndex (next unstarted record) and inFlight=windowSize (window is full). + if (dispatched >= windowSize) { + await continueAsNew({ + recordIds, + windowSize, + startIndex: nextIndex, + totalProcessed, + inFlight: windowSize, + }); + } + } + + // Wait for all remaining in-flight children to complete. + await condition(() => pendingSignals >= active); + return totalProcessed; +} +``` + +```python [Python] +# workflows.py +import asyncio +from datetime import timedelta +from temporalio import workflow +from temporalio.exceptions import ApplicationError +from temporalio.workflow import ParentClosePolicy, continue_as_new +from activities import process_record +from shared import TASK_QUEUE, WINDOW_SIZE + +COMPLETION_SIGNAL = "recordCompleted" + + +@workflow.defn +class RecordProcessorWorkflow: + @workflow.run + async def run(self, record_id: str, parent_workflow_id: str) -> None: + await workflow.execute_activity( + process_record, + record_id, + start_to_close_timeout=timedelta(seconds=30), + ) + # Ignore NOT_FOUND — the parent's final run may have already completed. + try: + handle = workflow.get_external_workflow_handle(parent_workflow_id) + await handle.signal(COMPLETION_SIGNAL, record_id) + except ApplicationError as e: + if "not found" not in str(e).lower(): + raise + + +@workflow.defn +class SlidingWindowWorkflow: + def __init__(self) -> None: + self._pending_signals = 0 + self._total_processed = 0 + + @workflow.signal(name=COMPLETION_SIGNAL) + def record_completed(self, record_id: str) -> None: + self._pending_signals += 1 + self._total_processed += 1 + + @workflow.run + async def run(self, input: SlidingWindowInput) -> int: + self._total_processed = input.total_processed + record_ids = input.record_ids + window_size = input.window_size + start_index = input.start_index + in_flight = input.in_flight + parent_id = workflow.info().workflow_id + next_index = start_index + dispatched = 0 + active = in_flight + + # Only start (window_size - in_flight) new children. Carried-over in-flight + # children from the previous run will signal us when they complete. + new_fill = min(window_size - in_flight, len(record_ids) - start_index) + for _ in range(new_fill): + await workflow.start_child_workflow( + RecordProcessorWorkflow.run, + args=[record_ids[next_index], parent_id], + id=f"{parent_id}/record-{record_ids[next_index]}", + task_queue=TASK_QUEUE, + parent_close_policy=ParentClosePolicy.ABANDON, + ) + next_index += 1 + dispatched += 1 + active += 1 + + # Slide the window. + while next_index < len(record_ids): + await workflow.wait_condition(lambda: self._pending_signals > 0) + self._pending_signals -= 1 + active -= 1 + await workflow.start_child_workflow( + RecordProcessorWorkflow.run, + args=[record_ids[next_index], parent_id], + id=f"{parent_id}/record-{record_ids[next_index]}", + task_queue=TASK_QUEUE, + parent_close_policy=ParentClosePolicy.ABANDON, + ) + next_index += 1 + dispatched += 1 + active += 1 + + # Pass next_index (next unstarted record) and in_flight=window_size (window is full). + if dispatched >= window_size: + continue_as_new(args=[SlidingWindowInput( + record_ids=record_ids, + window_size=window_size, + start_index=next_index, + total_processed=self._total_processed, + in_flight=window_size, + )]) + + # Wait for all remaining in-flight children to complete. + await workflow.wait_condition(lambda: self._pending_signals >= active) + return self._total_processed +``` + +```go [Go] +// workflows.go +package main + +import ( + "strings" + "go.temporal.io/sdk/workflow" +) + +const CompletionSignal = "recordCompleted" + +func RecordProcessorWorkflow(ctx workflow.Context, recordID string, parentWorkflowID string) error { + ao := workflow.ActivityOptions{StartToCloseTimeout: 30 * time.Second} + ctx = workflow.WithActivityOptions(ctx, ao) + + if err := workflow.ExecuteActivity(ctx, ProcessRecord, recordID).Get(ctx, nil); err != nil { + return err + } + + // Ignore not-found — the parent's final run may have already completed. + err := workflow.SignalExternalWorkflow(ctx, parentWorkflowID, "", CompletionSignal, recordID).Get(ctx, nil) + if err != nil && strings.Contains(err.Error(), "not found") { + return nil + } + return err +} + +func SlidingWindowWorkflow(ctx workflow.Context, input SlidingWindowInput) (int, error) { + windowSize := input.WindowSize + if windowSize <= 0 { + windowSize = WindowSize + } + recordIDs := input.RecordIDs + parentID := workflow.GetInfo(ctx).WorkflowExecution.ID + + completedCh := workflow.GetSignalChannel(ctx, CompletionSignal) + nextIndex := input.StartIndex + dispatched := 0 + active := input.InFlight + + startChild := func(recordID string) { + cwo := workflow.ChildWorkflowOptions{ + WorkflowID: parentID + "/record-" + recordID, + TaskQueue: TaskQueue, + ParentClosePolicy: enums.PARENT_CLOSE_POLICY_ABANDON, + } + workflow.ExecuteChildWorkflow(workflow.WithChildOptions(ctx, cwo), RecordProcessorWorkflow, recordID, parentID) + } + + // Only start (windowSize - inFlight) new children. Carried-over in-flight + // children from the previous run will signal us when they complete. + newFill := len(recordIDs) - input.StartIndex + if newFill > windowSize-input.InFlight { + newFill = windowSize - input.InFlight + } + for i := 0; i < newFill; i++ { + startChild(recordIDs[nextIndex]) + nextIndex++ + dispatched++ + active++ + } + + // Slide the window. + for nextIndex < len(recordIDs) { + workflow.GetSignalChannel(ctx, CompletionSignal).Receive(ctx, nil) + totalProcessed++ + active-- + startChild(recordIDs[nextIndex]) + nextIndex++ + dispatched++ + active++ + + // Pass nextIndex (next unstarted record) and inFlight=windowSize (window is full). + if dispatched >= windowSize { + return 0, workflow.NewContinueAsNewError(ctx, SlidingWindowWorkflow, SlidingWindowInput{ + RecordIDs: recordIDs, + WindowSize: windowSize, + StartIndex: nextIndex, + TotalProcessed: totalProcessed, + InFlight: windowSize, + }) + } + } + + // Drain all remaining in-flight children. + for active > 0 { + completedCh.Receive(ctx, nil) + totalProcessed++ + active-- + } + return totalProcessed, nil +} +``` + +```java [Java] +// SlidingWindowWorkflow.java +import io.temporal.workflow.*; +import java.util.List; + +@WorkflowInterface +public interface SlidingWindowWorkflow { + @WorkflowMethod + int run(Shared.SlidingWindowInput input); + + @SignalMethod + void recordCompleted(String recordId); +} + +// SlidingWindowWorkflowImpl.java +public class SlidingWindowWorkflowImpl implements SlidingWindowWorkflow { + private int pendingSignals = 0; + private int totalProcessed = 0; + + @Override + public void recordCompleted(String recordId) { + pendingSignals++; + totalProcessed++; + } + + @Override + public int run(Shared.SlidingWindowInput input) { + this.totalProcessed = input.totalProcessed; + int windowSize = input.windowSize > 0 ? input.windowSize : Shared.WINDOW_SIZE; + List recordIds = input.recordIds; + String parentId = Workflow.getInfo().getWorkflowId(); + int nextIndex = input.startIndex; + int dispatched = 0; + int active = input.inFlight; + + // Only start (windowSize - inFlight) new children. Carried-over in-flight + // children from the previous run will signal us when they complete. + int newFill = Math.min(windowSize - input.inFlight, recordIds.size() - input.startIndex); + for (int i = 0; i < newFill; i++) { + startChild(recordIds.get(nextIndex), parentId); + nextIndex++; + dispatched++; + active++; + } + + // Slide the window. + while (nextIndex < recordIds.size()) { + Workflow.await(() -> pendingSignals > 0); + pendingSignals--; + active--; + startChild(recordIds.get(nextIndex), parentId); + nextIndex++; + dispatched++; + active++; + + // Pass nextIndex (next unstarted record) and inFlight=windowSize (window is full). + if (dispatched >= windowSize) { + Workflow.newContinueAsNewStub(SlidingWindowWorkflow.class) + .run(new Shared.SlidingWindowInput(recordIds, windowSize, nextIndex, this.totalProcessed, windowSize)); + } + } + + // Drain all remaining in-flight children. + final int remainingActive = active; + Workflow.await(() -> pendingSignals >= remainingActive); + return this.totalProcessed; + } + + private void startChild(String recordId, String parentId) { + ChildWorkflowOptions opts = ChildWorkflowOptions.newBuilder() + .setWorkflowId(parentId + "/record-" + recordId) + .setTaskQueue(Shared.TASK_QUEUE) + .setParentClosePolicy(ParentClosePolicy.PARENT_CLOSE_POLICY_ABANDON) + .build(); + RecordProcessorWorkflow child = Workflow.newChildWorkflowStub(RecordProcessorWorkflow.class, opts); + Async.procedure(child::run, recordId, parentId); + } +} +``` +::: + +## Best Practices + +- **Preserve the parent Workflow ID across Continue-as-New.** The parent's Workflow ID is stable across `continueAsNew` runs — do not generate a new one. Children use `signalExternalWorkflow` with that ID, so they always reach the current run. +- **Use `PARENT_CLOSE_POLICY_ABANDON` on child Workflows.** This lets children that were started by a previous run complete normally even after the parent has continued as new. +- **Size the window conservatively at first.** Each in-flight child counts toward the 2,000 unfinished-actions limit for the parent. A window of 50–200 is a reasonable starting point depending on child duration and downstream capacity. +- **Pass only IDs (not full records) to child Workflows.** Workflow inputs are stored in event history. Keep them small. +- **Carry minimal state into `continueAsNew`.** Only pass `windowSize`, `startIndex`, and the record ID list (or a reference to it). Do not accumulate results in the parent — collect them out-of-band if needed. + +## Common Pitfalls + +- **Losing signals across Continue-as-New.** If a child signals before the parent's new run has registered the signal handler, the signal can be buffered and delivered correctly — Temporal buffers signals for existing Workflow IDs. However, ensure the signal handler is registered before any await, not conditionally. +- **Race between CAN and remaining signal draining.** After `continueAsNew`, the new run must handle signals from children started by the previous run. Pass `nextIndex` (the next *unstarted* record) and `inFlight = windowSize` to the new run so it knows how many carried-over children to expect signals from, without re-starting them. +- **Thundering herd on startup.** Starting hundreds of children simultaneously causes a burst of Activity polls. Ramp up the window gradually or use the [Batch Iterator](batch-iterator) if rate limiting is more important than throughput. + +## Related Resources + +- [Continue-as-New pattern](continue-as-new) — history management fundamentals +- [Batch Iterator](batch-iterator) — simpler alternative when sequential processing is acceptable +- [MapReduce Tree](mapreduce-tree) — fully parallel alternative when rate limiting is not needed +- [Temporal limits reference](https://docs.temporal.io/cloud/limits) +- [Sliding window sample (Java)](https://github.com/temporalio/samples-java/tree/main/core/src/main/java/io/temporal/samples/batch/slidingwindow) diff --git a/sandbox-runner/patterns/batch-iterator/go/activities.go b/sandbox-runner/patterns/batch-iterator/go/activities.go new file mode 100644 index 0000000..f74a63e --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/go/activities.go @@ -0,0 +1,24 @@ +package main + +import ( + "context" + "time" +) + +func FetchPage(_ context.Context, offset int, pageSize int) ([]int, error) { + end := offset + pageSize + if end > TotalRecords { + end = TotalRecords + } + page := make([]int, 0, end-offset) + for i := offset; i < end; i++ { + page = append(page, i) + } + return page, nil +} + +func ProcessRecord(_ context.Context, recordID int) error { + // Simulate processing work. + time.Sleep(50 * time.Millisecond) + return nil +} diff --git a/sandbox-runner/patterns/batch-iterator/go/go.mod b/sandbox-runner/patterns/batch-iterator/go/go.mod new file mode 100644 index 0000000..bfcc844 --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/go/go.mod @@ -0,0 +1,33 @@ +module batch-iterator + +go 1.22 + +require go.temporal.io/sdk v1.32.1 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + github.com/nexus-rpc/sdk-go v0.1.0 // indirect + github.com/pborman/uuid v1.2.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/robfig/cron v1.2.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/testify v1.10.0 // indirect + go.temporal.io/api v1.43.0 // indirect + golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.17.0 // indirect + golang.org/x/time v0.3.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect + google.golang.org/grpc v1.66.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/sandbox-runner/patterns/batch-iterator/go/go.sum b/sandbox-runner/patterns/batch-iterator/go/go.sum new file mode 100644 index 0000000..a88ecd1 --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/go/go.sum @@ -0,0 +1,179 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= +github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/nexus-rpc/sdk-go v0.1.0 h1:PUL/0vEY1//WnqyEHT5ao4LBRQ6MeNUihmnNGn0xMWY= +github.com/nexus-rpc/sdk-go v0.1.0/go.mod h1:TpfkM2Cw0Rlk9drGkoiSMpFqflKTiQLWUNyKJjF8mKQ= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= +github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= +github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.temporal.io/api v1.43.0 h1:lBhq+u5qFJqGMXwWsmg/i8qn1UA/3LCwVc88l2xUMHg= +go.temporal.io/api v1.43.0/go.mod h1:1WwYUMo6lao8yl0371xWUm13paHExN5ATYT/B7QtFis= +go.temporal.io/sdk v1.32.1 h1:slA8prhdFr4lxpsTcRusWVitD/cGjELfKUh0mBj73SU= +go.temporal.io/sdk v1.32.1/go.mod h1:8U8H7rF9u4Hyb4Ry9yiEls5716DHPNvVITPNkgWUwE8= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= +golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed h1:3RgNmBoI9MZhsj3QxC+AP/qQhNwpCLOvYDYYsFrhFt0= +google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed h1:J6izYgfBXAI3xTKLgxzTmUltdYaLsuBxFCgDHWJ/eXg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= +google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/sandbox-runner/patterns/batch-iterator/go/shared.go b/sandbox-runner/patterns/batch-iterator/go/shared.go new file mode 100644 index 0000000..078b060 --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/go/shared.go @@ -0,0 +1,8 @@ +package main + +const ( + TaskQueue = "batch-iterator-task-queue" + WorkflowIDPrefix = "batch-iterator" + TotalRecords = 30 + PageSize = 8 +) diff --git a/sandbox-runner/patterns/batch-iterator/go/starter.go b/sandbox-runner/patterns/batch-iterator/go/starter.go new file mode 100644 index 0000000..7fb32e5 --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/go/starter.go @@ -0,0 +1,45 @@ +package main + +import ( + "context" + "fmt" + "log" + "time" + + "go.temporal.io/sdk/client" +) + +func main() { + c, err := client.Dial(client.Options{HostPort: "localhost:7233"}) + if err != nil { + log.Fatalln("Unable to create client:", err) + } + defer c.Close() + + workflowID := fmt.Sprintf("%s-%d", WorkflowIDPrefix, time.Now().UnixMilli()) + we, err := c.ExecuteWorkflow( + context.Background(), + client.StartWorkflowOptions{ + ID: workflowID, + TaskQueue: TaskQueue, + }, + BatchIteratorWorkflow, + 0, // offset + 0, // totalProcessed + ) + if err != nil { + log.Fatalln("Unable to execute workflow:", err) + } + fmt.Printf("Started workflow: %s\n", we.GetID()) + fmt.Printf("Processing %d records (page size %d)…\n", TotalRecords, PageSize) + + var total int + if err := we.Get(context.Background(), &total); err != nil { + log.Fatalln("Workflow failed:", err) + } + fmt.Printf("Batch iterator complete: processed %d records\n", total) + fmt.Printf( + "Open the Temporal UI and search for '%s' to see the Continue-As-New chain.\n", + workflowID, + ) +} diff --git a/sandbox-runner/patterns/batch-iterator/go/warmup.go b/sandbox-runner/patterns/batch-iterator/go/warmup.go new file mode 100644 index 0000000..62afb71 --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/go/warmup.go @@ -0,0 +1,13 @@ +// Stub program compiled at image-build time so the Temporal Go SDK and its +// transitive deps land in the Go build cache before any user code runs. +// The image factory deletes this file after building. +package main + +import ( + _ "go.temporal.io/sdk/activity" + _ "go.temporal.io/sdk/client" + _ "go.temporal.io/sdk/worker" + _ "go.temporal.io/sdk/workflow" +) + +func main() {} diff --git a/sandbox-runner/patterns/batch-iterator/go/worker.go b/sandbox-runner/patterns/batch-iterator/go/worker.go new file mode 100644 index 0000000..973f220 --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/go/worker.go @@ -0,0 +1,26 @@ +package main + +import ( + "log" + + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/worker" +) + +func main() { + c, err := client.Dial(client.Options{HostPort: "localhost:7233"}) + if err != nil { + log.Fatalln("Unable to create client:", err) + } + defer c.Close() + + w := worker.New(c, TaskQueue, worker.Options{}) + w.RegisterWorkflow(BatchIteratorWorkflow) + w.RegisterActivity(FetchPage) + w.RegisterActivity(ProcessRecord) + + log.Printf("Worker listening on task queue '%s'", TaskQueue) + if err := w.Run(worker.InterruptCh()); err != nil { + log.Fatalln("Worker run failed:", err) + } +} diff --git a/sandbox-runner/patterns/batch-iterator/go/workflows.go b/sandbox-runner/patterns/batch-iterator/go/workflows.go new file mode 100644 index 0000000..df073e3 --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/go/workflows.go @@ -0,0 +1,39 @@ +package main + +import ( + "time" + + "go.temporal.io/sdk/workflow" +) + +// BatchIteratorWorkflow processes PageSize records per run, then calls +// ContinueAsNew with the next offset so history stays bounded. +func BatchIteratorWorkflow(ctx workflow.Context, offset int, totalProcessed int) (int, error) { + ao := workflow.ActivityOptions{StartToCloseTimeout: 10 * time.Second} + ctx = workflow.WithActivityOptions(ctx, ao) + + var page []int + if err := workflow.ExecuteActivity(ctx, FetchPage, offset, PageSize).Get(ctx, &page); err != nil { + return totalProcessed, err + } + + for _, recordID := range page { + if err := workflow.ExecuteActivity(ctx, ProcessRecord, recordID).Get(ctx, nil); err != nil { + return totalProcessed, err + } + totalProcessed++ + } + + workflow.GetLogger(ctx).Info("Processed page", + "offset", offset, + "pageSize", len(page), + "totalProcessed", totalProcessed) + + if len(page) == PageSize { + // More pages remain — continue as new with the next offset. + return totalProcessed, workflow.NewContinueAsNewError(ctx, BatchIteratorWorkflow, offset+PageSize, totalProcessed) + } + + // Reached here only on the final (partial) page. + return totalProcessed, nil +} diff --git a/sandbox-runner/patterns/batch-iterator/java/_warmup/Warmup.java b/sandbox-runner/patterns/batch-iterator/java/_warmup/Warmup.java new file mode 100644 index 0000000..2fc5ccb --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/java/_warmup/Warmup.java @@ -0,0 +1,23 @@ +// Stub class compiled at image-build time so the Temporal Java SDK and the +// Maven compiler plugin exercise their classloading paths once before any +// user code runs. The image factory copies this into src/main/java/, runs +// `mvn compile`, then deletes both the source and target/ from the snapshot. + +import io.temporal.activity.ActivityInterface; +import io.temporal.client.WorkflowClient; +import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.worker.WorkerFactory; +import io.temporal.workflow.WorkflowInterface; + +public class Warmup { + @SuppressWarnings("unused") + private static final Class[] FORCE_RESOLVE = { + ActivityInterface.class, + WorkflowInterface.class, + WorkflowClient.class, + WorkflowServiceStubs.class, + WorkerFactory.class, + }; + + public static void main(String[] args) {} +} diff --git a/sandbox-runner/patterns/batch-iterator/java/pom.xml b/sandbox-runner/patterns/batch-iterator/java/pom.xml new file mode 100644 index 0000000..539534e --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/java/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + example + batch-iterator + 0.1.0 + jar + + + 17 + 17 + UTF-8 + false + + + + + io.temporal + temporal-sdk + 1.27.0 + + + org.slf4j + slf4j-simple + 2.0.13 + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.4.1 + + + + diff --git a/sandbox-runner/patterns/batch-iterator/java/src/main/java/Activities.java b/sandbox-runner/patterns/batch-iterator/java/src/main/java/Activities.java new file mode 100644 index 0000000..4a36656 --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/java/src/main/java/Activities.java @@ -0,0 +1,31 @@ +import io.temporal.activity.ActivityInterface; + +import java.util.ArrayList; +import java.util.List; + +@ActivityInterface +public interface Activities { + List fetchPage(int offset, int pageSize); + void processRecord(int recordId); + + final class Impl implements Activities { + @Override + public List fetchPage(int offset, int pageSize) { + int end = Math.min(offset + pageSize, Shared.TOTAL_RECORDS); + List page = new ArrayList<>(); + for (int i = offset; i < end; i++) { + page.add(i); + } + return page; + } + + @Override + public void processRecord(int recordId) { + try { + Thread.sleep(50); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } +} diff --git a/sandbox-runner/patterns/batch-iterator/java/src/main/java/BatchIteratorWorkflow.java b/sandbox-runner/patterns/batch-iterator/java/src/main/java/BatchIteratorWorkflow.java new file mode 100644 index 0000000..ba1b206 --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/java/src/main/java/BatchIteratorWorkflow.java @@ -0,0 +1,44 @@ +import io.temporal.activity.ActivityOptions; +import io.temporal.workflow.Workflow; +import io.temporal.workflow.WorkflowInterface; +import io.temporal.workflow.WorkflowMethod; + +import java.time.Duration; +import java.util.List; + +@WorkflowInterface +public interface BatchIteratorWorkflow { + @WorkflowMethod + int run(int offset, int totalProcessed); + + final class Impl implements BatchIteratorWorkflow { + private final Activities activities = Workflow.newActivityStub( + Activities.class, + ActivityOptions.newBuilder() + .setStartToCloseTimeout(Duration.ofSeconds(10)) + .build()); + + @Override + public int run(int offset, int totalProcessed) { + List page = activities.fetchPage(offset, Shared.PAGE_SIZE); + + for (int recordId : page) { + activities.processRecord(recordId); + totalProcessed++; + } + + Workflow.getLogger(BatchIteratorWorkflow.class) + .info("Processed page: offset={} pageSize={} totalProcessed={}", + offset, page.size(), totalProcessed); + + if (page.size() == Shared.PAGE_SIZE) { + // More pages remain — continue as new with the next offset. + BatchIteratorWorkflow next = Workflow.newContinueAsNewStub(BatchIteratorWorkflow.class); + next.run(offset + Shared.PAGE_SIZE, totalProcessed); + } + + // Reached here only on the final (partial) page. + return totalProcessed; + } + } +} diff --git a/sandbox-runner/patterns/batch-iterator/java/src/main/java/Shared.java b/sandbox-runner/patterns/batch-iterator/java/src/main/java/Shared.java new file mode 100644 index 0000000..a56c30c --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/java/src/main/java/Shared.java @@ -0,0 +1,8 @@ +public final class Shared { + public static final String TASK_QUEUE = "batch-iterator-task-queue"; + public static final String WORKFLOW_ID_PREFIX = "batch-iterator"; + public static final int TOTAL_RECORDS = 30; + public static final int PAGE_SIZE = 8; + + private Shared() {} +} diff --git a/sandbox-runner/patterns/batch-iterator/java/src/main/java/Starter.java b/sandbox-runner/patterns/batch-iterator/java/src/main/java/Starter.java new file mode 100644 index 0000000..2312b43 --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/java/src/main/java/Starter.java @@ -0,0 +1,26 @@ +import io.temporal.client.WorkflowClient; +import io.temporal.client.WorkflowOptions; +import io.temporal.serviceclient.WorkflowServiceStubs; + +public class Starter { + public static void main(String[] args) { + WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs(); + WorkflowClient client = WorkflowClient.newInstance(service); + + String workflowId = Shared.WORKFLOW_ID_PREFIX + "-" + System.currentTimeMillis(); + BatchIteratorWorkflow workflow = client.newWorkflowStub( + BatchIteratorWorkflow.class, + WorkflowOptions.newBuilder() + .setTaskQueue(Shared.TASK_QUEUE) + .setWorkflowId(workflowId) + .build()); + + System.out.println("Started workflow: " + workflowId); + System.out.println("Processing " + Shared.TOTAL_RECORDS + " records (page size " + Shared.PAGE_SIZE + ")…"); + int total = workflow.run(0, 0); + System.out.println("Batch iterator complete: processed " + total + " records"); + System.out.println( + "Open the Temporal UI and search for '" + workflowId + + "' to see the Continue-As-New chain."); + } +} diff --git a/sandbox-runner/patterns/batch-iterator/java/src/main/java/Worker.java b/sandbox-runner/patterns/batch-iterator/java/src/main/java/Worker.java new file mode 100644 index 0000000..942f23e --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/java/src/main/java/Worker.java @@ -0,0 +1,24 @@ +import io.temporal.client.WorkflowClient; +import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.worker.WorkerFactory; + +public class Worker { + public static void main(String[] args) { + WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs(); + WorkflowClient client = WorkflowClient.newInstance(service); + + WorkerFactory factory = WorkerFactory.newInstance(client); + io.temporal.worker.Worker worker = factory.newWorker(Shared.TASK_QUEUE); + worker.registerWorkflowImplementationTypes(BatchIteratorWorkflow.Impl.class); + worker.registerActivitiesImplementations(new Activities.Impl()); + + System.out.println("Worker listening on task queue '" + Shared.TASK_QUEUE + "'"); + factory.start(); + + try { + Thread.currentThread().join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/sandbox-runner/patterns/batch-iterator/pattern.json b/sandbox-runner/patterns/batch-iterator/pattern.json new file mode 100644 index 0000000..f8597e4 --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/pattern.json @@ -0,0 +1,42 @@ +{ + "name": "Batch Iterator", + "languages": { + "typescript": { + "label": "TypeScript", + "files": ["starter.ts", "worker.ts", "workflows.ts", "activities.ts", "shared.ts"], + "worker": "npx tsx worker.ts", + "starter": "npx tsx starter.ts", + "workerProcessPattern": "tsx worker.ts" + }, + "python": { + "label": "Python", + "files": ["starter.py", "worker.py", "workflows.py", "activities.py", "shared.py"], + "worker": "uv run python worker.py", + "starter": "uv run python starter.py", + "workerProcessPattern": "python worker.py" + }, + "go": { + "label": "Go", + "files": ["starter.go", "worker.go", "workflows.go", "activities.go", "shared.go"], + "worker": "go run worker.go workflows.go activities.go shared.go", + "starter": "go run starter.go workflows.go activities.go shared.go", + "workerProcessPattern": "go run worker.go", + "diskGib": 2 + }, + "java": { + "label": "Java", + "files": [ + "src/main/java/Starter.java", + "src/main/java/Worker.java", + "src/main/java/BatchIteratorWorkflow.java", + "src/main/java/Activities.java", + "src/main/java/Shared.java" + ], + "worker": "mvn -q -B compile exec:java -Dexec.mainClass=Worker", + "starter": "mvn -q -B compile exec:java -Dexec.mainClass=Starter", + "workerProcessPattern": "exec.mainClass=Worker", + "diskGib": 3, + "memoryGib": 3 + } + } +} diff --git a/sandbox-runner/patterns/batch-iterator/python/activities.py b/sandbox-runner/patterns/batch-iterator/python/activities.py new file mode 100644 index 0000000..b6d7f8b --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/python/activities.py @@ -0,0 +1,17 @@ +import asyncio + +from temporalio import activity + +from shared import TOTAL_RECORDS + + +@activity.defn +async def fetch_page(offset: int, page_size: int) -> list[int]: + end = min(offset + page_size, TOTAL_RECORDS) + return list(range(offset, end)) + + +@activity.defn +async def process_record(record_id: int) -> None: + # Simulate processing work. + await asyncio.sleep(0.05) diff --git a/sandbox-runner/patterns/batch-iterator/python/pyproject.toml b/sandbox-runner/patterns/batch-iterator/python/pyproject.toml new file mode 100644 index 0000000..4f0909a --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/python/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "batch-iterator-python" +version = "0.1.0" +description = "Batch Iterator pattern demo (Python)" +requires-python = ">=3.12" +dependencies = [ + "temporalio==1.9.0", +] diff --git a/sandbox-runner/patterns/batch-iterator/python/shared.py b/sandbox-runner/patterns/batch-iterator/python/shared.py new file mode 100644 index 0000000..63bb6ff --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/python/shared.py @@ -0,0 +1,4 @@ +TASK_QUEUE = "batch-iterator-task-queue" +WORKFLOW_ID_PREFIX = "batch-iterator" +TOTAL_RECORDS = 30 +PAGE_SIZE = 8 diff --git a/sandbox-runner/patterns/batch-iterator/python/starter.py b/sandbox-runner/patterns/batch-iterator/python/starter.py new file mode 100644 index 0000000..2b7bf31 --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/python/starter.py @@ -0,0 +1,30 @@ +import asyncio +import time + +from temporalio.client import Client + +from shared import PAGE_SIZE, TASK_QUEUE, TOTAL_RECORDS, WORKFLOW_ID_PREFIX +from workflows import BatchIteratorWorkflow + + +async def main() -> None: + client = await Client.connect("localhost:7233") + workflow_id = f"{WORKFLOW_ID_PREFIX}-{int(time.time() * 1000)}" + handle = await client.start_workflow( + BatchIteratorWorkflow.run, + args=[0, 0], + id=workflow_id, + task_queue=TASK_QUEUE, + ) + print(f"Started workflow: {workflow_id}") + print(f"Processing {TOTAL_RECORDS} records (page size {PAGE_SIZE})…") + + total = await handle.result() + print(f"Batch iterator complete: processed {total} records") + print( + f"Open the Temporal UI and search for '{workflow_id}' to see the Continue-As-New chain." + ) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/sandbox-runner/patterns/batch-iterator/python/worker.py b/sandbox-runner/patterns/batch-iterator/python/worker.py new file mode 100644 index 0000000..221d1a5 --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/python/worker.py @@ -0,0 +1,24 @@ +import asyncio + +from temporalio.client import Client +from temporalio.worker import Worker + +from activities import fetch_page, process_record +from shared import TASK_QUEUE +from workflows import BatchIteratorWorkflow + + +async def main() -> None: + client = await Client.connect("localhost:7233") + worker = Worker( + client, + task_queue=TASK_QUEUE, + workflows=[BatchIteratorWorkflow], + activities=[fetch_page, process_record], + ) + print(f"Worker listening on task queue '{TASK_QUEUE}'", flush=True) + await worker.run() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/sandbox-runner/patterns/batch-iterator/python/workflows.py b/sandbox-runner/patterns/batch-iterator/python/workflows.py new file mode 100644 index 0000000..1d8d60b --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/python/workflows.py @@ -0,0 +1,40 @@ +from datetime import timedelta + +from temporalio import workflow +from temporalio.workflow import continue_as_new + +from activities import fetch_page, process_record +from shared import PAGE_SIZE + + +@workflow.defn +class BatchIteratorWorkflow: + """Processes PAGE_SIZE records per run, then calls continue_as_new with the + next offset so history stays bounded.""" + + @workflow.run + async def run(self, offset: int = 0, total_processed: int = 0) -> int: + page: list[int] = await workflow.execute_activity( + fetch_page, + args=[offset, PAGE_SIZE], + start_to_close_timeout=timedelta(seconds=10), + ) + + for record_id in page: + await workflow.execute_activity( + process_record, + record_id, + start_to_close_timeout=timedelta(seconds=10), + ) + total_processed += 1 + + workflow.logger.info( + f"Processed page: offset={offset} pageSize={len(page)} totalProcessed={total_processed}" + ) + + if len(page) == PAGE_SIZE: + # More pages remain — continue as new with the next offset. + continue_as_new(args=[offset + PAGE_SIZE, total_processed]) + + # Reached here only on the final (partial) page. + return total_processed diff --git a/sandbox-runner/patterns/batch-iterator/typescript/activities.ts b/sandbox-runner/patterns/batch-iterator/typescript/activities.ts new file mode 100644 index 0000000..d3bc5d5 --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/typescript/activities.ts @@ -0,0 +1,15 @@ +import { TOTAL_RECORDS } from "./shared"; + +export async function fetchPage(offset: number, pageSize: number): Promise { + const end = Math.min(offset + pageSize, TOTAL_RECORDS); + const page: number[] = []; + for (let i = offset; i < end; i++) { + page.push(i); + } + return page; +} + +export async function processRecord(recordId: number): Promise { + // Simulate processing work. + await new Promise((r) => setTimeout(r, 50)); +} diff --git a/sandbox-runner/patterns/batch-iterator/typescript/package.json b/sandbox-runner/patterns/batch-iterator/typescript/package.json new file mode 100644 index 0000000..3a0a12c --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/typescript/package.json @@ -0,0 +1,14 @@ +{ + "name": "temporal-design-patterns-sandbox", + "version": "0.1.0", + "private": true, + "type": "commonjs", + "dependencies": { + "@temporalio/activity": "^1.13.0", + "@temporalio/client": "^1.13.0", + "@temporalio/worker": "^1.13.0", + "@temporalio/workflow": "^1.13.0", + "tsx": "^4.19.2", + "typescript": "^5.6.3" + } +} diff --git a/sandbox-runner/patterns/batch-iterator/typescript/shared.ts b/sandbox-runner/patterns/batch-iterator/typescript/shared.ts new file mode 100644 index 0000000..d8a10f5 --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/typescript/shared.ts @@ -0,0 +1,4 @@ +export const TASK_QUEUE = "batch-iterator-task-queue"; +export const WORKFLOW_ID_PREFIX = "batch-iterator"; +export const TOTAL_RECORDS = 30; +export const PAGE_SIZE = 8; diff --git a/sandbox-runner/patterns/batch-iterator/typescript/starter.ts b/sandbox-runner/patterns/batch-iterator/typescript/starter.ts new file mode 100644 index 0000000..6a71bde --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/typescript/starter.ts @@ -0,0 +1,32 @@ +import { Client, Connection } from "@temporalio/client"; + +import { PAGE_SIZE, TASK_QUEUE, TOTAL_RECORDS, WORKFLOW_ID_PREFIX } from "./shared"; +import { batchIteratorWorkflow } from "./workflows"; + +async function main(): Promise { + const connection = await Connection.connect(); + try { + const client = new Client({ connection }); + const workflowId = `${WORKFLOW_ID_PREFIX}-${Date.now()}`; + const handle = await client.workflow.start(batchIteratorWorkflow, { + args: [0, 0], + taskQueue: TASK_QUEUE, + workflowId, + }); + console.log(`Started workflow: ${workflowId}`); + console.log(`Processing ${TOTAL_RECORDS} records (page size ${PAGE_SIZE})…`); + + const total = await handle.result(); + console.log(`Batch iterator complete: processed ${total} records`); + console.log( + `Open the Temporal UI and search for '${workflowId}' to see the Continue-As-New chain.` + ); + } finally { + await connection.close(); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/sandbox-runner/patterns/batch-iterator/typescript/worker.ts b/sandbox-runner/patterns/batch-iterator/typescript/worker.ts new file mode 100644 index 0000000..da949aa --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/typescript/worker.ts @@ -0,0 +1,19 @@ +import { Worker } from "@temporalio/worker"; + +import * as activities from "./activities"; +import { TASK_QUEUE } from "./shared"; + +async function main(): Promise { + const worker = await Worker.create({ + workflowsPath: require.resolve("./workflows"), + activities, + taskQueue: TASK_QUEUE, + }); + console.log(`Worker listening on task queue '${TASK_QUEUE}'`); + await worker.run(); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/sandbox-runner/patterns/batch-iterator/typescript/workflows.ts b/sandbox-runner/patterns/batch-iterator/typescript/workflows.ts new file mode 100644 index 0000000..395d975 --- /dev/null +++ b/sandbox-runner/patterns/batch-iterator/typescript/workflows.ts @@ -0,0 +1,34 @@ +import { continueAsNew, log, proxyActivities } from "@temporalio/workflow"; + +import type * as activities from "./activities"; +import { PAGE_SIZE } from "./shared"; + +const { fetchPage, processRecord } = proxyActivities({ + startToCloseTimeout: "10 seconds", +}); + +/** + * Batch Iterator Workflow: processes PAGE_SIZE records per run, then calls + * continueAsNew with the next offset so history stays bounded. + */ +export async function batchIteratorWorkflow( + offset: number = 0, + totalProcessed: number = 0 +): Promise { + const page = await fetchPage(offset, PAGE_SIZE); + + for (const recordId of page) { + await processRecord(recordId); + totalProcessed++; + } + + log.info(`Processed page`, { offset, pageSize: page.length, totalProcessed }); + + if (page.length === PAGE_SIZE) { + // More pages remain — continue as new with the next offset. + await continueAsNew(offset + PAGE_SIZE, totalProcessed); + } + + // Reached here only on the final (partial) page. + return totalProcessed; +} diff --git a/sandbox-runner/patterns/fanout-child-workflows/go/activities.go b/sandbox-runner/patterns/fanout-child-workflows/go/activities.go new file mode 100644 index 0000000..ccd7848 --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/go/activities.go @@ -0,0 +1,12 @@ +package main + +import ( + "context" + "time" +) + +func ProcessRecord(_ context.Context, recordID int) error { + // Simulate processing work. + time.Sleep(50 * time.Millisecond) + return nil +} diff --git a/sandbox-runner/patterns/fanout-child-workflows/go/go.mod b/sandbox-runner/patterns/fanout-child-workflows/go/go.mod new file mode 100644 index 0000000..b6f35f4 --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/go/go.mod @@ -0,0 +1,33 @@ +module fanout-child-workflows + +go 1.22 + +require go.temporal.io/sdk v1.32.1 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + github.com/nexus-rpc/sdk-go v0.1.0 // indirect + github.com/pborman/uuid v1.2.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/robfig/cron v1.2.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/testify v1.10.0 // indirect + go.temporal.io/api v1.43.0 // indirect + golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.17.0 // indirect + golang.org/x/time v0.3.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect + google.golang.org/grpc v1.66.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/sandbox-runner/patterns/fanout-child-workflows/go/go.sum b/sandbox-runner/patterns/fanout-child-workflows/go/go.sum new file mode 100644 index 0000000..a88ecd1 --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/go/go.sum @@ -0,0 +1,179 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= +github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/nexus-rpc/sdk-go v0.1.0 h1:PUL/0vEY1//WnqyEHT5ao4LBRQ6MeNUihmnNGn0xMWY= +github.com/nexus-rpc/sdk-go v0.1.0/go.mod h1:TpfkM2Cw0Rlk9drGkoiSMpFqflKTiQLWUNyKJjF8mKQ= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= +github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= +github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.temporal.io/api v1.43.0 h1:lBhq+u5qFJqGMXwWsmg/i8qn1UA/3LCwVc88l2xUMHg= +go.temporal.io/api v1.43.0/go.mod h1:1WwYUMo6lao8yl0371xWUm13paHExN5ATYT/B7QtFis= +go.temporal.io/sdk v1.32.1 h1:slA8prhdFr4lxpsTcRusWVitD/cGjELfKUh0mBj73SU= +go.temporal.io/sdk v1.32.1/go.mod h1:8U8H7rF9u4Hyb4Ry9yiEls5716DHPNvVITPNkgWUwE8= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= +golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed h1:3RgNmBoI9MZhsj3QxC+AP/qQhNwpCLOvYDYYsFrhFt0= +google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed h1:J6izYgfBXAI3xTKLgxzTmUltdYaLsuBxFCgDHWJ/eXg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= +google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/sandbox-runner/patterns/fanout-child-workflows/go/shared.go b/sandbox-runner/patterns/fanout-child-workflows/go/shared.go new file mode 100644 index 0000000..806a704 --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/go/shared.go @@ -0,0 +1,8 @@ +package main + +const ( + TaskQueue = "fanout-child-workflows-task-queue" + WorkflowIDPrefix = "fanout" + TotalRecords = 20 + ChunkSize = 5 +) diff --git a/sandbox-runner/patterns/fanout-child-workflows/go/starter.go b/sandbox-runner/patterns/fanout-child-workflows/go/starter.go new file mode 100644 index 0000000..dae1f04 --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/go/starter.go @@ -0,0 +1,45 @@ +package main + +import ( + "context" + "fmt" + "log" + "time" + + "go.temporal.io/sdk/client" +) + +func main() { + c, err := client.Dial(client.Options{HostPort: "localhost:7233"}) + if err != nil { + log.Fatalln("Unable to create client:", err) + } + defer c.Close() + + workflowID := fmt.Sprintf("%s-%d", WorkflowIDPrefix, time.Now().UnixMilli()) + we, err := c.ExecuteWorkflow( + context.Background(), + client.StartWorkflowOptions{ + ID: workflowID, + TaskQueue: TaskQueue, + }, + FanOutWorkflow, + TotalRecords, + ChunkSize, + ) + if err != nil { + log.Fatalln("Unable to execute workflow:", err) + } + fmt.Printf("Started workflow: %s\n", we.GetID()) + fmt.Printf("Processing %d records in chunks of %d…\n", TotalRecords, ChunkSize) + + var total int + if err := we.Get(context.Background(), &total); err != nil { + log.Fatalln("Workflow failed:", err) + } + fmt.Printf("Fan-out complete: processed %d records\n", total) + fmt.Printf( + "Open the Temporal UI and search for '%s' to see the parent and child workflows.\n", + workflowID, + ) +} diff --git a/sandbox-runner/patterns/fanout-child-workflows/go/warmup.go b/sandbox-runner/patterns/fanout-child-workflows/go/warmup.go new file mode 100644 index 0000000..62afb71 --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/go/warmup.go @@ -0,0 +1,13 @@ +// Stub program compiled at image-build time so the Temporal Go SDK and its +// transitive deps land in the Go build cache before any user code runs. +// The image factory deletes this file after building. +package main + +import ( + _ "go.temporal.io/sdk/activity" + _ "go.temporal.io/sdk/client" + _ "go.temporal.io/sdk/worker" + _ "go.temporal.io/sdk/workflow" +) + +func main() {} diff --git a/sandbox-runner/patterns/fanout-child-workflows/go/worker.go b/sandbox-runner/patterns/fanout-child-workflows/go/worker.go new file mode 100644 index 0000000..22a3be2 --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/go/worker.go @@ -0,0 +1,26 @@ +package main + +import ( + "log" + + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/worker" +) + +func main() { + c, err := client.Dial(client.Options{HostPort: "localhost:7233"}) + if err != nil { + log.Fatalln("Unable to create client:", err) + } + defer c.Close() + + w := worker.New(c, TaskQueue, worker.Options{}) + w.RegisterWorkflow(FanOutWorkflow) + w.RegisterWorkflow(RecordBatchWorkflow) + w.RegisterActivity(ProcessRecord) + + log.Printf("Worker listening on task queue '%s'", TaskQueue) + if err := w.Run(worker.InterruptCh()); err != nil { + log.Fatalln("Worker run failed:", err) + } +} diff --git a/sandbox-runner/patterns/fanout-child-workflows/go/workflows.go b/sandbox-runner/patterns/fanout-child-workflows/go/workflows.go new file mode 100644 index 0000000..fea7a0e --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/go/workflows.go @@ -0,0 +1,64 @@ +package main + +import ( + "fmt" + "time" + + "go.temporal.io/sdk/workflow" +) + +// RecordBatchWorkflow is the child workflow that processes a contiguous slice +// of records [offset, offset+length). +func RecordBatchWorkflow(ctx workflow.Context, offset int, length int) (int, error) { + ao := workflow.ActivityOptions{StartToCloseTimeout: 10 * time.Second} + ctx = workflow.WithActivityOptions(ctx, ao) + + processed := 0 + for i := offset; i < offset+length; i++ { + if err := workflow.ExecuteActivity(ctx, ProcessRecord, i).Get(ctx, nil); err != nil { + return processed, err + } + processed++ + } + workflow.GetLogger(ctx).Info("Batch complete", "offset", offset, "length", length, "processed", processed) + return processed, nil +} + +// FanOutWorkflow is the parent workflow that splits the total record set into +// chunks and starts one child workflow per chunk. +func FanOutWorkflow(ctx workflow.Context, totalRecords int, chunkSize int) (int, error) { + if chunkSize <= 0 { + chunkSize = ChunkSize + } + + parentID := workflow.GetInfo(ctx).WorkflowExecution.ID + var futures []workflow.Future + + for offset := 0; offset < totalRecords; offset += chunkSize { + length := chunkSize + if offset+chunkSize > totalRecords { + length = totalRecords - offset + } + off := offset // capture loop variable + cwo := workflow.ChildWorkflowOptions{ + WorkflowID: fmt.Sprintf("%s/batch-%d", parentID, off), + TaskQueue: TaskQueue, + } + cctx := workflow.WithChildOptions(ctx, cwo) + futures = append(futures, workflow.ExecuteChildWorkflow(cctx, RecordBatchWorkflow, off, length)) + } + + total := 0 + for _, f := range futures { + var n int + if err := f.Get(ctx, &n); err != nil { + return total, err + } + total += n + } + workflow.GetLogger(ctx).Info("Fan-out complete", + "totalRecords", totalRecords, + "chunks", len(futures), + "total", total) + return total, nil +} diff --git a/sandbox-runner/patterns/fanout-child-workflows/java/_warmup/Warmup.java b/sandbox-runner/patterns/fanout-child-workflows/java/_warmup/Warmup.java new file mode 100644 index 0000000..0ba640c --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/java/_warmup/Warmup.java @@ -0,0 +1,26 @@ +// Stub class compiled at image-build time so the Temporal Java SDK and the +// Maven compiler plugin exercise their classloading paths once before any +// user code runs. The image factory copies this into src/main/java/, runs +// `mvn compile`, then deletes both the source and target/ from the snapshot. +// +// Living under _warmup/ keeps it out of the runtime source tree the runner +// uploads on each launch. + +import io.temporal.activity.ActivityInterface; +import io.temporal.client.WorkflowClient; +import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.worker.WorkerFactory; +import io.temporal.workflow.WorkflowInterface; + +public class Warmup { + @SuppressWarnings("unused") + private static final Class[] FORCE_RESOLVE = { + ActivityInterface.class, + WorkflowInterface.class, + WorkflowClient.class, + WorkflowServiceStubs.class, + WorkerFactory.class, + }; + + public static void main(String[] args) {} +} diff --git a/sandbox-runner/patterns/fanout-child-workflows/java/pom.xml b/sandbox-runner/patterns/fanout-child-workflows/java/pom.xml new file mode 100644 index 0000000..70e8adb --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/java/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + example + fanout-child-workflows + 0.1.0 + jar + + + 17 + 17 + UTF-8 + false + + + + + io.temporal + temporal-sdk + 1.27.0 + + + org.slf4j + slf4j-simple + 2.0.13 + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.4.1 + + + + diff --git a/sandbox-runner/patterns/fanout-child-workflows/java/src/main/java/Activities.java b/sandbox-runner/patterns/fanout-child-workflows/java/src/main/java/Activities.java new file mode 100644 index 0000000..05f3231 --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/java/src/main/java/Activities.java @@ -0,0 +1,17 @@ +import io.temporal.activity.ActivityInterface; + +@ActivityInterface +public interface Activities { + void processRecord(int recordId); + + final class Impl implements Activities { + @Override + public void processRecord(int recordId) { + try { + Thread.sleep(50); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } +} diff --git a/sandbox-runner/patterns/fanout-child-workflows/java/src/main/java/FanOutWorkflow.java b/sandbox-runner/patterns/fanout-child-workflows/java/src/main/java/FanOutWorkflow.java new file mode 100644 index 0000000..1249709 --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/java/src/main/java/FanOutWorkflow.java @@ -0,0 +1,72 @@ +import io.temporal.activity.ActivityOptions; +import io.temporal.workflow.*; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +@WorkflowInterface +public interface FanOutWorkflow { + + /** Parent workflow: fans out to one child per chunk. */ + @WorkflowInterface + interface Parent { + @WorkflowMethod + int run(int totalRecords, int chunkSize); + } + + /** Child workflow: processes a contiguous slice [offset, offset+length). */ + @WorkflowInterface + interface Child { + @WorkflowMethod + int run(int offset, int length); + } + + final class ParentImpl implements Parent { + @Override + public int run(int totalRecords, int chunkSize) { + if (chunkSize <= 0) chunkSize = Shared.CHUNK_SIZE; + + String parentId = Workflow.getInfo().getWorkflowId(); + List> promises = new ArrayList<>(); + + for (int offset = 0; offset < totalRecords; offset += chunkSize) { + int length = Math.min(chunkSize, totalRecords - offset); + ChildWorkflowOptions opts = ChildWorkflowOptions.newBuilder() + .setWorkflowId(parentId + "/batch-" + offset) + .setTaskQueue(Shared.TASK_QUEUE) + .build(); + Child child = Workflow.newChildWorkflowStub(Child.class, opts); + promises.add(Async.function(child::run, offset, length)); + } + + int total = 0; + for (Promise p : promises) { + total += p.get(); + } + Workflow.getLogger(ParentImpl.class) + .info("Fan-out complete: totalRecords={} chunks={} total={}", totalRecords, promises.size(), total); + return total; + } + } + + final class ChildImpl implements Child { + private final Activities activities = Workflow.newActivityStub( + Activities.class, + ActivityOptions.newBuilder() + .setStartToCloseTimeout(Duration.ofSeconds(10)) + .build()); + + @Override + public int run(int offset, int length) { + int processed = 0; + for (int i = offset; i < offset + length; i++) { + activities.processRecord(i); + processed++; + } + Workflow.getLogger(ChildImpl.class) + .info("Batch complete: offset={} length={} processed={}", offset, length, processed); + return processed; + } + } +} diff --git a/sandbox-runner/patterns/fanout-child-workflows/java/src/main/java/Shared.java b/sandbox-runner/patterns/fanout-child-workflows/java/src/main/java/Shared.java new file mode 100644 index 0000000..0b637a0 --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/java/src/main/java/Shared.java @@ -0,0 +1,8 @@ +public final class Shared { + public static final String TASK_QUEUE = "fanout-child-workflows-task-queue"; + public static final String WORKFLOW_ID_PREFIX = "fanout"; + public static final int TOTAL_RECORDS = 20; + public static final int CHUNK_SIZE = 5; + + private Shared() {} +} diff --git a/sandbox-runner/patterns/fanout-child-workflows/java/src/main/java/Starter.java b/sandbox-runner/patterns/fanout-child-workflows/java/src/main/java/Starter.java new file mode 100644 index 0000000..644b11d --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/java/src/main/java/Starter.java @@ -0,0 +1,26 @@ +import io.temporal.client.WorkflowClient; +import io.temporal.client.WorkflowOptions; +import io.temporal.serviceclient.WorkflowServiceStubs; + +public class Starter { + public static void main(String[] args) { + WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs(); + WorkflowClient client = WorkflowClient.newInstance(service); + + String workflowId = Shared.WORKFLOW_ID_PREFIX + "-" + System.currentTimeMillis(); + FanOutWorkflow.Parent workflow = client.newWorkflowStub( + FanOutWorkflow.Parent.class, + WorkflowOptions.newBuilder() + .setTaskQueue(Shared.TASK_QUEUE) + .setWorkflowId(workflowId) + .build()); + + System.out.println("Started workflow: " + workflowId); + System.out.println("Processing " + Shared.TOTAL_RECORDS + " records in chunks of " + Shared.CHUNK_SIZE + "…"); + int total = workflow.run(Shared.TOTAL_RECORDS, Shared.CHUNK_SIZE); + System.out.println("Fan-out complete: processed " + total + " records"); + System.out.println( + "Open the Temporal UI and search for '" + workflowId + + "' to see the parent and child workflows."); + } +} diff --git a/sandbox-runner/patterns/fanout-child-workflows/java/src/main/java/Worker.java b/sandbox-runner/patterns/fanout-child-workflows/java/src/main/java/Worker.java new file mode 100644 index 0000000..90eceef --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/java/src/main/java/Worker.java @@ -0,0 +1,26 @@ +import io.temporal.client.WorkflowClient; +import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.worker.WorkerFactory; + +public class Worker { + public static void main(String[] args) { + WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs(); + WorkflowClient client = WorkflowClient.newInstance(service); + + WorkerFactory factory = WorkerFactory.newInstance(client); + io.temporal.worker.Worker worker = factory.newWorker(Shared.TASK_QUEUE); + worker.registerWorkflowImplementationTypes( + FanOutWorkflow.ParentImpl.class, + FanOutWorkflow.ChildImpl.class); + worker.registerActivitiesImplementations(new Activities.Impl()); + + System.out.println("Worker listening on task queue '" + Shared.TASK_QUEUE + "'"); + factory.start(); + + try { + Thread.currentThread().join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/sandbox-runner/patterns/fanout-child-workflows/pattern.json b/sandbox-runner/patterns/fanout-child-workflows/pattern.json new file mode 100644 index 0000000..773d13a --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/pattern.json @@ -0,0 +1,42 @@ +{ + "name": "Fan-Out with Child Workflows", + "languages": { + "typescript": { + "label": "TypeScript", + "files": ["starter.ts", "worker.ts", "workflows.ts", "activities.ts", "shared.ts"], + "worker": "npx tsx worker.ts", + "starter": "npx tsx starter.ts", + "workerProcessPattern": "tsx worker.ts" + }, + "python": { + "label": "Python", + "files": ["starter.py", "worker.py", "workflows.py", "activities.py", "shared.py"], + "worker": "uv run python worker.py", + "starter": "uv run python starter.py", + "workerProcessPattern": "python worker.py" + }, + "go": { + "label": "Go", + "files": ["starter.go", "worker.go", "workflows.go", "activities.go", "shared.go"], + "worker": "go run worker.go workflows.go activities.go shared.go", + "starter": "go run starter.go workflows.go activities.go shared.go", + "workerProcessPattern": "go run worker.go", + "diskGib": 2 + }, + "java": { + "label": "Java", + "files": [ + "src/main/java/Starter.java", + "src/main/java/Worker.java", + "src/main/java/FanOutWorkflow.java", + "src/main/java/Activities.java", + "src/main/java/Shared.java" + ], + "worker": "mvn -q -B compile exec:java -Dexec.mainClass=Worker", + "starter": "mvn -q -B compile exec:java -Dexec.mainClass=Starter", + "workerProcessPattern": "exec.mainClass=Worker", + "diskGib": 3, + "memoryGib": 3 + } + } +} diff --git a/sandbox-runner/patterns/fanout-child-workflows/python/activities.py b/sandbox-runner/patterns/fanout-child-workflows/python/activities.py new file mode 100644 index 0000000..c132f65 --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/python/activities.py @@ -0,0 +1,9 @@ +import asyncio + +from temporalio import activity + + +@activity.defn +async def process_record(record_id: int) -> None: + # Simulate processing work. + await asyncio.sleep(0.05) diff --git a/sandbox-runner/patterns/fanout-child-workflows/python/pyproject.toml b/sandbox-runner/patterns/fanout-child-workflows/python/pyproject.toml new file mode 100644 index 0000000..d1f17f0 --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/python/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "fanout-child-workflows-python" +version = "0.1.0" +description = "Fan-Out with Child Workflows pattern demo (Python)" +requires-python = ">=3.12" +dependencies = [ + "temporalio==1.9.0", +] diff --git a/sandbox-runner/patterns/fanout-child-workflows/python/shared.py b/sandbox-runner/patterns/fanout-child-workflows/python/shared.py new file mode 100644 index 0000000..e438a64 --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/python/shared.py @@ -0,0 +1,4 @@ +TASK_QUEUE = "fanout-child-workflows-task-queue" +WORKFLOW_ID_PREFIX = "fanout" +TOTAL_RECORDS = 20 +CHUNK_SIZE = 5 diff --git a/sandbox-runner/patterns/fanout-child-workflows/python/starter.py b/sandbox-runner/patterns/fanout-child-workflows/python/starter.py new file mode 100644 index 0000000..d48b79c --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/python/starter.py @@ -0,0 +1,30 @@ +import asyncio +import time + +from temporalio.client import Client + +from shared import CHUNK_SIZE, TASK_QUEUE, TOTAL_RECORDS, WORKFLOW_ID_PREFIX +from workflows import FanOutWorkflow + + +async def main() -> None: + client = await Client.connect("localhost:7233") + workflow_id = f"{WORKFLOW_ID_PREFIX}-{int(time.time() * 1000)}" + handle = await client.start_workflow( + FanOutWorkflow.run, + args=[TOTAL_RECORDS, CHUNK_SIZE], + id=workflow_id, + task_queue=TASK_QUEUE, + ) + print(f"Started workflow: {workflow_id}") + print(f"Processing {TOTAL_RECORDS} records in chunks of {CHUNK_SIZE}…") + + total = await handle.result() + print(f"Fan-out complete: processed {total} records") + print( + f"Open the Temporal UI and search for '{workflow_id}' to see the parent and child workflows." + ) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/sandbox-runner/patterns/fanout-child-workflows/python/worker.py b/sandbox-runner/patterns/fanout-child-workflows/python/worker.py new file mode 100644 index 0000000..05aec7f --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/python/worker.py @@ -0,0 +1,24 @@ +import asyncio + +from temporalio.client import Client +from temporalio.worker import Worker + +from activities import process_record +from shared import TASK_QUEUE +from workflows import FanOutWorkflow, RecordBatchWorkflow + + +async def main() -> None: + client = await Client.connect("localhost:7233") + worker = Worker( + client, + task_queue=TASK_QUEUE, + workflows=[FanOutWorkflow, RecordBatchWorkflow], + activities=[process_record], + ) + print(f"Worker listening on task queue '{TASK_QUEUE}'", flush=True) + await worker.run() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/sandbox-runner/patterns/fanout-child-workflows/python/workflows.py b/sandbox-runner/patterns/fanout-child-workflows/python/workflows.py new file mode 100644 index 0000000..92e6cce --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/python/workflows.py @@ -0,0 +1,56 @@ +import asyncio +from datetime import timedelta + +from temporalio import workflow +from temporalio.workflow import ChildWorkflowHandle + +from activities import process_record +from shared import CHUNK_SIZE, TASK_QUEUE + + +@workflow.defn +class RecordBatchWorkflow: + """Child workflow: processes a contiguous slice of records [offset, offset+length).""" + + @workflow.run + async def run(self, offset: int, length: int) -> int: + processed = 0 + for i in range(offset, offset + length): + await workflow.execute_activity( + process_record, + i, + start_to_close_timeout=timedelta(seconds=10), + ) + processed += 1 + workflow.logger.info(f"Batch complete: offset={offset} length={length} processed={processed}") + return processed + + +@workflow.defn +class FanOutWorkflow: + """Parent workflow: splits the total record set into chunks and fans out to + one child workflow per chunk.""" + + @workflow.run + async def run(self, total_records: int, chunk_size: int = CHUNK_SIZE) -> int: + parent_id = workflow.info().workflow_id + handles: list[ChildWorkflowHandle] = [] + + offset = 0 + while offset < total_records: + length = min(chunk_size, total_records - offset) + handle = await workflow.start_child_workflow( + RecordBatchWorkflow.run, + args=[offset, length], + id=f"{parent_id}/batch-{offset}", + task_queue=TASK_QUEUE, + ) + handles.append(handle) + offset += chunk_size + + results = await asyncio.gather(*handles) + total = sum(results) + workflow.logger.info( + f"Fan-out complete: totalRecords={total_records} chunks={len(handles)} total={total}" + ) + return total diff --git a/sandbox-runner/patterns/fanout-child-workflows/typescript/activities.ts b/sandbox-runner/patterns/fanout-child-workflows/typescript/activities.ts new file mode 100644 index 0000000..0b6632d --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/typescript/activities.ts @@ -0,0 +1,6 @@ +import { TOTAL_RECORDS } from "./shared"; + +export async function processRecord(recordId: number): Promise { + // Simulate processing work. + await new Promise((r) => setTimeout(r, 50)); +} diff --git a/sandbox-runner/patterns/fanout-child-workflows/typescript/package.json b/sandbox-runner/patterns/fanout-child-workflows/typescript/package.json new file mode 100644 index 0000000..3a0a12c --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/typescript/package.json @@ -0,0 +1,14 @@ +{ + "name": "temporal-design-patterns-sandbox", + "version": "0.1.0", + "private": true, + "type": "commonjs", + "dependencies": { + "@temporalio/activity": "^1.13.0", + "@temporalio/client": "^1.13.0", + "@temporalio/worker": "^1.13.0", + "@temporalio/workflow": "^1.13.0", + "tsx": "^4.19.2", + "typescript": "^5.6.3" + } +} diff --git a/sandbox-runner/patterns/fanout-child-workflows/typescript/shared.ts b/sandbox-runner/patterns/fanout-child-workflows/typescript/shared.ts new file mode 100644 index 0000000..e40579a --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/typescript/shared.ts @@ -0,0 +1,4 @@ +export const TASK_QUEUE = "fanout-child-workflows-task-queue"; +export const WORKFLOW_ID_PREFIX = "fanout"; +export const TOTAL_RECORDS = 20; +export const CHUNK_SIZE = 5; diff --git a/sandbox-runner/patterns/fanout-child-workflows/typescript/starter.ts b/sandbox-runner/patterns/fanout-child-workflows/typescript/starter.ts new file mode 100644 index 0000000..b8b1ccd --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/typescript/starter.ts @@ -0,0 +1,32 @@ +import { Client, Connection } from "@temporalio/client"; + +import { CHUNK_SIZE, TASK_QUEUE, TOTAL_RECORDS, WORKFLOW_ID_PREFIX } from "./shared"; +import { fanOutWorkflow } from "./workflows"; + +async function main(): Promise { + const connection = await Connection.connect(); + try { + const client = new Client({ connection }); + const workflowId = `${WORKFLOW_ID_PREFIX}-${Date.now()}`; + const handle = await client.workflow.start(fanOutWorkflow, { + args: [TOTAL_RECORDS, CHUNK_SIZE], + taskQueue: TASK_QUEUE, + workflowId, + }); + console.log(`Started workflow: ${workflowId}`); + console.log(`Processing ${TOTAL_RECORDS} records in chunks of ${CHUNK_SIZE}…`); + + const total = await handle.result(); + console.log(`Fan-out complete: processed ${total} records`); + console.log( + `Open the Temporal UI and search for '${workflowId}' to see the parent and child workflows.` + ); + } finally { + await connection.close(); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/sandbox-runner/patterns/fanout-child-workflows/typescript/worker.ts b/sandbox-runner/patterns/fanout-child-workflows/typescript/worker.ts new file mode 100644 index 0000000..da949aa --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/typescript/worker.ts @@ -0,0 +1,19 @@ +import { Worker } from "@temporalio/worker"; + +import * as activities from "./activities"; +import { TASK_QUEUE } from "./shared"; + +async function main(): Promise { + const worker = await Worker.create({ + workflowsPath: require.resolve("./workflows"), + activities, + taskQueue: TASK_QUEUE, + }); + console.log(`Worker listening on task queue '${TASK_QUEUE}'`); + await worker.run(); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/sandbox-runner/patterns/fanout-child-workflows/typescript/workflows.ts b/sandbox-runner/patterns/fanout-child-workflows/typescript/workflows.ts new file mode 100644 index 0000000..aeddc65 --- /dev/null +++ b/sandbox-runner/patterns/fanout-child-workflows/typescript/workflows.ts @@ -0,0 +1,57 @@ +import { + executeChild, + log, + proxyActivities, + workflowInfo, +} from "@temporalio/workflow"; + +import type * as activities from "./activities"; +import { CHUNK_SIZE, TASK_QUEUE } from "./shared"; + +const { processRecord } = proxyActivities({ + startToCloseTimeout: "10 seconds", +}); + +/** + * Child workflow: processes a contiguous slice of records [offset, offset+length). + */ +export async function recordBatchWorkflow( + offset: number, + length: number +): Promise { + let processed = 0; + for (let i = offset; i < offset + length; i++) { + await processRecord(i); + processed++; + } + log.info(`Batch complete`, { offset, length, processed }); + return processed; +} + +/** + * Parent workflow: splits the total record set into chunks and fans out to + * one child workflow per chunk. + */ +export async function fanOutWorkflow( + totalRecords: number, + chunkSize: number = CHUNK_SIZE +): Promise { + const parentId = workflowInfo().workflowId; + const children: Promise[] = []; + + for (let offset = 0; offset < totalRecords; offset += chunkSize) { + const length = Math.min(chunkSize, totalRecords - offset); + children.push( + executeChild(recordBatchWorkflow, { + args: [offset, length], + taskQueue: TASK_QUEUE, + workflowId: `${parentId}/batch-${offset}`, + }) + ); + } + + const results = await Promise.all(children); + const total = results.reduce((sum, n) => sum + n, 0); + log.info(`Fan-out complete`, { totalRecords, chunks: children.length, total }); + return total; +} diff --git a/sandbox-runner/patterns/mapreduce-tree/go/activities.go b/sandbox-runner/patterns/mapreduce-tree/go/activities.go new file mode 100644 index 0000000..ee5fcf2 --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/go/activities.go @@ -0,0 +1,13 @@ +package main + +import ( + "context" + "fmt" + "time" +) + +func ProcessLeaf(_ context.Context, record string) (string, error) { + // Simulate processing and return a result string. + time.Sleep(50 * time.Millisecond) + return fmt.Sprintf("processed(%s)", record), nil +} diff --git a/sandbox-runner/patterns/mapreduce-tree/go/go.mod b/sandbox-runner/patterns/mapreduce-tree/go/go.mod new file mode 100644 index 0000000..af33e51 --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/go/go.mod @@ -0,0 +1,33 @@ +module mapreduce-tree + +go 1.22 + +require go.temporal.io/sdk v1.32.1 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + github.com/nexus-rpc/sdk-go v0.1.0 // indirect + github.com/pborman/uuid v1.2.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/robfig/cron v1.2.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/testify v1.10.0 // indirect + go.temporal.io/api v1.43.0 // indirect + golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.17.0 // indirect + golang.org/x/time v0.3.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect + google.golang.org/grpc v1.66.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/sandbox-runner/patterns/mapreduce-tree/go/go.sum b/sandbox-runner/patterns/mapreduce-tree/go/go.sum new file mode 100644 index 0000000..a88ecd1 --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/go/go.sum @@ -0,0 +1,179 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= +github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/nexus-rpc/sdk-go v0.1.0 h1:PUL/0vEY1//WnqyEHT5ao4LBRQ6MeNUihmnNGn0xMWY= +github.com/nexus-rpc/sdk-go v0.1.0/go.mod h1:TpfkM2Cw0Rlk9drGkoiSMpFqflKTiQLWUNyKJjF8mKQ= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= +github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= +github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.temporal.io/api v1.43.0 h1:lBhq+u5qFJqGMXwWsmg/i8qn1UA/3LCwVc88l2xUMHg= +go.temporal.io/api v1.43.0/go.mod h1:1WwYUMo6lao8yl0371xWUm13paHExN5ATYT/B7QtFis= +go.temporal.io/sdk v1.32.1 h1:slA8prhdFr4lxpsTcRusWVitD/cGjELfKUh0mBj73SU= +go.temporal.io/sdk v1.32.1/go.mod h1:8U8H7rF9u4Hyb4Ry9yiEls5716DHPNvVITPNkgWUwE8= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= +golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed h1:3RgNmBoI9MZhsj3QxC+AP/qQhNwpCLOvYDYYsFrhFt0= +google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed h1:J6izYgfBXAI3xTKLgxzTmUltdYaLsuBxFCgDHWJ/eXg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= +google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/sandbox-runner/patterns/mapreduce-tree/go/shared.go b/sandbox-runner/patterns/mapreduce-tree/go/shared.go new file mode 100644 index 0000000..686362e --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/go/shared.go @@ -0,0 +1,26 @@ +package main + +import "fmt" + +const ( + TaskQueue = "mapreduce-tree-task-queue" + WorkflowIDPrefix = "mapreduce-tree" + LeafThreshold = 3 + MaxDepth = 5 + ResultSignal = "nodeResult" +) + +// Records is the demo record set. +var Records = func() []string { + r := make([]string, 9) + for i := range r { + r[i] = fmt.Sprintf("item-%d", i) + } + return r +}() + +// ResultPayload is the signal payload sent from child to parent. +type ResultPayload struct { + ID string `json:"id"` + Results []string `json:"results"` +} diff --git a/sandbox-runner/patterns/mapreduce-tree/go/starter.go b/sandbox-runner/patterns/mapreduce-tree/go/starter.go new file mode 100644 index 0000000..387854b --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/go/starter.go @@ -0,0 +1,48 @@ +package main + +import ( + "context" + "fmt" + "log" + "strings" + "time" + + "go.temporal.io/sdk/client" +) + +func main() { + c, err := client.Dial(client.Options{HostPort: "localhost:7233"}) + if err != nil { + log.Fatalln("Unable to create client:", err) + } + defer c.Close() + + workflowID := fmt.Sprintf("%s-%d", WorkflowIDPrefix, time.Now().UnixMilli()) + we, err := c.ExecuteWorkflow( + context.Background(), + client.StartWorkflowOptions{ + ID: workflowID, + TaskQueue: TaskQueue, + }, + NodeWorkflow, + Records, + 0, + "", + ) + if err != nil { + log.Fatalln("Unable to execute workflow:", err) + } + fmt.Printf("Started workflow: %s\n", we.GetID()) + fmt.Printf("Processing %d records via MapReduce Tree…\n", len(Records)) + + var results []string + if err := we.Get(context.Background(), &results); err != nil { + log.Fatalln("Workflow failed:", err) + } + fmt.Printf("MapReduce Tree complete: %d results\n", len(results)) + fmt.Printf("Results: %s\n", strings.Join(results, ", ")) + fmt.Printf( + "Open the Temporal UI and search for '%s' to see the full workflow tree.\n", + workflowID, + ) +} diff --git a/sandbox-runner/patterns/mapreduce-tree/go/warmup.go b/sandbox-runner/patterns/mapreduce-tree/go/warmup.go new file mode 100644 index 0000000..62afb71 --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/go/warmup.go @@ -0,0 +1,13 @@ +// Stub program compiled at image-build time so the Temporal Go SDK and its +// transitive deps land in the Go build cache before any user code runs. +// The image factory deletes this file after building. +package main + +import ( + _ "go.temporal.io/sdk/activity" + _ "go.temporal.io/sdk/client" + _ "go.temporal.io/sdk/worker" + _ "go.temporal.io/sdk/workflow" +) + +func main() {} diff --git a/sandbox-runner/patterns/mapreduce-tree/go/worker.go b/sandbox-runner/patterns/mapreduce-tree/go/worker.go new file mode 100644 index 0000000..6680e00 --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/go/worker.go @@ -0,0 +1,26 @@ +package main + +import ( + "log" + + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/worker" +) + +func main() { + c, err := client.Dial(client.Options{HostPort: "localhost:7233"}) + if err != nil { + log.Fatalln("Unable to create client:", err) + } + defer c.Close() + + w := worker.New(c, TaskQueue, worker.Options{}) + w.RegisterWorkflow(NodeWorkflow) + w.RegisterWorkflow(LeafWorkflow) + w.RegisterActivity(ProcessLeaf) + + log.Printf("Worker listening on task queue '%s'", TaskQueue) + if err := w.Run(worker.InterruptCh()); err != nil { + log.Fatalln("Worker run failed:", err) + } +} diff --git a/sandbox-runner/patterns/mapreduce-tree/go/workflows.go b/sandbox-runner/patterns/mapreduce-tree/go/workflows.go new file mode 100644 index 0000000..e0fae09 --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/go/workflows.go @@ -0,0 +1,84 @@ +package main + +import ( + "fmt" + "time" + + "go.temporal.io/sdk/workflow" +) + +// LeafWorkflow processes one record via an Activity and signals the result +// back to the parent Node workflow. +func LeafWorkflow(ctx workflow.Context, record string, parentWorkflowID string) error { + ao := workflow.ActivityOptions{StartToCloseTimeout: 30 * time.Second} + ctx = workflow.WithActivityOptions(ctx, ao) + + var result string + if err := workflow.ExecuteActivity(ctx, ProcessLeaf, record).Get(ctx, &result); err != nil { + return err + } + workflow.GetLogger(ctx).Info("Leaf processed", "record", record, "result", result) + + payload := ResultPayload{ID: record, Results: []string{result}} + return workflow.SignalExternalWorkflow(ctx, parentWorkflowID, "", ResultSignal, payload).Get(ctx, nil) +} + +// NodeWorkflow recursively splits the record set or fans out to leaf workflows, +// collects results via signals, then signals aggregated results up to its parent. +func NodeWorkflow(ctx workflow.Context, records []string, depth int, parentWorkflowID string) ([]string, error) { + if depth > MaxDepth { + return nil, fmt.Errorf("tree depth exceeded %d", MaxDepth) + } + + myID := workflow.GetInfo(ctx).WorkflowExecution.ID + resultCh := workflow.GetSignalChannel(ctx, ResultSignal) + + var collected []string + expected := 0 + + if len(records) <= LeafThreshold { + // Fan out to leaf workflows — one per record. + expected = len(records) + for _, record := range records { + cwo := workflow.ChildWorkflowOptions{ + WorkflowID: myID + "/leaf-" + record, + TaskQueue: TaskQueue, + } + workflow.ExecuteChildWorkflow(workflow.WithChildOptions(ctx, cwo), LeafWorkflow, record, myID) + } + } else { + // Split and recurse into child node workflows. + mid := len(records) / 2 + chunks := [][]string{records[:mid], records[mid:]} + expected = len(chunks) + for i, chunk := range chunks { + cwo := workflow.ChildWorkflowOptions{ + WorkflowID: fmt.Sprintf("%s/node-d%d-%d", myID, depth+1, i), + TaskQueue: TaskQueue, + } + workflow.ExecuteChildWorkflow(workflow.WithChildOptions(ctx, cwo), NodeWorkflow, chunk, depth+1, myID) + } + } + + // Collect all expected signals. + for i := 0; i < expected; i++ { + var payload ResultPayload + resultCh.Receive(ctx, &payload) + collected = append(collected, payload.Results...) + } + + workflow.GetLogger(ctx).Info("Node complete", + "depth", depth, + "records", len(records), + "results", len(collected)) + + // Signal aggregated results up to parent (if this is not the root). + if parentWorkflowID != "" { + payload := ResultPayload{ID: myID, Results: collected} + if err := workflow.SignalExternalWorkflow(ctx, parentWorkflowID, "", ResultSignal, payload).Get(ctx, nil); err != nil { + return collected, err + } + } + + return collected, nil +} diff --git a/sandbox-runner/patterns/mapreduce-tree/java/_warmup/Warmup.java b/sandbox-runner/patterns/mapreduce-tree/java/_warmup/Warmup.java new file mode 100644 index 0000000..2fc5ccb --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/java/_warmup/Warmup.java @@ -0,0 +1,23 @@ +// Stub class compiled at image-build time so the Temporal Java SDK and the +// Maven compiler plugin exercise their classloading paths once before any +// user code runs. The image factory copies this into src/main/java/, runs +// `mvn compile`, then deletes both the source and target/ from the snapshot. + +import io.temporal.activity.ActivityInterface; +import io.temporal.client.WorkflowClient; +import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.worker.WorkerFactory; +import io.temporal.workflow.WorkflowInterface; + +public class Warmup { + @SuppressWarnings("unused") + private static final Class[] FORCE_RESOLVE = { + ActivityInterface.class, + WorkflowInterface.class, + WorkflowClient.class, + WorkflowServiceStubs.class, + WorkerFactory.class, + }; + + public static void main(String[] args) {} +} diff --git a/sandbox-runner/patterns/mapreduce-tree/java/pom.xml b/sandbox-runner/patterns/mapreduce-tree/java/pom.xml new file mode 100644 index 0000000..f1b7e6a --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/java/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + example + mapreduce-tree + 0.1.0 + jar + + + 17 + 17 + UTF-8 + false + + + + + io.temporal + temporal-sdk + 1.27.0 + + + org.slf4j + slf4j-simple + 2.0.13 + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.4.1 + + + + diff --git a/sandbox-runner/patterns/mapreduce-tree/java/src/main/java/Activities.java b/sandbox-runner/patterns/mapreduce-tree/java/src/main/java/Activities.java new file mode 100644 index 0000000..2ccdb41 --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/java/src/main/java/Activities.java @@ -0,0 +1,18 @@ +import io.temporal.activity.ActivityInterface; + +@ActivityInterface +public interface Activities { + String processLeaf(String record); + + final class Impl implements Activities { + @Override + public String processLeaf(String record) { + try { + Thread.sleep(50); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return "processed(" + record + ")"; + } + } +} diff --git a/sandbox-runner/patterns/mapreduce-tree/java/src/main/java/NodeWorkflow.java b/sandbox-runner/patterns/mapreduce-tree/java/src/main/java/NodeWorkflow.java new file mode 100644 index 0000000..4cbca66 --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/java/src/main/java/NodeWorkflow.java @@ -0,0 +1,108 @@ +import io.temporal.activity.ActivityOptions; +import io.temporal.workflow.*; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +public interface NodeWorkflow { + + /** Node workflow (and root): splits or fans out, collects results, signals parent. */ + @WorkflowInterface + interface Node { + @WorkflowMethod + List run(List records, int depth, String parentWorkflowId); + + @SignalMethod + void nodeResult(String id, List results); + } + + /** Leaf workflow: processes one record and signals the parent node. */ + @WorkflowInterface + interface Leaf { + @WorkflowMethod + void run(String record, String parentWorkflowId); + } + + final class NodeImpl implements Node { + private final List collected = new ArrayList<>(); + private int received = 0; + + @Override + public void nodeResult(String id, List results) { + collected.addAll(results); + received++; + } + + @Override + public List run(List records, int depth, String parentWorkflowId) { + if (depth > Shared.MAX_DEPTH) { + throw new RuntimeException("Tree depth exceeded " + Shared.MAX_DEPTH); + } + + String myId = Workflow.getInfo().getWorkflowId(); + int expected; + + if (records.size() <= Shared.LEAF_THRESHOLD) { + // Fan out to leaf workflows — one per record. + expected = records.size(); + for (String record : records) { + ChildWorkflowOptions opts = ChildWorkflowOptions.newBuilder() + .setWorkflowId(myId + "/leaf-" + record) + .setTaskQueue(Shared.TASK_QUEUE) + .build(); + Leaf leaf = Workflow.newChildWorkflowStub(Leaf.class, opts); + Async.procedure(leaf::run, record, myId); + } + } else { + // Split and recurse into child node workflows. + int mid = records.size() / 2; + List> chunks = List.of( + records.subList(0, mid), + records.subList(mid, records.size())); + expected = chunks.size(); + for (int i = 0; i < chunks.size(); i++) { + ChildWorkflowOptions opts = ChildWorkflowOptions.newBuilder() + .setWorkflowId(String.format("%s/node-d%d-%d", myId, depth + 1, i)) + .setTaskQueue(Shared.TASK_QUEUE) + .build(); + Node child = Workflow.newChildWorkflowStub(Node.class, opts); + Async.function(child::run, chunks.get(i), depth + 1, myId); + } + } + + // Wait for all expected signals. + final int exp = expected; + Workflow.await(() -> received >= exp); + + Workflow.getLogger(NodeImpl.class) + .info("Node complete: depth={} records={} results={}", depth, records.size(), collected.size()); + + // Signal aggregated results up to parent (if not root). + if (parentWorkflowId != null && !parentWorkflowId.isEmpty()) { + ExternalWorkflowStub parent = Workflow.newUntypedExternalWorkflowStub(parentWorkflowId, ""); + parent.signal(Shared.RESULT_SIGNAL, myId, new ArrayList<>(collected)); + } + + return collected; + } + } + + final class LeafImpl implements Leaf { + private final Activities activities = Workflow.newActivityStub( + Activities.class, + ActivityOptions.newBuilder() + .setStartToCloseTimeout(Duration.ofSeconds(30)) + .build()); + + @Override + public void run(String record, String parentWorkflowId) { + String result = activities.processLeaf(record); + Workflow.getLogger(LeafImpl.class) + .info("Leaf processed: {} → {}", record, result); + + ExternalWorkflowStub parent = Workflow.newUntypedExternalWorkflowStub(parentWorkflowId, ""); + parent.signal(Shared.RESULT_SIGNAL, record, List.of(result)); + } + } +} diff --git a/sandbox-runner/patterns/mapreduce-tree/java/src/main/java/Shared.java b/sandbox-runner/patterns/mapreduce-tree/java/src/main/java/Shared.java new file mode 100644 index 0000000..199d614 --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/java/src/main/java/Shared.java @@ -0,0 +1,21 @@ +import java.util.ArrayList; +import java.util.List; + +public final class Shared { + public static final String TASK_QUEUE = "mapreduce-tree-task-queue"; + public static final String WORKFLOW_ID_PREFIX = "mapreduce-tree"; + public static final int LEAF_THRESHOLD = 3; + public static final int MAX_DEPTH = 5; + public static final String RESULT_SIGNAL = "nodeResult"; + + public static final List RECORDS; + + static { + RECORDS = new ArrayList<>(); + for (int i = 0; i < 9; i++) { + RECORDS.add("item-" + i); + } + } + + private Shared() {} +} diff --git a/sandbox-runner/patterns/mapreduce-tree/java/src/main/java/Starter.java b/sandbox-runner/patterns/mapreduce-tree/java/src/main/java/Starter.java new file mode 100644 index 0000000..c727689 --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/java/src/main/java/Starter.java @@ -0,0 +1,29 @@ +import io.temporal.client.WorkflowClient; +import io.temporal.client.WorkflowOptions; +import io.temporal.serviceclient.WorkflowServiceStubs; + +import java.util.List; + +public class Starter { + public static void main(String[] args) { + WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs(); + WorkflowClient client = WorkflowClient.newInstance(service); + + String workflowId = Shared.WORKFLOW_ID_PREFIX + "-" + System.currentTimeMillis(); + NodeWorkflow.Node workflow = client.newWorkflowStub( + NodeWorkflow.Node.class, + WorkflowOptions.newBuilder() + .setTaskQueue(Shared.TASK_QUEUE) + .setWorkflowId(workflowId) + .build()); + + System.out.println("Started workflow: " + workflowId); + System.out.println("Processing " + Shared.RECORDS.size() + " records via MapReduce Tree…"); + List results = workflow.run(Shared.RECORDS, 0, ""); + System.out.println("MapReduce Tree complete: " + results.size() + " results"); + System.out.println("Results: " + String.join(", ", results)); + System.out.println( + "Open the Temporal UI and search for '" + workflowId + + "' to see the full workflow tree."); + } +} diff --git a/sandbox-runner/patterns/mapreduce-tree/java/src/main/java/Worker.java b/sandbox-runner/patterns/mapreduce-tree/java/src/main/java/Worker.java new file mode 100644 index 0000000..845c485 --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/java/src/main/java/Worker.java @@ -0,0 +1,26 @@ +import io.temporal.client.WorkflowClient; +import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.worker.WorkerFactory; + +public class Worker { + public static void main(String[] args) { + WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs(); + WorkflowClient client = WorkflowClient.newInstance(service); + + WorkerFactory factory = WorkerFactory.newInstance(client); + io.temporal.worker.Worker worker = factory.newWorker(Shared.TASK_QUEUE); + worker.registerWorkflowImplementationTypes( + NodeWorkflow.NodeImpl.class, + NodeWorkflow.LeafImpl.class); + worker.registerActivitiesImplementations(new Activities.Impl()); + + System.out.println("Worker listening on task queue '" + Shared.TASK_QUEUE + "'"); + factory.start(); + + try { + Thread.currentThread().join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/sandbox-runner/patterns/mapreduce-tree/pattern.json b/sandbox-runner/patterns/mapreduce-tree/pattern.json new file mode 100644 index 0000000..7c76247 --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/pattern.json @@ -0,0 +1,42 @@ +{ + "name": "MapReduce Tree", + "languages": { + "typescript": { + "label": "TypeScript", + "files": ["starter.ts", "worker.ts", "workflows.ts", "activities.ts", "shared.ts"], + "worker": "npx tsx worker.ts", + "starter": "npx tsx starter.ts", + "workerProcessPattern": "tsx worker.ts" + }, + "python": { + "label": "Python", + "files": ["starter.py", "worker.py", "workflows.py", "activities.py", "shared.py"], + "worker": "uv run python worker.py", + "starter": "uv run python starter.py", + "workerProcessPattern": "python worker.py" + }, + "go": { + "label": "Go", + "files": ["starter.go", "worker.go", "workflows.go", "activities.go", "shared.go"], + "worker": "go run worker.go workflows.go activities.go shared.go", + "starter": "go run starter.go workflows.go activities.go shared.go", + "workerProcessPattern": "go run worker.go", + "diskGib": 2 + }, + "java": { + "label": "Java", + "files": [ + "src/main/java/Starter.java", + "src/main/java/Worker.java", + "src/main/java/NodeWorkflow.java", + "src/main/java/Activities.java", + "src/main/java/Shared.java" + ], + "worker": "mvn -q -B compile exec:java -Dexec.mainClass=Worker", + "starter": "mvn -q -B compile exec:java -Dexec.mainClass=Starter", + "workerProcessPattern": "exec.mainClass=Worker", + "diskGib": 3, + "memoryGib": 3 + } + } +} diff --git a/sandbox-runner/patterns/mapreduce-tree/python/activities.py b/sandbox-runner/patterns/mapreduce-tree/python/activities.py new file mode 100644 index 0000000..788396c --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/python/activities.py @@ -0,0 +1,10 @@ +import asyncio + +from temporalio import activity + + +@activity.defn +async def process_leaf(record: str) -> str: + # Simulate processing and return a result string. + await asyncio.sleep(0.05) + return f"processed({record})" diff --git a/sandbox-runner/patterns/mapreduce-tree/python/pyproject.toml b/sandbox-runner/patterns/mapreduce-tree/python/pyproject.toml new file mode 100644 index 0000000..1c659f0 --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/python/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "mapreduce-tree-python" +version = "0.1.0" +description = "MapReduce Tree pattern demo (Python)" +requires-python = ">=3.12" +dependencies = [ + "temporalio==1.9.0", +] diff --git a/sandbox-runner/patterns/mapreduce-tree/python/shared.py b/sandbox-runner/patterns/mapreduce-tree/python/shared.py new file mode 100644 index 0000000..c904925 --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/python/shared.py @@ -0,0 +1,6 @@ +TASK_QUEUE = "mapreduce-tree-task-queue" +WORKFLOW_ID_PREFIX = "mapreduce-tree" +LEAF_THRESHOLD = 3 +MAX_DEPTH = 5 +RESULT_SIGNAL = "nodeResult" +RECORDS = [f"item-{i}" for i in range(9)] diff --git a/sandbox-runner/patterns/mapreduce-tree/python/starter.py b/sandbox-runner/patterns/mapreduce-tree/python/starter.py new file mode 100644 index 0000000..56f94b7 --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/python/starter.py @@ -0,0 +1,31 @@ +import asyncio +import time + +from temporalio.client import Client + +from shared import RECORDS, TASK_QUEUE, WORKFLOW_ID_PREFIX +from workflows import NodeWorkflow + + +async def main() -> None: + client = await Client.connect("localhost:7233") + workflow_id = f"{WORKFLOW_ID_PREFIX}-{int(time.time() * 1000)}" + handle = await client.start_workflow( + NodeWorkflow.run, + args=[RECORDS, 0, ""], + id=workflow_id, + task_queue=TASK_QUEUE, + ) + print(f"Started workflow: {workflow_id}") + print(f"Processing {len(RECORDS)} records via MapReduce Tree…") + + results = await handle.result() + print(f"MapReduce Tree complete: {len(results)} results") + print(f"Results: {', '.join(results)}") + print( + f"Open the Temporal UI and search for '{workflow_id}' to see the full workflow tree." + ) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/sandbox-runner/patterns/mapreduce-tree/python/worker.py b/sandbox-runner/patterns/mapreduce-tree/python/worker.py new file mode 100644 index 0000000..e924de5 --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/python/worker.py @@ -0,0 +1,24 @@ +import asyncio + +from temporalio.client import Client +from temporalio.worker import Worker + +from activities import process_leaf +from shared import TASK_QUEUE +from workflows import LeafWorkflow, NodeWorkflow + + +async def main() -> None: + client = await Client.connect("localhost:7233") + worker = Worker( + client, + task_queue=TASK_QUEUE, + workflows=[NodeWorkflow, LeafWorkflow], + activities=[process_leaf], + ) + print(f"Worker listening on task queue '{TASK_QUEUE}'", flush=True) + await worker.run() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/sandbox-runner/patterns/mapreduce-tree/python/workflows.py b/sandbox-runner/patterns/mapreduce-tree/python/workflows.py new file mode 100644 index 0000000..2dab9dc --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/python/workflows.py @@ -0,0 +1,96 @@ +from dataclasses import dataclass +from datetime import timedelta + +from temporalio import workflow +from temporalio.workflow import ChildWorkflowHandle + +from activities import process_leaf +from shared import LEAF_THRESHOLD, MAX_DEPTH, RESULT_SIGNAL, TASK_QUEUE + + +@dataclass +class ResultPayload: + id: str + results: list[str] + + +@workflow.defn +class LeafWorkflow: + """Leaf workflow: processes one record and signals the result back to parent.""" + + @workflow.run + async def run(self, record: str, parent_workflow_id: str) -> None: + result = await workflow.execute_activity( + process_leaf, + record, + start_to_close_timeout=timedelta(seconds=30), + ) + workflow.logger.info(f"Leaf processed: {record} → {result}") + + parent = workflow.get_external_workflow_handle(parent_workflow_id) + await parent.signal(RESULT_SIGNAL, ResultPayload(id=record, results=[result])) + + +@workflow.defn +class NodeWorkflow: + """Node workflow: splits or fans out to leaves, collects results via signals, + and signals aggregated results up to its parent.""" + + def __init__(self) -> None: + self._collected: list[str] = [] + self._received = 0 + + @workflow.signal(name=RESULT_SIGNAL) + def node_result(self, payload: ResultPayload) -> None: + self._collected.extend(payload.results) + self._received += 1 + + @workflow.run + async def run( + self, + records: list[str], + depth: int = 0, + parent_workflow_id: str = "", + ) -> list[str]: + if depth > MAX_DEPTH: + raise RuntimeError(f"Tree depth exceeded {MAX_DEPTH}") + + my_id = workflow.info().workflow_id + expected = 0 + + if len(records) <= LEAF_THRESHOLD: + # Fan out to leaf workflows — one per record. + expected = len(records) + for record in records: + await workflow.start_child_workflow( + LeafWorkflow.run, + args=[record, my_id], + id=f"{my_id}/leaf-{record}", + task_queue=TASK_QUEUE, + ) + else: + # Split and recurse into child node workflows. + mid = len(records) // 2 + chunks = [records[:mid], records[mid:]] + expected = len(chunks) + for i, chunk in enumerate(chunks): + await workflow.start_child_workflow( + NodeWorkflow.run, + args=[chunk, depth + 1, my_id], + id=f"{my_id}/node-d{depth+1}-{i}", + task_queue=TASK_QUEUE, + ) + + await workflow.wait_condition(lambda: self._received >= expected) + + workflow.logger.info( + f"Node complete: depth={depth} records={len(records)} results={len(self._collected)}" + ) + + if parent_workflow_id: + parent = workflow.get_external_workflow_handle(parent_workflow_id) + await parent.signal( + RESULT_SIGNAL, ResultPayload(id=my_id, results=self._collected) + ) + + return self._collected diff --git a/sandbox-runner/patterns/mapreduce-tree/typescript/activities.ts b/sandbox-runner/patterns/mapreduce-tree/typescript/activities.ts new file mode 100644 index 0000000..78ec46a --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/typescript/activities.ts @@ -0,0 +1,5 @@ +export async function processLeaf(record: string): Promise { + // Simulate processing and return a result string. + await new Promise((r) => setTimeout(r, 50)); + return `processed(${record})`; +} diff --git a/sandbox-runner/patterns/mapreduce-tree/typescript/package.json b/sandbox-runner/patterns/mapreduce-tree/typescript/package.json new file mode 100644 index 0000000..3a0a12c --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/typescript/package.json @@ -0,0 +1,14 @@ +{ + "name": "temporal-design-patterns-sandbox", + "version": "0.1.0", + "private": true, + "type": "commonjs", + "dependencies": { + "@temporalio/activity": "^1.13.0", + "@temporalio/client": "^1.13.0", + "@temporalio/worker": "^1.13.0", + "@temporalio/workflow": "^1.13.0", + "tsx": "^4.19.2", + "typescript": "^5.6.3" + } +} diff --git a/sandbox-runner/patterns/mapreduce-tree/typescript/shared.ts b/sandbox-runner/patterns/mapreduce-tree/typescript/shared.ts new file mode 100644 index 0000000..d18ce96 --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/typescript/shared.ts @@ -0,0 +1,10 @@ +export const TASK_QUEUE = "mapreduce-tree-task-queue"; +export const WORKFLOW_ID_PREFIX = "mapreduce-tree"; +// When a node has at most this many records, it fans out to leaf workflows. +export const LEAF_THRESHOLD = 3; +// Maximum allowed recursion depth — fail fast if exceeded. +export const MAX_DEPTH = 5; +export const RESULT_SIGNAL = "nodeResult"; + +// Demo record set. +export const RECORDS: string[] = Array.from({ length: 9 }, (_, i) => `item-${i}`); diff --git a/sandbox-runner/patterns/mapreduce-tree/typescript/starter.ts b/sandbox-runner/patterns/mapreduce-tree/typescript/starter.ts new file mode 100644 index 0000000..0ac4c48 --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/typescript/starter.ts @@ -0,0 +1,33 @@ +import { Client, Connection } from "@temporalio/client"; + +import { RECORDS, TASK_QUEUE, WORKFLOW_ID_PREFIX } from "./shared"; +import { nodeWorkflow } from "./workflows"; + +async function main(): Promise { + const connection = await Connection.connect(); + try { + const client = new Client({ connection }); + const workflowId = `${WORKFLOW_ID_PREFIX}-${Date.now()}`; + const handle = await client.workflow.start(nodeWorkflow, { + args: [RECORDS, 0, ""], + taskQueue: TASK_QUEUE, + workflowId, + }); + console.log(`Started workflow: ${workflowId}`); + console.log(`Processing ${RECORDS.length} records via MapReduce Tree…`); + + const results = await handle.result(); + console.log(`MapReduce Tree complete: ${results.length} results`); + console.log(`Results: ${results.join(", ")}`); + console.log( + `Open the Temporal UI and search for '${workflowId}' to see the full workflow tree.` + ); + } finally { + await connection.close(); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/sandbox-runner/patterns/mapreduce-tree/typescript/worker.ts b/sandbox-runner/patterns/mapreduce-tree/typescript/worker.ts new file mode 100644 index 0000000..da949aa --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/typescript/worker.ts @@ -0,0 +1,19 @@ +import { Worker } from "@temporalio/worker"; + +import * as activities from "./activities"; +import { TASK_QUEUE } from "./shared"; + +async function main(): Promise { + const worker = await Worker.create({ + workflowsPath: require.resolve("./workflows"), + activities, + taskQueue: TASK_QUEUE, + }); + console.log(`Worker listening on task queue '${TASK_QUEUE}'`); + await worker.run(); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/sandbox-runner/patterns/mapreduce-tree/typescript/workflows.ts b/sandbox-runner/patterns/mapreduce-tree/typescript/workflows.ts new file mode 100644 index 0000000..1621f60 --- /dev/null +++ b/sandbox-runner/patterns/mapreduce-tree/typescript/workflows.ts @@ -0,0 +1,98 @@ +import { + condition, + defineSignal, + getExternalWorkflowHandle, + log, + proxyActivities, + setHandler, + startChild, + workflowInfo, +} from "@temporalio/workflow"; + +import type * as activities from "./activities"; +import { LEAF_THRESHOLD, MAX_DEPTH, RESULT_SIGNAL, TASK_QUEUE } from "./shared"; + +const { processLeaf } = proxyActivities({ + startToCloseTimeout: "30 seconds", +}); + +/** Signal payload sent from child to parent. */ +export interface ResultPayload { + id: string; + results: string[]; +} + +export const resultSignal = defineSignal<[ResultPayload]>(RESULT_SIGNAL); + +/** + * Leaf workflow: processes one record via an Activity and signals the result + * back to the parent Node workflow. + */ +export async function leafWorkflow(record: string, parentWorkflowId: string): Promise { + const result = await processLeaf(record); + log.info(`Leaf processed`, { record, result }); + + const parent = getExternalWorkflowHandle(parentWorkflowId); + await parent.signal(resultSignal, { id: record, results: [result] }); +} + +/** + * Node workflow: recursively splits the record set or fans out to leaf workflows, + * collects results via signals, then signals its own result up to its parent. + */ +export async function nodeWorkflow( + records: string[], + depth: number = 0, + parentWorkflowId: string = "" +): Promise { + if (depth > MAX_DEPTH) { + throw new Error(`Tree depth exceeded ${MAX_DEPTH}`); + } + + const myId = workflowInfo().workflowId; + const collectedResults: string[] = []; + let received = 0; + let expected = 0; + + setHandler(resultSignal, (payload: ResultPayload) => { + collectedResults.push(...payload.results); + received++; + }); + + if (records.length <= LEAF_THRESHOLD) { + // Fan out to leaf workflows — one per record. + expected = records.length; + for (const record of records) { + void startChild(leafWorkflow, { + args: [record, myId], + workflowId: `${myId}/leaf-${record}`, + taskQueue: TASK_QUEUE, + }); + } + } else { + // Split and recurse into child node workflows. + const mid = Math.floor(records.length / 2); + const chunks = [records.slice(0, mid), records.slice(mid)]; + expected = chunks.length; + for (let i = 0; i < chunks.length; i++) { + void startChild(nodeWorkflow, { + args: [chunks[i], depth + 1, myId], + workflowId: `${myId}/node-d${depth + 1}-${i}`, + taskQueue: TASK_QUEUE, + }); + } + } + + // Wait until all expected signals have arrived. + await condition(() => received >= expected); + + log.info(`Node complete`, { depth, records: records.length, results: collectedResults.length }); + + // Signal aggregated results up to parent (if this is not the root). + if (parentWorkflowId) { + const parent = getExternalWorkflowHandle(parentWorkflowId); + await parent.signal(resultSignal, { id: myId, results: collectedResults }); + } + + return collectedResults; +} diff --git a/sandbox-runner/patterns/sliding-window/go/activities.go b/sandbox-runner/patterns/sliding-window/go/activities.go new file mode 100644 index 0000000..26e7082 --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/go/activities.go @@ -0,0 +1,12 @@ +package main + +import ( + "context" + "time" +) + +func ProcessRecord(_ context.Context, recordID string) error { + // Simulate processing work. + time.Sleep(300 * time.Millisecond) + return nil +} diff --git a/sandbox-runner/patterns/sliding-window/go/go.mod b/sandbox-runner/patterns/sliding-window/go/go.mod new file mode 100644 index 0000000..a7ed79d --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/go/go.mod @@ -0,0 +1,33 @@ +module sliding-window + +go 1.22 + +require go.temporal.io/sdk v1.32.1 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + github.com/nexus-rpc/sdk-go v0.1.0 // indirect + github.com/pborman/uuid v1.2.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/robfig/cron v1.2.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/testify v1.10.0 // indirect + go.temporal.io/api v1.43.0 // indirect + golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.17.0 // indirect + golang.org/x/time v0.3.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect + google.golang.org/grpc v1.66.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/sandbox-runner/patterns/sliding-window/go/go.sum b/sandbox-runner/patterns/sliding-window/go/go.sum new file mode 100644 index 0000000..a88ecd1 --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/go/go.sum @@ -0,0 +1,179 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= +github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/nexus-rpc/sdk-go v0.1.0 h1:PUL/0vEY1//WnqyEHT5ao4LBRQ6MeNUihmnNGn0xMWY= +github.com/nexus-rpc/sdk-go v0.1.0/go.mod h1:TpfkM2Cw0Rlk9drGkoiSMpFqflKTiQLWUNyKJjF8mKQ= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= +github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= +github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.temporal.io/api v1.43.0 h1:lBhq+u5qFJqGMXwWsmg/i8qn1UA/3LCwVc88l2xUMHg= +go.temporal.io/api v1.43.0/go.mod h1:1WwYUMo6lao8yl0371xWUm13paHExN5ATYT/B7QtFis= +go.temporal.io/sdk v1.32.1 h1:slA8prhdFr4lxpsTcRusWVitD/cGjELfKUh0mBj73SU= +go.temporal.io/sdk v1.32.1/go.mod h1:8U8H7rF9u4Hyb4Ry9yiEls5716DHPNvVITPNkgWUwE8= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= +golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed h1:3RgNmBoI9MZhsj3QxC+AP/qQhNwpCLOvYDYYsFrhFt0= +google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed h1:J6izYgfBXAI3xTKLgxzTmUltdYaLsuBxFCgDHWJ/eXg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= +google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/sandbox-runner/patterns/sliding-window/go/shared.go b/sandbox-runner/patterns/sliding-window/go/shared.go new file mode 100644 index 0000000..c915d2a --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/go/shared.go @@ -0,0 +1,29 @@ +package main + +import "fmt" + +const ( + TaskQueue = "sliding-window-task-queue" + WorkflowIDPrefix = "sliding-window" + WindowSize = 3 + CompletionSignal = "recordCompleted" +) + +var RecordIDs = func() []string { + ids := make([]string, 12) + for i := range ids { + ids[i] = fmt.Sprintf("record-%d", i) + } + return ids +}() + +// SlidingWindowInput is the single input argument for SlidingWindowWorkflow. +// Bundling all fields into one struct keeps the ContinueAsNew call shape +// consistent across every run. +type SlidingWindowInput struct { + RecordIDs []string + WindowSize int + StartIndex int + TotalProcessed int + InFlight int +} diff --git a/sandbox-runner/patterns/sliding-window/go/starter.go b/sandbox-runner/patterns/sliding-window/go/starter.go new file mode 100644 index 0000000..5af1b00 --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/go/starter.go @@ -0,0 +1,47 @@ +package main + +import ( + "context" + "fmt" + "log" + "time" + + "go.temporal.io/sdk/client" +) + +func main() { + c, err := client.Dial(client.Options{HostPort: "localhost:7233"}) + if err != nil { + log.Fatalln("Unable to create client:", err) + } + defer c.Close() + + workflowID := fmt.Sprintf("%s-%d", WorkflowIDPrefix, time.Now().UnixMilli()) + we, err := c.ExecuteWorkflow( + context.Background(), + client.StartWorkflowOptions{ + ID: workflowID, + TaskQueue: TaskQueue, + }, + SlidingWindowWorkflow, + SlidingWindowInput{ + RecordIDs: RecordIDs, + WindowSize: WindowSize, + }, + ) + if err != nil { + log.Fatalln("Unable to execute workflow:", err) + } + fmt.Printf("Started workflow: %s\n", we.GetID()) + fmt.Printf("Processing %d records with window size %d…\n", len(RecordIDs), WindowSize) + + var totalProcessed int + if err := we.Get(context.Background(), &totalProcessed); err != nil { + log.Fatalln("Workflow failed:", err) + } + fmt.Printf("Sliding window complete: processed %d records\n", totalProcessed) + fmt.Printf( + "Open the Temporal UI and search for '%s' to see the parent and child workflows.\n", + workflowID, + ) +} diff --git a/sandbox-runner/patterns/sliding-window/go/warmup.go b/sandbox-runner/patterns/sliding-window/go/warmup.go new file mode 100644 index 0000000..62afb71 --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/go/warmup.go @@ -0,0 +1,13 @@ +// Stub program compiled at image-build time so the Temporal Go SDK and its +// transitive deps land in the Go build cache before any user code runs. +// The image factory deletes this file after building. +package main + +import ( + _ "go.temporal.io/sdk/activity" + _ "go.temporal.io/sdk/client" + _ "go.temporal.io/sdk/worker" + _ "go.temporal.io/sdk/workflow" +) + +func main() {} diff --git a/sandbox-runner/patterns/sliding-window/go/worker.go b/sandbox-runner/patterns/sliding-window/go/worker.go new file mode 100644 index 0000000..3b6da42 --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/go/worker.go @@ -0,0 +1,26 @@ +package main + +import ( + "log" + + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/worker" +) + +func main() { + c, err := client.Dial(client.Options{HostPort: "localhost:7233"}) + if err != nil { + log.Fatalln("Unable to create client:", err) + } + defer c.Close() + + w := worker.New(c, TaskQueue, worker.Options{}) + w.RegisterWorkflow(SlidingWindowWorkflow) + w.RegisterWorkflow(RecordProcessorWorkflow) + w.RegisterActivity(ProcessRecord) + + log.Printf("Worker listening on task queue '%s'", TaskQueue) + if err := w.Run(worker.InterruptCh()); err != nil { + log.Fatalln("Worker run failed:", err) + } +} diff --git a/sandbox-runner/patterns/sliding-window/go/workflows.go b/sandbox-runner/patterns/sliding-window/go/workflows.go new file mode 100644 index 0000000..625ea02 --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/go/workflows.go @@ -0,0 +1,121 @@ +package main + +import ( + "fmt" + "strings" + "time" + + enums "go.temporal.io/api/enums/v1" + "go.temporal.io/sdk/workflow" +) + +// RecordProcessorWorkflow is the child workflow that processes one record +// and signals the parent on completion. +func RecordProcessorWorkflow(ctx workflow.Context, recordID string, parentWorkflowID string) error { + ao := workflow.ActivityOptions{StartToCloseTimeout: 30 * time.Second} + ctx = workflow.WithActivityOptions(ctx, ao) + + if err := workflow.ExecuteActivity(ctx, ProcessRecord, recordID).Get(ctx, nil); err != nil { + return err + } + workflow.GetLogger(ctx).Info("Processed record", "recordID", recordID) + + // Signal the parent that this slot is now free. + // Ignore if the parent has already completed (final run finished before us). + err := workflow.SignalExternalWorkflow(ctx, parentWorkflowID, "", CompletionSignal, recordID).Get(ctx, nil) + if err != nil && strings.Contains(err.Error(), "not found") { + workflow.GetLogger(ctx).Info("Parent already completed, signal not needed", "recordID", recordID) + return nil + } + return err +} + +// SlidingWindowWorkflow is the parent workflow that maintains a fixed window of +// concurrent child workflows. It calls ContinueAsNew after dispatching windowSize +// children so history stays bounded. +func SlidingWindowWorkflow(ctx workflow.Context, input SlidingWindowInput) (int, error) { + windowSize := input.WindowSize + if windowSize <= 0 { + windowSize = WindowSize + } + recordIDs := input.RecordIDs + startIndex := input.StartIndex + totalProcessed := input.TotalProcessed + inFlight := input.InFlight + parentID := workflow.GetInfo(ctx).WorkflowExecution.ID + + completedCh := workflow.GetSignalChannel(ctx, CompletionSignal) + dispatched := 0 + active := inFlight + + startChild := func(recordID string) { + cwo := workflow.ChildWorkflowOptions{ + WorkflowID: fmt.Sprintf("%s/record-%s", parentID, recordID), + TaskQueue: TaskQueue, + ParentClosePolicy: enums.PARENT_CLOSE_POLICY_ABANDON, + } + workflow.ExecuteChildWorkflow(workflow.WithChildOptions(ctx, cwo), RecordProcessorWorkflow, recordID, parentID) + } + + // Only start (windowSize - inFlight) new children. Carried-over in-flight + // children from the previous run will signal us when they complete. + newFill := len(recordIDs) - startIndex + if newFill > windowSize-inFlight { + newFill = windowSize - inFlight + } + nextIndex := startIndex + for i := 0; i < newFill; i++ { + startChild(recordIDs[nextIndex]) + nextIndex++ + dispatched++ + active++ + } + + // If the window is full after the initial fill, continue-as-new immediately + // so the parent doesn't wait before handing off to the next run. + if dispatched >= windowSize { + workflow.GetLogger(ctx).Info("ContinueAsNew", "nextIndex", nextIndex, "totalProcessed", totalProcessed) + return 0, workflow.NewContinueAsNewError(ctx, SlidingWindowWorkflow, SlidingWindowInput{ + RecordIDs: recordIDs, + WindowSize: windowSize, + StartIndex: nextIndex, + TotalProcessed: totalProcessed, + InFlight: windowSize, + }) + } + + // Slide the window. + for nextIndex < len(recordIDs) { + var sig string + completedCh.Receive(ctx, &sig) + totalProcessed++ + active-- + + startChild(recordIDs[nextIndex]) + nextIndex++ + dispatched++ + active++ + + if dispatched >= windowSize { + workflow.GetLogger(ctx).Info("ContinueAsNew", "nextIndex", nextIndex, "totalProcessed", totalProcessed) + // Pass nextIndex as the next unstarted record; inFlight=windowSize because + // the window is always full at CAN time. + return 0, workflow.NewContinueAsNewError(ctx, SlidingWindowWorkflow, SlidingWindowInput{ + RecordIDs: recordIDs, + WindowSize: windowSize, + StartIndex: nextIndex, + TotalProcessed: totalProcessed, + InFlight: windowSize, + }) + } + } + + // Drain all remaining in-flight children. + for active > 0 { + completedCh.Receive(ctx, nil) + totalProcessed++ + active-- + } + workflow.GetLogger(ctx).Info("Sliding window complete", "total", len(recordIDs), "totalProcessed", totalProcessed) + return totalProcessed, nil +} diff --git a/sandbox-runner/patterns/sliding-window/java/_warmup/Warmup.java b/sandbox-runner/patterns/sliding-window/java/_warmup/Warmup.java new file mode 100644 index 0000000..2fc5ccb --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/java/_warmup/Warmup.java @@ -0,0 +1,23 @@ +// Stub class compiled at image-build time so the Temporal Java SDK and the +// Maven compiler plugin exercise their classloading paths once before any +// user code runs. The image factory copies this into src/main/java/, runs +// `mvn compile`, then deletes both the source and target/ from the snapshot. + +import io.temporal.activity.ActivityInterface; +import io.temporal.client.WorkflowClient; +import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.worker.WorkerFactory; +import io.temporal.workflow.WorkflowInterface; + +public class Warmup { + @SuppressWarnings("unused") + private static final Class[] FORCE_RESOLVE = { + ActivityInterface.class, + WorkflowInterface.class, + WorkflowClient.class, + WorkflowServiceStubs.class, + WorkerFactory.class, + }; + + public static void main(String[] args) {} +} diff --git a/sandbox-runner/patterns/sliding-window/java/pom.xml b/sandbox-runner/patterns/sliding-window/java/pom.xml new file mode 100644 index 0000000..3b8a904 --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/java/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + example + sliding-window + 0.1.0 + jar + + + 17 + 17 + UTF-8 + false + + + + + io.temporal + temporal-sdk + 1.27.0 + + + org.slf4j + slf4j-simple + 2.0.13 + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.4.1 + + + + diff --git a/sandbox-runner/patterns/sliding-window/java/src/main/java/Activities.java b/sandbox-runner/patterns/sliding-window/java/src/main/java/Activities.java new file mode 100644 index 0000000..4265f6c --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/java/src/main/java/Activities.java @@ -0,0 +1,18 @@ +import io.temporal.activity.ActivityInterface; + +@ActivityInterface +public interface Activities { + void processRecord(String recordId); + + final class Impl implements Activities { + @Override + public void processRecord(String recordId) { + try { + // Simulate processing work. + Thread.sleep(300); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } +} diff --git a/sandbox-runner/patterns/sliding-window/java/src/main/java/Shared.java b/sandbox-runner/patterns/sliding-window/java/src/main/java/Shared.java new file mode 100644 index 0000000..caa0df5 --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/java/src/main/java/Shared.java @@ -0,0 +1,44 @@ +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public final class Shared { + public static final String TASK_QUEUE = "sliding-window-task-queue"; + public static final String WORKFLOW_ID_PREFIX = "sliding-window"; + public static final int WINDOW_SIZE = 3; + public static final String COMPLETION_SIGNAL = "recordCompleted"; + + public static final List RECORD_IDS; + + static { + RECORD_IDS = new ArrayList<>(); + for (int i = 0; i < 12; i++) { + RECORD_IDS.add("record-" + i); + } + } + + private Shared() {} + + /** Input for the parent sliding-window workflow. Bundled as one object so + * every Continue-as-New call has a consistent argument shape. */ + public static final class SlidingWindowInput { + public List recordIds; + public int windowSize; + public int startIndex; + public int totalProcessed; + /** IDs of child workflows still in-flight from the previous run. */ + public Set currentRecords; + + public SlidingWindowInput() {} + + public SlidingWindowInput(List recordIds, int windowSize, + int startIndex, int totalProcessed, Set currentRecords) { + this.recordIds = recordIds; + this.windowSize = windowSize; + this.startIndex = startIndex; + this.totalProcessed = totalProcessed; + this.currentRecords = currentRecords; + } + } +} diff --git a/sandbox-runner/patterns/sliding-window/java/src/main/java/SlidingWindowWorkflow.java b/sandbox-runner/patterns/sliding-window/java/src/main/java/SlidingWindowWorkflow.java new file mode 100644 index 0000000..c800574 --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/java/src/main/java/SlidingWindowWorkflow.java @@ -0,0 +1,137 @@ +import io.temporal.activity.ActivityOptions; +import io.temporal.api.common.v1.WorkflowExecution; +import io.temporal.api.enums.v1.ParentClosePolicy; +import io.temporal.workflow.*; + +import java.time.Duration; +import java.util.*; + +public interface SlidingWindowWorkflow { + + /** Parent workflow: maintains a fixed window of concurrent child workflows. */ + @WorkflowInterface + interface Parent { + @WorkflowMethod + int run(Shared.SlidingWindowInput input); + + @SignalMethod + void recordCompleted(String recordId); + } + + /** Child workflow: processes one record and signals the parent on completion. */ + @WorkflowInterface + interface Child { + @WorkflowMethod + void run(String recordId, String parentWorkflowId); + } + + final class ParentImpl implements Parent { + /** IDs of records currently being processed; null until run() initialises it. */ + private Set currentRecords; + /** Completions that arrive via signal before run() sets currentRecords. */ + private final Set recordsToRemove = new HashSet<>(); + private int totalProcessed = 0; + + @Override + public void recordCompleted(String recordId) { + if (currentRecords == null) { + // Signal arrived before run() started — buffer it. + recordsToRemove.add(recordId); + return; + } + // Dedupe: remove returns false if the ID was already absent. + if (currentRecords.remove(recordId)) { + totalProcessed++; + } + } + + @Override + public int run(Shared.SlidingWindowInput input) { + this.totalProcessed = input.totalProcessed; + int windowSize = input.windowSize > 0 ? input.windowSize : Shared.WINDOW_SIZE; + List recordIds = input.recordIds; + String parentId = Workflow.getInfo().getWorkflowId(); + int nextIndex = input.startIndex; + + // Restore the in-flight set carried over from the previous run. + this.currentRecords = new HashSet<>( + input.currentRecords != null ? input.currentRecords : Collections.emptySet()); + // Apply any completions that signalled before run() began. + int earlyCompleted = currentRecords.size(); + currentRecords.removeAll(recordsToRemove); + this.totalProcessed += earlyCompleted - currentRecords.size(); + + // Track start promises so we can wait before continuing-as-new. + List> childrenStarted = new ArrayList<>(); + + while (true) { + // Block until the window has a free slot. + Workflow.await(() -> currentRecords.size() < windowSize); + + // No more records to launch — drain remaining children and finish. + if (nextIndex >= recordIds.size()) { + Workflow.await(() -> currentRecords.isEmpty()); + Workflow.getLogger(ParentImpl.class) + .info("Sliding window complete: total={} totalProcessed={}", + recordIds.size(), this.totalProcessed); + return this.totalProcessed; + } + + String recordId = recordIds.get(nextIndex); + ChildWorkflowOptions opts = ChildWorkflowOptions.newBuilder() + .setWorkflowId(parentId + "/record-" + recordId) + .setTaskQueue(Shared.TASK_QUEUE) + .setParentClosePolicy(ParentClosePolicy.PARENT_CLOSE_POLICY_ABANDON) + .build(); + Child child = Workflow.newChildWorkflowStub(Child.class, opts); + Async.procedure(child::run, recordId, parentId); + // Resolves when the child has actually started (needed before CAN). + childrenStarted.add(Workflow.getWorkflowExecution(child)); + currentRecords.add(recordId); + nextIndex++; + + // Continue-as-New after starting windowSize children to keep history short. + if (childrenStarted.size() >= windowSize) { + // Wait for all children to confirm start before handing off. + // Without this, CAN could race child startup and they'd never run. + Promise.allOf(childrenStarted).get(); + Workflow.getLogger(ParentImpl.class) + .info("ContinueAsNew: nextIndex={} totalProcessed={}", nextIndex, this.totalProcessed); + Workflow.newContinueAsNewStub(Parent.class) + .run(new Shared.SlidingWindowInput( + recordIds, windowSize, nextIndex, this.totalProcessed, currentRecords)); + return 0; // unreachable; CAN throws + } + } + } + } + + final class ChildImpl implements Child { + private final Activities activities = Workflow.newActivityStub( + Activities.class, + ActivityOptions.newBuilder() + .setStartToCloseTimeout(Duration.ofSeconds(30)) + .build()); + + @Override + public void run(String recordId, String parentWorkflowId) { + activities.processRecord(recordId); + Workflow.getLogger(ChildImpl.class).info("Processed record: {}", recordId); + + // Signal the parent that this slot is now free. + // Ignore if the parent has already completed (final run finished before us). + ExternalWorkflowStub parent = Workflow.newUntypedExternalWorkflowStub(parentWorkflowId); + try { + parent.signal(Shared.COMPLETION_SIGNAL, recordId); + } catch (Exception e) { + String msg = e.getMessage() != null ? e.getMessage() : ""; + if (msg.contains("workflow not found") || msg.contains("not found")) { + Workflow.getLogger(ChildImpl.class) + .info("Parent already completed, signal not needed: {}", recordId); + } else { + throw e; + } + } + } + } +} diff --git a/sandbox-runner/patterns/sliding-window/java/src/main/java/Starter.java b/sandbox-runner/patterns/sliding-window/java/src/main/java/Starter.java new file mode 100644 index 0000000..e61063c --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/java/src/main/java/Starter.java @@ -0,0 +1,30 @@ +import io.temporal.client.WorkflowClient; +import io.temporal.client.WorkflowOptions; +import io.temporal.serviceclient.WorkflowServiceStubs; + +import java.util.HashSet; + +public class Starter { + public static void main(String[] args) { + WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs(); + WorkflowClient client = WorkflowClient.newInstance(service); + + String workflowId = Shared.WORKFLOW_ID_PREFIX + "-" + System.currentTimeMillis(); + SlidingWindowWorkflow.Parent workflow = client.newWorkflowStub( + SlidingWindowWorkflow.Parent.class, + WorkflowOptions.newBuilder() + .setTaskQueue(Shared.TASK_QUEUE) + .setWorkflowId(workflowId) + .build()); + + System.out.println("Started workflow: " + workflowId); + System.out.println("Processing " + Shared.RECORD_IDS.size() + + " records with window size " + Shared.WINDOW_SIZE + "…"); + int totalProcessed = workflow.run( + new Shared.SlidingWindowInput(Shared.RECORD_IDS, Shared.WINDOW_SIZE, 0, 0, new HashSet<>())); + System.out.println("Sliding window complete: processed " + totalProcessed + " records"); + System.out.println( + "Open the Temporal UI and search for '" + workflowId + + "' to see the parent and child workflows."); + } +} diff --git a/sandbox-runner/patterns/sliding-window/java/src/main/java/Worker.java b/sandbox-runner/patterns/sliding-window/java/src/main/java/Worker.java new file mode 100644 index 0000000..ea7b018 --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/java/src/main/java/Worker.java @@ -0,0 +1,26 @@ +import io.temporal.client.WorkflowClient; +import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.worker.WorkerFactory; + +public class Worker { + public static void main(String[] args) { + WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs(); + WorkflowClient client = WorkflowClient.newInstance(service); + + WorkerFactory factory = WorkerFactory.newInstance(client); + io.temporal.worker.Worker worker = factory.newWorker(Shared.TASK_QUEUE); + worker.registerWorkflowImplementationTypes( + SlidingWindowWorkflow.ParentImpl.class, + SlidingWindowWorkflow.ChildImpl.class); + worker.registerActivitiesImplementations(new Activities.Impl()); + + System.out.println("Worker listening on task queue '" + Shared.TASK_QUEUE + "'"); + factory.start(); + + try { + Thread.currentThread().join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/sandbox-runner/patterns/sliding-window/pattern.json b/sandbox-runner/patterns/sliding-window/pattern.json new file mode 100644 index 0000000..be2659b --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/pattern.json @@ -0,0 +1,42 @@ +{ + "name": "Sliding Window", + "languages": { + "typescript": { + "label": "TypeScript", + "files": ["starter.ts", "worker.ts", "workflows.ts", "activities.ts", "shared.ts"], + "worker": "npx tsx worker.ts", + "starter": "npx tsx starter.ts", + "workerProcessPattern": "tsx worker.ts" + }, + "python": { + "label": "Python", + "files": ["starter.py", "worker.py", "workflows.py", "activities.py", "shared.py"], + "worker": "uv run python worker.py", + "starter": "uv run python starter.py", + "workerProcessPattern": "python worker.py" + }, + "go": { + "label": "Go", + "files": ["starter.go", "worker.go", "workflows.go", "activities.go", "shared.go"], + "worker": "go run worker.go workflows.go activities.go shared.go", + "starter": "go run starter.go workflows.go activities.go shared.go", + "workerProcessPattern": "go run worker.go", + "diskGib": 2 + }, + "java": { + "label": "Java", + "files": [ + "src/main/java/Starter.java", + "src/main/java/Worker.java", + "src/main/java/SlidingWindowWorkflow.java", + "src/main/java/Activities.java", + "src/main/java/Shared.java" + ], + "worker": "mvn -q -B compile exec:java -Dexec.mainClass=Worker", + "starter": "mvn -q -B compile exec:java -Dexec.mainClass=Starter", + "workerProcessPattern": "exec.mainClass=Worker", + "diskGib": 3, + "memoryGib": 3 + } + } +} diff --git a/sandbox-runner/patterns/sliding-window/python/activities.py b/sandbox-runner/patterns/sliding-window/python/activities.py new file mode 100644 index 0000000..75775ce --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/python/activities.py @@ -0,0 +1,9 @@ +import asyncio + +from temporalio import activity + + +@activity.defn +async def process_record(record_id: str) -> None: + # Simulate processing work. + await asyncio.sleep(0.3) diff --git a/sandbox-runner/patterns/sliding-window/python/pyproject.toml b/sandbox-runner/patterns/sliding-window/python/pyproject.toml new file mode 100644 index 0000000..1766d5a --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/python/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "sliding-window-python" +version = "0.1.0" +description = "Sliding Window pattern demo (Python)" +requires-python = ">=3.12" +dependencies = [ + "temporalio==1.9.0", +] diff --git a/sandbox-runner/patterns/sliding-window/python/shared.py b/sandbox-runner/patterns/sliding-window/python/shared.py new file mode 100644 index 0000000..52df593 --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/python/shared.py @@ -0,0 +1,18 @@ +from dataclasses import dataclass, field + +TASK_QUEUE = "sliding-window-task-queue" +WORKFLOW_ID_PREFIX = "sliding-window" +WINDOW_SIZE = 3 +RECORD_IDS = [f"record-{i}" for i in range(12)] +COMPLETION_SIGNAL = "recordCompleted" + + +@dataclass +class SlidingWindowInput: + """Input for SlidingWindowWorkflow. Bundled as a single object so all runs + (including continues-as-new) share one consistent argument shape.""" + record_ids: list + window_size: int = WINDOW_SIZE + start_index: int = 0 + total_processed: int = 0 + in_flight: int = 0 diff --git a/sandbox-runner/patterns/sliding-window/python/starter.py b/sandbox-runner/patterns/sliding-window/python/starter.py new file mode 100644 index 0000000..445cbbc --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/python/starter.py @@ -0,0 +1,30 @@ +import asyncio +import time + +from temporalio.client import Client + +from shared import RECORD_IDS, TASK_QUEUE, WINDOW_SIZE, WORKFLOW_ID_PREFIX, SlidingWindowInput +from workflows import SlidingWindowWorkflow + + +async def main() -> None: + client = await Client.connect("localhost:7233") + workflow_id = f"{WORKFLOW_ID_PREFIX}-{int(time.time() * 1000)}" + handle = await client.start_workflow( + SlidingWindowWorkflow.run, + args=[SlidingWindowInput(record_ids=RECORD_IDS)], + id=workflow_id, + task_queue=TASK_QUEUE, + ) + print(f"Started workflow: {workflow_id}") + print(f"Processing {len(RECORD_IDS)} records with window size {WINDOW_SIZE}…") + + total_processed = await handle.result() + print(f"Sliding window complete: processed {total_processed} records") + print( + f"Open the Temporal UI and search for '{workflow_id}' to see the parent and child workflows." + ) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/sandbox-runner/patterns/sliding-window/python/worker.py b/sandbox-runner/patterns/sliding-window/python/worker.py new file mode 100644 index 0000000..8de2dca --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/python/worker.py @@ -0,0 +1,24 @@ +import asyncio + +from temporalio.client import Client +from temporalio.worker import Worker + +from activities import process_record +from shared import TASK_QUEUE +from workflows import RecordProcessorWorkflow, SlidingWindowWorkflow + + +async def main() -> None: + client = await Client.connect("localhost:7233") + worker = Worker( + client, + task_queue=TASK_QUEUE, + workflows=[SlidingWindowWorkflow, RecordProcessorWorkflow], + activities=[process_record], + ) + print(f"Worker listening on task queue '{TASK_QUEUE}'", flush=True) + await worker.run() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/sandbox-runner/patterns/sliding-window/python/workflows.py b/sandbox-runner/patterns/sliding-window/python/workflows.py new file mode 100644 index 0000000..f385218 --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/python/workflows.py @@ -0,0 +1,123 @@ +from datetime import timedelta + +from temporalio import workflow +from temporalio.exceptions import ApplicationError +from temporalio.workflow import ParentClosePolicy, continue_as_new + +from activities import process_record +from shared import COMPLETION_SIGNAL, TASK_QUEUE, WINDOW_SIZE, SlidingWindowInput + + +@workflow.defn +class RecordProcessorWorkflow: + """Child workflow: processes one record and signals the parent on completion.""" + + @workflow.run + async def run(self, record_id: str, parent_workflow_id: str) -> None: + await workflow.execute_activity( + process_record, + record_id, + start_to_close_timeout=timedelta(seconds=30), + ) + workflow.logger.info(f"Processed record: {record_id}") + + # Signal the parent that this slot is now free. + # Ignore if the parent has already completed (final run finished before us). + parent = workflow.get_external_workflow_handle(parent_workflow_id) + try: + await parent.signal(COMPLETION_SIGNAL, record_id) + except ApplicationError as e: + if "not found" in str(e).lower(): + workflow.logger.info(f"Parent already completed, signal not needed: {record_id}") + else: + raise + + +@workflow.defn +class SlidingWindowWorkflow: + """Parent workflow: maintains a fixed window of concurrent child workflows. + Calls continue_as_new after dispatching window_size children.""" + + def __init__(self) -> None: + self._pending_signals = 0 + self._total_processed = 0 + + @workflow.signal(name=COMPLETION_SIGNAL) + def record_completed(self, record_id: str) -> None: + self._pending_signals += 1 + self._total_processed += 1 + + @workflow.run + async def run(self, input: SlidingWindowInput) -> int: + # Use += so any completions that signal before run() starts are preserved. + self._total_processed += input.total_processed + record_ids = input.record_ids + window_size = input.window_size + start_index = input.start_index + in_flight = input.in_flight + parent_id = workflow.info().workflow_id + next_index = start_index + dispatched = 0 + active = in_flight + + # Only start (window_size - in_flight) new children. Carried-over in-flight + # children from the previous run will signal us when they complete. + new_fill = min(window_size - in_flight, len(record_ids) - start_index) + for _ in range(new_fill): + await workflow.start_child_workflow( + RecordProcessorWorkflow.run, + args=[record_ids[next_index], parent_id], + id=f"{parent_id}/record-{record_ids[next_index]}", + task_queue=TASK_QUEUE, + parent_close_policy=ParentClosePolicy.ABANDON, + ) + next_index += 1 + dispatched += 1 + active += 1 + + # If the window is full after the initial fill, continue-as-new immediately + # so the parent doesn't wait before handing off to the next run. + if dispatched >= window_size: + workflow.logger.info(f"ContinueAsNew: nextIndex={next_index} totalProcessed={self._total_processed}") + continue_as_new(args=[SlidingWindowInput( + record_ids=record_ids, + window_size=window_size, + start_index=next_index, + total_processed=self._total_processed, + in_flight=window_size, + )]) + return + + # Slide the window. + while next_index < len(record_ids): + await workflow.wait_condition(lambda: self._pending_signals > 0) + self._pending_signals -= 1 + active -= 1 + await workflow.start_child_workflow( + RecordProcessorWorkflow.run, + args=[record_ids[next_index], parent_id], + id=f"{parent_id}/record-{record_ids[next_index]}", + task_queue=TASK_QUEUE, + parent_close_policy=ParentClosePolicy.ABANDON, + ) + next_index += 1 + dispatched += 1 + active += 1 + + if dispatched >= window_size: + workflow.logger.info(f"ContinueAsNew: nextIndex={next_index} totalProcessed={self._total_processed}") + # Pass next_index as the next unstarted record; in_flight=window_size + # because the window is always full at CAN time. + continue_as_new(args=[SlidingWindowInput( + record_ids=record_ids, + window_size=window_size, + start_index=next_index, + total_processed=self._total_processed, + in_flight=window_size, + )]) + return + + # Wait for all remaining in-flight children to complete. + await workflow.wait_condition(lambda: self._pending_signals >= active) + workflow.logger.info(f"Sliding window complete: total={len(record_ids)} totalProcessed={self._total_processed}") + return self._total_processed diff --git a/sandbox-runner/patterns/sliding-window/typescript/activities.ts b/sandbox-runner/patterns/sliding-window/typescript/activities.ts new file mode 100644 index 0000000..ecac307 --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/typescript/activities.ts @@ -0,0 +1,4 @@ +export async function processRecord(recordId: string): Promise { + // Simulate processing work. + await new Promise((r) => setTimeout(r, 300)); +} diff --git a/sandbox-runner/patterns/sliding-window/typescript/package.json b/sandbox-runner/patterns/sliding-window/typescript/package.json new file mode 100644 index 0000000..3a0a12c --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/typescript/package.json @@ -0,0 +1,14 @@ +{ + "name": "temporal-design-patterns-sandbox", + "version": "0.1.0", + "private": true, + "type": "commonjs", + "dependencies": { + "@temporalio/activity": "^1.13.0", + "@temporalio/client": "^1.13.0", + "@temporalio/worker": "^1.13.0", + "@temporalio/workflow": "^1.13.0", + "tsx": "^4.19.2", + "typescript": "^5.6.3" + } +} diff --git a/sandbox-runner/patterns/sliding-window/typescript/shared.ts b/sandbox-runner/patterns/sliding-window/typescript/shared.ts new file mode 100644 index 0000000..ef09f3f --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/typescript/shared.ts @@ -0,0 +1,16 @@ +export const TASK_QUEUE = "sliding-window-task-queue"; +export const WORKFLOW_ID_PREFIX = "sliding-window"; +export const WINDOW_SIZE = 3; +// Total records to process in the demo. +export const RECORD_IDS: string[] = Array.from({ length: 12 }, (_, i) => `record-${i}`); +export const COMPLETION_SIGNAL = "recordCompleted"; + +/** Input for SlidingWindowWorkflow. Bundled as a single object so all runs + * (including continues-as-new) share one consistent argument shape. */ +export interface SlidingWindowInput { + recordIds: string[]; + windowSize?: number; + startIndex?: number; + totalProcessed?: number; + inFlight?: number; +} diff --git a/sandbox-runner/patterns/sliding-window/typescript/starter.ts b/sandbox-runner/patterns/sliding-window/typescript/starter.ts new file mode 100644 index 0000000..4e17c61 --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/typescript/starter.ts @@ -0,0 +1,32 @@ +import { Client, Connection } from "@temporalio/client"; + +import { RECORD_IDS, TASK_QUEUE, WINDOW_SIZE, WORKFLOW_ID_PREFIX } from "./shared"; +import { slidingWindowWorkflow } from "./workflows"; + +async function main(): Promise { + const connection = await Connection.connect(); + try { + const client = new Client({ connection }); + const workflowId = `${WORKFLOW_ID_PREFIX}-${Date.now()}`; + const handle = await client.workflow.start(slidingWindowWorkflow, { + args: [{ recordIds: RECORD_IDS, windowSize: WINDOW_SIZE }], + taskQueue: TASK_QUEUE, + workflowId, + }); + console.log(`Started workflow: ${workflowId}`); + console.log(`Processing ${RECORD_IDS.length} records with window size ${WINDOW_SIZE}…`); + + const totalProcessed = await handle.result(); + console.log(`Sliding window complete: processed ${totalProcessed} records`); + console.log( + `Open the Temporal UI and search for '${workflowId}' to see the parent and child workflows.` + ); + } finally { + await connection.close(); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/sandbox-runner/patterns/sliding-window/typescript/worker.ts b/sandbox-runner/patterns/sliding-window/typescript/worker.ts new file mode 100644 index 0000000..da949aa --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/typescript/worker.ts @@ -0,0 +1,19 @@ +import { Worker } from "@temporalio/worker"; + +import * as activities from "./activities"; +import { TASK_QUEUE } from "./shared"; + +async function main(): Promise { + const worker = await Worker.create({ + workflowsPath: require.resolve("./workflows"), + activities, + taskQueue: TASK_QUEUE, + }); + console.log(`Worker listening on task queue '${TASK_QUEUE}'`); + await worker.run(); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/sandbox-runner/patterns/sliding-window/typescript/workflows.ts b/sandbox-runner/patterns/sliding-window/typescript/workflows.ts new file mode 100644 index 0000000..db77812 --- /dev/null +++ b/sandbox-runner/patterns/sliding-window/typescript/workflows.ts @@ -0,0 +1,125 @@ +import { + ApplicationFailure, + ParentClosePolicy, + condition, + continueAsNew, + defineSignal, + getExternalWorkflowHandle, + log, + proxyActivities, + setHandler, + startChild, + workflowInfo, +} from "@temporalio/workflow"; + +import type * as activities from "./activities"; +import { COMPLETION_SIGNAL, TASK_QUEUE, WINDOW_SIZE, type SlidingWindowInput } from "./shared"; + +const { processRecord } = proxyActivities({ + startToCloseTimeout: "30 seconds", +}); + +export const completionSignal = defineSignal<[string]>(COMPLETION_SIGNAL); + +/** + * Child workflow: processes one record and signals the parent on completion. + * The parent's workflow ID is stable across continueAsNew runs. + */ +export async function recordProcessorWorkflow( + recordId: string, + parentWorkflowId: string +): Promise { + await processRecord(recordId); + log.info(`Processed record`, { recordId }); + + // Signal the parent that this slot is now free. + // Ignore if the parent has already completed (final run finished before us). + try { + const parent = getExternalWorkflowHandle(parentWorkflowId); + await parent.signal(completionSignal, recordId); + } catch (err) { + if (err instanceof ApplicationFailure && err.type === 'NOT_FOUND') { + log.info('Parent already completed, signal not needed', { recordId }); + } else { + throw err; + } + } +} + +/** + * Parent workflow: maintains a fixed window of concurrent child workflows. + * Calls continueAsNew after dispatching windowSize children so history stays bounded. + * Children signal back to free a slot; the parent starts the next child immediately. + */ +export async function slidingWindowWorkflow(input: SlidingWindowInput): Promise { + const { + recordIds, + windowSize = WINDOW_SIZE, + startIndex = 0, + inFlight = 0, + } = input; + let totalProcessed = input.totalProcessed ?? 0; + const parentId = workflowInfo().workflowId; + let pendingSignals = 0; + let dispatched = 0; + + setHandler(completionSignal, (_recordId: string) => { + pendingSignals++; + totalProcessed++; + }); + + // Only start (windowSize - inFlight) new children. The carried-over in-flight + // children from the previous run will signal us when they complete. + const newFill = Math.min(windowSize - inFlight, recordIds.length - startIndex); + let nextIndex = startIndex; + let active = inFlight; + + for (let i = 0; i < newFill; i++) { + void startChild(recordProcessorWorkflow, { + args: [recordIds[nextIndex], parentId], + workflowId: `${parentId}/record-${recordIds[nextIndex]}`, + taskQueue: TASK_QUEUE, + parentClosePolicy: ParentClosePolicy.ABANDON, + }); + nextIndex++; + dispatched++; + active++; + } + + // If the window is full after the initial fill, continue-as-new immediately + // so the parent doesn't wait before handing off to the next run. + if (dispatched >= windowSize) { + log.info(`ContinueAsNew`, { nextIndex, totalProcessed }); + await continueAsNew({ recordIds, windowSize, startIndex: nextIndex, totalProcessed, inFlight: windowSize }); + return; + } + + // Slide the window: as each slot frees, start the next child. + while (nextIndex < recordIds.length) { + await condition(() => pendingSignals > 0); + pendingSignals--; + active--; + void startChild(recordProcessorWorkflow, { + args: [recordIds[nextIndex], parentId], + workflowId: `${parentId}/record-${recordIds[nextIndex]}`, + taskQueue: TASK_QUEUE, + parentClosePolicy: ParentClosePolicy.ABANDON, + }); + nextIndex++; + dispatched++; + active++; + + if (dispatched >= windowSize) { + log.info(`ContinueAsNew`, { nextIndex, totalProcessed }); + // Pass nextIndex as the next unstarted record; inFlight=windowSize because + // the window is always full at CAN time. + await continueAsNew({ recordIds, windowSize, startIndex: nextIndex, totalProcessed, inFlight: windowSize }); + return; + } + } + + // Wait for all remaining in-flight children to complete. + await condition(() => pendingSignals >= active); + log.info(`Sliding window complete`, { total: recordIds.length, totalProcessed }); + return totalProcessed; +} From 2ded8236fc12fdca57b0d10f1b57b87ccd58ffb4 Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Thu, 14 May 2026 10:46:52 -0400 Subject: [PATCH 2/3] fixes to docs and sample code for fanout, mapreduce, sliding window --- docs/fanout-child-workflows.md | 14 +++--- docs/mapreduce-tree.md | 50 ++++++++++--------- docs/sliding-window.md | 45 +++++++++++++---- .../patterns/sliding-window/go/workflows.go | 14 ++++-- .../sliding-window/typescript/workflows.ts | 4 +- 5 files changed, 83 insertions(+), 44 deletions(-) diff --git a/docs/fanout-child-workflows.md b/docs/fanout-child-workflows.md index ce6e0ed..3e38e96 100644 --- a/docs/fanout-child-workflows.md +++ b/docs/fanout-child-workflows.md @@ -2,7 +2,7 @@ # Fan-Out with Child Workflows :::info TLDR -Split your record set into fixed-size chunks and start **one child Workflow per chunk** so that each chunk's history stays within Temporal's limits. Use this when your record set fits within ~4 million items, you want maximum concurrency with no rate control, and you can pre-compute how many chunks you need before the job starts. +Split your record set into fixed-size chunks and start **one child Workflow per chunk** so that each chunk's history stays within Temporal's limits. Use this when you want maximum concurrency with no rate control and you can pre-compute how many chunks you need before the job starts. Keep the total number of children per parent under 1,000; use [Sliding Window](sliding-window) or [Batch Iterator](batch-iterator) for larger workloads. ::: ## Overview @@ -150,7 +150,7 @@ class FanOutWorkflow: handles.append(handle) offset += chunk_size - results = await asyncio.gather(*[h.result() for h in handles]) + results = await asyncio.gather(*handles) return sum(results) ``` @@ -159,7 +159,9 @@ class FanOutWorkflow: package main import ( - "go.temporal.io/sdk/temporal" + "fmt" + "time" + "go.temporal.io/sdk/workflow" ) @@ -189,7 +191,7 @@ func FanOutWorkflow(ctx workflow.Context, totalRecords int, chunkSize int) (int, for _, f := range futures { var n int if err := f.Get(ctx, &n); err != nil { - return total, temporal.NewApplicationError("child failed", "ChildFailed", err) + return total, err } total += n } @@ -261,12 +263,12 @@ public class FanOutWorkflowImpl implements FanOutWorkflow { - **Use offset and length, not explicit IDs.** Pass only two integers to each child rather than a full slice of IDs. The child fetches its own records. This keeps history events small. - **Size chunks to stay under the Activity limit.** Each child Workflow can have at most 2,000 in-flight Activities. Aim for chunks of 500 records or fewer if each record maps to one Activity. - **Cap concurrent children in the parent.** Starting thousands of child Workflows simultaneously puts pressure on the namespace. Consider batching child starts or using [Sliding Window](sliding-window) if you need tighter concurrency control. -- **Set `PARENT_CLOSE_POLICY_ABANDON`** if you do not need the parent to collect results. This lets children complete independently even if the parent is cancelled or timed out. +- **Set `PARENT_CLOSE_POLICY_ABANDON`** for fire-and-forget fan-outs where the parent does not need to collect results. With the default `TERMINATE` policy, cancelling or timing out the parent will terminate all in-flight children. - **Give each child a deterministic Workflow ID** (`parentId/batch-`). This makes it safe to re-run the parent: Temporal deduplicates child starts by Workflow ID, so already-completed children are not re-executed. ## Common Pitfalls -- **Starting too many children at once.** Each child start adds to the parent's history. If you have thousands of chunks, consider paging through them or switching to the [Batch Iterator](batch-iterator) or [Sliding Window](sliding-window). +- **Starting too many children at once.** Each child start adds to the parent's history. Keep total children per parent under 1,000 per [Temporal guidance](https://docs.temporal.io/workflows#when-to-use-child-workflows). If you need more children, switch to [MapReduce Tree](mapreduce-tree) or [Sliding Window](sliding-window). - **Passing large lists of IDs.** Workflow inputs are stored in event history. Passing millions of record IDs as a list will blow the history size limit. Use offset + length instead. - **Ignoring child failures.** A failed child does not automatically fail the parent unless you await all results. Always await child handles and handle errors explicitly. diff --git a/docs/mapreduce-tree.md b/docs/mapreduce-tree.md index c4ae411..7d94d07 100644 --- a/docs/mapreduce-tree.md +++ b/docs/mapreduce-tree.md @@ -79,9 +79,10 @@ The following examples show how each SDK implements the MapReduce Tree pattern. import { condition, defineSignal, - executeChild, + getExternalWorkflowHandle, proxyActivities, setHandler, + startChild, workflowInfo, } from "@temporalio/workflow"; import type * as activities from "./activities"; @@ -91,7 +92,12 @@ const { processRecord } = proxyActivities({ startToCloseTimeout: "30 seconds", }); -export const resultSignal = defineSignal<[string, string]>("leafResult"); +interface ResultPayload { + id: string; + results: string[]; +} + +export const resultSignal = defineSignal<[ResultPayload]>("leafResult"); export async function leafWorkflow( record: string, @@ -99,15 +105,10 @@ export async function leafWorkflow( ): Promise { const result = await processRecord(record); // Signal result back to parent node. - await executeChild(signalProxy, { - workflowId: parentWorkflowId, - args: [record, result], - }); + const parent = getExternalWorkflowHandle(parentWorkflowId); + await parent.signal(resultSignal, { id: record, results: [result] }); } -// Placeholder — in real usage call signalExternalWorkflow / getExternalWorkflowHandle. -async function signalProxy(_record: string, _result: string): Promise {} - export async function nodeWorkflow( records: string[], depth: number = 0, @@ -118,48 +119,49 @@ export async function nodeWorkflow( } const myId = workflowInfo().workflowId; - const results: string[] = []; + const collectedResults: string[] = []; let received = 0; + let expected = 0; - setHandler(resultSignal, (_record: string, result: string) => { - results.push(result); + setHandler(resultSignal, (payload: ResultPayload) => { + collectedResults.push(...payload.results); received++; }); if (records.length <= LEAF_THRESHOLD) { // Start one leaf per record. + expected = records.length; for (const record of records) { - executeChild(leafWorkflow, { + void startChild(leafWorkflow, { args: [record, myId], workflowId: `${myId}/leaf-${record}`, taskQueue: TASK_QUEUE, }); } - await condition(() => received === records.length); } else { // Split and recurse. const mid = Math.floor(records.length / 2); const chunks = [records.slice(0, mid), records.slice(mid)]; - + expected = chunks.length; for (let i = 0; i < chunks.length; i++) { - executeChild(nodeWorkflow, { + void startChild(nodeWorkflow, { args: [chunks[i], depth + 1, myId], workflowId: `${myId}/node-d${depth + 1}-${i}`, taskQueue: TASK_QUEUE, }); } - await condition(() => received === chunks.length); } - // Signal aggregated result up to parent. + // Wait until all expected signals have arrived. + await condition(() => received >= expected); + + // Signal aggregated results up to parent (if this is not the root). if (parentWorkflowId) { - await executeChild(signalProxy, { - workflowId: parentWorkflowId, - args: [myId, results.join(",")], - }); + const parent = getExternalWorkflowHandle(parentWorkflowId); + await parent.signal(resultSignal, { id: myId, results: collectedResults }); } - return results; + return collectedResults; } ``` @@ -246,6 +248,8 @@ package main import ( "fmt" "strings" + "time" + "go.temporal.io/sdk/workflow" ) diff --git a/docs/sliding-window.md b/docs/sliding-window.md index 0c1f22c..d6b6291 100644 --- a/docs/sliding-window.md +++ b/docs/sliding-window.md @@ -66,10 +66,10 @@ import { condition, continueAsNew, defineSignal, - executeChild, getExternalWorkflowHandle, proxyActivities, setHandler, + startChild, workflowInfo, } from "@temporalio/workflow"; import type * as activities from "./activities"; @@ -119,7 +119,7 @@ export async function slidingWindowWorkflow(input: SlidingWindowInput): Promise< // children from the previous run will signal us when they complete. const newFill = Math.min(windowSize - inFlight, recordIds.length - startIndex); for (let i = 0; i < newFill; i++) { - executeChild(recordProcessorWorkflow, { + await startChild(recordProcessorWorkflow, { args: [recordIds[nextIndex], parentId], workflowId: `${parentId}/record-${recordIds[nextIndex]}`, taskQueue: TASK_QUEUE, @@ -130,12 +130,18 @@ export async function slidingWindowWorkflow(input: SlidingWindowInput): Promise< active++; } - // As slots free up, start the next child. + // If the window is full after the initial fill, continue-as-new immediately. + if (dispatched >= windowSize) { + await continueAsNew({ recordIds, windowSize, startIndex: nextIndex, totalProcessed, inFlight: windowSize }); + return; + } + + // Slide the window: as each slot frees, start the next child. while (nextIndex < recordIds.length) { await condition(() => pendingSignals > 0); pendingSignals--; active--; - executeChild(recordProcessorWorkflow, { + await startChild(recordProcessorWorkflow, { args: [recordIds[nextIndex], parentId], workflowId: `${parentId}/record-${recordIds[nextIndex]}`, taskQueue: TASK_QUEUE, @@ -155,6 +161,7 @@ export async function slidingWindowWorkflow(input: SlidingWindowInput): Promise< totalProcessed, inFlight: windowSize, }); + return; } } @@ -208,7 +215,7 @@ class SlidingWindowWorkflow: @workflow.run async def run(self, input: SlidingWindowInput) -> int: - self._total_processed = input.total_processed + self._total_processed += input.total_processed record_ids = input.record_ids window_size = input.window_size start_index = input.start_index @@ -270,6 +277,9 @@ package main import ( "strings" + "time" + + enums "go.temporal.io/api/enums/v1" "go.temporal.io/sdk/workflow" ) @@ -301,16 +311,18 @@ func SlidingWindowWorkflow(ctx workflow.Context, input SlidingWindowInput) (int, completedCh := workflow.GetSignalChannel(ctx, CompletionSignal) nextIndex := input.StartIndex + totalProcessed := input.TotalProcessed dispatched := 0 active := input.InFlight - startChild := func(recordID string) { + startChild := func(recordID string) error { cwo := workflow.ChildWorkflowOptions{ WorkflowID: parentID + "/record-" + recordID, TaskQueue: TaskQueue, ParentClosePolicy: enums.PARENT_CLOSE_POLICY_ABANDON, } - workflow.ExecuteChildWorkflow(workflow.WithChildOptions(ctx, cwo), RecordProcessorWorkflow, recordID, parentID) + future := workflow.ExecuteChildWorkflow(workflow.WithChildOptions(ctx, cwo), RecordProcessorWorkflow, recordID, parentID) + return future.GetChildWorkflowExecution().Get(ctx, nil) } // Only start (windowSize - inFlight) new children. Carried-over in-flight @@ -320,18 +332,33 @@ func SlidingWindowWorkflow(ctx workflow.Context, input SlidingWindowInput) (int, newFill = windowSize - input.InFlight } for i := 0; i < newFill; i++ { - startChild(recordIDs[nextIndex]) + if err := startChild(recordIDs[nextIndex]); err != nil { + return totalProcessed, err + } nextIndex++ dispatched++ active++ } + // If the window is full after the initial fill, continue-as-new immediately. + if dispatched >= windowSize { + return 0, workflow.NewContinueAsNewError(ctx, SlidingWindowWorkflow, SlidingWindowInput{ + RecordIDs: recordIDs, + WindowSize: windowSize, + StartIndex: nextIndex, + TotalProcessed: totalProcessed, + InFlight: windowSize, + }) + } + // Slide the window. for nextIndex < len(recordIDs) { workflow.GetSignalChannel(ctx, CompletionSignal).Receive(ctx, nil) totalProcessed++ active-- - startChild(recordIDs[nextIndex]) + if err := startChild(recordIDs[nextIndex]); err != nil { + return totalProcessed, err + } nextIndex++ dispatched++ active++ diff --git a/sandbox-runner/patterns/sliding-window/go/workflows.go b/sandbox-runner/patterns/sliding-window/go/workflows.go index 625ea02..3a68367 100644 --- a/sandbox-runner/patterns/sliding-window/go/workflows.go +++ b/sandbox-runner/patterns/sliding-window/go/workflows.go @@ -48,13 +48,15 @@ func SlidingWindowWorkflow(ctx workflow.Context, input SlidingWindowInput) (int, dispatched := 0 active := inFlight - startChild := func(recordID string) { + startChild := func(recordID string) error { cwo := workflow.ChildWorkflowOptions{ WorkflowID: fmt.Sprintf("%s/record-%s", parentID, recordID), TaskQueue: TaskQueue, ParentClosePolicy: enums.PARENT_CLOSE_POLICY_ABANDON, } - workflow.ExecuteChildWorkflow(workflow.WithChildOptions(ctx, cwo), RecordProcessorWorkflow, recordID, parentID) + future := workflow.ExecuteChildWorkflow(workflow.WithChildOptions(ctx, cwo), RecordProcessorWorkflow, recordID, parentID) + // Wait for the child to be started so the command is committed before any ContinueAsNew. + return future.GetChildWorkflowExecution().Get(ctx, nil) } // Only start (windowSize - inFlight) new children. Carried-over in-flight @@ -65,7 +67,9 @@ func SlidingWindowWorkflow(ctx workflow.Context, input SlidingWindowInput) (int, } nextIndex := startIndex for i := 0; i < newFill; i++ { - startChild(recordIDs[nextIndex]) + if err := startChild(recordIDs[nextIndex]); err != nil { + return 0, err + } nextIndex++ dispatched++ active++ @@ -91,7 +95,9 @@ func SlidingWindowWorkflow(ctx workflow.Context, input SlidingWindowInput) (int, totalProcessed++ active-- - startChild(recordIDs[nextIndex]) + if err := startChild(recordIDs[nextIndex]); err != nil { + return 0, err + } nextIndex++ dispatched++ active++ diff --git a/sandbox-runner/patterns/sliding-window/typescript/workflows.ts b/sandbox-runner/patterns/sliding-window/typescript/workflows.ts index db77812..806125c 100644 --- a/sandbox-runner/patterns/sliding-window/typescript/workflows.ts +++ b/sandbox-runner/patterns/sliding-window/typescript/workflows.ts @@ -75,7 +75,7 @@ export async function slidingWindowWorkflow(input: SlidingWindowInput): Promise< let active = inFlight; for (let i = 0; i < newFill; i++) { - void startChild(recordProcessorWorkflow, { + await startChild(recordProcessorWorkflow, { args: [recordIds[nextIndex], parentId], workflowId: `${parentId}/record-${recordIds[nextIndex]}`, taskQueue: TASK_QUEUE, @@ -99,7 +99,7 @@ export async function slidingWindowWorkflow(input: SlidingWindowInput): Promise< await condition(() => pendingSignals > 0); pendingSignals--; active--; - void startChild(recordProcessorWorkflow, { + await startChild(recordProcessorWorkflow, { args: [recordIds[nextIndex], parentId], workflowId: `${parentId}/record-${recordIds[nextIndex]}`, taskQueue: TASK_QUEUE, From 7a7b1078ffc92c92cdfdf6f68d33a65009c93042 Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Mon, 18 May 2026 09:15:01 -0400 Subject: [PATCH 3/3] Add Batch Processing Patterns category with 4 new patterns ## Summary Adds a new **Batch Processing Patterns** category to the catalog with an overview page and four fully-documented patterns, each with runnable sandbox samples in TypeScript, Python, Go, and Java. ## New patterns | Pattern | Description | |---|---| | **Overview** (`batch-processing-patterns.md`) | Decision table and tile grid helping readers pick the right pattern based on record set size, parallelism model, and rate-control needs | | **Fan-Out with Child Workflows** (`fanout-child-workflows.md`) | Splits a record set into fixed-size chunks and assigns each to an independent child Workflow. Best for record sets up to ~4M items. | | **Batch Iterator** (`batch-iterator.md`) | Processes an unbounded record set one page at a time using Continue-As-New, with configurable per-page rate control. | | **Sliding Window** (`sliding-window.md`) | Maintains a bounded window of concurrently running child Workflows, launching a new one each time one completes. | | **MapReduce Tree** (`mapreduce-tree.md`) | Recursively splits the record set into halves until each leaf processes a small chunk, then aggregates results back up the tree. Maximum parallelism, no rate control. | ## Sandbox samples Each pattern ships runnable code in all four SDKs under `sandbox-runner/patterns/{batch-iterator,sliding-window,fanout-child-workflows,mapreduce-tree}/`. Each directory includes `pattern.json`, per-language source files, Go build-cache warmup stubs, and Java Maven warmup stubs. ## Sidebar Added a "Batch Processing Patterns" section to `docs/.vitepress/config.mts`. ## Out of scope The event-accumulator skeleton (`sandbox-runner/patterns/event-accumulator/`) is not included and will be completed separately. --- Design & Best Practices Guide.md | 913 ++++++++++++++++++++++++ batch_workflow_design_best_practices.md | 286 ++++++++ todo.md | 29 + 3 files changed, 1228 insertions(+) create mode 100644 Design & Best Practices Guide.md create mode 100644 batch_workflow_design_best_practices.md create mode 100644 todo.md diff --git a/Design & Best Practices Guide.md b/Design & Best Practices Guide.md new file mode 100644 index 0000000..014a935 --- /dev/null +++ b/Design & Best Practices Guide.md @@ -0,0 +1,913 @@ +# SA Workflow Design & Best Practices Guide + +[**Overview 2**](#overview) + +[📋 Prerequisites 2](#📋-prerequisites) + +[🎯 Goals 2](#🎯-goals) + +[⛔ Non-Goals 2](#⛔-non-goals) + +[**Preparation 2**](#preparation) + +[For the SA 2](#for-the-sa) + +[For the Customer 3](#for-the-customer) + +[**Workflow Design 4**](#workflow-design) + +[General 4](#general) + +[Common Design Questions 5](#common-design-questions) + +[**Best Practices 8**](#best-practices) + +[Workflows 8](#workflows) + +[Child Workflows 10](#child-workflows) + +[Activities 11](#activities) + +[Signals 13](#signals) + +[Queries 15](#queries) + +[Update 15](#update) + +[Workers & Task Queues 16](#workers-&-task-queues) + +[Timers 17](#timers) + +[Schedules 17](#schedules) + +[Cron Jobs 18](#cron-jobs) + +[Side Effects 19](#side-effects) + +[Data Converter 19](#data-converter) + +[Visibility 19](#visibility) + +[Versioning 20](#versioning) + +[Interceptors 21](#interceptors) + +[Sessions/Worker-Specific Task Queues 21](#sessions/worker-specific-task-queues) + +[Storage Optimization (Long Running Workflows) 21](#storage-optimization-\(long-running-workflows\)) + +[**Appendix 21**](#appendix) + +[1\. Best Practices Checklist 21](#best-practices-checklist) + +[2\. Useful Links: SDK Features Matrix 23](#useful-links:) + +# + +# Overview {#overview} + +## 📋 Prerequisites {#📋-prerequisites} + +The customer has already vetted Temporal and/or Temporal Cloud as a suitable platform for building their use case(s). + +## 🎯 Goals {#🎯-goals} + +The goals of a Workflow design session are to: + +* Ensure the customer is using the Temporal primitives correctly and aligned with our best practices +* Answer specific customer questions on Workflow design +* Advise on any gotchas or potential issues with the Workflow +* Build customer confidence on their path to production with Temporal + +## ⛔ Non-Goals {#⛔-non-goals} + +* Discover or Confirm Temporal or Temporal Cloud suitability for this use case. See [SA Customer Evaluation Runbook](https://docs.google.com/document/d/18f5K7wmOKy5luT1gajKqkCuys1q9tNkM9DwDNXVsTmw/edit#heading=h.o0epy3bs2n54)for that. +* Deep debugging or troubleshooting of production issues (enter a support ticket for that) +* Perfection: no 1-hour session will yield a perfect workflow. It will be an iterative process + +# Preparation {#preparation} + +## For the SA {#for-the-sa} + +The SA must be familiar with all of the Temporal concepts and primitives in this document. This document is not a replacement for the [Temporal documentation](https://docs.temporal.io/), the [Developer’s Guide](https://docs.temporal.io/dev-guide), the [SDK Samples](https://github.com/temporalio?q=samples-&type=public&language=&sort=stargazers), the [Community Forums](https://community.temporal.io/), or Slack. Rather, this document highlights (and references) knowledge from those sources that is relevant to Workflow Design and Best Practices. + +This guide should help the SA focus on key Workflow design elements and usage of Temporal primitives in a Workflow. However, it will take time for you to review and become familiar with all of the topics. Take that time. Read the docs. Play with the samples. Shadow your fellow SAs. Contribute back to this doc with your newly acquired knowledge. Relax with your favorite beverage as you bask in the growth of your Temporal expertise and the value you bring to our customers everyday. Cheers\! + +## For the Customer {#for-the-customer} + +Prior to the meeting, request that your customer provides the following: + +* A description of the use case and the business process during which it runs +* A diagram of the Workflow with Activities, etc. +* Access to Workflow Execution history in the Temporal UI (if code has been written and run successfully) +* Access to code (if code has been written and customer is comfortable with sharing) + +## Suggested Session Outline + +# Workflow Design {#workflow-design} + +The following sections include questions, prompts and areas to guide the user throughout the design session. + +## General {#general} + +### ● What is the customer’s **level of experience** with Temporal? + +* Is this the first Workflow they have designed or implemented? +* What is their overall comfort level with and knowledge of Temporal, its primitives, and the execution model? + +### ● Have the customer **walk through the diagram**. + +* What is the business process? +* Where does Temporal fit in the overall architecture? + * Is everything a Workflow, or is it just being used for one part of the overall system? +* What invokes the Workflow? + * Is it a user action, a system event, or a cron/schedule? +* How often is the Workflow called? + * What is the expected peak volume? (e.g. 10x/day? 10x/second? More? Less?) +* How long does the Workflow run? + * Does it complete in seconds? Months? Longer? +* How many Activities, or other steps/actions, are in the Workflow? + * Are they sequential, or do they run in parallel? +* What is done in the Activities? + * Are Activities too granular or too broad? + * Are there multiple steps within an Activity? + +At this point, the discussion may still be high level. Perhaps your customer has attempted to steer the conversation in another direction. + +* Ensure you have a good high level understanding before going too deep into the weeds. +* If your customer has immediate questions, and areas to focus on, you can follow their lead using the sections below to guide your review of their Workflows, Activities, Signals, Queries, etc… as needed. +* If they bring you deep into code, you can pull back out (if you need to) by asking to view the workflow history in the UI, or revisit the diagram + +### ● **Gut check** for Use Case and Design + +* #### Is this an appropriate use-case for Temporal? + + * Almost anything can be a valid use case, but watch out for: + * Extreme low latency, e.g. high-frequency algo trading where milliseconds matter + * Synchronous read-only operations, i.e. if you just need to query records out of a DB, you don’t need Temporal. + * Big data passing through Temporal. Temporal is a control-plane, it should not be used for the data-plane. + +* #### Indicators of a good design + + * Workflow [determinism](#✔-do-they-understand-workflow-determinism-requirements?) + * Activity [idempotency](#✔-do-they-understand-activity-idempotency-as-a-best-practice?) + * Appropriate management of the [Event History](#✔-do-they-understand-the-event-history?) size + * Understanding and consideration of [Temporal limits](https://docs.temporal.io/self-hosted-guide/defaults) and [Cloud limits](https://docs.temporal.io/cloud/limits) + * Appropriate use of [timeouts](#✔-what-are-the-activity-timeout-settings?) and [retry](#✔-what-is-the-activity-retry-policy?) options + +* #### Indicators of a sub-optimal design + + * DIY state management + * Are they unnecessarily persisting status or entities to a database? + * It is valid/necessary to persist data for historical, reporting or aggregate query purposes + * Are they sending messages (via Kafka, RabbitMQ, etc.) for the purpose of choreography to other components in the system? (perhaps those should be Workflows as well) + * Misuse of [Child Workflows](#✔-do-they-use-child-workflows?) + * Misuse of [Local Activities](#✔-do-they-use-local-activities?) + * Misuse of [Search Attributes](#✔-do-they-use-search-attributes?) + +## Common Design Questions {#common-design-questions} + +### ● When to use Local Activity (vs. Activity)? + +* Use regular Activities unless your use case requires very high throughput and large Activity fan-outs of very short-lived Activities. +* Reference: + * Best practices for [Local Activities](#✔-do-they-use-local-activities?) + * Community forum: [Local Activity vs Activity](https://community.temporal.io/t/local-activity-vs-activity/290/3) + * Slide deck [Workflow Latency: Regular Activities vs Local Activities](https://docs.google.com/presentation/d/1BFipVEynxs5-fC7aeOsPO4xSeWan5L7-0WTdIi1DZU0/edit#slide=id.p) + +### ● When to use Child Workflow (vs. Activity)? + +* When in doubt, use an Activity +* Reference: + * Best practices for [Child Workflows](#✔-do-they-use-child-workflows?) + * Docs: [When to use a Child Workflow versus an Activity](https://docs.temporal.io/encyclopedia/child-workflows#child-workflow-versus-an-activity) + +### ● Can a WF communicate with another WF? + +* Yes, via Nexus sync operations +* Yes. Can be done with: + * Signals + * Queries + * Updates + + You can Signal from a Workflow and Query/Update through an Activity within a Workflow. + + +### ● Can a WF communicate with another WF **in a different Namespace**? + +* Yes, via Nexus +* Old Way – Yes through an Activity as follows: + * Create a new Temporal Client + * Use that Client within an Activity to Signal, Query or Update the WF in another Namespace + * See for reference: [this sample](https://github.com/temporalio/samples-java/tree/main/core/src/main/java/io/temporal/samples/batch/slidingwindow) + +### ● How to handle large payloads? + +* Pass references to data (e.g. filenames/handles) +* Consider [External Storage](https://docs.temporal.io/external-storage) + * (Previously: Consider [Temporal Large Payload Codec](https://github.com/DataDog/temporal-large-payload-codec) (from DataDog) to automatically replace data with reference + * Some caution using a Large Payload Codec + * Causes a remote access for every payload + * Do it explicitly on a case-by-case basis (Don’t do this implicitly) + * Lots of remote writes/reads can affect workflow performance. +* Consider moving the work into activities + * The first activity takes the ID and gets the data and continues the workflow + * The last activity takes the results, stores it and returns an ID + * Challenge is knowing which bits of data are needed inside the workflow logic +* Store data on local disk and use sticky Activities +* Consider a broader Activity that gathers AND processes data within a single Activity +* Reference: + * Best practices for [Activity Input and Outputs](#✔-what-are-the-input-and-output-sizes-for-activity-payloads?) + +### ● How to handle large Workflow History? + +* Use Continue-As-New at or before the WARN levels (10mb or 10k events) + * Note that Cloud users will *not* see the default warning, as it is emitted on the server side, however they can obtain WF history length through the SDK. + * Also, the SDKs now contain a “suggestContinueAsNew()” API that can be used to determine when to CAN. + * Java \- [Workflow.getInfo().isContinueAsNewSuggested()](https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/workflow/WorkflowInfo.html) + * Go \- [GetContinueAsNewSuggested()](https://pkg.go.dev/go.temporal.io/sdk@v1.25.1/internal#GetWorkflowInfo) from GetWorkflowInfo + * Python \- [is\_continue\_as\_new\_suggested](https://python.temporal.io/temporalio.workflow.Info.html#is_continue_as_new_suggested) from Workflow.info + * TypeScript \- [workflowInfo().continueAsNewSuggested](https://typescript.temporal.io/api/interfaces/workflow.WorkflowInfo#continueasnewsuggested) + * Dotnet \- [Workflow.ContinueAsNewSuggested](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html#Temporalio_Workflows_Workflow_ContinueAsNewSuggested) +* Use Child Workflows to partition the work +* Reference: + * Best practices for [WF Event History](#✔-do-they-understand-the-event-history?) + +### ● How to run Activities / Child WFs in parallel and aggregate results? + +* All executions by the API (e.g. Execute Activity, Execute Child WF) return some form of a Promise, Future, or Awaitable (depending on the SDK). +* This is by default asynchronous invocation. To run execs in parallel, just don’t block on getting the result of a promise +* Gather the promises in an array and when appropriate, iterate over the array and Get the results from the promises + +### ● When to use Worker-specific Activity Task Queues? + +Use Worker-specific Activity Task Queues for: + +* Special purpose hardware or capabilities for workers (e.g. GPUs) +* Activities that need to run on the same worker + * Such as when workers need to access local filesystem files. + * Such as when workers need to run on a machine with elevated access. + * Such as when workers need to run on a machine with differentiated hardware, such as GPUs. +* Rate-limiting +* Reference: + * Best practices on [Activity specific task queues](#✔-do-any-activities-run-on-unique-task-queues?) + +### ● When to use a schedule vs a timer? + +* Use timers when: + * Your delay is relative (e.g. wait 2 days) + * Your delay happens inside a Workflow Execution + * Would exceed 200 schedule actions per second (default is 10\) +* Use schedules when: + * Your delay is for an entire Workflow Execution + * Your delay is for a set time (e.g. 3pm Wednesdays) + * Your delay is recurring (though you can limit schedule runs to a single execution) + +### ● Can I intentionally let a task queue backlog grow by underprovisioning workers for some period of time? + +* Yes, **BUT**… be aware that Temporal does not guarantee FIFO. Task processing currently prioritizes sync match over async match. Therefore a task on the backlog may be processed after a newer task is scheduled (when the newer task is able to sync match). \[Issue [2517](https://github.com/temporalio/temporal/issues/2517) will provide config alternative when implemented\] + +# Best Practices {#best-practices} + +## Workflows {#workflows} + +### ✔ Do they understand Workflow [**Determinism**](https://docs.temporal.io/workflows#deterministic-constraints) requirements? {#✔-do-they-understand-workflow-determinism-requirements?} + +* A Workflow Definition can (and will) be executed many times. Any re-execution must follow the same execution path for a given input to the Workflow Definition. +* Have they encountered non-determinism errors (NDEs)? + * Are they unit testing using the Replayer with past Workflow histories? + * If NDEs occur due to code changes, see the Versioning section. +* Ref: [community post about determinism](https://community.temporal.io/t/workflow-determinism/4027) + +### ✔ Do they understand the [**Event History**](https://docs.temporal.io/workflows#event-history)? {#✔-do-they-understand-the-event-history?} + +* A single execution has limits on the size of the Event History. Are they at risk of hitting the limits? + * Is there a strategy to use [Continue-As-New](https://docs.temporal.io/workflows#continue-as-new) within a single workflow or use [Child Workflows](https://docs.temporal.io/workflows#child-workflow) to partition the work across multiple Workflows? +* Temporal scales out incredibly well. Partition work *across* Workflows rather than performing too much work *within* a single Workflow. + +### ✔ Do they set [**Workflow Id**](https://docs.temporal.io/workflows#workflow-id) to a meaningful business identifier (or not)? {#✔-do-they-set-workflow-id-to-a-meaningful-business-identifier-(or-not)?} + +* Are they leveraging Workflow Id uniqueness constraints for running Workflows? +* Do they explicitly set a [Workflow Id Reuse Policy](https://docs.temporal.io/workflows#workflow-id-reuse-policy) for closed Workflows? If so, why? +* A Workflow Id cannot be reused for Open Workflows. + * It can effectively act as an idempotency key in designs where the Workflow may be started more than once. +* Do not reuse the same Workflow Id with high frequency as it can result in server performance issues \[[ref](https://temporaltechnologies.slack.com/archives/C01H1G7J98F/p1702041104745229?thread_ts=1701805613.351819&cid=C01H1G7J98F)\] +* In addition, the Workflow ***Run Id*** can change during a Workflow Execution (e.g. during Retry). + * Do *not* rely on Run Id for any logical choices in a Workflow, as this will lead to non-determinism issues. + +### ✔ Do they explicitly set any **Workflow Timeout** options? {#✔-do-they-explicitly-set-any-workflow-timeout-options?} + +* Generally, the defaults for Workflow Timeouts are sufficient. We do *not recommend changing the defaults*. + * Workflow Execution Timeout and Workflow Run Timeout default to infinite + * Workflow Task Timeout defaults to 10 seconds + * Potential reasons to increase WFT timeout: + * Time consuming data converters + * Large WF history + * Maximum value is 120 seconds + * Can be overridden at a namespace level via Dynamic Config via a support ticket + * Monitor CPU & Garbage Collection (if applicable) and the following SDK metrics + * workflow\_task\_execution\_latency + * request\_failure on the API RespondWorkflowTaskCompleted + * Possible causes of Workflow Task Timeouts include + * CPU going over 100% + * Starting a ton of async activities and combined inputs go over 4 MB + * Returning more than 2 MB of data + * Workflow tasks will retry with a backoff until a maximum retry interval of 10 minutes is reached \[[ref](https://temporaltechnologies.slack.com/archives/CTEFJ76QG/p1721703278870979)\] +* If you need to timeout a Workflow, use explicit Timers within the Workflow, rather than setting exec or run timeouts. + +### ✔ Do they understand **Workflow Failure** / error / exception handling? {#✔-do-they-understand-workflow-failure-/-error-/-exception-handling?} + +* Any error that is **not** a [Temporal Failure](https://docs.temporal.io/references/failures) will fail the WF Task, which will be retried indefinitely with exponential backoff until it succeeds + * The exception is the Go SDK, where \`error\` fails the WF and \`panic\` fails the WFT + +### ✔ Do they set a **Workflow Retry** policy? {#✔-do-they-set-a-workflow-retry-policy?} + +* Why do they? + * This is generally not recommended. + * By design, a Workflow should not fail due to intermittent issues + +## Child Workflows {#child-workflows} + +### ✔ Do they use [**Child Workflows**](https://docs.temporal.io/workflows#child-workflow)? {#✔-do-they-use-child-workflows?} + +* **Do** use Child Workflows strategically to: + * Partition large workloads into smaller chunks to stay under history size limits + * Target specific hosts (eg TaskQueue) due to security, workload profile, or other *strategic* reasons + * Extract behavior to simplify or explicitly define team ownership (eg [Shared Kernel](https://yoan-thirion.gitbook.io/knowledge-base/software-architecture/ddd-re-distilled#shared-kernel)) +* **Do not** use Child Workflows to: + * Organize code + * Use standard features of your programming language (e.g. packages, objects, structs, etc.) for code organization and modularity + * Reduce cost + * Child WFs will result in more events and actions than just using an Activity within the main WF (and on cloud Child WF counts as 2 actions) +* [When to use a Child Workflow versus an Activity](https://docs.temporal.io/encyclopedia/child-workflows#when-to-use-child-workflows) + * **When in doubt, use an Activity** +* [Valid reasons to use a Child Workflow](https://community.temporal.io/t/purpose-of-child-workflows/652/2) +* As of July 2025, starting hundreds of child workflow per parent can cause multi-minute delays + * Details [here](https://temporaltechnologies.slack.com/archives/C03HNADRLKY/p1752262100562919?thread_ts=1752262034.541529&cid=C03HNADRLKY) + +### ✔ **How many** Child WFs are being started by a Parent? {#✔-how-many-child-wfs-are-being-started-by-a-parent?} + +* A single Parent SHOULD NOT start more than 1000 Children (per [docs](https://docs.temporal.io/workflows#when-to-use-child-workflows)) + * **Note** that this is *not* a hardcoded limit though, but rather a code-smell. + * See [this thread](https://temporaltechnologies.slack.com/archives/C01RN061UMR/p1660750792060049?thread_ts=1660747402.058829&cid=C01RN061UMR) for a discussion where this “limit” was clarified as being *guidance*, not *absolute.* + * Also see this [discussion](https://temporaltechnologies.slack.com/archives/C03BY3HR2RH/p1669136625638709?thread_ts=1669121603.628799&cid=C03BY3HR2RH) where an alternative is proposed to remove the code-smell. + * Alternatives are proposed [here](https://community.temporal.io/t/batch-processing-vs-multiple-workflows/1688/2) + +### ✔ Do the Parent and Child WF **need to share state**? {#✔-do-the-parent-and-child-wf-need-to-share-state?} + +* If so, they can only communicate via Signals + * Local state cannot be shared + +### ✔ Does the Parent **need to wait** on the Child Workflow result? {#✔-does-the-parent-need-to-wait-on-the-child-workflow-result?} + +* Review the [Parent Close Policy](https://docs.temporal.io/encyclopedia/child-workflows#parent-close-policy) configuration + +## Activities {#activities} + +### ✔ Do they understand Activity [**Idempotency**](https://docs.temporal.io/activities#idempotency) as a best practice? {#✔-do-they-understand-activity-idempotency-as-a-best-practice?} + +* An Activity Definition may be executed multiple times during failure scenarios. + * An Activity will only be executed *once* if it is successful, but has *at-least-once* semantics due to potential failure during execution +* We recommend using idempotency keys +* See [https://temporal.io/blog/idempotency-and-durable-execution](https://temporal.io/blog/idempotency-and-durable-execution) + +### ✔ Are Activities **short-term or long**\-**running**? {#✔-are-activities-short-term-or-long-running?} + +* Are [timeouts](#bookmark=id.omql4akvpemu) set appropriately for the Activity duration? +* There is not a firm definition of “short” (perhaps a few minutes or less?) +* “Long” running activities should [Heartbeat](https://docs.temporal.io/activities#activity-heartbeat) + * Use a short Heartbeat Timeout value + * Heartbeat frequently + * Include custom information/payload on the Heartbeat + * For saving progress. + * Do they understand that Heartbeats are [Throttled](https://docs.temporal.io/activities#throttling) by the SDK? + * An activity is a unit of failure detection (through timeouts), retries and visibility. It is OK to pack multiple operations in a single activity if you are OK with specifying a single timeout for all of them together and retrying them together. It also makes troubleshooting harder as it is less clear at which point the process is having issues. (source: Max in [community slack channel](https://temporalio.slack.com/archives/C04S80QKB2Q/p1711302488204919?thread_ts=1711297768.296809&cid=C04S80QKB2Q)) +* OK but what about *really long running (months)?* + * See [this excellent thread](https://temporaltechnologies.slack.com/archives/C04NYM5D3U6/p1757449292278879?thread_ts=1757447403.123839&cid=C04NYM5D3U6) in slack + * Heartbeats 👍, [signal-back-to workflow pattern](https://docs.temporal.io/activity-execution#when-to-use-async-completion), async completion + +### ✔ What are the **Input and Output sizes** for Activity Payloads? {#✔-what-are-the-input-and-output-sizes-for-activity-payloads?} + +* Is there risk of reaching the 2MB Blob Size Limit for Payloads? + * Or the History total size limit of 50MB? +* Should they only pass references to the data, rather than the actual data? +* Should they use sticky Activity queues (also known as “Worker-specific Activities”) to allow for the data to be stored locally and shared across Activities? +* Are they compressing the payloads via a Data Converter? + * Compression is recommended by default. + +### ✔ Is the Activity performing any **polling**? {#✔-is-the-activity-performing-any-polling?} + +* [What is the best practice for a polling activity?](https://community.temporal.io/t/what-is-the-best-practice-for-a-polling-activity/328/2) +* If polling interval is frequent, perform polling within the Activity using iteration +* If polling interval is infrequent, then perform polling by using Retry Options +* Can they use a Signal or [Async Activity Completion](https://docs.temporal.io/activities#asynchronous-activity-completion) approach instead? + +### ✔ Is the Activity **listening** on a port or socket? {#✔-is-the-activity-listening-on-a-port-or-socket?} + +* The listening process should be run outside the Workflow. + * Use Signal or SignalWithStart to start the Workflow from the listening process. + +### ✔ What are the Activity **Timeout** settings? {#✔-what-are-the-activity-timeout-settings?} + +* [Schedule-To-Start](https://docs.temporal.io/activities#schedule-to-start-timeout) should generally **not** be set (default is ♾️), but temporal\_activity\_schedule\_to\_start\_latency metric should be monitored. Schedule-to-start should only be used if workflow wants to take action in case worker(s) are busy or otherwise unavailable, for example when using host-specific task queues. +* Either [Start-To-Close](https://docs.temporal.io/activities#start-to-close-timeout) or [Schedule-To-Close](https://docs.temporal.io/activities#schedule-to-close-timeout) **must** be set. + * Setting Start-To-Close is **strongly** recommended + * This timer resets on each retry + * Schedule-To-Close default is ♾️ + * This timer is inclusive of all retries (i.e. it does **not** reset on each retry) +* Each Activity should be individually considered for its own optimal timeout settings + * [One does not simply](https://www.dictionary.com/e/memes/one-does-not-simply/) use the same Timeout settings for every Activity in a WF +* Do they know Activity Timeout is enforced on the server side? + * Timeout setting should be **greater** than the longest potential time in which the Activity would complete under normal circumstances. + * Or in other words they should have **shorter** timeout enforced on the worker side with upstream actions that may take longer to complete, e.g. DB write, API requests. If not this will lead to duplicate actions, and other resource contention when Activity Retry kicks in. + + [The 4 Types of Activity timeouts](https://temporal.io/blog/activity-timeouts) + +### ✔ Do Activity Tasks run on Workers separate from the Workflow Tasks? {#✔-do-activity-tasks-run-on-workers-separate-from-the-workflow-tasks?} + +* Do they require optimized compute resources or hardware? (e.g. CPU/Mem, GPUs) + +### ✔ Do sequential Activities run on the same Worker (i.e. are they sticky)? {#✔-do-sequential-activities-run-on-the-same-worker-(i.e.-are-they-sticky)?} + +* Are there large payloads that are used by multiple Activities that can’t or shouldn’t go through Temporal? + +### ✔ Do they use [**Local Activities**](https://docs.temporal.io/activities#local-activity)? {#✔-do-they-use-local-activities?} + +* **We recommend using regular Activities unless your use case requires very high throughput and large Activity fan outs of very short-lived Activities.** +* How long is the Local Activity expected to run? + * Should not run for more than a few seconds, *inclusive of retries* +* With LA, they lose the ability to rate limit & route tasks to workers +* Reference: [Local Activity vs Activity](https://community.temporal.io/t/local-activity-vs-activity/290/3) +* Reference: [Regular Activities vs Local Activities](https://docs.google.com/presentation/d/1BFipVEynxs5-fC7aeOsPO4xSeWan5L7-0WTdIi1DZU0/edit?slide=id.g36c5e7f8258_1_781#slide=id.g36c5e7f8258_1_781) + +### ✔ What is the [**Activity Retry**](https://docs.temporal.io/retry-policies) policy? {#✔-what-is-the-activity-retry-policy?} + +* Each Activity should be individually considered for its own optimal retry settings. +* Do they use the default policy or set a custom policy? +* Do they configure any specific [Non-Retryable](https://docs.temporal.io/retry-policies#non-retryable-errors) errors? +* Reference: [https://docs.temporal.io/encyclopedia/detecting-activity-failures](https://docs.temporal.io/encyclopedia/detecting-activity-failures) +* Reference: [https://temporal.io/blog/failure-handling-in-practice](https://temporal.io/blog/failure-handling-in-practice) + +### ✔ Do they understand **Activity Failure** / error / exception handling? {#✔-do-they-understand-activity-failure-/-error-/-exception-handling?} + +* If an Activity Execution fails, the error is returned to the Workflow, which decides how to handle it. +* Reference: [https://docs.temporal.io/references/failures](https://docs.temporal.io/references/failures) + +### ✔ Would an Activity need to receive [**Cancellation**](https://docs.temporal.io/activities#cancellation)? {#✔-would-an-activity-need-to-receive-cancellation?} + +* If so, the Activity *must* Heartbeat (or be a Local Activity in Core-based SDKs only). + +### ✔ Do they use too many Activities, or too few? + +* Guidance here: [https://temporal.io/blog/how-many-activities-should-i-use-in-my-temporal-workflow](https://temporal.io/blog/how-many-activities-should-i-use-in-my-temporal-workflow) + +## Signals {#signals} + +### ✔ Do they use [**Signals**](https://docs.temporal.io/workflows#signal)? {#✔-do-they-use-signals?} + +* If not, should they be? + * Do they have a need to update the state of a Workflow during its execution? +* If so, what is sending the Signal? + * Is it another Workflow or a Temporal Client in another application? + +### ✔ Do they understand that Signals are… {#✔-do-they-understand-that-signals-are…} + +* Recorded in the Event History (i.e. they will be replayed when a Workflow is replayed) +* Delivered to a Workflow as part of the next scheduled Workflow Task + * Therefore, there may be some latency in delivery, depending on the current Workflow Task completing and the next being scheduled +* Delivered in the order they are received +* A single workflow can only handle a few signals per second (≤5/sec) and flooding with signals will result in not being able to continue-as-new / eventually hitting event limits of workflow history. + +✔ Are they checking the return value or exceptions from sending a Signal? + +* A Signal call can give errors/throw exceptions: + * A workflow execution doesn’t exist + * A workflow execution is closed +* See [Problems When Sending a Signal](https://docs.temporal.io/develop/java/message-passing#message-handler-troubleshooting), and SDK-specific info, for example [Java](https://docs.temporal.io/develop/java/message-passing#message-handler-troubleshooting): + * The Client can't contact the server: You'll receive a [WorkflowServiceException](https://javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/client/WorkflowServiceException.html) on which the cause is a [StatusRuntimeException](https://grpc.github.io/grpc-java/javadoc/io/grpc/StatusRuntimeException.html) and status of UNAVAILABLE (after some retries). + * The Workflow does not exist: You'll receive a [WorkflowNotFoundException](https://javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/client/WorkflowNotFoundException.html). + +### ✔ Are Signal handlers **idempotent** and **deterministic**? {#✔-are-signal-handlers-idempotent-and-deterministic?} + +* It is possible (though unlikely) that a Signal could be delivered more than once ([reference](https://docs.temporal.io/encyclopedia/application-message-passing#footnote-label)). +* Signal-handling code is Workflow code; therefore, it must adhere to the constraints of Workflow code (e.g. determinism) + +### ✔ Is there a **high rate or volume** of Signals? {#✔-is-there-a-high-rate-or-volume-of-signals?} + +* The recommended guidance is to limit signals to ≤5 per second sustained for optimal workflow performance + * The theoretical limit is tied to database latency (1/database\_latency), so \~20/sec with 50ms latency +* All updates to a workflow are performed under a single lock, and workflows need resources for other operations beyond just signal processing \- executing workflow tasks, activities, etc., which all require database updates, + * so a high volume of Signals or Updates can prevent workflow progress +* Is the total *volume* of Signals a concern for exceeding the Event History size limits? + * A single Workflow Execution is limited to 10000 Signals received in Temporal Cloud +* Does the Workflow receiving the Signal also use Continue-As-New? + * Is there potential for the *rate* of Signals to be too fast for Continue-As-New to be successfully invoked? + * In order for CAN to run there must be a moment (\~100ms) when there are no unhandled Signals in the Workflow + * If the workflow cannot continue-as-new then the event history size increases until a WorkflowExecutionTerminated occurs with reason: Workflow History Size/count exceeds limit +* Signals briefly lock a workflow execution, many signals to the same workflow can cause latency and limit throughput + * [Engineering/latency slack thread](https://temporaltechnologies.slack.com/archives/C02LGH6BG3A/p1684975109797379?thread_ts=1684963896.489869&cid=C02LGH6BG3A) + * [SA slack design discussion about signal volume](https://temporaltechnologies.slack.com/archives/C03HNADRLKY/p1752073205981859?thread_ts=1752008525.412319&cid=C03HNADRLKY). + +### ✔ Do Signal handlers **invoke Activities**? {#✔-do-signal-handlers-invoke-activities?} + +* This should be avoided. Try to limit the scope of the Signal handler to updating the Workflow state. Let the Workflow code react to state changes to invoke Activities. + * [Recommendation from Max in the Community Forum](https://community.temporal.io/t/signal-method-invocation-and-workflow-thread-safety/1679/2) + +## Queries {#queries} + +### ✔ Do they use [**Queries**](https://docs.temporal.io/workflows#query)? {#✔-do-they-use-queries?} + +* If not, should they be? + * Are they manually storing or exporting Workflow state elsewhere during execution? Why? + +### ✔ Do they understand that Queries are… {#✔-do-they-understand-that-queries-are…} + +* A synchronous operation +* Available for both *running* and *completed* Workflows? + * Note: a Worker must be running and listening on the Task Queue + +### ✔ Do Query handlers perform **read-only** operations? {#✔-do-query-handlers-perform-read-only-operations?} + +* Queries must never mutate the state of a Workflow + +## Update {#update} + +### ✔ Do they use [**Update**](https://docs.temporal.io/workflows#update)? {#✔-do-they-use-update?} + +* The Update handler function must be idempotent and deterministic +* The handler function runs as part of the Workflow code and is subject to Workflow code constraints + +### ✔ Do they perform **Validation** on the Update request? {#✔-do-they-perform-validation-on-the-update-request?} + +* This is optional (although recommended) + * Validation function cannot mutate the Workflow state + * Validators have the same basic restrictions as Queries +* Updates that are rejected due to Validation are not recorded in the event history. + +### ✔ Is it ok to invoke activities within the Update Handler? {#✔-is-it-ok-to-invoke-activities-within-the-update-handler?} + +* [From Maxim](https://temporaltechnologies.slack.com/archives/CTEFJ76QG/p1713543088839059?thread_ts=1709568936.227519&cid=CTEFJ76QG): + * Beside Java, it’s ok to invoke activities within the update handler. I advise Java due to how the system thread is per handler level. + +### ✔ Do they use Early Return to reduce latency? + +* See [Temporal Interactive Latency Options](https://docs.google.com/presentation/d/1dYU3lug3PdbliyEH2X_I9L2A27VA9CXfl5Jnw4t_azE/edit?pli=1&slide=id.g3129f517e9e_0_109#slide=id.g3129f517e9e_0_109) for techniques + +## Workers & Task Queues {#workers-&-task-queues} + +### ✔ How many Workers do they run? {#✔-how-many-workers-do-they-run?} + +* Run at least 2 for high availability + +### ✔ Do Workers register all Workflows and Activities that can be dispatched on the Task Queue? {#✔-do-workers-register-all-workflows-and-activities-that-can-be-dispatched-on-the-task-queue?} + +* All Workers listening to a given Task Queue must have identical registrations of Activities and/or Workflows + +### ✔ Do any Activities run on unique Task Queues? {#✔-do-any-activities-run-on-unique-task-queues?} + +* Reasons to do so: + * Rate-limiting + * Activities that need to run on the same worker + * Special purpose workers, e.g. GPUs + * You can do this for [Differing priority for the work](https://community.temporal.io/t/activity-with-priorities/3398/5) \- but see [TQ Priority and Fairness](https://docs.temporal.io/develop/task-queue-priority-fairness) for an easier way + +### ✔ Do they configure rate-limiting for a Worker or Task Queue? {#✔-do-they-configure-rate-limiting-for-a-worker-or-task-queue?} + +* Worker-side rate limiting + * \`maxConcurrentWorkflowTaskExecutionSize\`, \`maxConcurrentActivityExecutionSize\` and \`maxConcurrentLocalActivityExecutionSize\` define the number of total available slots for that Worker + * \`**maxWorkerActivitiesPerSecond**\` +* Server-side rate limiting + * \`maxTaskQueueActivitiesPerSecond\` + * Must set the *same* value in each Worker that connects to the TQ +* [Discussion about rate limiting options and best practices](https://community.temporal.io/t/rate-limit-configuration-and-best-practices/5498/2) +* [Task Queue Activities per second architecture Diagram](https://lucid.app/lucidchart/408e3936-667b-435d-8fc3-603771ce0298/edit?invitationId=inv_465a4565-10e2-41af-b1a3-daa9280c43ac&page=0_0#) + +### ✔ Do they require strict ordering for Tasks? {#✔-do-they-require-strict-ordering-for-tasks?} + +* Without Priority and Fairness, Task Queues **do not** have any ordering guarantees + * For example, Tasks *across* separate WF executions may be executed in an order different than they were received. + * However, the order of executing *within* a single WF is fully controlled by the WF logic. + * Also, Signals for a single WF execution will always be delivered in the order they were received. +* TQ [Priority and Fairness](https://docs.temporal.io/develop/task-queue-priority-fairness) allow for adjustments to how tasks are distributed in a task queue. + * [Priority](https://docs.temporal.io/develop/task-queue-priority-fairness#task-queue-priority) allows [Tasks](https://docs.temporal.io/tasks) to be executed in Priority order + * . [Fairness](https://docs.temporal.io/develop/task-queue-priority-fairness#task-queue-fairness) prevents one set of Tasks from blocking others within the same priority level. + * You can use Priority and Fairness individually or combine them to express Fairness within a Priority level. + + +### ✔ Can multiple workflows share one Task Queue? When should multiple task queues be used? {#✔-can-multiple-workflows-share-one-task-queue?-when-should-multiple-task-queues-be-used?} + +* You can use multiple workflows per task queue +* Reasons to use multiple queues: + * Multiple deployment units (services). + * Rate limiting and flow control. + * Routing to specific hosts. + * More info in [this community post from Maxim](https://community.temporal.io/t/in-what-situation-should-we-use-multiple-separated-task-queues/1254). + +## Timers {#timers} + +### ✔ What is the duration set for a timer or sleep? {#✔-what-is-the-duration-set-for-a-timer-or-sleep?} + +* The shortest reliable duration is 1 second + * Anything less than one second may not be reliable + +## Schedules {#schedules} + +### ✔ Do they use [**Schedules**](https://docs.temporal.io/workflows#schedule)? {#✔-do-they-use-schedules?} + +* How are they creating and managing the Schedule? + * SDK or CLI or Web UI? + +### ✔ How many Schedules will they have? {#✔-how-many-schedules-will-they-have?} + +* Will many run at the same time? + * Consider setting **Jitter** to offset execution, so they do not all run at once +* What is the potential total Actions Per Second across all Schedules in a namespace? + * Temporal Cloud has a default limit of 10 (and can be raised to 100 with a short turnaround \- a couple of business days) + * If greater than 100, then the request to raise may take longer and should involve a discussion with Temporal engineering + +### ✔ Are they using Schedules to achieve “delayed start”? {#✔-are-they-using-schedules-to-achieve-“delayed-start”?} + +* As of 2024-02-20, [Start Delay](https://docs.temporal.io/workflows#delay-workflow-execution) is an experimental feature available in Go, Java and Python, and this should be considered. +* Alternatively, they should use a normal Workflow with a timer/sleep at the beginning (instead of a Schedule with a run limit of 1). + * A normal Workflow will be cheaper for them, and easier to scale. + +### ✔ Will they need to [**Pause**](https://docs.temporal.io/workflows#pause) and/or [**Backfill**](https://docs.temporal.io/workflows#backfill) Schedules? {#✔-will-they-need-to-pause-and/or-backfill-schedules?} + +* Will they allow Backfills to run in parallel (AllowAll overlap policy) or sequentially (BufferAll)? + * There is currently an (undocumented) limit of 1000 on the number of executions that can be Buffered. They will need to batch or partition Backfill executions to stay under this limit. + +### ✔ What is the configured [**Overlap Policy**](https://docs.temporal.io/workflows#overlap-policy)? {#✔-what-is-the-configured-overlap-policy?} + +* The default is Skip (i.e. nothing happens; the Workflow Execution is not started) + +### ✔ Do executions depend on [**Last Completion Result**](https://docs.temporal.io/workflows#last-completion-result)? {#✔-do-executions-depend-on-last-completion-result?} + +* If their Overlap Policy allows overlaps, ensure they understand that the last completion means the run that successfully completed when the new run was started (and there may have been other executions started and currently running) + +## Cron Jobs {#cron-jobs} + +### ✔ Do they use [**Cron Jobs**](https://docs.temporal.io/workflows#temporal-cron-job)? {#✔-do-they-use-cron-jobs?} + +* Why? + * Suggest using [Schedules](#schedules) instead + +### ✔ Are they aware of Cron limitations? {#✔-are-they-aware-of-cron-limitations?} + +* A Cron Workflow should not call Continue As New, as the cron schedule will be dropped/lost + +## Side Effects {#side-effects} + +### ✔ Do they use [**Side Effects**](https://docs.temporal.io/workflow-execution/event#side-effect)? {#✔-do-they-use-side-effects?} + +* If there is *any* chance the Side Effect could fail, use an Activity instead. +* Side Effects are not implemented in Core SDKs. Use local activities instead. + +## Data Converter {#data-converter} + +### ✔ Do they use a custom [**Data Converter**](https://docs.temporal.io/dataconversion)? {#✔-do-they-use-a-custom-data-converter?} + +* Why? + * For encryption, compression, custom serialization, etc? + * Compression is recommended +* If used for encryption, is there a way to rotate keys? +* Search Attribute values are *not* processed through a [custom Data Converter](https://docs.temporal.io/dataconversion#custom-data-converter). + +### ✔ Are they using datetime/duration data types as input or return parameters? {#✔-are-they-using-datetime/duration-data-types-as-input-or-return-parameters?} + +* The challenge with datetime/duration data types is that some languages like Python and Go don’t know how to natively serialize them. And JSON doesn’t support datetime/duration data type. That means it is up to every JSON library to determine if they implement a custom serialization for datetime/duration data types. +* If you are using Python or Go, and/or plan on using multiple languages, you will need to implement your own custom Data Converter. This will require a common format that each SDK can convert back to its native datetime/duration data type. + +### ✔ What latency is added by the custom Data Converter or Payload Codec? {#✔-what-latency-is-added-by-the-custom-data-converter-or-payload-codec?} + +* Do these functions execute in the Workflow context, and contribute to Workflow Task Execution? + * Could they result in timeouts? + +## Visibility {#visibility} + +### ✔ Do they use [**Search Attributes**](https://docs.temporal.io/visibility#search-attribute)? {#✔-do-they-use-search-attributes?} + +* These should be used for operational purposes only, and **not part of any business logic** in the Workflows + * For Workflows, use Queries or store the data in external datastore +* They should not contain sensitive data, (e.g. PII, etc.) as SA values are not encoded by Data Converters or Payload Codecs +* Are they storing too much data in Search Attributes? + * Is the data related to the execution of the Workflow? + * Are they going to be querying this data in the UI or the CLI? +* Are they aware that Search Attribute updates are eventually consistent, i.e. there will be a delay (\~1-2 seconds) before the value is updated in the Visibility data store + +### ✔ Do they use [**Memos**](https://docs.temporal.io/workflow-execution#memo)? {#✔-do-they-use-memos?} + +* These are *eventually consistent*. The data may not be up-to-date when retrieved through the describe or list workflow operations. +* Reference: [Memo vs Search Attributes vs Visibility Records](https://community.temporal.io/t/memo-vs-serach-attributes-vs-visibility-records/3003) + +### ✔ Do they use any of the Visibility APIs within their app? {#✔-do-they-use-any-of-the-visibility-apis-within-their-app?} + +* These are *eventually consistent*. The data may not be up-to-date. + +## Versioning {#versioning} + +### ✔ Do they (plan to) use [**Workflow Versioning**](https://docs.temporal.io/workflow-definition#workflow-versioning) (aka **Patch** Versioning) APIs within their app? {#✔-do-they-(plan-to)-use-workflow-versioning-(aka-patch-versioning)-apis-within-their-app?} + +* Do they need to, or can they use [**Worker** Versioning](https://docs.temporal.io/worker-versioning) instead? + * *Don’t recommend yet as it is pre-release and the API is changing* + * Worker Versioning replaces **Task Queue** Versioning \- where you would have recommended TQ Versioning in the past, suggest Worker Versioning now. +* When do they plan to remove the Versions / Patches? + * Never let the Versions / Patches accumulate indefinitely, it will lead to difficult to maintain code. +* How long do the Workflows run for? + * They should have a plan to remove the Version/Patches after a finite period of time, e.g. 2 weeks or 30 days. + * Long-running Workflows need to keep patches in place until they either terminate or execute a Continue-As-New. Patches also need to be kept around if you plan on Querying them after they are closed. Patches should only be removed once the retention period has expired. + * For short running workflows, suggest **Worker** Versioning instead + +### ✔ Do they **test** Version changes **using** the Workflow **Replayer**? {#✔-do-they-test-version-changes-using-the-workflow-replayer?} + +* WorkflowReplayer in [Go](https://docs.temporal.io/dev-guide/go/testing#replay), [Java](https://docs.temporal.io/dev-guide/java/testing#replay) +* worker.runReplayHistory in [TypeScript](https://docs.temporal.io/dev-guide/typescript/testing#replay) +* replay\_workflow in [Python](https://docs.temporal.io/dev-guide/python/testing#replay) + +## Continue As New + +### ✔ Do they use Continue As New? + +* With CAN just remember + * Timers won't be carried on, you have to calculate the remaining time and pass it to the new CAN execution, and there, schedule the timers. + * If Child Workflows are not started with parentClosePolicy ABANDON they will be terminated (or REQUEST\_CANCEL depending on the parentClosePolicy) when the parent workflow closes + * Ensure you wait for pending activities to complete (completed/failed..) before CAN + * Use Workflow.isEveryHandlerFinished() to ensure signals and update handlers have finished executing before CAN + +## Interceptors {#interceptors} + +Use Interceptors to change workflow and activity behavior at the worker level. +For example, if you want some code that runs before every workflow starts, put that in a WorkflowInterceptor named “execute\_workflow”. +Powerful, advanced, can cause confusing behavior if people forget they have interceptors enabled. + +- [Blog that’s pretty good](https://platformatory.io/blog/Understanding-Temporal-Interceptors/) +- [Java Workflow Interceptor docs](https://github.com/temporalio/sdk-java/blob/master/temporal-sdk/src/main/java/io/temporal/common/interceptors/WorkerInterceptor.java) + +See Mike’s Github Project \- [https://github.com/temporalio/temporal-interceptor-seed](https://github.com/temporalio/temporal-interceptor-seed) + +## Sessions/Worker-Specific Task Queues {#sessions/worker-specific-task-queues} + +Sessions are only available only in Go + +* Note that when you use sessions, if the worker process dies, unless all the session activities have been processed, retry will call all of the session related activities again. + + Worker Specific Task Queues available in every SDK + +* Use these to task Activities to specific workers + +## Storage Optimization (Long Running Workflows) {#storage-optimization-(long-running-workflows)} + +See [Cost Optimization \- Storage](https://docs.google.com/document/d/1CVsZ4kGHMI7X79HzZKPf2MDWBPbT4lpUDz-3yZOlE48/edit#heading=h.bsn3wts9qcyw), and in particular the section on [Continue As New](https://docs.google.com/document/d/1CVsZ4kGHMI7X79HzZKPf2MDWBPbT4lpUDz-3yZOlE48/edit#heading=h.spve8qj2rtv9) (also [this](https://docs.google.com/presentation/d/1mZvV53b49HaqPdj8fZnLTwcHZojDgbedclJYhak8d3o/edit#slide=id.g32c7d9e6922_0_232)) + +# Appendix {#appendix} + +1. ## Best Practices Checklist {#best-practices-checklist} + +[Workflows](#workflows) + +[✔ Do they understand Workflow Determinism requirements?](#✔-do-they-understand-workflow-determinism-requirements?) + +[✔ Do they understand the Event History?](#✔-do-they-understand-the-event-history?) + +[✔ Do they set Workflow Id to a meaningful business identifier (or not)?](#✔-do-they-set-workflow-id-to-a-meaningful-business-identifier-\(or-not\)?) + +[✔ Do they explicitly set any Workflow Timeout options?](#✔-do-they-explicitly-set-any-workflow-timeout-options?) + +[✔ Do they understand Workflow Failure / error / exception handling?](#✔-do-they-understand-workflow-failure-/-error-/-exception-handling?) + +[✔ Do they set a Workflow Retry policy?](#✔-do-they-set-a-workflow-retry-policy?) + +[Child Workflows](#child-workflows) + +[✔ Do they use Child Workflows?](#✔-do-they-use-child-workflows?) + +[✔ How many Child WFs are being started by a Parent?](#✔-how-many-child-wfs-are-being-started-by-a-parent?) + +[✔ Do the Parent and Child WF need to share state?](#✔-do-the-parent-and-child-wf-need-to-share-state?) + +[✔ Does the Parent need to wait on the Child Workflow result?](#✔-does-the-parent-need-to-wait-on-the-child-workflow-result?) + +[Activities](#activities) + +[✔ Do they understand Activity Idempotency as a best practice?](#✔-do-they-understand-activity-idempotency-as-a-best-practice?) + +[✔ Are Activities short-term or long-running?](#✔-are-activities-short-term-or-long-running?) + +[✔ What are the Input and Output sizes for Activity Payloads?](#✔-what-are-the-input-and-output-sizes-for-activity-payloads?) + +[✔ Is the Activity performing any polling?](#✔-is-the-activity-performing-any-polling?) + +[✔ Is the Activity listening on a port or socket?](#✔-is-the-activity-listening-on-a-port-or-socket?) + +[✔ What are the Activity Timeout settings?](#✔-what-are-the-activity-timeout-settings?) + +[✔ Do Activity Tasks run on Workers separate from the Workflow Tasks?](#✔-do-activity-tasks-run-on-workers-separate-from-the-workflow-tasks?) + +[✔ Do sequential Activities run on the same Worker (i.e. are they sticky)?](#✔-do-sequential-activities-run-on-the-same-worker-\(i.e.-are-they-sticky\)?) + +[✔ Do they use Local Activities?](#✔-do-they-use-local-activities?) + +[✔ What is the Activity Retry policy?](#✔-what-is-the-activity-retry-policy?) + +[✔ Do they understand Activity Failure / error / exception handling?](#✔-do-they-understand-activity-failure-/-error-/-exception-handling?) + +[✔ Would an Activity need to receive Cancellation?](#✔-would-an-activity-need-to-receive-cancellation?) + +[Signals](#signals) + +[✔ Do they use Signals?](#✔-do-they-use-signals?) + +[✔ Do they understand that Signals are…](#✔-do-they-understand-that-signals-are…) + +[✔ Are Signal handlers idempotent and deterministic?](#✔-are-signal-handlers-idempotent-and-deterministic?) + +[✔ Is there a high rate or volume of Signals?](#✔-is-there-a-high-rate-or-volume-of-signals?) + +[✔ Do Signal handlers invoke Activities?](#✔-do-signal-handlers-invoke-activities?) + +[Queries](#queries) + +[✔ Do they use Queries?](#✔-do-they-use-queries?) + +[✔ Do they understand that Queries are…](#✔-do-they-understand-that-queries-are…) + +[✔ Do Query handlers perform read-only operations?](#✔-do-query-handlers-perform-read-only-operations?) + +[Update](#update) + +[✔ Do they use Update?](#✔-do-they-use-update?) + +[✔ Do they perform Validation on the Update request?](#✔-do-they-perform-validation-on-the-update-request?) + +[✔ Is it ok to invoke activities within the Update Handler?](#✔-is-it-ok-to-invoke-activities-within-the-update-handler?) + +[Workers & Task Queues](#workers-&-task-queues) + +[✔ How many Workers do they run?](#✔-how-many-workers-do-they-run?) + +[✔ Do Workers register all Workflows and Activities that can be dispatched on the Task Queue?](#✔-do-workers-register-all-workflows-and-activities-that-can-be-dispatched-on-the-task-queue?) + +[✔ Do any Activities run on unique Task Queues?](#✔-do-any-activities-run-on-unique-task-queues?) + +[✔ Do they configure rate-limiting for a Worker or Task Queue?](#✔-do-they-configure-rate-limiting-for-a-worker-or-task-queue?) + +[✔ Do they require strict ordering for Tasks?](#✔-do-they-require-strict-ordering-for-tasks?) + +[✔ Can multiple workflows share one Task Queue? When should multiple task queues be used?](#✔-can-multiple-workflows-share-one-task-queue?-when-should-multiple-task-queues-be-used?) + +[Timers](#timers) + +[✔ What is the duration set for a timer or sleep?](#✔-what-is-the-duration-set-for-a-timer-or-sleep?) + +[Schedules](#schedules) + +[✔ Do they use Schedules?](#✔-do-they-use-schedules?) + +[✔ How many Schedules will they have?](#✔-how-many-schedules-will-they-have?) + +[✔ Are they using Schedules to achieve “delayed start”?](#✔-are-they-using-schedules-to-achieve-“delayed-start”?) + +[✔ Will they need to Pause and/or Backfill Schedules?](#✔-will-they-need-to-pause-and/or-backfill-schedules?) + +[✔ What is the configured Overlap Policy?](#✔-what-is-the-configured-overlap-policy?) + +[✔ Do executions depend on Last Completion Result?](#✔-do-executions-depend-on-last-completion-result?) + +[Cron Jobs](#cron-jobs) + +[✔ Do they use Cron Jobs?](#✔-do-they-use-cron-jobs?) + +[✔ Are they aware of Cron limitations?](#✔-are-they-aware-of-cron-limitations?) + +[Side Effects](#side-effects) + +[✔ Do they use Side Effects?](#✔-do-they-use-side-effects?) + +[Data Converter](#data-converter) + +[✔ Do they use a custom Data Converter?](#✔-do-they-use-a-custom-data-converter?) + +[✔ Are they using datetime/duration data types as input or return parameters?](#✔-are-they-using-datetime/duration-data-types-as-input-or-return-parameters?) + +[✔ What latency is added by the custom Data Converter or Payload Codec?](#✔-what-latency-is-added-by-the-custom-data-converter-or-payload-codec?) + +[Visibility](#visibility) + +[✔ Do they use Search Attributes?](#✔-do-they-use-search-attributes?) + +[✔ Do they use Memos?](#✔-do-they-use-memos?) + +[✔ Do they use any of the Visibility APIs within their app?](#✔-do-they-use-any-of-the-visibility-apis-within-their-app?) + +[Versioning](#versioning) + +[✔ Do they (plan to) use Workflow Versioning (aka Patch Versioning) APIs within their app?](#✔-do-they-\(plan-to\)-use-workflow-versioning-\(aka-patch-versioning\)-apis-within-their-app?) + +[✔ Do they test Version changes using the Workflow Replayer?](#✔-do-they-test-version-changes-using-the-workflow-replayer?) + +2. ## Useful Links: {#useful-links:} + +- [Common Design Patterns](https://taonic.github.io/temporal-design-patterns/) +- [SDK Features Matrix](https://www.notion.so/temporalio/d86479c52be643c6a7c5f22cef5807e4?v=46d47ff7e32643dbb29950136fb3e5cd) +- [Temporal 102 deck, covers a lot of topics, with diagrams](https://www.google.com/url?q=https://docs.google.com/presentation/d/1DsK9ZE-XHpLac2jBTf29UUSol1HBghCjzV8-3PRWT7Y/edit?slide%3Did.g2d0bcd56d06_0_392%23slide%3Did.g2d0bcd56d06_0_392&sa=D&source=docs&ust=1752690322127243&usg=AOvVaw1pdJ2swgV1ikET-UTTSmyD) that are discussed above +- [Temporal Python Troubleshooting Guide](https://github.com/temporalio/dev-success/blob/main/python/troubleshooting_guide.md#the-thread-inside-an-async-def-python-function-is-blocked) +- [Notes for Code Reviews](https://docs.google.com/document/d/1RiFq1ExYvjNqdLvI_Suuo8rGS1DhqQy3SgrOLcwWaqQ/edit?tab=t.0#heading=h.abk8gemnj13u) \- the Code Review companion guide \ No newline at end of file diff --git a/batch_workflow_design_best_practices.md b/batch_workflow_design_best_practices.md new file mode 100644 index 0000000..765b9ca --- /dev/null +++ b/batch_workflow_design_best_practices.md @@ -0,0 +1,286 @@ +# Batch Workflow Best Practices + +--- + +## Table of Contents + +- [Schedules](#schedules) +- [01 Basic Workflow](#01-basic-workflow) +- [02 Fan-Out using Basic Child Workflows](#02-fan-out-using-basic-child-workflows) +- [03 Batch Iterator Workflow](#03-batch-iterator-workflow) +- [04 Sliding Window Workflow](#04-sliding-window-workflow) +- [05 MapReduce Tree](#05-mapreduce-tree) +- [06 Batch Signalling](#06-batch-signalling) +- [07 Limits](#07-limits) + +--- + +## Schedules + +Schedules allow Workflows to be executed on a recurring basis. Think of them as a more powerful Cron: + +- Supports `start` / `pause` / `stop` / `update` / `backfill` of scheduled workflow executions +- Can have overlapping schedules, configurable with **Overlap Policies** +- Full history visibility +- Schedules can be created via the UI or CLI + +**References:** +- https://temporal.io/blog/temporal-schedules-reliable-scalable-and-more-flexible-than-cron-jobs +- https://docs.temporal.io/workflows#schedule +- https://docs.temporal.io/cli/schedule + +```bash +$ temporal schedule create \ + --schedule-id 'your-schedule-id' \ + --workflow-id 'your-workflow-id' \ + --task-queue 'your-task-queue' \ + --workflow-type 'YourWorkflowType' +``` + +--- + +## 01 Basic Workflow + +This is just a standard workflow. + +- Workflow fetches, or is started with, record IDs to process +- Runs activity/activities required to retrieve and process each record: + - Activities can be blocking or non-blocking + - If non-blocking, the workflow must block to allow all activities to complete + - **Can only have 2k in-flight activities; ideally limit to 500** +- If the workflow history is likely to exceed 2k events (hard 50k limit), and/or you need Continue-as-New, consider the **Batch Iterator** pattern instead + +**Pros:** Simple +**Cons:** Limited number of records that can be processed; can potentially overwhelm downstream systems; all-or-nothing approach to parallelism + +```mermaid +flowchart TD + Records["📋 Record IDs\n(fetched or passed in)"] + WF["Workflow"] + A1["Activity"] + A2["Activity"] + AN["Activity ..."] + + Records --> WF + WF --> A1 + WF --> A2 + WF --> AN +``` + +--- + +## 02 Fan-Out using Basic Child Workflows + +Slightly better than the Basic Workflow. Useful when you have between **2K and 4M records**. + +- Parent workflow assigns blocks of IDs to child workflows: + - IDs can be explicit, e.g. `[1, 2, 3, …, n]` + - Better: use **offset and length** +- Child workflows follow the Basic Workflow pattern +- If the result of processing isn't needed, use `PARENT_CLOSE_POLICY_ABANDON` on child workflows +- If workflow history is likely to exceed 2k events (hard 50k limit), and/or you need Continue-as-New, consider the **Batch Iterator** pattern instead + +**Pros:** Relatively simple +**Cons:** Limited number of records that can be processed; can potentially overwhelm downstream systems; all-or-nothing approach to parallelism + +```mermaid +flowchart TD + Records["📋 Record IDs"] + Parent["Parent Workflow"] + C1["Child Workflow\n(offset 0, len N)"] + C2["Child Workflow\n(offset N, len N)"] + C3["Child Workflow\n(offset 2N, len N)"] + + Records --> Parent + Parent --> C1 + Parent --> C2 + Parent --> C3 + + C1 --> A1["Activities"] + C2 --> A2["Activities"] + C3 --> A3["Activities"] +``` + +--- + +## 03 Batch Iterator Workflow + +Process a batch of records, then **Continue-as-New** to process the next batch. + +- Workflow loads a **page** of record IDs (from an offset) +- Executes child workflows or activities to process each ID in the page +- Calls `continue-as-new` with the last page token / offset: + - Next run of the workflow does the same with the next page +- Limited parallelism +- Continue-as-New manages event history size + +**Reference:** https://github.com/temporalio/samples-java/tree/main/core/src/main/java/io/temporal/samples/batch/iterator + +**Pros:** Can rate-limit traffic to downstream systems; no limit to total size of record set +**Cons:** Batch progresses at the rate of the slowest processor + +```mermaid +flowchart TD + Records["📋 Full Record Set"] + WF1["Workflow Run 1\n(page 1)"] + WF2["Workflow Run 2\n(page 2)"] + WF3["Workflow Run 3\n(page N)"] + DB[("Data Source\n(paginated)")] + + Records --> DB + DB -->|"fetch page 1"| WF1 + WF1 -->|"process records"| Acts1["Activities"] + WF1 -->|"continue-as-new\n(offset = page 2)"| WF2 + DB -->|"fetch page 2"| WF2 + WF2 -->|"process records"| Acts2["Activities"] + WF2 -->|"continue-as-new\n(offset = page N)"| WF3 + DB -->|"fetch page N"| WF3 + WF3 -->|"process records"| Acts3["Activities"] +``` + +--- + +## 04 Sliding Window Workflow + +Similar to the Batch Iterator, but maximizes throughput by maintaining a **fixed-size window** of concurrent child workflows. As each child completes, a new one starts immediately for the next record. + +- A parent workflow starts a configured number of child workflows in parallel — **one child per record** +- As each child completes, a new one is started for the next record +- Limits the number of concurrent child workflows to prevent overwhelming downstream systems +- The parent calls `continue-as-new` after starting the preconfigured number of children +- A child signals its completion to the parent (since a parent cannot directly wait for a child started by a previous run) + +**Reference:** https://github.com/temporalio/samples-java/tree/main/core/src/main/java/io/temporal/samples/batch/slidingwindow + +**Pros:** Can rate-limit traffic; no limit to total record set size; window progresses at the rate of the **fastest** processor +**Cons:** Complicated + +```mermaid +flowchart TD + Records["📋 Record IDs"] + Parent["Parent Workflow\n(window size = W)"] + C1["Child 1\n✅ done"] + C2["Child 2\n⏳ running"] + C3["Child 3\n⏳ running"] + C4["Child 4\n🆕 started"] + + Records --> Parent + Parent --> C1 + Parent --> C2 + Parent --> C3 + + C1 -->|"Signal: complete"| Parent + Parent -->|"slot freed → start next"| C4 + + CAN["continue-as-new\n(after W children started)"] + Parent -->|"after W children"| CAN +``` + +--- + +## 05 MapReduce Tree + +Used for **embarrassingly parallel** workloads where speed matters more than rate-limiting. + +- Recordset is received by a **Node** workflow +- **Map phase:** + - If the recordset is small enough to be processed by `n` leaves → start `n` **Leaf** workflows as children + - Otherwise → split recordset into `n` chunks and pass to `n` **Node** child workflows (recurse) +- **Reduce phase:** + - Results are signalled from child to parent + - Parent blocks until all results are received + - Can be skipped if results aren't needed +- External reads *might* be okay — **avoid external/downstream writes** +- Can be tricky to get correct; track tree depth and fail if too deep +- If rate limiting is needed (e.g. thundering herd), use **Batch Sliding Window** or **Batch Iterator** instead + +**Pros:** No limit to total record set size; entire recordset processed in parallel +**Cons:** Complicated + +```mermaid +flowchart TD + Records["📋 Full Record Set"] + Root["Root Node Workflow"] + + Node1["Node Workflow"] + Node2["Node Workflow"] + + L1["Leaf Workflow"] + L2["Leaf Workflow"] + L3["Leaf Workflow"] + L4["Leaf Workflow"] + L5["Leaf Workflow"] + L6["Leaf Workflow"] + + Records --> Root + Root -->|"chunk 1"| Node1 + Root -->|"chunk 2"| Node2 + + Node1 --> L1 + Node1 --> L2 + Node1 --> L3 + + Node2 --> L4 + Node2 --> L5 + Node2 --> L6 + + L1 -->|"Signal result"| Node1 + L2 -->|"Signal result"| Node1 + L3 -->|"Signal result"| Node1 + L4 -->|"Signal result"| Node2 + L5 -->|"Signal result"| Node2 + L6 -->|"Signal result"| Node2 + + Node1 -->|"Signal result"| Root + Node2 -->|"Signal result"| Root +``` + +--- + +## 06 Batch Signalling + +The Temporal CLI batch signal feature notifies multiple workflows with a single command. + +**Supported commands:** +- Signal +- Reset +- Cancel +- Terminate + +Use by adding the `--query` parameter to the command. + +**Limits:** +- 1 running batch job per namespace +- 50 workflows per second per batch + +**Reference:** https://docs.temporal.io/cli/batch + +```bash +# Terminate all running workflows of a given type +$ temporal workflow terminate \ + --query 'ExecutionStatus = "Running" AND WorkflowType="SomeWorkflowType"' \ + --reason "Terminate Test Workflows Batch" + +# Signal all running workflows of a given type +$ temporal workflow signal \ + --workflow-id MyWorkflowId \ + --name MySignal \ + --input '{"Input": "As-JSON"}' \ + --query 'ExecutionStatus = "Running" AND WorkflowType="YourWorkflow"' \ + --reason "Testing" +``` + +--- + +## 07 Limits + +Key numbers to know. Full reference: https://docs.temporal.io/cloud/limits + +| Limit | Value | +|---|---| +| **Actions per second per namespace** | Dynamically allocated based on usage | +| **Unfinished actions per workflow** | 2,000 max (aim for 500). Includes activities, signals, child workflows, cancellation requests | +| **Events per workflow** | 50,000 events max (aim for 2,000) **or** 50MB total history size | +| **Signals per workflow** | 10,000 | +| **Updates per workflow** | 10 in-flight, 2,000 total | +| **Batch signalling** | 1 batch job per namespace; 50 workflows/sec per batch | diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..ab64e81 --- /dev/null +++ b/todo.md @@ -0,0 +1,29 @@ + +# new patterns + +## Error Handling & Retry Patterns (new sidebar category) + +[ ] get batch published + [ ] let Jessica know + +[ ] look at accumulator, make pr, get published + +[ ] add benign to polling activity pattern - https://github.com/temporalio/samples-java/blob/main/core/src/main/java/io/temporal/samples/polling/infrequent/InfrequentPollingActivityImpl.java#L26 + + +[ ] long running/entity/actor + +[ ] add low latency patterns/latency optimization patterns + +[ ] https://keithtenzer.com/temporal/Temporal_Fundamentals_Workflow_Patterns/ +[ ] versioning + +[ ] Anti patterns like outbox pattern and dead letter queue + + +[ ] Suggest ways to pattern-ify some of the primitives +[x] Add more patterns as PR + + +[i] cd /home/josh/projects/temporal-design-patterns && npm run dev +