Skip to content

Commit dd3e23e

Browse files
authored
Merge branch 'main' into FixJSONLD
2 parents 9511826 + eed5e39 commit dd3e23e

89 files changed

Lines changed: 17829 additions & 2 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/design-patterns/activity-dependency-injection.mdx

Lines changed: 787 additions & 0 deletions
Large diffs are not rendered by default.

docs/design-patterns/approval.mdx

Lines changed: 882 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
---
2+
id: batch-iterator
3+
title: "Batch Iterator"
4+
sidebar_label: "Batch Iterator"
5+
description: "Pages through unbounded datasets using Continue-As-New to prevent history overflow while maintaining exactly-once processing guarantees."
6+
---
7+
8+
import Tabs from '@theme/Tabs';
9+
import TabItem from '@theme/TabItem';
10+
11+
:::info[TLDR]
12+
**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.
13+
:::
14+
15+
## Overview
16+
17+
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.
18+
19+
## Problem
20+
21+
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.
22+
23+
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.
24+
25+
## Solution
26+
27+
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.
28+
29+
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.
30+
31+
```mermaid
32+
flowchart TD
33+
DB[("Data Source\n(paginated)")]
34+
WF1["Workflow Run 1\n(offset=0)"]
35+
WF2["Workflow Run 2\n(offset=PAGE_SIZE)"]
36+
WF3["Workflow Run N\n(offset=N×PAGE_SIZE)"]
37+
Done(["Complete"])
38+
39+
DB -->|"fetch page 1"| WF1
40+
WF1 -->|"processRecord ×PAGE_SIZE"| Acts1["Activities"]
41+
WF1 -->|"continueAsNew\n(offset=PAGE_SIZE)"| WF2
42+
43+
DB -->|"fetch page 2"| WF2
44+
WF2 -->|"processRecord ×PAGE_SIZE"| Acts2["Activities"]
45+
WF2 -->|"continueAsNew\n(offset=N×PAGE_SIZE)"| WF3
46+
47+
DB -->|"fetch page N"| WF3
48+
WF3 -->|"processRecord ×PAGE_SIZE"| Acts3["Activities"]
49+
WF3 -->|"last page → return"| Done
50+
```
51+
52+
The following describes each step in the diagram:
53+
54+
1. The Workflow starts with `offset=0` and calls `fetchPage(offset, pageSize)` to retrieve the first page of records.
55+
2. It processes each record in the page by executing the `processRecord` Activity.
56+
3. After the page is fully processed, it calls `continueAsNew` with `offset + pageSize`, passing the updated offset to the next run.
57+
4. The next run begins with a clean history and repeats the same steps for the next page.
58+
5. When `fetchPage` returns fewer records than `pageSize`, the Workflow knows it has reached the last page and returns normally.
59+
60+
## Implementation
61+
62+
The following examples show how each SDK implements the Batch Iterator pattern.
63+
64+
<Tabs groupId="language" queryString>
65+
<TabItem value="typescript" label="TypeScript">
66+
67+
```typescript
68+
// workflows.ts
69+
import { continueAsNew, log, proxyActivities } from "@temporalio/workflow";
70+
import type * as activities from "./activities";
71+
import { PAGE_SIZE } from "./shared";
72+
73+
const { fetchPage, processRecord } = proxyActivities<typeof activities>({
74+
startToCloseTimeout: "10 seconds",
75+
});
76+
77+
export async function batchIteratorWorkflow(
78+
offset: number = 0,
79+
totalProcessed: number = 0
80+
): Promise<number> {
81+
const page = await fetchPage(offset, PAGE_SIZE);
82+
83+
for (const record of page) {
84+
await processRecord(record);
85+
totalProcessed++;
86+
}
87+
88+
log.info(`Processed page at offset ${offset} (${page.length} records, running total: ${totalProcessed})`);
89+
90+
if (page.length === PAGE_SIZE) {
91+
await continueAsNew<typeof batchIteratorWorkflow>(offset + PAGE_SIZE, totalProcessed);
92+
}
93+
94+
return totalProcessed;
95+
}
96+
```
97+
98+
</TabItem>
99+
<TabItem value="python" label="Python">
100+
101+
```python
102+
# workflows.py
103+
from temporalio import workflow
104+
from temporalio.workflow import continue_as_new
105+
from datetime import timedelta
106+
from activities import fetch_page, process_record
107+
from shared import PAGE_SIZE
108+
109+
110+
@workflow.defn
111+
class BatchIteratorWorkflow:
112+
@workflow.run
113+
async def run(self, offset: int = 0, total_processed: int = 0) -> int:
114+
page = await workflow.execute_activity(
115+
fetch_page,
116+
args=[offset, PAGE_SIZE],
117+
start_to_close_timeout=timedelta(seconds=10),
118+
)
119+
120+
for record in page:
121+
await workflow.execute_activity(
122+
process_record,
123+
record,
124+
start_to_close_timeout=timedelta(seconds=10),
125+
)
126+
total_processed += 1
127+
128+
workflow.logger.info(
129+
f"Processed page at offset {offset} ({len(page)} records, running total: {total_processed})"
130+
)
131+
132+
if len(page) == PAGE_SIZE:
133+
continue_as_new(args=[offset + PAGE_SIZE, total_processed])
134+
135+
return total_processed
136+
```
137+
138+
</TabItem>
139+
<TabItem value="go" label="Go">
140+
141+
```go
142+
// workflows.go
143+
package main
144+
145+
import (
146+
"time"
147+
148+
"go.temporal.io/sdk/workflow"
149+
)
150+
151+
func BatchIteratorWorkflow(ctx workflow.Context, offset int, totalProcessed int) (int, error) {
152+
ao := workflow.ActivityOptions{
153+
StartToCloseTimeout: 10 * time.Second,
154+
}
155+
ctx = workflow.WithActivityOptions(ctx, ao)
156+
157+
var page []Record
158+
if err := workflow.ExecuteActivity(ctx, FetchPage, offset, PageSize).Get(ctx, &page); err != nil {
159+
return totalProcessed, err
160+
}
161+
162+
for _, record := range page {
163+
if err := workflow.ExecuteActivity(ctx, ProcessRecord, record).Get(ctx, nil); err != nil {
164+
return totalProcessed, err
165+
}
166+
totalProcessed++
167+
}
168+
169+
workflow.GetLogger(ctx).Info("Processed page",
170+
"offset", offset,
171+
"pageSize", len(page),
172+
"totalProcessed", totalProcessed)
173+
174+
if len(page) == PageSize {
175+
return totalProcessed, workflow.NewContinueAsNewError(ctx, BatchIteratorWorkflow, offset+PageSize, totalProcessed)
176+
}
177+
178+
return totalProcessed, nil
179+
}
180+
```
181+
182+
</TabItem>
183+
<TabItem value="java" label="Java">
184+
185+
```java
186+
// BatchIteratorWorkflow.java
187+
import io.temporal.activity.ActivityOptions;
188+
import io.temporal.workflow.*;
189+
import java.time.Duration;
190+
import java.util.List;
191+
192+
@WorkflowInterface
193+
public interface BatchIteratorWorkflow {
194+
@WorkflowMethod
195+
int run(int offset, int totalProcessed);
196+
}
197+
198+
// BatchIteratorWorkflowImpl.java
199+
public class BatchIteratorWorkflowImpl implements BatchIteratorWorkflow {
200+
private final Activities activities = Workflow.newActivityStub(
201+
Activities.class,
202+
ActivityOptions.newBuilder()
203+
.setStartToCloseTimeout(Duration.ofSeconds(10))
204+
.build()
205+
);
206+
207+
@Override
208+
public int run(int offset, int totalProcessed) {
209+
List<Record> page = activities.fetchPage(offset, Shared.PAGE_SIZE);
210+
211+
for (Record record : page) {
212+
activities.processRecord(record);
213+
totalProcessed++;
214+
}
215+
216+
Workflow.getLogger(BatchIteratorWorkflowImpl.class).info(
217+
"Processed page at offset " + offset + " (" + page.size() + " records, total: " + totalProcessed + ")"
218+
);
219+
220+
if (page.size() == Shared.PAGE_SIZE) {
221+
BatchIteratorWorkflow next = Workflow.newContinueAsNewStub(BatchIteratorWorkflow.class);
222+
next.run(offset + Shared.PAGE_SIZE, totalProcessed);
223+
}
224+
225+
return totalProcessed;
226+
}
227+
}
228+
```
229+
230+
</TabItem>
231+
</Tabs>
232+
233+
## Best practices
234+
235+
- **Choose a page size that keeps history under 2,000 events.** Each page produces roughly `3 × pageSize` history events (`ActivityTaskScheduled` + `ActivityTaskStarted` + `ActivityTaskCompleted`). A page size of 500–800 records is a safe target.
236+
- **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.
237+
- **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.
238+
- **Make `processRecord` idempotent.** Activities have at-least-once execution semantics. If a worker crashes after an Activity completes externally but before the completion is recorded in history, Temporal will retry it. Your downstream system must tolerate receiving the same record more than once.
239+
- **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.
240+
241+
## Common pitfalls
242+
243+
- **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.
244+
- **Passing unnecessary state into `continueAsNew`.** All arguments are serialized and stored in history. Pass only the minimal state needed (offset, counters) — not accumulated result lists or large collections that grow with each page.
245+
- **Sequential processing bottlenecks.** The default implementation processes one record at a time per page. You can fan out Activities concurrently within a page using the SDK's async primitives for higher per-page throughput — note this increases per-page event count accordingly. If record-set-wide throughput matters more than rate limiting, consider [Sliding Window](/design-patterns/sliding-window) or [MapReduce Tree](/design-patterns/mapreduce-tree).
246+
247+
## Related resources
248+
249+
- [Continue-as-New pattern](/design-patterns/continue-as-new) — core concepts for history management via `continueAsNew`
250+
- [Sliding Window](/design-patterns/sliding-window) — bounded concurrency that progresses at the rate of the fastest processor
251+
- [MapReduce Tree](/design-patterns/mapreduce-tree) — fully parallel processing for maximum speed
252+
- [Temporal limits reference](/cloud/limits)
253+
- [Batch samples (Java)](https://github.com/temporalio/samples-java/tree/main/core/src/main/java/io/temporal/samples/batch/iterator)
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
---
2+
id: batch-processing-patterns
3+
title: "Batch Processing Patterns"
4+
sidebar_label: "Batch Processing Patterns"
5+
description: "Batch Processing Patterns"
6+
---
7+
8+
import PatternCards from '@site/src/components/PatternCards';
9+
10+
These patterns process 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.
11+
12+
## When to use which pattern
13+
14+
| Pattern | Record set size | Parallelism model | Workflow-based rate control |
15+
|---|---|---|---|
16+
| [Basic Workflow](#basic-workflow-single-tier-fan-out) | Small (up to a few hundred records) | Sequential or parallel activities in one Workflow | No |
17+
| [Fan-Out with Child Workflows](/design-patterns/fanout-child-workflows) | Up to ~4M records | Fixed concurrency (one child per chunk) | No |
18+
| [Batch Iterator](/design-patterns/batch-iterator) | Unlimited | Limited (activities per page) | Yes — fixed page rate |
19+
| [Sliding Window](/design-patterns/sliding-window) | Unlimited | Bounded window of concurrent children | Yes — configurable window |
20+
| [MapReduce Tree](/design-patterns/mapreduce-tree) | Unlimited | Fully parallel recursive tree | No — maximum speed |
21+
22+
## Patterns in this section
23+
24+
<PatternCards items={[
25+
{
26+
href: "/design-patterns/fanout-child-workflows",
27+
icon: "fanout-child-workflows-icon.svg",
28+
title: "Fan-Out with Child Workflows",
29+
description: "Splits a record set into fixed-size chunks and assigns each to an independent child Workflow. Direct to reason about; best for record sets up to ~4M items.",
30+
},
31+
{
32+
href: "/design-patterns/batch-iterator",
33+
icon: "batch-iterator-icon.svg",
34+
title: "Batch Iterator",
35+
description: "Processes one page of records per Workflow run and continues as new with the next page offset. Handles unlimited record sets while controlling downstream traffic.",
36+
},
37+
{
38+
href: "/design-patterns/sliding-window",
39+
icon: "sliding-window-icon.svg",
40+
title: "Sliding Window",
41+
description: "Maintains a fixed-size window of concurrent child Workflows. As each child completes it signals the parent, which immediately starts a replacement — maximizing throughput within a concurrency budget.",
42+
},
43+
{
44+
href: "/design-patterns/mapreduce-tree",
45+
icon: "mapreduce-tree-icon.svg",
46+
title: "MapReduce Tree",
47+
description: "Recursively splits a record set into chunks, fans out to leaf Workflows for parallel processing, and signals results back up the tree. Maximizes speed for embarrassingly parallel workloads.",
48+
},
49+
]} />
50+
51+
---
52+
53+
## Schedules
54+
55+
Schedules allow Workflows to be executed on a recurring basis — think of them as a more flexible cron with start, pause, stop, update, and backfill controls.
56+
57+
- Supports `start` / `pause` / `stop` / `update` / `backfill` of scheduled Workflow executions
58+
- Configurable **Overlap Policies** control what happens when the previous run is still running
59+
- Full execution history visibility in the Temporal UI
60+
- Schedules can be created via the UI, CLI, or SDK
61+
62+
```bash
63+
temporal schedule create \
64+
--schedule-id 'your-schedule-id' \
65+
--workflow-id 'your-workflow-id' \
66+
--task-queue 'your-task-queue' \
67+
--workflow-type 'YourWorkflowType'
68+
```
69+
70+
**References:**
71+
- [Temporal Schedules](/schedule)
72+
- [CLI schedule commands](/cli/command-reference/schedule)
73+
74+
---
75+
76+
## Basic Workflow (single-tier fan-out)
77+
78+
The most direct form of batch processing: the Workflow fetches or receives record IDs and executes one Activity per record.
79+
80+
- Activities can be executed sequentially or concurrently (using the SDK's async primitives)
81+
- **Limit: 2,000 in-flight Activities per Workflow run** (aim for 500)
82+
- If total event count is likely to exceed 2,000 (hard limit: 51,200), use the [Batch Iterator](/design-patterns/batch-iterator) instead
83+
84+
**Pros:** Minimal code and orchestration overhead
85+
**Cons:** Hard cap on concurrent Activities; all-or-nothing failure model; can overwhelm downstream systems
86+
87+
```mermaid
88+
flowchart TD
89+
Records["📋 Record IDs\n(fetched or passed in)"]
90+
WF["Workflow"]
91+
A1["Activity"]
92+
A2["Activity"]
93+
AN["Activity ..."]
94+
95+
Records --> WF
96+
WF --> A1
97+
WF --> A2
98+
WF --> AN
99+
```
100+
101+
---
102+
103+
## Batch signalling
104+
105+
The Temporal CLI lets you signal, reset, cancel, or terminate multiple Workflows with a single command using a visibility query.
106+
107+
- 1 running batch job per namespace
108+
- 50 Workflows per second per batch
109+
110+
```bash
111+
# Signal all running Workflows of a given type
112+
temporal workflow signal \
113+
--name MySignal \
114+
--input '{"Input": "As-JSON"}' \
115+
--query 'ExecutionStatus = "Running" AND WorkflowType="YourWorkflow"' \
116+
--reason "Testing"
117+
118+
# Terminate all running Workflows of a given type
119+
temporal workflow terminate \
120+
--query 'ExecutionStatus = "Running" AND WorkflowType="SomeWorkflowType"' \
121+
--reason "Terminate Test Workflows"
122+
```
123+
124+
**Reference:** [CLI batch commands](/cli/command-reference/batch)
125+
126+
---
127+
128+
## Key limits
129+
130+
Full reference: [Temporal Cloud limits](/cloud/limits)
131+
132+
| Limit | Value |
133+
|---|---|
134+
| Unfinished actions per Workflow | 2,000 max (aim for 500). Includes Activities, Signals, Child Workflows, cancellation requests |
135+
| Events per Workflow history | 51,200 events max (aim for a few thousand) **or** 50 MB total history size; warns at 10,240 events / 10 MB |
136+
| Signals per Workflow | 10,000 |
137+
| Updates per Workflow | 10 in-flight, 2,000 total |
138+
| Batch Signalling | 1 batch job per namespace; 50 Workflows/sec per batch |
139+
140+
## Related sections
141+
142+
- [Task Orchestration Patterns](/design-patterns/task-orchestration-patterns) — the child-Workflow and parallel primitives these patterns scale up
143+
- [QoS & Throughput Patterns](/design-patterns/qos-throughput-patterns) — rate-limit and prioritize the work a batch generates
144+
- [Performance & Latency Patterns](/design-patterns/performance-latency-patterns) — reduce the latency of each record's processing

0 commit comments

Comments
 (0)