@temporalio/core-bridge 1.13.0 → 1.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +239 -382
- package/Cargo.toml +11 -11
- package/lib/native.d.ts +10 -3
- package/package.json +3 -3
- package/releases/aarch64-apple-darwin/index.node +0 -0
- package/releases/aarch64-unknown-linux-gnu/index.node +0 -0
- package/releases/x86_64-apple-darwin/index.node +0 -0
- package/releases/x86_64-pc-windows-msvc/index.node +0 -0
- package/releases/x86_64-unknown-linux-gnu/index.node +0 -0
- package/sdk-core/.cargo/config.toml +71 -11
- package/sdk-core/.clippy.toml +1 -0
- package/sdk-core/.github/workflows/heavy.yml +2 -0
- package/sdk-core/.github/workflows/per-pr.yml +50 -18
- package/sdk-core/ARCHITECTURE.md +44 -48
- package/sdk-core/Cargo.toml +26 -7
- package/sdk-core/README.md +4 -0
- package/sdk-core/arch_docs/diagrams/TimerMachine_Coverage.puml +14 -0
- package/sdk-core/arch_docs/diagrams/initial_event_history.png +0 -0
- package/sdk-core/arch_docs/sdks_intro.md +299 -0
- package/sdk-core/client/Cargo.toml +8 -7
- package/sdk-core/client/src/callback_based.rs +1 -2
- package/sdk-core/client/src/lib.rs +485 -299
- package/sdk-core/client/src/metrics.rs +32 -8
- package/sdk-core/client/src/proxy.rs +124 -5
- package/sdk-core/client/src/raw.rs +598 -307
- package/sdk-core/client/src/replaceable.rs +253 -0
- package/sdk-core/client/src/retry.rs +9 -6
- package/sdk-core/client/src/worker_registry/mod.rs +19 -3
- package/sdk-core/client/src/workflow_handle/mod.rs +20 -17
- package/sdk-core/core/Cargo.toml +100 -31
- package/sdk-core/core/src/core_tests/activity_tasks.rs +55 -225
- package/sdk-core/core/src/core_tests/mod.rs +2 -8
- package/sdk-core/core/src/core_tests/queries.rs +3 -5
- package/sdk-core/core/src/core_tests/replay_flag.rs +3 -62
- package/sdk-core/core/src/core_tests/updates.rs +4 -5
- package/sdk-core/core/src/core_tests/workers.rs +4 -3
- package/sdk-core/core/src/core_tests/workflow_cancels.rs +10 -7
- package/sdk-core/core/src/core_tests/workflow_tasks.rs +28 -291
- package/sdk-core/core/src/ephemeral_server/mod.rs +15 -3
- package/sdk-core/core/src/internal_flags.rs +11 -1
- package/sdk-core/core/src/lib.rs +50 -36
- package/sdk-core/core/src/pollers/mod.rs +5 -5
- package/sdk-core/core/src/pollers/poll_buffer.rs +2 -2
- package/sdk-core/core/src/protosext/mod.rs +13 -5
- package/sdk-core/core/src/protosext/protocol_messages.rs +4 -11
- package/sdk-core/core/src/retry_logic.rs +256 -108
- package/sdk-core/core/src/telemetry/metrics.rs +1 -0
- package/sdk-core/core/src/telemetry/mod.rs +8 -2
- package/sdk-core/core/src/telemetry/prometheus_meter.rs +2 -2
- package/sdk-core/core/src/test_help/integ_helpers.rs +971 -0
- package/sdk-core/core/src/test_help/mod.rs +10 -1100
- package/sdk-core/core/src/test_help/unit_helpers.rs +218 -0
- package/sdk-core/core/src/worker/activities/activity_heartbeat_manager.rs +42 -6
- package/sdk-core/core/src/worker/activities/local_activities.rs +19 -19
- package/sdk-core/core/src/worker/activities.rs +10 -3
- package/sdk-core/core/src/worker/client/mocks.rs +3 -3
- package/sdk-core/core/src/worker/client.rs +130 -93
- package/sdk-core/core/src/worker/heartbeat.rs +12 -13
- package/sdk-core/core/src/worker/mod.rs +31 -21
- package/sdk-core/core/src/worker/nexus.rs +14 -3
- package/sdk-core/core/src/worker/slot_provider.rs +9 -0
- package/sdk-core/core/src/worker/tuner.rs +159 -0
- package/sdk-core/core/src/worker/workflow/history_update.rs +3 -265
- package/sdk-core/core/src/worker/workflow/machines/activity_state_machine.rs +1 -54
- package/sdk-core/core/src/worker/workflow/machines/cancel_external_state_machine.rs +0 -82
- package/sdk-core/core/src/worker/workflow/machines/cancel_workflow_state_machine.rs +0 -67
- package/sdk-core/core/src/worker/workflow/machines/child_workflow_state_machine.rs +1 -192
- package/sdk-core/core/src/worker/workflow/machines/continue_as_new_workflow_state_machine.rs +0 -43
- package/sdk-core/core/src/worker/workflow/machines/local_activity_state_machine.rs +6 -554
- package/sdk-core/core/src/worker/workflow/machines/modify_workflow_properties_state_machine.rs +0 -71
- package/sdk-core/core/src/worker/workflow/machines/nexus_operation_state_machine.rs +102 -3
- package/sdk-core/core/src/worker/workflow/machines/patch_state_machine.rs +10 -539
- package/sdk-core/core/src/worker/workflow/machines/signal_external_state_machine.rs +0 -139
- package/sdk-core/core/src/worker/workflow/machines/timer_state_machine.rs +1 -119
- package/sdk-core/core/src/worker/workflow/machines/upsert_search_attributes_state_machine.rs +6 -63
- package/sdk-core/core/src/worker/workflow/machines/workflow_machines.rs +9 -4
- package/sdk-core/core/src/worker/workflow/mod.rs +5 -1
- package/sdk-core/core/src/worker/workflow/workflow_stream.rs +8 -3
- package/sdk-core/core-api/Cargo.toml +4 -4
- package/sdk-core/core-api/src/envconfig.rs +153 -54
- package/sdk-core/core-api/src/lib.rs +68 -0
- package/sdk-core/core-api/src/telemetry/metrics.rs +2 -1
- package/sdk-core/core-api/src/telemetry.rs +13 -0
- package/sdk-core/core-c-bridge/Cargo.toml +13 -8
- package/sdk-core/core-c-bridge/include/temporal-sdk-core-c-bridge.h +184 -22
- package/sdk-core/core-c-bridge/src/client.rs +462 -184
- package/sdk-core/core-c-bridge/src/envconfig.rs +314 -0
- package/sdk-core/core-c-bridge/src/lib.rs +1 -0
- package/sdk-core/core-c-bridge/src/random.rs +4 -4
- package/sdk-core/core-c-bridge/src/runtime.rs +22 -23
- package/sdk-core/core-c-bridge/src/testing.rs +1 -4
- package/sdk-core/core-c-bridge/src/tests/context.rs +31 -31
- package/sdk-core/core-c-bridge/src/tests/mod.rs +32 -28
- package/sdk-core/core-c-bridge/src/tests/utils.rs +7 -7
- package/sdk-core/core-c-bridge/src/worker.rs +319 -66
- package/sdk-core/fsm/rustfsm_procmacro/src/lib.rs +6 -1
- package/sdk-core/fsm/rustfsm_procmacro/tests/trybuild/dupe_transitions_fail.stderr +5 -5
- package/sdk-core/sdk/Cargo.toml +8 -2
- package/sdk-core/sdk/src/activity_context.rs +1 -1
- package/sdk-core/sdk/src/app_data.rs +1 -1
- package/sdk-core/sdk/src/interceptors.rs +1 -4
- package/sdk-core/sdk/src/lib.rs +1 -5
- package/sdk-core/sdk/src/workflow_context/options.rs +10 -1
- package/sdk-core/sdk/src/workflow_future.rs +1 -1
- package/sdk-core/sdk-core-protos/Cargo.toml +6 -6
- package/sdk-core/sdk-core-protos/build.rs +10 -23
- package/sdk-core/sdk-core-protos/protos/api_upstream/.github/workflows/create-release.yml +9 -1
- package/sdk-core/sdk-core-protos/protos/api_upstream/openapi/openapiv2.json +254 -5
- package/sdk-core/sdk-core-protos/protos/api_upstream/openapi/openapiv3.yaml +234 -5
- package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/common/v1/message.proto +1 -1
- package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/deployment/v1/message.proto +6 -0
- package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/namespace/v1/message.proto +6 -2
- package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/workflowservice/v1/request_response.proto +60 -2
- package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/workflowservice/v1/service.proto +30 -6
- package/sdk-core/sdk-core-protos/protos/local/temporal/sdk/core/workflow_activation/workflow_activation.proto +2 -0
- package/sdk-core/{test-utils → sdk-core-protos}/src/canned_histories.rs +5 -5
- package/sdk-core/sdk-core-protos/src/history_builder.rs +2 -2
- package/sdk-core/sdk-core-protos/src/lib.rs +25 -9
- package/sdk-core/sdk-core-protos/src/test_utils.rs +89 -0
- package/sdk-core/sdk-core-protos/src/utilities.rs +14 -5
- package/sdk-core/tests/c_bridge_smoke_test.c +10 -0
- package/sdk-core/tests/cloud_tests.rs +10 -8
- package/sdk-core/tests/common/http_proxy.rs +134 -0
- package/sdk-core/{test-utils/src/lib.rs → tests/common/mod.rs} +214 -281
- package/sdk-core/{test-utils/src → tests/common}/workflows.rs +4 -3
- package/sdk-core/tests/fuzzy_workflow.rs +1 -1
- package/sdk-core/tests/global_metric_tests.rs +8 -7
- package/sdk-core/tests/heavy_tests.rs +7 -3
- package/sdk-core/tests/integ_tests/client_tests.rs +111 -24
- package/sdk-core/tests/integ_tests/ephemeral_server_tests.rs +14 -9
- package/sdk-core/tests/integ_tests/heartbeat_tests.rs +4 -4
- package/sdk-core/tests/integ_tests/metrics_tests.rs +114 -14
- package/sdk-core/tests/integ_tests/pagination_tests.rs +273 -0
- package/sdk-core/tests/integ_tests/polling_tests.rs +311 -93
- package/sdk-core/tests/integ_tests/queries_tests.rs +4 -4
- package/sdk-core/tests/integ_tests/update_tests.rs +13 -7
- package/sdk-core/tests/integ_tests/visibility_tests.rs +26 -9
- package/sdk-core/tests/integ_tests/worker_tests.rs +668 -13
- package/sdk-core/tests/integ_tests/worker_versioning_tests.rs +40 -24
- package/sdk-core/tests/integ_tests/workflow_tests/activities.rs +244 -11
- package/sdk-core/tests/integ_tests/workflow_tests/appdata_propagation.rs +1 -1
- package/sdk-core/tests/integ_tests/workflow_tests/cancel_external.rs +78 -2
- package/sdk-core/tests/integ_tests/workflow_tests/cancel_wf.rs +61 -2
- package/sdk-core/tests/integ_tests/workflow_tests/child_workflows.rs +465 -7
- package/sdk-core/tests/integ_tests/workflow_tests/continue_as_new.rs +41 -2
- package/sdk-core/tests/integ_tests/workflow_tests/determinism.rs +315 -3
- package/sdk-core/tests/integ_tests/workflow_tests/eager.rs +1 -1
- package/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs +1990 -14
- package/sdk-core/tests/integ_tests/workflow_tests/modify_wf_properties.rs +65 -2
- package/sdk-core/tests/integ_tests/workflow_tests/nexus.rs +123 -23
- package/sdk-core/tests/integ_tests/workflow_tests/patches.rs +525 -3
- package/sdk-core/tests/integ_tests/workflow_tests/replay.rs +65 -16
- package/sdk-core/tests/integ_tests/workflow_tests/resets.rs +32 -23
- package/sdk-core/tests/integ_tests/workflow_tests/signals.rs +126 -5
- package/sdk-core/tests/integ_tests/workflow_tests/stickyness.rs +1 -2
- package/sdk-core/tests/integ_tests/workflow_tests/timers.rs +124 -8
- package/sdk-core/tests/integ_tests/workflow_tests/upsert_search_attrs.rs +62 -2
- package/sdk-core/tests/integ_tests/workflow_tests.rs +67 -8
- package/sdk-core/tests/main.rs +26 -17
- package/sdk-core/tests/manual_tests.rs +5 -1
- package/sdk-core/tests/runner.rs +22 -40
- package/sdk-core/tests/shared_tests/mod.rs +1 -1
- package/sdk-core/tests/shared_tests/priority.rs +1 -1
- package/sdk-core/{core/benches/workflow_replay.rs → tests/workflow_replay_bench.rs} +10 -5
- package/src/client.rs +97 -20
- package/src/helpers/callbacks.rs +4 -4
- package/src/helpers/errors.rs +7 -1
- package/src/helpers/handles.rs +1 -0
- package/src/helpers/try_from_js.rs +4 -3
- package/src/lib.rs +3 -2
- package/src/metrics.rs +3 -0
- package/src/runtime.rs +5 -2
- package/src/worker.rs +9 -12
- package/ts/native.ts +13 -3
- package/sdk-core/arch_docs/diagrams/workflow_internals.svg +0 -1
- package/sdk-core/core/src/core_tests/child_workflows.rs +0 -281
- package/sdk-core/core/src/core_tests/determinism.rs +0 -318
- package/sdk-core/core/src/core_tests/local_activities.rs +0 -1442
- package/sdk-core/test-utils/Cargo.toml +0 -38
- package/sdk-core/test-utils/src/histfetch.rs +0 -28
- package/sdk-core/test-utils/src/interceptors.rs +0 -46
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# Intro to Temporal SDKs
|
|
2
|
+
|
|
3
|
+
This document is primarily meant as an onboarding guide for new Temporal SDK developers, to be
|
|
4
|
+
accompanied by a live presentation. Hence, content does not have maximal context. If reading this
|
|
5
|
+
independently, you will also want to refer to our documentation [site](https://docs.temporal.io).
|
|
6
|
+
|
|
7
|
+
## Primary SDK Concepts
|
|
8
|
+
|
|
9
|
+
See also our
|
|
10
|
+
[official docs on basic concepts](https://docs.temporal.io/evaluate/understanding-temporal).
|
|
11
|
+
|
|
12
|
+
### Workflows
|
|
13
|
+
|
|
14
|
+
Workflows are durable programs. Their state is materialized from event history and they can be
|
|
15
|
+
replayed zero or more times. Workflow code must be deterministic and must use SDK Workflow-specific
|
|
16
|
+
APIs. Side effects and non-deterministic APIs should be placed in Activities.
|
|
17
|
+
|
|
18
|
+
**Example Workflow Code (Python):**
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from datetime import timedelta
|
|
22
|
+
from temporalio import workflow
|
|
23
|
+
|
|
24
|
+
@workflow.defn
|
|
25
|
+
class SayHello:
|
|
26
|
+
@workflow.run
|
|
27
|
+
async def run(self, name: str) -> str:
|
|
28
|
+
return await workflow.execute_activity(
|
|
29
|
+
say_hello, name, schedule_to_close_timeout=timedelta(seconds=5)
|
|
30
|
+
)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Activities
|
|
34
|
+
|
|
35
|
+
Activities are invoked from a Workflow and can run arbitrary code. They can be retried, are subject
|
|
36
|
+
to timeouts, and should be idempotent/reentrant. Activities can be short or long-running and can
|
|
37
|
+
heartbeat.
|
|
38
|
+
|
|
39
|
+
**Example Activity Code (Python):**
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from temporalio import activity
|
|
43
|
+
|
|
44
|
+
@activity.defn
|
|
45
|
+
async def say_hello(name: str) -> str:
|
|
46
|
+
return f"Hello, {name}!"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Clients
|
|
50
|
+
|
|
51
|
+
Clients serve as a driver for interaction with the Temporal server. They provide a high-level API
|
|
52
|
+
for a subset of gRPC calls, including Workflow "CRUD" operations, Activity completion, and
|
|
53
|
+
Schedules.
|
|
54
|
+
|
|
55
|
+
**Example Client Code (Python):**
|
|
56
|
+
|
|
57
|
+
``` python
|
|
58
|
+
from temporalio.client import Client
|
|
59
|
+
|
|
60
|
+
# Import the workflow from the previous code
|
|
61
|
+
from .workflows import SayHello
|
|
62
|
+
|
|
63
|
+
async def main():
|
|
64
|
+
client = await Client.connect("localhost:7233")
|
|
65
|
+
|
|
66
|
+
result = await client.execute_workflow(
|
|
67
|
+
SayHello.run, "my name", id="my-workflow-id", task_queue="my-task-queue")
|
|
68
|
+
|
|
69
|
+
print(f"Result: {result}")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Workers
|
|
73
|
+
|
|
74
|
+
Workers are stateless entities that poll the server for Workflow, Activity, and Nexus tasks. They
|
|
75
|
+
run user-defined Workflows, Activities, and Nexus Operations, manage concurrency and rate limits,
|
|
76
|
+
and provide the language runtime for workflows.
|
|
77
|
+
|
|
78
|
+
**Example Worker Code (Python):**
|
|
79
|
+
|
|
80
|
+
``` python
|
|
81
|
+
from temporalio.client import Client
|
|
82
|
+
from temporalio.worker import Worker
|
|
83
|
+
|
|
84
|
+
from .activities import say_hello
|
|
85
|
+
from .workflows import SayHello
|
|
86
|
+
|
|
87
|
+
async def main():
|
|
88
|
+
client = await Client.connect("localhost:7233")
|
|
89
|
+
|
|
90
|
+
worker = Worker(
|
|
91
|
+
client,
|
|
92
|
+
task_queue="my-task-queue",
|
|
93
|
+
workflows=[SayHello],
|
|
94
|
+
activities=[say_hello])
|
|
95
|
+
await worker.run()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## SDK \<-\> Server Interaction
|
|
99
|
+
|
|
100
|
+
### Event History and it's construction
|
|
101
|
+
|
|
102
|
+
Event history is a record of all events that have occurred in a Workflow. Workflow tasks are the
|
|
103
|
+
fundamental unit of progress for Workflows. Any time your Workflow code makes progress, it does so
|
|
104
|
+
because your worker processed one of these Workflow tasks. Workflow tasks contain some or all of the
|
|
105
|
+
Workflow’s history, along with possibly queries and other bits of information the worker might care
|
|
106
|
+
about.
|
|
107
|
+
|
|
108
|
+
The first few events in a workflow's history always look like this:
|
|
109
|
+
|
|
110
|
+

|
|
111
|
+
|
|
112
|
+
The first Workflow task for a Workflow always includes the initial events. When the SDK receives a
|
|
113
|
+
task, it processes the history, runs your Workflow code, and eventually responds with commands to
|
|
114
|
+
the server. Commands are actions like "start a timer" or "run an Activity."
|
|
115
|
+
|
|
116
|
+
A sequence diagram of this process is shown below. Not everything may make sense yet, but we can
|
|
117
|
+
refer back to this diagram during later explanations.
|
|
118
|
+
|
|
119
|
+
```mermaid
|
|
120
|
+
%%{ init : { "theme" : "default", "themeVariables" : { "background" : "#fff" }}}%%
|
|
121
|
+
sequenceDiagram
|
|
122
|
+
box rgb(255, 255, 255)
|
|
123
|
+
participant Client
|
|
124
|
+
participant Server
|
|
125
|
+
participant SDK
|
|
126
|
+
participant UserCode as "User Code"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
Client ->> Server: StartWorkflowExecution RPC
|
|
130
|
+
Server ->> Server: Generate Workflow Task & persist
|
|
131
|
+
Server -->> Client: runID
|
|
132
|
+
|
|
133
|
+
loop Workflow Task Polling
|
|
134
|
+
rect rgb(200, 230, 200)
|
|
135
|
+
SDK ->> Server: PollWorkflowTaskQueueRequest
|
|
136
|
+
opt Something triggers new Workflow Task
|
|
137
|
+
Server ->> Server: Generate Workflow Task & persist
|
|
138
|
+
end
|
|
139
|
+
Server -->> SDK: PollWorkflowTaskQueueResponse
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
rect rgb(220, 200, 220)
|
|
143
|
+
loop Processing one Workflow Task
|
|
144
|
+
SDK ->> SDK: Apply workflow history to state
|
|
145
|
+
SDK ->> UserCode: Activate & Unblock Awaitables
|
|
146
|
+
UserCode -->> SDK: Blocked on some awaitable or complete
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
SDK ->> Server: RespondWorkflowTaskCompletedRequest
|
|
150
|
+
Server ->> Server: Persist & possibly reply with new Workflow Task
|
|
151
|
+
Server -->> SDK: RespondWorkflowTaskCompletedResponse
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**How History is Constructed:**
|
|
157
|
+
|
|
158
|
+
Workflow tasks contain history and are responded to with commands. Every command is turned into a
|
|
159
|
+
corresponding event (referred to as "dual" or "command-events"). The Temporal server takes action on
|
|
160
|
+
received commands, and the consequences of those actions also become events (e.g., `TimerFired`,
|
|
161
|
+
`ActivityTaskCompleted`). When events that trigger a new Workflow task occur, the Temporal server
|
|
162
|
+
generates a new Workflow task because a worker needs to run the Workflow code to process this new
|
|
163
|
+
information.
|
|
164
|
+
|
|
165
|
+
This cycle continues until the Workflow is complete: new Workflow Tasks are created, processed by
|
|
166
|
+
the SDK, responded to with commands, until a new Workflow task begins, and so on.
|
|
167
|
+
|
|
168
|
+
We will use the following example Workflow to illustrate this process:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
@activity.defn
|
|
172
|
+
async def say_hello(name: str) -> str:
|
|
173
|
+
return f"Hello, {name}!"
|
|
174
|
+
|
|
175
|
+
@workflow.defn
|
|
176
|
+
class ReplayExampleWorkflow:
|
|
177
|
+
@workflow.run
|
|
178
|
+
async def run(self, name: str) -> str:
|
|
179
|
+
my_hello = workflow.execute_activity(
|
|
180
|
+
say_hello, name, schedule_to_close_timeout=timedelta(seconds=5)
|
|
181
|
+
)
|
|
182
|
+
timer = workflow.sleep(10)
|
|
183
|
+
(activity_result, _) = await asyncio.gather(my_hello, timer)
|
|
184
|
+
return activity_result
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Example Workflow History Analysis:**
|
|
188
|
+
|
|
189
|
+
1. The initial Workflow task is processed. The `execute_activity` call creates a command for
|
|
190
|
+
scheduling an Activity, and `workflow.sleep` creates a command for a timer.
|
|
191
|
+
2. The `asyncio.gather` line indicates a wait for both the activity and timer to resolve.
|
|
192
|
+
3. The SDK knows when to respond to the task because the Workflow cannot make any more progress. All
|
|
193
|
+
buffered commands are sent to the server (activity and timer commands, in that order). This
|
|
194
|
+
completes the first Workflow task.
|
|
195
|
+
4. The server receives these commands, which are converted into `ActivityTaskScheduled` and
|
|
196
|
+
`TimerStarted` events (duals of the commands).
|
|
197
|
+
5. When the Activity completes, an `ActivityTaskCompleted` event is added to the history. This
|
|
198
|
+
triggers a new Workflow task.
|
|
199
|
+
6. The worker processes this new task, and the future `my_hello` is resolved. The Workflow code,
|
|
200
|
+
however, is still blocked on `gather` because the timer hasn't fired. The SDK replies with an
|
|
201
|
+
empty list of commands, indicating no new actions.
|
|
202
|
+
7. The timer fires, resulting in a `TimerFired` event. This generates another Workflow task.
|
|
203
|
+
8. When this task is processed, the `gather` call resolves. The Workflow function returns, producing
|
|
204
|
+
a `CompleteWorkflowExecution` command.
|
|
205
|
+
9. This command is converted into a `WorkflowExecutionCompleted` event, and the Workflow finishes.
|
|
206
|
+
|
|
207
|
+
The complete history at the end of all this is the following:
|
|
208
|
+
|
|
209
|
+
1. Workflow Execution Started
|
|
210
|
+
2. Workflow Task Scheduled
|
|
211
|
+
3. Workflow Task Started
|
|
212
|
+
4. Workflow Task Completed
|
|
213
|
+
5. Activity Task Scheduled `say_hello`
|
|
214
|
+
6. Timer Started `1 (1s)`
|
|
215
|
+
7. Activity Task Started `say_hello`
|
|
216
|
+
8. Activity Task Completed `say_hello`
|
|
217
|
+
9. Workflow Task Scheduled
|
|
218
|
+
10. Workflow Task Started
|
|
219
|
+
11. Workflow Task Completed `Notice: No commands sent`
|
|
220
|
+
12. Timer Fired `1 (1s)`
|
|
221
|
+
13. Workflow Task Scheduled
|
|
222
|
+
14. Workflow Task Started
|
|
223
|
+
15. Workflow Task Completed
|
|
224
|
+
16. Workflow Execution Completed
|
|
225
|
+
|
|
226
|
+
This repetitive nature of processing Workflow tasks, where the SDK polls for tasks, processes
|
|
227
|
+
history, unblocks user code, and sends new commands, continues until Workflow completion.
|
|
228
|
+
|
|
229
|
+
### How Replay Works
|
|
230
|
+
|
|
231
|
+
Above we described the sequence of events as if the Python objects remained in memory between
|
|
232
|
+
workflow tasks, with asyncio futures "waiting" to be resolved. That is indeed typically the case,
|
|
233
|
+
but technically it's a performance optimization (we refer to it as the "sticky" optimization), where
|
|
234
|
+
history is delivered in incremental pieces rather than in its entirety. When a worker doesn't have
|
|
235
|
+
the workflow already in-cache, it "replays" the workflow code from the beginning (this is why we
|
|
236
|
+
said workers are stateless), making use of the supplied history to deterministically reach precisely
|
|
237
|
+
the state it had reached previously.
|
|
238
|
+
|
|
239
|
+
Replay is fundamental to Temporal's durability and determinism, and so we'll now look at it in a bit
|
|
240
|
+
more detail.
|
|
241
|
+
|
|
242
|
+
**History Processing:**
|
|
243
|
+
|
|
244
|
+
SDKs process events from history serially, one at a time. Each event is fed to a specific instance
|
|
245
|
+
of a state machine. There is a type of state machine for every command, as well as some non-command
|
|
246
|
+
events/concepts. The state of a particular Workflow run consists of the combination of the states of
|
|
247
|
+
these state machines, and importantly, the order in which they are created.
|
|
248
|
+
|
|
249
|
+
**Timer State Machine in Core (Example):**
|
|
250
|
+
|
|
251
|
+
We can use the Timer state machine to illustrate how determinism is enforced:
|
|
252
|
+
|
|
253
|
+
<img src="https://uml.planttext.com/plantuml/svg/fL912iCW4Bpx2g6tWX_eeGI4Fg3s5d8mkj206h3w_oOR8q9jfFJaBhCpiuvsho1zYDQG_ZnGngwKUv01D4adPVrfD1661HBhB9-jbKud-4A5UeAE1aW5RP9JZzXZik1_KRc3chrUPP2AqB9uGu5Bfy2WELQa9baIRfFF7bWt6Pim4Zukl7b-dTYUu1_-qHe2NCYX5t15Rnqjbia9x2tPYyDiCQzAc0dkmEdbxxq1" width="500" alt="Timer FSM Diagram"/>
|
|
254
|
+
|
|
255
|
+
When we first create the timer, we immediately schedule it, which produces the `StartTimer` command,
|
|
256
|
+
which becomes the `TimerStarted` event. The `Created` state isn’t really necessary here, but the
|
|
257
|
+
schedule transition serves as a convenient way to produce the start timer command.
|
|
258
|
+
|
|
259
|
+
When we see a `TimerStarted` event on history, we know that the command we issued exists in Workflow
|
|
260
|
+
history, and we move to the StartCommandRecorded state and wait to see what happens - it can either
|
|
261
|
+
fire or be canceled.
|
|
262
|
+
|
|
263
|
+
In our example, there’s a `TimerFired` event, so when the SDK processes that event, we’d take that
|
|
264
|
+
transition, and the machine ends up in the final fired state
|
|
265
|
+
|
|
266
|
+
At this point, you might be wondering - why does that `StartCommandRecorded` state matter? Why have
|
|
267
|
+
a transition for `TimerStarted` at all? We know we sent the command to the server, so why not just
|
|
268
|
+
wait to see if the timer fires or is canceled from the `StartCommandCreated` state? The answer is
|
|
269
|
+
that's a big part of the secret sauce for how we enforce determinism.
|
|
270
|
+
|
|
271
|
+
**Enforcing Determinism:**
|
|
272
|
+
|
|
273
|
+
A Workflow must always emit the same commands in the same sequence. Temporal can detect violations
|
|
274
|
+
of this rule because of the "dual" events that each command produces. The `TimerStarted` transition
|
|
275
|
+
ensures that when replaying a Workflow, the current code attempting to start a timer does so at the
|
|
276
|
+
same point and in the same way as what exists in history. The order in which state machines are
|
|
277
|
+
created by Workflow code also matters to ensure the same sequence of commands is produced. This
|
|
278
|
+
prevents scenarios where changed code might run a command that should have been mutually exclusive
|
|
279
|
+
with a command already in history.
|
|
280
|
+
|
|
281
|
+
**Example of Nondeterminism:**
|
|
282
|
+
|
|
283
|
+
If the order of arguments to `asyncio.gather` is swapped in the previous example Workflow, replaying
|
|
284
|
+
the Workflow will result in a nondeterminism error (often referred to as an "NDE"):
|
|
285
|
+
|
|
286
|
+
```
|
|
287
|
+
temporalio.workflow.NondeterminismError: Workflow activation completion failed: Failure { failure:
|
|
288
|
+
Some(Failure { message: "[TMPRL1100] Nondeterminism error: Timer machine does not handle this event:
|
|
289
|
+
HistoryEvent(id: 5, ActivityTaskScheduled)", source: "", stack_trace: "", encoded_attributes: None,
|
|
290
|
+
cause: None, failure_info: Some(ApplicationFailureInfo(ApplicationFailureInfo { r#type: "",
|
|
291
|
+
non_retryable: false, details: None, next_retry_delay: None })) }), force_cause:
|
|
292
|
+
NonDeterministicError }
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
This error occurs because the "dual" events in the history are not in the same order as the commands
|
|
296
|
+
produced by the modified Workflow code. The timer machine, expecting a `TimerStarted` event,
|
|
297
|
+
encounters an `ActivityTaskScheduled` event instead, leading to a nondeterminism error. In other
|
|
298
|
+
words, we did not produce the same commands in the same order. This mechanism ensures Workflow state
|
|
299
|
+
consistency.
|
|
@@ -20,23 +20,24 @@ backoff = "0.4"
|
|
|
20
20
|
base64 = "0.22"
|
|
21
21
|
derive_builder = { workspace = true }
|
|
22
22
|
derive_more = { workspace = true }
|
|
23
|
+
dyn-clone = "1.0"
|
|
23
24
|
bytes = "1.10"
|
|
24
25
|
futures-util = { version = "0.3", default-features = false }
|
|
25
26
|
futures-retry = "0.6.0"
|
|
26
|
-
http = "1.
|
|
27
|
+
http = "1.3"
|
|
27
28
|
http-body-util = "0.1"
|
|
28
|
-
hyper = { version = "1.
|
|
29
|
-
hyper-util = "0.1.
|
|
29
|
+
hyper = { version = "1.7.0" }
|
|
30
|
+
hyper-util = "0.1.16"
|
|
30
31
|
opentelemetry = { workspace = true, features = ["metrics"], optional = true }
|
|
31
32
|
parking_lot = "0.12"
|
|
32
33
|
slotmap = "1.0"
|
|
33
34
|
thiserror = { workspace = true }
|
|
34
|
-
tokio = "1.
|
|
35
|
+
tokio = "1.47"
|
|
35
36
|
tonic = { workspace = true, features = ["tls-ring", "tls-native-roots"] }
|
|
36
37
|
tower = { version = "0.5", features = ["util"] }
|
|
37
38
|
tracing = "0.1"
|
|
38
|
-
url = "2.
|
|
39
|
-
uuid = { version = "1.
|
|
39
|
+
url = "2.5"
|
|
40
|
+
uuid = { version = "1.18", features = ["v4"] }
|
|
40
41
|
|
|
41
42
|
[dependencies.temporal-sdk-core-protos]
|
|
42
43
|
path = "../sdk-core-protos"
|
|
@@ -47,7 +48,7 @@ path = "../core-api"
|
|
|
47
48
|
[dev-dependencies]
|
|
48
49
|
assert_matches = "1"
|
|
49
50
|
mockall = "0.13"
|
|
50
|
-
rstest = "0.
|
|
51
|
+
rstest = "0.26"
|
|
51
52
|
|
|
52
53
|
[lints]
|
|
53
54
|
workspace = true
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
use anyhow::anyhow;
|
|
5
5
|
use bytes::{BufMut, BytesMut};
|
|
6
|
-
use futures_util::future::BoxFuture;
|
|
7
|
-
use futures_util::stream;
|
|
6
|
+
use futures_util::{future::BoxFuture, stream};
|
|
8
7
|
use http::{HeaderMap, Request, Response};
|
|
9
8
|
use http_body_util::{BodyExt, StreamBody, combinators::BoxBody};
|
|
10
9
|
use hyper::body::{Bytes, Frame};
|