Skip to content

Commit 03c9901

Browse files
authored
Merge branch 'main' into AddCustomOpenGraphImages
2 parents 939fc18 + facf12a commit 03c9901

4 files changed

Lines changed: 217 additions & 43 deletions

File tree

docs/develop/python/best-practices/testing-suite.mdx

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ Some SDKs have support or examples for popular test frameworks, runners, or libr
3636

3737
One recommended framework for testing in Python for the Temporal SDK is [pytest](https://docs.pytest.org/), which can help with fixtures to stand up and tear down test environments, provide useful test discovery, and make it easy to write parameterized tests.
3838

39+
If you do use `pytest`, consider using `-s` (`--show-capture=no`) so you can see the logs live.
40+
3941
## Testing Activities {/* #test-activities */}
4042

4143
An Activity can be tested with a mock Activity environment, which provides a way to mock the Activity context, listen to Heartbeats, and cancel the Activity.
@@ -75,13 +77,45 @@ assert heartbeats == ["param: test", "second heartbeat"]
7577

7678
## Testing Workflows {/* #test-workflows */}
7779

78-
### How to mock Activities {/* #mock-activities */}
80+
The simplest test case we can write is to have the test environment execute the Workflow and then evaluate the results. `WorkflowEnvironment.start_local` configures a local environment for running and testing Workflows:
81+
82+
```python
83+
import uuid
84+
85+
import pytest
86+
from temporalio import activity
87+
from temporalio.testing import WorkflowEnvironment
88+
from temporalio.worker import Worker
89+
90+
from activities import greet
91+
from workflows import SayHelloWorkflow
92+
7993

80-
Mock the Activity invocation when unit testing your Workflows.
94+
@pytest.mark.asyncio
95+
async def test_say_hello_workflow():
96+
"""Execute the workflow end-to-end with its real activity."""
97+
task_queue_name = str(uuid.uuid4())
98+
async with await WorkflowEnvironment.start_local(ui=True, ui_port=8233) as env:
99+
async with Worker(
100+
env.client,
101+
task_queue=task_queue_name,
102+
workflows=[SayHelloWorkflow],
103+
activities=[greet],
104+
):
105+
result = await env.client.execute_workflow(
106+
SayHelloWorkflow.run,
107+
"Temporal",
108+
id=str(uuid.uuid4()),
109+
task_queue=task_queue_name,
110+
)
111+
assert result == "Hello Temporal"
112+
```
81113

82-
When integration testing Workflows with a Worker, you can mock Activities by providing mock Activity implementations to the Worker.
114+
You can also pass `ui=True` to `start_local` to see the UI.
115+
116+
### How to mock Activities {/* #mock-activities */}
83117

84-
Provide mock Activity implementations to the Worker.
118+
When running unit tests on Workflows, many times you want to test the Workflow logic in isolation. When integration testing Workflows with a Worker, you can mock Activities by providing mock Activity implementations to the Worker.
85119

86120
```python
87121
import uuid
@@ -130,24 +164,21 @@ For example, if you have a Workflow sleep for a day, or have an Activity failure
130164
Instead, test the logic that happens after the sleep by skipping forward in time and complete your tests in a timely manner.
131165

132166
The test framework included in most SDKs is an in-memory implementation of Temporal Server that supports skipping time.
133-
Time is a global property of an instance of `TestWorkflowEnvironment`: skipping time (either automatically or manually) applies to all currently running tests.
167+
Time is a global property of an instance of `WorkflowEnvironment`: skipping time (either automatically or manually) applies to all currently running tests.
134168
If you need different time behaviors for different tests, run your tests in a series or with separate instances of the test server.
135169
For example, you could run all tests with automatic time skipping in parallel, and then all tests with manual time skipping in series, and then all tests without time skipping in parallel.
136170

137171
#### Skip time automatically {/* #automatic-method */}
138172

139-
Start a test server process that skips time as needed.
140-
For example, in the time-skipping mode, Timers, which include sleeps and conditional timeouts, are fast-forwarded except when Activities are running.
173+
Use the [`start_time_skipping()`](https://python.temporal.io/temporalio.testing.WorkflowEnvironment.html#start_time_skipping) method to start a test server process and skip time automatically. In time-skipping mode, Timers, which include sleeps and conditional timeouts, are fast-forwarded except when Activities are running.
141174

142-
Use the [`start_time_skipping()`](https://python.temporal.io/temporalio.testing.WorkflowEnvironment.html#start_time_skipping) method to start a test server process and skip time automatically.
143-
144-
Use the [`start_local()`](https://python.temporal.io/temporalio.testing.WorkflowEnvironment.html#start_local) method for a full local Temporal Server.
175+
Use the [`start_local()`](https://python.temporal.io/temporalio.testing.WorkflowEnvironment.html#start_local) method for a full local Temporal Server. You can find an example of this being used in [the code above](/develop/python/best-practices/testing-suite#test-workflows).
145176

146177
Use the [`from_client()`](https://python.temporal.io/temporalio.testing.WorkflowEnvironment.html#from_client) method for an existing Temporal Server.
147178

148179
#### Skip time manually {/* #manual-method */}
149180

150-
To implement time skipping, use the [`start_time_skipping()`](https://python.temporal.io/temporalio.testing.WorkflowEnvironment.html#start_time_skipping) static method.
181+
To implement time skipping manually, use the [`sleep`](https://python.temporal.io/temporalio.testing.WorkflowEnvironment.html#sleep) method inside the `WorkflowEnvironment`. This will manually advance time by the duration you specify.
151182

152183
```python
153184
from temporalio.testing import WorkflowEnvironment
@@ -160,14 +191,6 @@ async def test_manual_time_skipping():
160191
# Your code here
161192
```
162193

163-
### Assert in Workflow {/* #assert-in-workflow */}
164-
165-
The `assert` statement is a convenient way to insert debugging assertions into the Workflow context.
166-
167-
The `assert` method is available in Python and TypeScript.
168-
169-
For information about assert statements in Python, see [`assert`](https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement) in the Python Language Reference.
170-
171194
## How to Replay a Workflow Execution {/* #replay */}
172195

173196
Replay recreates the exact state of a Workflow Execution.

docs/encyclopedia/activities/local-activity.mdx

Lines changed: 147 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,47 +2,171 @@
22
id: local-activity
33
title: Local Activity
44
sidebar_label: Local Activity
5-
description: Learn about Local Activities in Temporal, their benefits, execution model, and when to use them.
5+
description: Learn about Local Activities in Temporal, how they work, when to use them, and how they differ from regular Activities.
66
slug: /local-activity
77
toc_max_heading_level: 4
88
keywords:
99
- explanation
1010
- term
1111
- timeouts
12+
- activities
13+
- local activities
14+
- workflow
1215
tags:
1316
- Concepts
1417
- Activities
1518
- Durable Execution
1619
---
1720

18-
This page discusses [Local Activity](#local-activity).
19-
2021
## What is a Local Activity? {/* #local-activity */}
2122

22-
A Local Activity is an [Activity Execution](/activity-execution) that executes in the same process as the [Workflow Execution](/workflow-execution) that spawns it.
23+
A Local Activity is an [Activity Execution](/activity-execution) that executes in the same Worker process as the [Workflow Execution](/workflow-execution) that schedules it.
24+
25+
Unlike a regular Activity, a Local Activity never enters an Activity Task Queue. Instead, the Workflow Worker executes it directly in an in-process queue. Because it avoids the round trip through the Temporal Service, a Local Activity has significantly lower latency and produces fewer Event History entries than a regular Activity.
26+
27+
Local Activities help with performance optimization and are not a replacement for regular Activities.
28+
29+
Consider using a Local Activity only when the operation:
30+
31+
- is short-lived (completes in a few seconds), including retries
32+
- can execute in the same binary as the Workflow
33+
- does not require routing to a specific Worker or Task Queue
34+
- does not require global rate limiting
35+
- [is idempotent](/develop/python/best-practices/error-handling#make-activities-idempotent)
36+
37+
For most production workloads, regular Activities remain the recommended default.
38+
39+
:::tip Recommendation
40+
41+
Use Local Activities only when your use case requires the performance optimization they provide, such as high-throughput Workflows with many very short-lived operations. For most business logic, regular Activities are the better choice.
42+
43+
:::
44+
45+
## How Local Activities execute
46+
47+
Regular Activities are coordinated through the Temporal Service.
48+
49+
The execution flow is:
50+
51+
1. A Workflow schedules an Activity.
52+
2. The Workflow completes its Workflow Task by sending a `ScheduleActivityTask` command to the Temporal Service.
53+
3. The Temporal Service creates an Activity Task.
54+
4. An Activity Worker polls the Activity Task Queue and executes the Activity.
55+
5. The Activity result is recorded in Event history.
56+
6. The Workflow resumes in a new Workflow Task.
57+
58+
Local Activities follow a shorter execution path:
59+
60+
1. A Workflow schedules a Local Activity.
61+
2. The Local Activity is placed into an in-process queue within the Workflow Worker.
62+
3. The Workflow Worker executes the Local Activity immediately.
63+
4. The result is returned directly to the Workflow.
64+
5. When the Workflow Task completes, the Worker records a `MarkerRecorded` event containing the Local Activity result.
65+
66+
Unlike regular Activities, scheduling and execution occur entirely within the Worker process. Only the final marker is persisted to Event history.
67+
68+
## Workflow Task heartbeating
69+
70+
Local Activities do **not** support Activity heartbeats. Instead, the SDK supports Workflow Task heartbeating.
71+
72+
Workflow Task heartbeating means that if a Local Activity approaches approximately 80% of the Workflow Task Timeout (10 seconds by default), the Worker completes the current Workflow Task and requests a new one from the Temporal Service. This renews the Worker's authorization to continue executing the Workflow and allows the Local Activity to keep running without exceeding the Workflow Task Timeout.
73+
74+
This enables Local Activities to run longer than a single Workflow Task timeout, but it comes with tradeoffs:
75+
76+
- Each Workflow Task heartbeat adds additional Events to the Event history.
77+
- Signals and other external Workflow events are not processed until the Local Activities finish.
78+
- Commands generated by the Workflow are not sent to the Temporal Service until one of the following occurs:
79+
- the Local Activity completes
80+
- the next Workflow Task heartbeat occurs
81+
82+
If your operation regularly approaches the Workflow Task timeout, it is usually better implemented as a regular Activity.
83+
84+
You can monitor your Local Activities by using the [`local_activity_total` metric](/references/sdk-metrics#local_activity_total) to determine how many Local Activity Executions have been made. You can also check your Event History for `RecordMarker` entries.
85+
86+
## Failure and durability
87+
88+
Regular Activities are durably tracked by the Temporal Service. Scheduling, completion, retries, and failures are all recorded in the Event history. If a Worker crashes after an Activity completes, the completed Activity is not executed again.
89+
90+
Local Activities behave differently. A Local Activity result becomes durable only when the enclosing Workflow Task successfully completes and records a `MarkerRecorded` event. Until then, execution exists only in Worker memory.
91+
92+
If the Worker crashes before the Workflow Task completes, the Workflow Task is retried, causing Local Activities executed during that Workflow Task to run again.
93+
94+
Because of this behavior, Local Activities provide at-least-once execution semantics and should always be idempotent. You can learn more about the behavior at shutdown in the [Workers section on Local Activities](encyclopedia/workers/worker-shutdown#local-activities).
95+
96+
Once a `MarkerRecorded` event has been written to the Event history, replay uses the recorded result rather than executing the Local Activity again.
97+
98+
## Mixing Local Activities and regular Activities
99+
100+
A Workflow can freely combine Local Activities and regular Activities.
101+
102+
For example:
103+
104+
```mermaid
105+
flowchart LR
106+
LA[Local Activity A] --> LB[Local Activity B] --> RC[Regular Activity C]
107+
```
108+
109+
When the Workflow Task completes after scheduling Activity C, the Worker sends commands similar to:
110+
111+
- `MarkerRecorded` (Local Activity A)
112+
- `MarkerRecorded` (Local Activity B)
113+
- `ScheduleActivityTask` (Activity C)
114+
115+
If Activity C later fails or retries, the Workflow replays using the recorded markers for A and B. Those completed Local Activities are not executed again because their results have already been persisted in the Event history. Only Activity C is retried according to its Retry Policy.
116+
117+
The only time completed Local Activities execute again is if the Worker fails before their completion markers are recorded. Long retry intervals are inefficient because retries eventually require Workflow Timers and additional Event history entries.
118+
119+
## Choosing between regular Activities and Local Activities
120+
121+
Choose a regular Activity unless you have a specific need for the performance optimization that Local Activities provide.
122+
123+
Use a Local Activity when:
124+
125+
- execution completes in a few seconds
126+
- retries are expected to be short
127+
- the operation is idempotent
128+
- low latency is more important than full durability
129+
- routing, rate limiting, and separate Activity Workers are unnecessary
130+
131+
Use a regular Activity when:
132+
133+
- interacting with external systems
134+
- execution may take longer than a few seconds
135+
- retries may span minutes or hours
136+
- Activity heartbeating is required
137+
- strong durability guarantees are important
138+
139+
Regular Activities are the right choice for most applications. Local Activities are an optimization for specialized, high-throughput workloads where minimizing latency and Event history size outweighs their reduced durability guarantees.
23140

24-
Some Activity Executions are very short-living and do not need the queuing semantic, flow control, rate limiting, and routing capabilities.
25-
For this case, Temporal supports the Local Activity feature.
141+
### Use cases for Local Activities
26142

27-
The main benefit of Local Activities is that they use less Temporal Service resources (for example, fewer History events) and have much lower latency overhead (because no need to roundtrip to the Temporal Service) compared to normal Activity Executions.
28-
However, Local Activities are subject to shorter durations and a lack of rate limiting.
143+
Good use cases for Local Activities include:
29144

30-
Consider using Local Activities for functions that are the following:
145+
- Lightweight data transformations
146+
- Small computations
147+
- Reading from an in-memory cache
148+
- Fast local filesystem operations
149+
- High-throughput Workflows with many very short-lived operations
31150

32-
- can be implemented in the same binary as the Workflow that calls them.
33-
- do not require global rate limiting.
34-
- do not require routing to a specific Worker or Worker pool.
35-
- no longer than a few seconds, inclusive of retries.
151+
Use regular Activities for:
36152

37-
If it takes longer than 80% of the Workflow Task Timeout (which is 10 seconds by default), the Worker will ask the Temporal Service to create a new Workflow Task to extend the "lease" for processing the Local Activity.
38-
The Worker will continue doing so until the Local Activity has completed.
39-
This is called Workflow Task Heartbeating.
40-
The drawbacks of long-running Local Activities are:
153+
- Network requests
154+
- Database operations
155+
- External API calls
156+
- Long-running work
157+
- Operations requiring durable retries
158+
- Operations that benefit from Task Queue routing or rate limiting
41159

42-
- Each new Workflow Task results in 3 more Events in History.
43-
- The Workflow won't get notified of new events like Signals and completions until the next Workflow Task Heartbeat.
44-
- New Commands created by the Workflow concurrently with the Local Activity will not be sent to the Temporal Service until either the Local Activity completes or the next Workflow Task Heartbeat.
160+
### Activity vs. Local Activity
45161

46-
Using a Local Activity without understanding its limitations can cause various production issues.
47-
**We recommend using regular Activities unless your use case requires very high throughput and large Activity fan outs of very short-lived Activities.**
48-
More guidance in choosing between [Local Activity vs Activity](https://community.temporal.io/t/local-activity-vs-activity/290/3) is available in our forums.
162+
| Feature | Activity | Local Activity |
163+
| --- | --- | --- |
164+
| Execution | Activity Worker | Workflow Worker |
165+
| Task Queue | Yes | No |
166+
| Service round trip | Required | Not required |
167+
| Latency | Higher | Lower |
168+
| Event history | Fully recorded | `MarkerRecorded` on completion |
169+
| Heartbeating | Activity heartbeats | Workflow Task heartbeating |
170+
| Retry durability | Durable | At-least-once until marker is recorded |
171+
| Signal responsiveness | Unaffected | Delayed while the Workflow Task executes |
172+
| Best for | General-purpose work | Short, high-throughput operations |

docs/evaluate/temporal-cloud/pricing.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ When upgrading an existing Namespace, some points to consider:
339339

340340
**How does pricing for Fairness work?**
341341

342-
When [Fairness](/develop/task-queue-priority-fairness#task-queue-fairness) is enabled on a Namespace, an additional `0.1` Action is charged per Action in that Namespace for each hour the feature is on, regardless of whether individual Workflows or Activities use fairness keys.
342+
When [Fairness](/develop/task-queue-priority-fairness#task-queue-fairness) is enabled on a Namespace, an additional `0.1` Action is charged per Action in that Namespace during each hour the feature is on, regardless of whether individual Workflows or Activities use fairness keys.
343343

344344
The examples below use a Namespace, `your-namespace`, that normally generates 10,000 Actions per hour.
345345

docs/production-deployment/self-hosted-guide/visibility.mdx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,33 @@ Note that the script uses
261261
[temporal-sql-tool](https://github.com/temporalio/temporal/blob/3b982585bf0124839e697952df4bba01fe4d9543/tools/sql/main.go)
262262
to run the setup.
263263

264+
### Maintaining Visibility index health {/* #postgresql-index-maintenance */}
265+
266+
On high-volume deployments, the total size of the indexes on `executions_visibility` can grow much larger than the table itself and keep growing even with a Retention Period set. This is expected PostgreSQL b-tree behavior rather than a data leak:
267+
268+
- Each pre-allocated custom and system Search Attribute has its own nullable column and its own index. PostgreSQL b-tree indexes store entries for `NULL` values, so a Search Attribute that a deployment never sets still has one index entry for every Workflow Execution row.
269+
- Closing or updating a Workflow Execution upserts its Visibility row, and because these indexes order on `close_time`, the updates leave dead index entries over time.
270+
- `VACUUM` reclaims table (heap) space but does not compact b-tree index bloat. `REINDEX` rebuilds an index and reclaims that space; removing rows through the Retention Period does not.
271+
272+
To reclaim index space online, run `REINDEX` with `CONCURRENTLY`, which does not block reads or writes. It needs roughly the size of the index in extra disk space and I/O, so prefer a lower-traffic window:
273+
274+
```sql
275+
REINDEX TABLE CONCURRENTLY executions_visibility;
276+
```
277+
278+
You can also target a single index with `REINDEX INDEX CONCURRENTLY <index_name>;`.
279+
280+
Because `executions_visibility` is update-heavy, more aggressive autovacuum settings for the table help keep dead tuples, and the resulting index bloat, under control:
281+
282+
```sql
283+
ALTER TABLE executions_visibility SET (
284+
autovacuum_vacuum_scale_factor = 0.05,
285+
autovacuum_analyze_scale_factor = 0.05
286+
);
287+
```
288+
289+
Track sizes with `pg_relation_size('executions_visibility')` and `pg_indexes_size('executions_visibility')`, and use the [`pgstattuple`](https://www.postgresql.org/docs/current/pgstattuple.html) extension for precise bloat estimates. For sustained high-volume Visibility workloads, consider [Elasticsearch or OpenSearch](#elasticsearch), which Temporal recommends for production at scale.
290+
264291
## How to set up SQLite Visibility store {/* #sqlite */}
265292

266293
:::tip Support, stability, and dependency info

0 commit comments

Comments
 (0)