temporalio 0.2.0 → 0.4.0
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.
- checksums.yaml +4 -4
- data/.yardopts +2 -0
- data/Cargo.lock +980 -583
- data/Cargo.toml +2 -2
- data/Gemfile +7 -3
- data/README.md +769 -54
- data/Rakefile +10 -296
- data/ext/Cargo.toml +2 -0
- data/lib/temporalio/activity/complete_async_error.rb +1 -1
- data/lib/temporalio/activity/context.rb +18 -2
- data/lib/temporalio/activity/definition.rb +180 -65
- data/lib/temporalio/activity/info.rb +25 -21
- data/lib/temporalio/activity.rb +2 -59
- data/lib/temporalio/api/activity/v1/message.rb +25 -0
- data/lib/temporalio/api/batch/v1/message.rb +6 -1
- data/lib/temporalio/api/cloud/account/v1/message.rb +28 -0
- data/lib/temporalio/api/cloud/cloudservice/v1/request_response.rb +34 -1
- data/lib/temporalio/api/cloud/cloudservice/v1/service.rb +1 -1
- data/lib/temporalio/api/cloud/identity/v1/message.rb +6 -1
- data/lib/temporalio/api/cloud/namespace/v1/message.rb +8 -1
- data/lib/temporalio/api/cloud/nexus/v1/message.rb +31 -0
- data/lib/temporalio/api/cloud/operation/v1/message.rb +2 -1
- data/lib/temporalio/api/cloud/region/v1/message.rb +2 -1
- data/lib/temporalio/api/cloud/resource/v1/message.rb +23 -0
- data/lib/temporalio/api/cloud/sink/v1/message.rb +24 -0
- data/lib/temporalio/api/cloud/usage/v1/message.rb +31 -0
- data/lib/temporalio/api/command/v1/message.rb +1 -1
- data/lib/temporalio/api/common/v1/message.rb +8 -1
- data/lib/temporalio/api/deployment/v1/message.rb +38 -0
- data/lib/temporalio/api/enums/v1/batch_operation.rb +1 -1
- data/lib/temporalio/api/enums/v1/common.rb +1 -1
- data/lib/temporalio/api/enums/v1/deployment.rb +23 -0
- data/lib/temporalio/api/enums/v1/event_type.rb +1 -1
- data/lib/temporalio/api/enums/v1/failed_cause.rb +1 -1
- data/lib/temporalio/api/enums/v1/nexus.rb +21 -0
- data/lib/temporalio/api/enums/v1/reset.rb +1 -1
- data/lib/temporalio/api/enums/v1/workflow.rb +2 -1
- data/lib/temporalio/api/errordetails/v1/message.rb +3 -1
- data/lib/temporalio/api/failure/v1/message.rb +3 -1
- data/lib/temporalio/api/history/v1/message.rb +3 -1
- data/lib/temporalio/api/nexus/v1/message.rb +3 -2
- data/lib/temporalio/api/operatorservice/v1/service.rb +1 -1
- data/lib/temporalio/api/payload_visitor.rb +1581 -0
- data/lib/temporalio/api/query/v1/message.rb +2 -1
- data/lib/temporalio/api/schedule/v1/message.rb +2 -1
- data/lib/temporalio/api/taskqueue/v1/message.rb +4 -1
- data/lib/temporalio/api/testservice/v1/request_response.rb +31 -0
- data/lib/temporalio/api/testservice/v1/service.rb +23 -0
- data/lib/temporalio/api/workflow/v1/message.rb +9 -1
- data/lib/temporalio/api/workflowservice/v1/request_response.rb +46 -2
- data/lib/temporalio/api/workflowservice/v1/service.rb +1 -1
- data/lib/temporalio/api.rb +2 -0
- data/lib/temporalio/cancellation.rb +34 -14
- data/lib/temporalio/client/async_activity_handle.rb +12 -37
- data/lib/temporalio/client/connection/cloud_service.rb +309 -231
- data/lib/temporalio/client/connection/operator_service.rb +36 -84
- data/lib/temporalio/client/connection/service.rb +6 -5
- data/lib/temporalio/client/connection/test_service.rb +111 -0
- data/lib/temporalio/client/connection/workflow_service.rb +474 -441
- data/lib/temporalio/client/connection.rb +90 -44
- data/lib/temporalio/client/interceptor.rb +199 -60
- data/lib/temporalio/client/schedule.rb +991 -0
- data/lib/temporalio/client/schedule_handle.rb +126 -0
- data/lib/temporalio/client/with_start_workflow_operation.rb +115 -0
- data/lib/temporalio/client/workflow_execution.rb +26 -10
- data/lib/temporalio/client/workflow_handle.rb +41 -98
- data/lib/temporalio/client/workflow_update_handle.rb +3 -5
- data/lib/temporalio/client.rb +247 -44
- data/lib/temporalio/common_enums.rb +17 -0
- data/lib/temporalio/contrib/open_telemetry.rb +470 -0
- data/lib/temporalio/converters/data_converter.rb +4 -7
- data/lib/temporalio/converters/failure_converter.rb +5 -3
- data/lib/temporalio/converters/payload_converter/composite.rb +4 -0
- data/lib/temporalio/converters/payload_converter.rb +6 -8
- data/lib/temporalio/converters/raw_value.rb +20 -0
- data/lib/temporalio/error/failure.rb +1 -1
- data/lib/temporalio/error.rb +11 -2
- data/lib/temporalio/internal/bridge/api/activity_task/activity_task.rb +1 -1
- data/lib/temporalio/internal/bridge/api/common/common.rb +2 -1
- data/lib/temporalio/internal/bridge/api/core_interface.rb +5 -1
- data/lib/temporalio/internal/bridge/api/nexus/nexus.rb +33 -0
- data/lib/temporalio/internal/bridge/api/workflow_activation/workflow_activation.rb +5 -1
- data/lib/temporalio/internal/bridge/api/workflow_commands/workflow_commands.rb +4 -1
- data/lib/temporalio/internal/bridge/api/workflow_completion/workflow_completion.rb +2 -1
- data/lib/temporalio/internal/bridge/client.rb +11 -6
- data/lib/temporalio/internal/bridge/runtime.rb +3 -0
- data/lib/temporalio/internal/bridge/testing.rb +23 -0
- data/lib/temporalio/internal/bridge/worker.rb +2 -0
- data/lib/temporalio/internal/bridge.rb +1 -1
- data/lib/temporalio/internal/client/implementation.rb +468 -71
- data/lib/temporalio/internal/metric.rb +122 -0
- data/lib/temporalio/internal/proto_utils.rb +118 -7
- data/lib/temporalio/internal/worker/activity_worker.rb +69 -29
- data/lib/temporalio/internal/worker/multi_runner.rb +53 -9
- data/lib/temporalio/internal/worker/workflow_instance/child_workflow_handle.rb +54 -0
- data/lib/temporalio/internal/worker/workflow_instance/context.rb +383 -0
- data/lib/temporalio/internal/worker/workflow_instance/details.rb +46 -0
- data/lib/temporalio/internal/worker/workflow_instance/external_workflow_handle.rb +32 -0
- data/lib/temporalio/internal/worker/workflow_instance/externally_immutable_hash.rb +22 -0
- data/lib/temporalio/internal/worker/workflow_instance/handler_execution.rb +25 -0
- data/lib/temporalio/internal/worker/workflow_instance/handler_hash.rb +41 -0
- data/lib/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rb +97 -0
- data/lib/temporalio/internal/worker/workflow_instance/inbound_implementation.rb +62 -0
- data/lib/temporalio/internal/worker/workflow_instance/outbound_implementation.rb +400 -0
- data/lib/temporalio/internal/worker/workflow_instance/replay_safe_logger.rb +37 -0
- data/lib/temporalio/internal/worker/workflow_instance/replay_safe_metric.rb +40 -0
- data/lib/temporalio/internal/worker/workflow_instance/scheduler.rb +183 -0
- data/lib/temporalio/internal/worker/workflow_instance.rb +774 -0
- data/lib/temporalio/internal/worker/workflow_worker.rb +239 -0
- data/lib/temporalio/metric.rb +109 -0
- data/lib/temporalio/retry_policy.rb +37 -14
- data/lib/temporalio/runtime/metric_buffer.rb +94 -0
- data/lib/temporalio/runtime.rb +160 -79
- data/lib/temporalio/search_attributes.rb +93 -37
- data/lib/temporalio/testing/activity_environment.rb +44 -16
- data/lib/temporalio/testing/workflow_environment.rb +276 -7
- data/lib/temporalio/version.rb +1 -1
- data/lib/temporalio/worker/activity_executor/thread_pool.rb +9 -217
- data/lib/temporalio/worker/activity_executor.rb +3 -3
- data/lib/temporalio/worker/interceptor.rb +343 -66
- data/lib/temporalio/worker/thread_pool.rb +237 -0
- data/lib/temporalio/worker/tuner.rb +38 -0
- data/lib/temporalio/worker/workflow_executor/thread_pool.rb +235 -0
- data/lib/temporalio/worker/workflow_executor.rb +26 -0
- data/lib/temporalio/worker/workflow_replayer.rb +350 -0
- data/lib/temporalio/worker.rb +235 -58
- data/lib/temporalio/workflow/activity_cancellation_type.rb +20 -0
- data/lib/temporalio/workflow/child_workflow_cancellation_type.rb +21 -0
- data/lib/temporalio/workflow/child_workflow_handle.rb +43 -0
- data/lib/temporalio/workflow/definition.rb +598 -0
- data/lib/temporalio/workflow/external_workflow_handle.rb +41 -0
- data/lib/temporalio/workflow/future.rb +151 -0
- data/lib/temporalio/workflow/handler_unfinished_policy.rb +13 -0
- data/lib/temporalio/workflow/info.rb +104 -0
- data/lib/temporalio/workflow/parent_close_policy.rb +19 -0
- data/lib/temporalio/workflow/update_info.rb +20 -0
- data/lib/temporalio/workflow.rb +575 -0
- data/lib/temporalio/workflow_history.rb +26 -1
- data/lib/temporalio.rb +4 -0
- data/temporalio.gemspec +4 -3
- metadata +73 -10
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
|
1
|
+
<div style="overflow: hidden"><img src="https://raw.githubusercontent.com/temporalio/assets/main/files/w/ruby.png" alt="Temporal Ruby SDK" /></div>
|
2
2
|
|
3
|
-

|
4
4
|
[](LICENSE)
|
5
5
|
[](https://rubygems.org/gems/temporalio)
|
6
6
|
|
@@ -11,21 +11,20 @@ execute asynchronous, long-running business logic in a scalable and resilient wa
|
|
11
11
|
|
12
12
|
Also see:
|
13
13
|
|
14
|
+
* [Ruby SDK](https://github.com/temporalio/sdk-ruby)
|
14
15
|
* [Ruby Samples](https://github.com/temporalio/samples-ruby)
|
15
|
-
* [API Documentation](https://
|
16
|
+
* [API Documentation](https://ruby.temporal.io)
|
17
|
+
|
18
|
+
**NOTE: This README is for the current branch and not necessarily what's released on RubyGems.**
|
16
19
|
|
17
20
|
⚠️ UNDER ACTIVE DEVELOPMENT
|
18
21
|
|
19
22
|
This SDK is under active development and has not released a stable version yet. APIs may change in incompatible ways
|
20
|
-
until the SDK is marked stable.
|
21
|
-
this refresh is [v0.1.1](https://github.com/temporalio/sdk-ruby/tree/v0.1.1). Please reference that tag for the
|
22
|
-
previous code if needed.
|
23
|
-
|
24
|
-
Notably missing from this SDK:
|
23
|
+
until the SDK is marked stable.
|
25
24
|
|
26
|
-
|
27
|
-
|
28
|
-
|
25
|
+
During this time, we are requesting any/all feedback from early adopters. We welcome all forms of suggestions or
|
26
|
+
opinions. Please communicate with us on [Slack](https://t.mp/slack) in the `#ruby-sdk` channel or via email at
|
27
|
+
`sdk@temporal.io`.
|
29
28
|
|
30
29
|
---
|
31
30
|
|
@@ -35,15 +34,32 @@ Notably missing from this SDK:
|
|
35
34
|
|
36
35
|
- [Quick Start](#quick-start)
|
37
36
|
- [Installation](#installation)
|
38
|
-
- [Implementing
|
39
|
-
- [Running a
|
37
|
+
- [Implementing a Workflow and Activity](#implementing-a-workflow-and-activity)
|
38
|
+
- [Running a Worker](#running-a-worker)
|
39
|
+
- [Executing a Workflow](#executing-a-workflow)
|
40
40
|
- [Usage](#usage)
|
41
41
|
- [Client](#client)
|
42
42
|
- [Cloud Client Using mTLS](#cloud-client-using-mtls)
|
43
|
+
- [Cloud Client Using API Key](#cloud-client-using-api-key)
|
43
44
|
- [Data Conversion](#data-conversion)
|
44
45
|
- [ActiveRecord and ActiveModel](#activerecord-and-activemodel)
|
45
46
|
- [Workers](#workers)
|
46
47
|
- [Workflows](#workflows)
|
48
|
+
- [Workflow Definition](#workflow-definition)
|
49
|
+
- [Running Workflows](#running-workflows)
|
50
|
+
- [Invoking Activities](#invoking-activities)
|
51
|
+
- [Invoking Child Workflows](#invoking-child-workflows)
|
52
|
+
- [Timers and Conditions](#timers-and-conditions)
|
53
|
+
- [Workflow Fiber Scheduling and Cancellation](#workflow-fiber-scheduling-and-cancellation)
|
54
|
+
- [Workflow Futures](#workflow-futures)
|
55
|
+
- [Workflow Utilities](#workflow-utilities)
|
56
|
+
- [Workflow Exceptions](#workflow-exceptions)
|
57
|
+
- [Workflow Logic Constraints](#workflow-logic-constraints)
|
58
|
+
- [Workflow Testing](#workflow-testing)
|
59
|
+
- [Automatic Time Skipping](#automatic-time-skipping)
|
60
|
+
- [Manual Time Skipping](#manual-time-skipping)
|
61
|
+
- [Mocking Activities](#mocking-activities)
|
62
|
+
- [Workflow Replay](#workflow-replay)
|
47
63
|
- [Activities](#activities)
|
48
64
|
- [Activity Definition](#activity-definition)
|
49
65
|
- [Activity Context](#activity-context)
|
@@ -51,6 +67,11 @@ Notably missing from this SDK:
|
|
51
67
|
- [Activity Worker Shutdown](#activity-worker-shutdown)
|
52
68
|
- [Activity Concurrency and Executors](#activity-concurrency-and-executors)
|
53
69
|
- [Activity Testing](#activity-testing)
|
70
|
+
- [Telemetry](#telemetry)
|
71
|
+
- [Metrics](#metrics)
|
72
|
+
- [OpenTelemetry Tracing](#opentelemetry-tracing)
|
73
|
+
- [OpenTelemetry Tracing in Workflows](#opentelemetry-tracing-in-workflows)
|
74
|
+
- [Ractors](#ractors)
|
54
75
|
- [Platform Support](#platform-support)
|
55
76
|
- [Development](#development)
|
56
77
|
- [Build](#build)
|
@@ -65,6 +86,8 @@ Notably missing from this SDK:
|
|
65
86
|
|
66
87
|
### Installation
|
67
88
|
|
89
|
+
The Ruby SDK works with Ruby 3.2, 3.3, and 3.4.
|
90
|
+
|
68
91
|
Can require in a Gemfile like:
|
69
92
|
|
70
93
|
```
|
@@ -79,64 +102,91 @@ gem install temporalio
|
|
79
102
|
|
80
103
|
**NOTE**: Only macOS ARM/x64 and Linux ARM/x64 are supported, and the platform-specific gem chosen is based on when the
|
81
104
|
gem/bundle install is performed. A source gem is published but cannot be used directly and will fail to build if tried.
|
82
|
-
MinGW-based Windows
|
83
|
-
information.
|
105
|
+
MinGW-based Windows is not currently supported. There are caveats with the Google Protobuf dependency on musl-based
|
106
|
+
Linux. See the [Platform Support](#platform-support) section for more information.
|
84
107
|
|
85
108
|
**NOTE**: Due to [an issue](https://github.com/temporalio/sdk-ruby/issues/162), fibers (and `async` gem) are only
|
86
109
|
supported on Ruby versions 3.3 and newer.
|
87
110
|
|
88
|
-
### Implementing
|
111
|
+
### Implementing a Workflow and Activity
|
89
112
|
|
90
|
-
|
113
|
+
Activities are classes. Here is an example of a simple activity that can be put in `say_hello_activity.rb`:
|
91
114
|
|
92
|
-
For example, if you have a `SayHelloWorkflow` workflow in another Temporal language that invokes `SayHello` activity on
|
93
|
-
`my-task-queue` in Ruby, you can have the following Ruby script:
|
94
115
|
|
95
116
|
```ruby
|
96
117
|
require 'temporalio/activity'
|
97
|
-
require 'temporalio/cancellation'
|
98
|
-
require 'temporalio/client'
|
99
|
-
require 'temporalio/worker'
|
100
118
|
|
101
119
|
# Implementation of a simple activity
|
102
|
-
class SayHelloActivity < Temporalio::Activity
|
120
|
+
class SayHelloActivity < Temporalio::Activity::Definition
|
103
121
|
def execute(name)
|
104
122
|
"Hello, #{name}!"
|
105
123
|
end
|
106
124
|
end
|
125
|
+
```
|
126
|
+
|
127
|
+
Workflows are also classes. To create the workflow, put the following in `say_hello_workflow.rb`:
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
require 'temporalio/workflow'
|
131
|
+
require_relative 'say_hello_activity'
|
132
|
+
|
133
|
+
class SayHelloWorkflow < Temporalio::Workflow::Definition
|
134
|
+
def execute(name)
|
135
|
+
Temporalio::Workflow.execute_activity(
|
136
|
+
SayHelloActivity,
|
137
|
+
name,
|
138
|
+
schedule_to_close_timeout: 300
|
139
|
+
)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
```
|
143
|
+
|
144
|
+
This is a simple workflow that executes the `SayHelloActivity` activity.
|
145
|
+
|
146
|
+
### Running a Worker
|
147
|
+
|
148
|
+
To run this in a worker, put the following in `worker.rb`:
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
require 'temporalio/client'
|
152
|
+
require 'temporalio/worker'
|
153
|
+
require_relative 'say_hello_activity'
|
154
|
+
require_relative 'say_hello_workflow'
|
107
155
|
|
108
156
|
# Create a client
|
109
157
|
client = Temporalio::Client.connect('localhost:7233', 'my-namespace')
|
110
158
|
|
111
|
-
# Create a worker with the client and
|
159
|
+
# Create a worker with the client, activities, and workflows
|
112
160
|
worker = Temporalio::Worker.new(
|
113
161
|
client:,
|
114
162
|
task_queue: 'my-task-queue',
|
115
|
-
|
163
|
+
workflows: [SayHelloWorkflow],
|
164
|
+
# There are various forms an activity can take, see "Activities" section for details
|
116
165
|
activities: [SayHelloActivity]
|
117
166
|
)
|
118
167
|
|
119
|
-
# Run the worker until SIGINT. This can be done in many ways, see
|
120
|
-
# section for details.
|
168
|
+
# Run the worker until SIGINT. This can be done in many ways, see "Workers" section for details.
|
121
169
|
worker.run(shutdown_signals: ['SIGINT'])
|
122
170
|
```
|
123
171
|
|
124
|
-
Running that will run the worker until Ctrl+C pressed.
|
172
|
+
Running that will run the worker until Ctrl+C is pressed.
|
125
173
|
|
126
|
-
###
|
174
|
+
### Executing a Workflow
|
127
175
|
|
128
|
-
|
176
|
+
To start and wait on the workflow result, with the worker program running elsewhere, put the following in
|
177
|
+
`execute_workflow.rb`:
|
129
178
|
|
130
179
|
```ruby
|
131
180
|
require 'temporalio/client'
|
181
|
+
require_relative 'say_hello_workflow'
|
132
182
|
|
133
183
|
# Create a client
|
134
184
|
client = Temporalio::Client.connect('localhost:7233', 'my-namespace')
|
135
185
|
|
136
186
|
# Run workflow
|
137
187
|
result = client.execute_workflow(
|
138
|
-
|
139
|
-
'Temporal',
|
188
|
+
SayHelloWorkflow,
|
189
|
+
'Temporal', # This is the input to the workflow
|
140
190
|
id: 'my-workflow-id',
|
141
191
|
task_queue: 'my-task-queue'
|
142
192
|
)
|
@@ -163,8 +213,8 @@ client = Temporalio::Client.connect('localhost:7233', 'my-namespace')
|
|
163
213
|
|
164
214
|
# Start a workflow
|
165
215
|
handle = client.start_workflow(
|
166
|
-
|
167
|
-
'
|
216
|
+
MyWorkflow,
|
217
|
+
'arg1', 'arg2',
|
168
218
|
id: 'my-workflow-id',
|
169
219
|
task_queue: 'my-task-queue'
|
170
220
|
)
|
@@ -179,6 +229,8 @@ Notes about the above code:
|
|
179
229
|
* Temporal clients are not explicitly closed.
|
180
230
|
* To enable TLS, the `tls` option can be set to `true` or a `Temporalio::Client::Connection::TLSOptions` instance.
|
181
231
|
* Instead of `start_workflow` + `result` above, `execute_workflow` shortcut can be used if the handle is not needed.
|
232
|
+
* Both `start_workflow` and `execute_workflow` accept either the workflow class or the string/symbol name of the
|
233
|
+
workflow.
|
182
234
|
* The `handle` above is a `Temporalio::Client::WorkflowHandle` which has several other operations that can be performed
|
183
235
|
on a workflow. To get a handle to an existing workflow, use `workflow_handle` on the client.
|
184
236
|
* Clients are thread safe and are fiber-compatible (but fiber compatibility only supported for Ruby 3.3+ at this time).
|
@@ -201,6 +253,22 @@ client = Temporalio::Client.connect(
|
|
201
253
|
))
|
202
254
|
```
|
203
255
|
|
256
|
+
#### Cloud Client Using API Key
|
257
|
+
|
258
|
+
Assuming the API key is 'my-api-key', this is how to connect to Temporal cloud:
|
259
|
+
|
260
|
+
```ruby
|
261
|
+
require 'temporalio/client'
|
262
|
+
|
263
|
+
# Create a client
|
264
|
+
client = Temporalio::Client.connect(
|
265
|
+
'my-namespace.a1b2c.tmprl.cloud:7233',
|
266
|
+
'my-namespace.a1b2c',
|
267
|
+
api_key: 'my-api-key'
|
268
|
+
tls: true
|
269
|
+
)
|
270
|
+
```
|
271
|
+
|
204
272
|
#### Data Conversion
|
205
273
|
|
206
274
|
Data converters are used to convert raw Temporal payloads to/from actual Ruby types. A custom data converter can be set
|
@@ -215,11 +283,12 @@ which supports the following types:
|
|
215
283
|
* `nil`
|
216
284
|
* "bytes" (i.e. `String` with `Encoding::ASCII_8BIT` encoding)
|
217
285
|
* `Google::Protobuf::MessageExts` instances
|
218
|
-
* [
|
286
|
+
* [JSON module](https://docs.ruby-lang.org/en/master/JSON.html) for everything else
|
219
287
|
|
220
288
|
This means that normal Ruby objects will use `JSON.generate` when serializing and `JSON.parse` when deserializing (with
|
221
|
-
`create_additions: true` set by default). So a Ruby object will often appear as a hash when deserialized.
|
222
|
-
|
289
|
+
`create_additions: true` set by default). So a Ruby object will often appear as a hash when deserialized. Also, hashes
|
290
|
+
that are passed in with symbol keys end up with string keys when deserialized. While "JSON Additions" are supported, it
|
291
|
+
is not cross-SDK-language compatible since this is a Ruby-specific construct.
|
223
292
|
|
224
293
|
The default payload converter is a collection of "encoding payload converters". On serialize, each encoding converter
|
225
294
|
will be tried in order until one accepts (default falls through to the JSON one). The encoding converter sets an
|
@@ -280,8 +349,7 @@ well.
|
|
280
349
|
|
281
350
|
### Workers
|
282
351
|
|
283
|
-
Workers host workflows and/or activities.
|
284
|
-
an activity worker:
|
352
|
+
Workers host workflows and/or activities. Here's how to run a worker:
|
285
353
|
|
286
354
|
```ruby
|
287
355
|
require 'temporalio/client'
|
@@ -291,11 +359,12 @@ require 'my_module'
|
|
291
359
|
# Create a client
|
292
360
|
client = Temporalio::Client.connect('localhost:7233', 'my-namespace')
|
293
361
|
|
294
|
-
# Create a worker with the client and
|
362
|
+
# Create a worker with the client, activities, and workflows
|
295
363
|
worker = Temporalio::Worker.new(
|
296
364
|
client:,
|
297
365
|
task_queue: 'my-task-queue',
|
298
|
-
|
366
|
+
workflows: [MyModule::MyWorkflow],
|
367
|
+
# There are various forms an activity can take, see "Activities" section for details
|
299
368
|
activities: [MyModule::MyActivity]
|
300
369
|
)
|
301
370
|
|
@@ -311,19 +380,531 @@ Notes about the above code:
|
|
311
380
|
* This just shows providing an activity class, but there are other forms, see the "Activities" section for details.
|
312
381
|
* The worker `run` method accepts an optional `Temporalio::Cancellation` object that can be used to cancel instead or in
|
313
382
|
addition to providing a block that waits for completion.
|
314
|
-
* The worker `run` method accepts
|
383
|
+
* The worker `run` method accepts a `shutdown_signals` array which will trap the signal and start shutdown when
|
315
384
|
received.
|
316
385
|
* Workers work with threads or fibers (but fiber compatibility only supported for Ruby 3.3+ at this time). Fiber-based
|
317
386
|
activities (see "Activities" section) only work if the worker is created within a fiber.
|
318
387
|
* The `run` method does not return until the worker is shut down. This means even if shutdown is triggered (e.g. via
|
319
388
|
`Cancellation` or block completion), it may not return immediately. Activities not completing may hang worker
|
320
389
|
shutdown, see the "Activities" section.
|
321
|
-
* Workers can have many more options not shown here (e.g.
|
390
|
+
* Workers can have many more options not shown here (e.g. tuners and interceptors).
|
322
391
|
* The `Temporalio::Worker.run_all` class method is available for running multiple workers concurrently.
|
323
392
|
|
324
393
|
### Workflows
|
325
394
|
|
326
|
-
|
395
|
+
#### Workflow Definition
|
396
|
+
|
397
|
+
Workflows are defined as classes that extend `Temporalio::Workflow::Definition`. The entry point for a workflow is
|
398
|
+
`execute` and must be defined. Methods for handling signals, queries, and updates are marked with `workflow_signal`,
|
399
|
+
`workflow_query`, and `workflow_update` just before the method is defined. Here is an example of a workflow definition:
|
400
|
+
|
401
|
+
```ruby
|
402
|
+
require 'temporalio/workflow'
|
403
|
+
|
404
|
+
class GreetingWorkflow < Temporalio::Workflow::Definition
|
405
|
+
workflow_query_attr_reader :current_greeting
|
406
|
+
|
407
|
+
def execute(params)
|
408
|
+
loop do
|
409
|
+
# Call activity called CreateGreeting to create greeting and store as attribute
|
410
|
+
@current_greeting = Temporalio::Workflow.execute_activity(
|
411
|
+
CreateGreeting,
|
412
|
+
params,
|
413
|
+
schedule_to_close_timeout: 300
|
414
|
+
)
|
415
|
+
Temporalio::Workflow.logger.debug("Greeting set to #{@current_greeting}")
|
416
|
+
|
417
|
+
# Wait for param update or complete signal. Note, cancellation can occur by default
|
418
|
+
# on wait_condition calls, so Cancellation object doesn't need to be passed
|
419
|
+
# explicitly.
|
420
|
+
Temporalio::Workflow.wait_condition { @greeting_params_update || @complete }
|
421
|
+
|
422
|
+
# If there was an update, exchange and rerun. If it's _only_ a complete, finish
|
423
|
+
# workflow with the greeting.
|
424
|
+
if @greeting_params_update
|
425
|
+
params, @greeting_params_update = @greeting_params_update, nil
|
426
|
+
else
|
427
|
+
return @current_greeting
|
428
|
+
end
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
workflow_update
|
433
|
+
def update_greeting_params(greeting_params_update)
|
434
|
+
@greeting_params_update = greeting_params_update
|
435
|
+
end
|
436
|
+
|
437
|
+
workflow_signal
|
438
|
+
def complete_with_greeting
|
439
|
+
@complete = true
|
440
|
+
end
|
441
|
+
end
|
442
|
+
```
|
443
|
+
|
444
|
+
Notes about the above code:
|
445
|
+
|
446
|
+
* `execute` is the primary entrypoint and its result/exception represents the workflow result/failure.
|
447
|
+
* `workflow_signal`, `workflow_query` (and the shortcut seen above, `workflow_query_attr_reader`), and `workflow_update`
|
448
|
+
implicitly create class methods usable by callers/clients. A workflow definition with no methods actually implemented
|
449
|
+
can even be created for use by clients if the workflow is implemented elsewhere and/or in another language.
|
450
|
+
* Workflow code must be deterministic. See the "Workflow Logic Constraints" section below.
|
451
|
+
* `execute_activity` accepts either the activity class or the string/symbol for the name.
|
452
|
+
|
453
|
+
The following protected class methods are available on `Temporalio::Workflow::Definition` to customize the overall
|
454
|
+
workflow definition/behavior:
|
455
|
+
|
456
|
+
* `workflow_name` - Accepts a string or symbol to change the name. Otherwise the name is defaulted to the unqualified
|
457
|
+
class name.
|
458
|
+
* `workflow_dynamic` - Marks a workflow as dynamic. Dynamic workflows do not have names and handle any workflow that is
|
459
|
+
not otherwise registered. A worker can only have one dynamic workflow. It is often useful to use `workflow_raw_args`
|
460
|
+
with this.
|
461
|
+
* `workflow_raw_args` - Have workflow arguments delivered to `execute` (and `initialize` if `workflow_init` in use) as
|
462
|
+
`Temporalio::Converters::RawValue`s. These are wrappers for the raw payloads that have not been decoded. They can be
|
463
|
+
decoded with `Temporalio::Workflow.payload_converter`. Using this with `*args` splat can be helpful in dynamic
|
464
|
+
situations.
|
465
|
+
* `workflow_failure_exception_type` - Accepts one or more exception classes that will be considered workflow failure
|
466
|
+
instead of task failure. See the "Exceptions" section later on what this means. This can be called multiple times.
|
467
|
+
* `workflow_query_attr_reader` - Is a helper that accepts one or more symbols for attributes to expose as `attr_reader`
|
468
|
+
_and_ `workflow_query`. This means it is a superset of `attr_reader` and will not work if also using `attr_reader` or
|
469
|
+
`attr_accessor`. If a writer is needed alongside this, use `attr_writer`.
|
470
|
+
|
471
|
+
The following protected class methods can be called just before defining instance methods to customize the
|
472
|
+
definition/behavior of the method:
|
473
|
+
|
474
|
+
* `workflow_init` - Mark an `initialize` method as needing the workflow start arguments. Otherwise, `initialize` must
|
475
|
+
accept no required arguments. This must be placed above the `initialize` method or it will fail.
|
476
|
+
* `workflow_signal` - Mark the next method as a workflow signal. The signal name is defaulted to the method name but can
|
477
|
+
be customized by the `name` kwarg. See the API documentation for more kwargs that can be set. Return values for
|
478
|
+
signals are discarded and exceptions raised in signal handlers are treated as if they occurred in the primary workflow
|
479
|
+
method. This also defines a class method of the same name to return the definition for use by clients.
|
480
|
+
* `workflow_query` - Mark the next method as a workflow query. The query name is defaulted to the method name but can
|
481
|
+
be customized by the `name` kwarg. See the API documentation for more kwargs that can be set. The result of the method
|
482
|
+
is the result of the query. Queries must never have any side effects, meaning they should never mutate state or try to
|
483
|
+
wait on anything. This also defines a class method of the same name to return the definition for use by clients.
|
484
|
+
* `workflow_update` - Mark the next method as a workflow update. The update name is defaulted to the method name but can
|
485
|
+
be customized by the `name` kwarg. See the API documentation for more kwargs that can be set. The result of the method
|
486
|
+
is the result of the update. This also defines a class method of the same name to return the definition for use by
|
487
|
+
clients.
|
488
|
+
* `workflow_update_validator` - Mark the next method as a validator to an update. This accepts a symbol for the
|
489
|
+
`workflow_update` method it validates. Validators are used to do early rejection of updates and must never have any
|
490
|
+
side effects, meaning they should never mutate state or try to wait on anything.
|
491
|
+
|
492
|
+
Workflows can be inherited, but subclass workflow-level decorators override superclass ones, and the same method can't
|
493
|
+
be decorated with different handler types/names in the hierarchy.
|
494
|
+
|
495
|
+
#### Running Workflows
|
496
|
+
|
497
|
+
To start a workflow from a client, you can `start_workflow` and use the resulting handle:
|
498
|
+
|
499
|
+
```ruby
|
500
|
+
# Start the workflow
|
501
|
+
handle = my_client.start_workflow(
|
502
|
+
GreetingWorkflow,
|
503
|
+
{ salutation: 'Hello', name: 'Temporal' },
|
504
|
+
id: 'my-workflow-id',
|
505
|
+
task_queue: 'my-task-queue'
|
506
|
+
)
|
507
|
+
|
508
|
+
# Check current greeting via query
|
509
|
+
puts "Current greeting: #{handle.query(GreetingWorkflow.current_greeting)}"
|
510
|
+
|
511
|
+
# Change the params via update
|
512
|
+
handle.execute_update(
|
513
|
+
GreetingWorkflow.update_greeting_params,
|
514
|
+
{ salutation: 'Aloha', name: 'John' }
|
515
|
+
)
|
516
|
+
|
517
|
+
# Tell it to complete via signal
|
518
|
+
handle.signal(GreetingWorkflow.complete_with_greeting)
|
519
|
+
|
520
|
+
# Wait for workflow result
|
521
|
+
puts "Final greeting: #{handle.result}"
|
522
|
+
```
|
523
|
+
|
524
|
+
Some things to note about the above code:
|
525
|
+
|
526
|
+
* This uses the `GreetingWorkflow` workflow from the previous section.
|
527
|
+
* The output of this code is "Current greeting: Hello, Temporal!" and "Final greeting: Aloha, John!".
|
528
|
+
* ID and task queue are required for starting a workflow.
|
529
|
+
* Signal, query, and update calls here use the class methods created on the definition for safety. So if the
|
530
|
+
`update_greeting_params` method didn't exist or wasn't marked as an update, the code will fail client side before even
|
531
|
+
attempting the call. Static typing tooling may also take advantage of this for param/result type checking.
|
532
|
+
* A helper `execute_workflow` method is available on the client that is just `start_workflow` + handle `result`.
|
533
|
+
|
534
|
+
#### Invoking Activities
|
535
|
+
|
536
|
+
* Activities are executed with `Temporalio::Workflow.execute_activity`, which accepts the activity class or a
|
537
|
+
string/symbol activity name.
|
538
|
+
* Activity options are kwargs on the `execute_activity` method. Either `schedule_to_close_timeout` or
|
539
|
+
`start_to_close_timeout` must be set.
|
540
|
+
* Other options like `retry_policy`, `cancellation_type`, etc can also be set.
|
541
|
+
* The `cancellation` can be set to a `Cancellation` to send a cancel request to the activity. By default, the
|
542
|
+
`cancellation` is the overall `Temporalio::Workflow.cancellation` which is the overarching workflow cancellation.
|
543
|
+
* Activity failures are raised from the call as `Temporalio::Error::ActivityError`.
|
544
|
+
* `execute_local_activity` exists with mostly the same options for local activities.
|
545
|
+
|
546
|
+
#### Invoking Child Workflows
|
547
|
+
|
548
|
+
* Child workflows are started with `Temporalio::Workflow.start_child_workflow`, which accepts the workflow class or
|
549
|
+
string/symbol name, arguments, and other options.
|
550
|
+
* Result for `start_child_workflow` is a `Temporalio::Workflow::ChildWorkflowHandle` which has the `id`, the ability to
|
551
|
+
wait on the `result`, and the ability to `signal` the child.
|
552
|
+
* The `start_child_workflow` call does not complete until the start has been accepted by the server.
|
553
|
+
* A helper `execute_child_workflow` method is available that is just `start_child_workflow` + handle `result`.
|
554
|
+
|
555
|
+
#### Timers and Conditions
|
556
|
+
|
557
|
+
* A timer is represented by `Temporalio::Workflow.sleep`.
|
558
|
+
* Timers are also started on `Temporalio::Workflow.timeout`.
|
559
|
+
* _Technically_ `Kernel.sleep` and `Timeout.timeout` also delegate to the above calls, but the more explicit workflow
|
560
|
+
forms are encouraged because they accept more options and are not subject to Ruby standard library implementation
|
561
|
+
changes.
|
562
|
+
* Each timer accepts a `Cancellation`, but if none is given, it defaults to `Temporalio::Workflow.cancellation`.
|
563
|
+
* `Temporalio::Workflow.wait_condition` accepts a block that waits until the evaluated block result is truthy, then
|
564
|
+
returns the value.
|
565
|
+
* This function is invoked on each iteration of the internal event loop. This means it cannot have any side effects.
|
566
|
+
* This is commonly used for checking if a variable is changed from some other part of a workflow (e.g. a signal
|
567
|
+
handler).
|
568
|
+
* Each wait conditions accepts a `Cancellation`, but if none is given, it defaults to
|
569
|
+
`Temporalio::Workflow.cancellation`.
|
570
|
+
|
571
|
+
#### Workflow Fiber Scheduling and Cancellation
|
572
|
+
|
573
|
+
Workflows are backed by a custom, deterministic `Fiber::Scheduler`. All fiber calls inside a workflow use this scheduler
|
574
|
+
to ensure coroutines run deterministically.
|
575
|
+
|
576
|
+
Every workflow contains a `Temporalio::Cancellation` at `Temporalio::Workflow.cancellation`. This is canceled when the
|
577
|
+
workflow is canceled. For all workflow calls that accept a cancellation token, this is the default. So if a workflow is
|
578
|
+
waiting on `execute_activity` and the workflow is canceled, that cancellation will propagate to the waiting activity.
|
579
|
+
|
580
|
+
`Cancellation`s may be created to perform cancellation more specifically. A `Cancellation` token derived from the
|
581
|
+
workflow one can be created via `my_cancel, my_cancel_proc = Cancellation.new(Temporalio::Workflow.cancellation)`. Then
|
582
|
+
`my_cancel` can be passed as `cancellation` to cancel something more specifically when `my_cancel_proc.call` is invoked.
|
583
|
+
|
584
|
+
`Cancellation`s don't have to be derived from the workflow one, they can just be created standalone or "detached". This
|
585
|
+
is useful for executing, say, a cleanup activity in an `ensure` block that needs to run even on cancel. If the cleanup
|
586
|
+
activity had instead used the workflow cancellation or one derived from it, then on cancellation it would be cancelled
|
587
|
+
before it even started.
|
588
|
+
|
589
|
+
#### Workflow Futures
|
590
|
+
|
591
|
+
`Temporalio::Workflow::Future` can be used for running things in the background or concurrently. This is basically a
|
592
|
+
safe wrapper around `Fiber.schedule` for starting and `Workflow.wait_condition` for waiting.
|
593
|
+
|
594
|
+
Nothing uses futures by default, but they work with all workflow code/constructs. For instance, to run 3 activities and
|
595
|
+
wait for them all to complete, something like this can be written:
|
596
|
+
|
597
|
+
```ruby
|
598
|
+
# Start 3 activities in background
|
599
|
+
fut1 = Temporalio::Workflow::Future.new do
|
600
|
+
Temporalio::Workflow.execute_activity(MyActivity1, schedule_to_close_timeout: 300)
|
601
|
+
end
|
602
|
+
fut2 = Temporalio::Workflow::Future.new do
|
603
|
+
Temporalio::Workflow.execute_activity(MyActivity2, schedule_to_close_timeout: 300)
|
604
|
+
end
|
605
|
+
fut3 = Temporalio::Workflow::Future.new do
|
606
|
+
Temporalio::Workflow.execute_activity(MyActivity3, schedule_to_close_timeout: 300)
|
607
|
+
end
|
608
|
+
|
609
|
+
# Wait for them all to complete
|
610
|
+
Temporalio::Workflow::Future.all_of(fut1, fut2, fut3).wait
|
611
|
+
|
612
|
+
Temporalio::Workflow.logger.debug("Got: #{fut1.result}, #{fut2.result}, #{fut3.result}")
|
613
|
+
```
|
614
|
+
|
615
|
+
Or, say, to wait on the first of 5 activities or a timeout to complete:
|
616
|
+
|
617
|
+
```ruby
|
618
|
+
# Start 5 activities
|
619
|
+
act_futs = 5.times.map do |i|
|
620
|
+
Temporalio::Workflow::Future.new do
|
621
|
+
Temporalio::Workflow.execute_activity(MyActivity, "my-arg-#{i}", schedule_to_close_timeout: 300)
|
622
|
+
end
|
623
|
+
end
|
624
|
+
# Start a timer
|
625
|
+
sleep_fut = Temporalio::Workflow::Future.new { Temporalio::Workflow.sleep(30) }
|
626
|
+
|
627
|
+
# Wait for first act result or sleep fut
|
628
|
+
act_result = Temporalio::Workflow::Future.any_of(sleep_fut, *act_futs).wait
|
629
|
+
# Fail if timer done first
|
630
|
+
raise Temporalio::Error::ApplicationError, 'Timer expired' if sleep_fut.done?
|
631
|
+
# Print act result otherwise
|
632
|
+
puts "Act result: #{act_result}"
|
633
|
+
```
|
634
|
+
|
635
|
+
There are several other details not covered here about futures, such as how exceptions are handled, how to use a setter
|
636
|
+
proc instead of a block, etc. See the API documentation for details.
|
637
|
+
|
638
|
+
#### Workflow Utilities
|
639
|
+
|
640
|
+
In addition to the pieces documented above, additional methods are available on `Temporalio::Workflow` that can be used
|
641
|
+
from workflows including:
|
642
|
+
|
643
|
+
* `in_workflow?` - Returns `true` if in the workflow or `false` otherwise. This is the only method on the class that can
|
644
|
+
be called outside of a workflow without raising an exception.
|
645
|
+
* `info` - Immutable workflow information.
|
646
|
+
* `logger` - A Ruby logger that adds contextual information and takes care not to log on replay.
|
647
|
+
* `metric_meter` - A metric meter for making custom metrics that adds contextual information and takes care not to
|
648
|
+
record on replay.
|
649
|
+
* `random` - A deterministic `Random` instance.
|
650
|
+
* `memo` - A read-only hash of the memo (updated via `upsert_memo`).
|
651
|
+
* `search_attributes` - A read-only `SearchAttributes` collection (updated via `upsert_search_attributes`).
|
652
|
+
* `now` - Current, deterministic UTC time for the workflow.
|
653
|
+
* `all_handlers_finished?` - Returns true when all signal and update handlers are done. Useful as
|
654
|
+
`Temporalio::Workflow.wait_condition { Temporalio::Workflow.all_handlers_finished? }` for making sure not to return
|
655
|
+
from the primary workflow method until all handlers are done.
|
656
|
+
* `patched` and `deprecate_patch` - Support for patch-based versioning inside the workflow.
|
657
|
+
* `continue_as_new_suggested` - Returns true when the server recommends performing a continue as new.
|
658
|
+
* `current_update_info` - Returns `Temporalio::Workflow::UpdateInfo` if the current code is inside an update, or nil
|
659
|
+
otherwise.
|
660
|
+
* `external_workflow_handle` - Obtain an handle to an external workflow for signalling or cancelling.
|
661
|
+
* `payload_converter` - Payload converter if needed for converting raw args.
|
662
|
+
* `signal_handlers`, `query_handlers`, and `update_handlers` - Hashes for the current set of handlers keyed by name (or
|
663
|
+
nil key for dynamic). `[]=` or `store` can be called on these to update the handlers, though defined handlers are
|
664
|
+
encouraged over runtime-set ones.
|
665
|
+
|
666
|
+
`Temporalio::Workflow::ContinueAsNewError` can be raised to continue-as-new the workflow. It accepts positional args and
|
667
|
+
defaults the workflow to the same as the current, though it can be changed with the `workflow` kwarg. See API
|
668
|
+
documentation for other details.
|
669
|
+
|
670
|
+
#### Workflow Exceptions
|
671
|
+
|
672
|
+
* Workflows can raise exceptions to fail the workflow/update or the "workflow task" (i.e. suspend the workflow, retrying
|
673
|
+
until code update allows it to continue).
|
674
|
+
* By default, exceptions that are instances of `Temporalio::Error::Failure` (or `Timeout::Error`) will fail the
|
675
|
+
workflow/update with that exception.
|
676
|
+
* For failing the workflow/update explicitly with a user exception, explicitly raise
|
677
|
+
`Temporalio::Error::ApplicationError`. This can be marked non-retryable or include details as needed.
|
678
|
+
* Other exceptions that come from activity execution, child execution, cancellation, etc are already instances of
|
679
|
+
`Temporalio::Error::Failure` and will fail the workflow/update if uncaught.
|
680
|
+
* By default, all other exceptions fail the "workflow task" which means the workflow/update will continually retry until
|
681
|
+
the code is fixed. This is helpful for bad code or other non-predictable exceptions. To actually fail the
|
682
|
+
workflow/update, use `Temporalio::Error::ApplicationError` as mentioned above.
|
683
|
+
* By default, all non-deterministic exceptions that are detected internally fail the "workflow task".
|
684
|
+
|
685
|
+
The default behavior can be customized at the worker level for all workflows via the
|
686
|
+
`workflow_failure_exception_types` worker option or per workflow via the `workflow_failure_exception_type` definition
|
687
|
+
method on the workflow itself. When a workflow encounters a "workflow task" fail (i.e. suspend), it will first check
|
688
|
+
either of these collections to see if the exception is an instance of any of the types and if so, will turn into a
|
689
|
+
workflow/update failure. As a special case, when a non-deterministic exception occurs and
|
690
|
+
`Temporalio::Workflow::NondeterminismError` is assignable to any of the types in the collection, that too
|
691
|
+
will turn into a workflow/update failure. However unlike other exceptions, non-deterministic exceptions that match
|
692
|
+
during update handlers become workflow failures not update failures because a non-deterministic exception is an
|
693
|
+
entire-workflow-failure situation.
|
694
|
+
|
695
|
+
#### Workflow Logic Constraints
|
696
|
+
|
697
|
+
Temporal Workflows [must be deterministic](https://docs.temporal.io/workflows#deterministic-constraints), which includes
|
698
|
+
Ruby workflows. This means there are several things workflows cannot do such as:
|
699
|
+
|
700
|
+
* Perform IO (network, disk, stdio, etc)
|
701
|
+
* Access/alter external mutable state
|
702
|
+
* Do any threading
|
703
|
+
* Do anything using the system clock (e.g. `Time.Now`)
|
704
|
+
* Make any random calls
|
705
|
+
* Make any not-guaranteed-deterministic calls
|
706
|
+
|
707
|
+
#### Workflow Testing
|
708
|
+
|
709
|
+
Workflow testing can be done in an integration-test fashion against a real server. However, it is hard to simulate
|
710
|
+
timeouts and other long time-based code. Using the time-skipping workflow test environment can help there.
|
711
|
+
|
712
|
+
A non-time-skipping `Temporalio::Testing::WorkflowEnvironment` can be started via `start_local` which supports all
|
713
|
+
standard Temporal features. It is actually a real Temporal server lazily downloaded on first use and run as a
|
714
|
+
subprocess in the background.
|
715
|
+
|
716
|
+
A time-skipping `Temporalio::Testing::WorkflowEnvironment` can be started via `start_time_skipping` which is a
|
717
|
+
reimplementation of the Temporal server with special time skipping capabilities. This too lazily downloads the process
|
718
|
+
to run when first called. Note, this class is not thread safe nor safe for use with independent tests. It can be reused,
|
719
|
+
but only for one test at a time because time skipping is locked/unlocked at the environment level. Note, the
|
720
|
+
time-skipping test server does not work on ARM-based processors at this time, though macOS ARM users can use it via the
|
721
|
+
built-in x64 translation in macOS.
|
722
|
+
|
723
|
+
##### Automatic Time Skipping
|
724
|
+
|
725
|
+
Anytime a workflow result is waited on, the time-skipping server automatically advances to the next event it can. To
|
726
|
+
manually advance time before waiting on the result of the workflow, the `WorkflowEnvironment.sleep` method can be used
|
727
|
+
on the environment itself. If an activity is running, time-skipping is disabled.
|
728
|
+
|
729
|
+
Here's a simple example of a workflow that sleeps for 24 hours:
|
730
|
+
|
731
|
+
```ruby
|
732
|
+
require 'temporalio/workflow'
|
733
|
+
|
734
|
+
class WaitADayWorkflow < Temporalio::Workflow::Definition
|
735
|
+
def execute
|
736
|
+
Temporalio::Workflow.sleep(1 * 24 * 60 * 60)
|
737
|
+
'all done'
|
738
|
+
end
|
739
|
+
end
|
740
|
+
```
|
741
|
+
|
742
|
+
A regular integration test of this workflow on a normal server would be way too slow. However, the time-skipping server
|
743
|
+
automatically skips to the next event when we wait on the result. Here's a minitest for that workflow:
|
744
|
+
|
745
|
+
```ruby
|
746
|
+
class MyTest < Minitest::Test
|
747
|
+
def test_wait_a_day
|
748
|
+
Temporalio::Testing::WorkflowEnvironment.start_time_skipping do |env|
|
749
|
+
worker = Temporalio::Worker.new(
|
750
|
+
client: env.client,
|
751
|
+
task_queue: "tq-#{SecureRandom.uuid}",
|
752
|
+
workflows: [WaitADayWorkflow],
|
753
|
+
workflow_executor: Temporalio::Worker::WorkflowExecutor::ThreadPool.default
|
754
|
+
)
|
755
|
+
worker.run do
|
756
|
+
result = env.client.execute_workflow(
|
757
|
+
WaitADayWorkflow,
|
758
|
+
id: "wf-#{SecureRandom.uuid}",
|
759
|
+
task_queue: worker.task_queue
|
760
|
+
)
|
761
|
+
assert_equal 'all done', result
|
762
|
+
end
|
763
|
+
end
|
764
|
+
end
|
765
|
+
end
|
766
|
+
```
|
767
|
+
|
768
|
+
This test will run almost instantly. This is because by calling `execute_workflow` on our client, we are actually
|
769
|
+
calling `start_workflow` + handle `result`, and `result` automatically skips time as much as it can (basically until the
|
770
|
+
end of the workflow or until an activity is run).
|
771
|
+
|
772
|
+
To disable automatic time-skipping while waiting for a workflow result, run code inside a block passed to
|
773
|
+
`auto_time_skipping_disabled`.
|
774
|
+
|
775
|
+
##### Manual Time Skipping
|
776
|
+
|
777
|
+
Until a workflow is waited on, all time skipping in the time-skipping environment is done manually via
|
778
|
+
`WorkflowEnvironment.sleep`.
|
779
|
+
|
780
|
+
Here's a workflow that waits for a signal or times out:
|
781
|
+
|
782
|
+
```ruby
|
783
|
+
require 'temporalio/workflow'
|
784
|
+
|
785
|
+
class SignalWorkflow < Temporalio::Workflow::Definition
|
786
|
+
def execute
|
787
|
+
Temporalio::Workflow.timeout(45) do
|
788
|
+
Temporalio::Workflow.wait_condition { @signal_received }
|
789
|
+
'got signal'
|
790
|
+
rescue Timeout::Error
|
791
|
+
'got timeout'
|
792
|
+
end
|
793
|
+
end
|
794
|
+
|
795
|
+
workflow_signal
|
796
|
+
def some_signal
|
797
|
+
@signal_received = true
|
798
|
+
end
|
799
|
+
end
|
800
|
+
```
|
801
|
+
|
802
|
+
To test a normal signal, you might:
|
803
|
+
|
804
|
+
```ruby
|
805
|
+
class MyTest < Minitest::Test
|
806
|
+
def test_signal_workflow_success
|
807
|
+
Temporalio::Testing::WorkflowEnvironment.start_time_skipping do |env|
|
808
|
+
worker = Temporalio::Worker.new(
|
809
|
+
client: env.client,
|
810
|
+
task_queue: "tq-#{SecureRandom.uuid}",
|
811
|
+
workflows: [SignalWorkflow],
|
812
|
+
workflow_executor: Temporalio::Worker::WorkflowExecutor::ThreadPool.default
|
813
|
+
)
|
814
|
+
worker.run do
|
815
|
+
handle = env.client.start_workflow(
|
816
|
+
SignalWorkflow,
|
817
|
+
id: "wf-#{SecureRandom.uuid}",
|
818
|
+
task_queue: worker.task_queue
|
819
|
+
)
|
820
|
+
handle.signal(SignalWorkflow.some_signal)
|
821
|
+
assert_equal 'got signal', handle.result
|
822
|
+
end
|
823
|
+
end
|
824
|
+
end
|
825
|
+
end
|
826
|
+
```
|
827
|
+
|
828
|
+
But how would you test the timeout part? Like so:
|
829
|
+
|
830
|
+
```ruby
|
831
|
+
class MyTest < Minitest::Test
|
832
|
+
def test_signal_workflow_timeout
|
833
|
+
Temporalio::Testing::WorkflowEnvironment.start_time_skipping do |env|
|
834
|
+
worker = Temporalio::Worker.new(
|
835
|
+
client: env.client,
|
836
|
+
task_queue: "tq-#{SecureRandom.uuid}",
|
837
|
+
workflows: [SignalWorkflow],
|
838
|
+
workflow_executor: Temporalio::Worker::WorkflowExecutor::ThreadPool.default
|
839
|
+
)
|
840
|
+
worker.run do
|
841
|
+
handle = env.client.start_workflow(
|
842
|
+
SignalWorkflow,
|
843
|
+
id: "wf-#{SecureRandom.uuid}",
|
844
|
+
task_queue: worker.task_queue
|
845
|
+
)
|
846
|
+
env.sleep(50)
|
847
|
+
assert_equal 'got timeout', handle.result
|
848
|
+
end
|
849
|
+
end
|
850
|
+
end
|
851
|
+
end
|
852
|
+
```
|
853
|
+
|
854
|
+
This test will run almost instantly. The `env.sleep(50)` manually skips 50 seconds of time, allowing the timeout to be
|
855
|
+
triggered without actually waiting the full 45 seconds to time out.
|
856
|
+
|
857
|
+
##### Mocking Activities
|
858
|
+
|
859
|
+
When testing workflows, often you don't want to actually run the activities. Activities are just classes that extend
|
860
|
+
`Temporalio::Activity::Definition`. Simply write different/empty/fake/asserting ones and pass those to the worker to
|
861
|
+
have different activities called during the test. You may need to use `activity_name :MyRealActivityClassName` inside
|
862
|
+
the mock activity class to make it appear as the real name.
|
863
|
+
|
864
|
+
#### Workflow Replay
|
865
|
+
|
866
|
+
Given a workflow's history, it can be replayed locally to check for things like non-determinism errors. For example,
|
867
|
+
assuming the `history_json` parameter below is given a JSON string of history exported from the CLI or web UI, the
|
868
|
+
following function will replay it:
|
869
|
+
|
870
|
+
```ruby
|
871
|
+
def replay_from_json(history_json)
|
872
|
+
replayer = Temporalio::Worker::WorkflowReplayer.new(workflows: [MyWorkflow])
|
873
|
+
replayer.replay_workflow(Temporalio::WorkflowHistory.from_history_json(history_json))
|
874
|
+
end
|
875
|
+
```
|
876
|
+
|
877
|
+
If there is a non-determinism, this will raise an exception by default.
|
878
|
+
|
879
|
+
Workflow history can be loaded from more than just JSON. It can be fetched individually from a workflow handle, or even
|
880
|
+
in a list. For example, the following code will check that all workflow histories for a certain workflow type (i.e.
|
881
|
+
workflow class) are safe with the current workflow code.
|
882
|
+
|
883
|
+
```ruby
|
884
|
+
def check_past_histories(client)
|
885
|
+
replayer = Temporalio::Worker::WorkflowReplayer.new(workflows: [MyWorkflow])
|
886
|
+
results = replayer.replay_workflows(client.list_workflows("WorkflowType = 'MyWorkflow'").map do |desc|
|
887
|
+
client.workflow_handle(desc.id, run_id: desc.run_id).fetch_history
|
888
|
+
end)
|
889
|
+
results.each { |res| raise res.replay_failure if res.replay_failure }
|
890
|
+
end
|
891
|
+
```
|
892
|
+
|
893
|
+
But this only raises at the end because by default `replay_workflows` does not raise on failure like `replay_workflow`
|
894
|
+
does. The `raise_on_replay_failure: true` parameter could be set, or the replay worker can be used to process each one
|
895
|
+
like so:
|
896
|
+
|
897
|
+
```ruby
|
898
|
+
def check_past_histories(client)
|
899
|
+
Temporalio::Worker::WorkflowReplayer.new(workflows: [MyWorkflow]) do |worker|
|
900
|
+
client.list_workflows("WorkflowType = 'MyWorkflow'").each do |desc|
|
901
|
+
worker.replay_workflow(client.workflow_handle(desc.id, run_id: desc.run_id).fetch_history)
|
902
|
+
end
|
903
|
+
end
|
904
|
+
end
|
905
|
+
```
|
906
|
+
|
907
|
+
See the `WorkflowReplayer` API documentation for more details.
|
327
908
|
|
328
909
|
### Activities
|
329
910
|
|
@@ -334,16 +915,16 @@ Activities can be defined in a few different ways. They are usually classes, but
|
|
334
915
|
Here is a common activity definition:
|
335
916
|
|
336
917
|
```ruby
|
337
|
-
class FindUserActivity < Temporalio::Activity
|
918
|
+
class FindUserActivity < Temporalio::Activity::Definition
|
338
919
|
def execute(user_id)
|
339
920
|
User.find(user_id)
|
340
921
|
end
|
341
922
|
end
|
342
923
|
```
|
343
924
|
|
344
|
-
Activities are defined as classes that extend `Temporalio::Activity` and provide an `execute` method. When
|
345
|
-
is provided to the worker as a _class_ (e.g. `activities: [FindUserActivity]`), it will be instantiated
|
346
|
-
_every attempt_. Many users may prefer using the same instance across activities, for example:
|
925
|
+
Activities are defined as classes that extend `Temporalio::Activity::Definition` and provide an `execute` method. When
|
926
|
+
this activity is provided to the worker as a _class_ (e.g. `activities: [FindUserActivity]`), it will be instantiated
|
927
|
+
for _every attempt_. Many users may prefer using the same instance across activities, for example:
|
347
928
|
|
348
929
|
```ruby
|
349
930
|
class FindUserActivity < Temporalio::Activity
|
@@ -367,8 +948,13 @@ Some notes about activity definition:
|
|
367
948
|
* Long running activities should heartbeat regularly, see "Activity Heartbeating and Cancellation" later.
|
368
949
|
* By default every activity attempt is executed in a thread on a thread pool, but fibers are also supported. See
|
369
950
|
"Activity Concurrency and Executors" section later for more details.
|
370
|
-
* Technically an activity definition can be created manually via `Temporalio::Activity::Definition.new` that
|
371
|
-
proc or a block, but the class form is recommended.
|
951
|
+
* Technically an activity definition can be created manually via `Temporalio::Activity::Definition::Info.new` that
|
952
|
+
accepts a proc or a block, but the class form is recommended.
|
953
|
+
* `activity_dynamic` can be used to mark an activity dynamic. Dynamic activities do not have names and handle any
|
954
|
+
activity that is not otherwise registered. A worker can only have one dynamic activity.
|
955
|
+
* `workflow_raw_args` can be used to have activity arguments delivered to `execute` as
|
956
|
+
`Temporalio::Converters::RawValue`s. These are wrappers for the raw payloads that have not been converted to types
|
957
|
+
(but they have been decoded by the codec if present). They can be converted with `payload_converter` on the context.
|
372
958
|
|
373
959
|
#### Activity Context
|
374
960
|
|
@@ -452,27 +1038,156 @@ it will raise the error raised in the activity.
|
|
452
1038
|
The constructor of the environment has multiple keyword arguments that can be set to affect the activity context for the
|
453
1039
|
activity.
|
454
1040
|
|
1041
|
+
### Telemetry
|
1042
|
+
|
1043
|
+
#### Metrics
|
1044
|
+
|
1045
|
+
Metrics can be configured on a `Temporalio::Runtime`. Only one runtime is expected to be created for the entire
|
1046
|
+
application and it should be created before any clients are created. For example, this configures Prometheus to export
|
1047
|
+
metrics at `http://127.0.0.1:9000/metrics`:
|
1048
|
+
|
1049
|
+
```ruby
|
1050
|
+
require 'temporalio/runtime'
|
1051
|
+
|
1052
|
+
Temporalio::Runtime.default = Temporalio::Runtime.new(
|
1053
|
+
telemetry: Temporalio::Runtime::TelemetryOptions.new(
|
1054
|
+
metrics: Temporalio::Runtime::MetricsOptions.new(
|
1055
|
+
prometheus: Temporalio::Runtime::PrometheusMetricsOptions.new(
|
1056
|
+
bind_address: '127.0.0.1:9000'
|
1057
|
+
)
|
1058
|
+
)
|
1059
|
+
)
|
1060
|
+
)
|
1061
|
+
```
|
1062
|
+
|
1063
|
+
Now every client created will use this runtime. Setting the default will fail if a runtime has already been requested or
|
1064
|
+
a default already set. Technically a runtime can be created without setting the default and be set on each client via
|
1065
|
+
the `runtime` parameter, but this is discouraged because a runtime represents a heavy internal engine not meant to be
|
1066
|
+
created multiple times.
|
1067
|
+
|
1068
|
+
OpenTelemetry metrics can be configured instead by passing `Temporalio::Runtime::OpenTelemetryMetricsOptions` as the
|
1069
|
+
`opentelemetry` parameter to the metrics options. See API documentation for details.
|
1070
|
+
|
1071
|
+
#### OpenTelemetry Tracing
|
1072
|
+
|
1073
|
+
OpenTelemetry tracing for clients, activities, and workflows can be enabled using the
|
1074
|
+
`Temporalio::Contrib::OpenTelemetry::TracingInterceptor`. Specifically, when creating a client, set the interceptor like
|
1075
|
+
so:
|
1076
|
+
|
1077
|
+
```ruby
|
1078
|
+
require 'opentelemetry/api'
|
1079
|
+
require 'opentelemetry/sdk'
|
1080
|
+
require 'temporalio/client'
|
1081
|
+
require 'temporalio/contrib/open_telemetry'
|
1082
|
+
|
1083
|
+
# ... assumes my_otel_tracer_provider is a tracer provider created by the user
|
1084
|
+
my_tracer = my_otel_tracer_provider.tracer('my-otel-tracer')
|
1085
|
+
|
1086
|
+
my_client = Temporalio::Client.connect(
|
1087
|
+
'localhost:7233', 'my-namespace',
|
1088
|
+
interceptors: [Temporalio::Contrib::OpenTelemetry::TracingInterceptor.new(my_tracer)]
|
1089
|
+
)
|
1090
|
+
```
|
1091
|
+
|
1092
|
+
Now many high-level client calls and activities/workflows on workers using this client will have spans created on that
|
1093
|
+
OpenTelemetry tracer.
|
1094
|
+
|
1095
|
+
##### OpenTelemetry Tracing in Workflows
|
1096
|
+
|
1097
|
+
OpenTelemetry works by creating spans as necessary and in some cases serializing them to Temporal headers to be
|
1098
|
+
deserialized by workflows/activities to be set on the context. However, OpenTelemetry requires spans to be finished
|
1099
|
+
where they start, so spans cannot be resumed. This is fine for client calls and activity attempts, but Temporal
|
1100
|
+
workflows are resumable functions that may start on a different machine than they complete. Due to this, spans created
|
1101
|
+
by workflows are immediately closed since there is no way for the span to actually span machines. They are also not
|
1102
|
+
created during replay. The spans still become the proper parents of other spans if they are created.
|
1103
|
+
|
1104
|
+
Custom spans can be created inside of workflows using class methods on the
|
1105
|
+
`Temporalio::Contrib::OpenTelemetry::Workflow` module. For example:
|
1106
|
+
|
1107
|
+
```ruby
|
1108
|
+
class MyWorkflow < Temporalio::Workflow::Definition
|
1109
|
+
def execute
|
1110
|
+
# Sleep for a bit
|
1111
|
+
Temporalio::Workflow.sleep(10)
|
1112
|
+
# Run activity in span
|
1113
|
+
Temporalio::Contrib::OpenTelemetry::Workflow.with_completed_span(
|
1114
|
+
'my-span',
|
1115
|
+
attributes: { 'my-attr' => 'some val' }
|
1116
|
+
) do
|
1117
|
+
# Execute an activity
|
1118
|
+
Temporalio::Workflow.execute_activity(MyActivity, start_to_close_timeout: 10)
|
1119
|
+
end
|
1120
|
+
end
|
1121
|
+
end
|
1122
|
+
```
|
1123
|
+
|
1124
|
+
If this all executes on one worker (because Temporal has a concept of stickiness that caches instances), the span tree
|
1125
|
+
may look like:
|
1126
|
+
|
1127
|
+
```
|
1128
|
+
StartWorkflow:MyWorkflow <-- created by client outbound
|
1129
|
+
RunWorkflow:MyWorkflow <-- created inside workflow on first task
|
1130
|
+
my-span <-- created inside workflow by code
|
1131
|
+
StartActivity:MyActivity <-- created inside workflow when first called
|
1132
|
+
RunActivity:MyActivity <-- created inside activity attempt 1
|
1133
|
+
CompleteWorkflow:MyWorkflow <-- created inside workflow on last task
|
1134
|
+
```
|
1135
|
+
|
1136
|
+
However if, say, the worker crashed during the 10s sleep and the workflow was resumed (i.e. replayed) on another worker,
|
1137
|
+
the span tree may look like:
|
1138
|
+
|
1139
|
+
```
|
1140
|
+
StartWorkflow:MyWorkflow <-- created by client outbound
|
1141
|
+
RunWorkflow:MyWorkflow <-- created by workflow inbound on first task
|
1142
|
+
my-span <-- created inside the workflow
|
1143
|
+
StartActivity:MyActivity <-- created by workflow outbound
|
1144
|
+
RunActivity:MyActivity <-- created by activity attempt 1 inbound
|
1145
|
+
CompleteWorkflow:MyWorkflow <-- created by workflow inbound on last task
|
1146
|
+
```
|
1147
|
+
|
1148
|
+
Notice how the spans are no longer under `RunWorkflow`. This is because spans inside the workflow are not created on
|
1149
|
+
replay, so there is no parent on replay. But there are no orphans because we still have the overarching parent of
|
1150
|
+
`StartWorkflow` that was created by the client and is serialized into Temporal headers so it can always be the parent.
|
1151
|
+
|
1152
|
+
And reminder that `StartWorkflow` and `RunActivity` spans do last the length of their calls (so time to start the
|
1153
|
+
workflow and time to run the activity attempt respectively), but the other spans have no measurable time because they
|
1154
|
+
are created in workflows and closed immediately since long-lived spans cannot work for durable software that may resume
|
1155
|
+
on other machines.
|
1156
|
+
|
1157
|
+
### Ractors
|
1158
|
+
|
1159
|
+
It was an original goal to have workflows actually be Ractors for deterministic state isolation and have the library
|
1160
|
+
support Ractors in general. However, due to the SDK's heavy use of the Google Protobuf library which
|
1161
|
+
[is not Ractor-safe](https://github.com/protocolbuffers/protobuf/issues/19321), the Temporal Ruby SDK does not currently
|
1162
|
+
work with Ractors.
|
1163
|
+
|
455
1164
|
### Platform Support
|
456
1165
|
|
457
1166
|
This SDK is backed by a Ruby C extension written in Rust leveraging the
|
458
1167
|
[Temporal Rust Core](https://github.com/temporalio/sdk-core). Gems are currently published for the following platforms:
|
459
1168
|
|
460
1169
|
* `aarch64-linux`
|
1170
|
+
* `aarch64-linux-musl`
|
461
1171
|
* `x86_64-linux`
|
1172
|
+
* `x86_64-linux-musl`
|
462
1173
|
* `arm64-darwin`
|
463
1174
|
* `x86_64-darwin`
|
464
1175
|
|
465
|
-
This means Linux and macOS for ARM and x64 have published gems.
|
466
|
-
`aarch64-linux-musl` so Alpine Linux users may need to build from scratch or use a libc-based distro.
|
1176
|
+
This means Linux and macOS for ARM and x64 have published gems.
|
467
1177
|
|
468
1178
|
Due to [an issue](https://github.com/temporalio/sdk-ruby/issues/172) with Windows and multi-threaded Rust, MinGW-based
|
469
1179
|
Windows (i.e. `x64-mingw-ucrt`) is not supported. But WSL is supported using the normal Linux gem.
|
470
1180
|
|
1181
|
+
Due to [an issue](https://github.com/protocolbuffers/protobuf/issues/16853) with Google Protobuf, latest Linux versions
|
1182
|
+
of Google Protobuf gems will not work in musl-based environments. Instead use the pure "ruby" platform which will build
|
1183
|
+
the Google Protobuf gem on install (e.g.
|
1184
|
+
`gem 'google-protobuf', force_ruby_platform: RUBY_PLATFORM.include?('linux-musl')` in the `Gemfile`).
|
1185
|
+
|
471
1186
|
At this time a pure source gem is published for documentation reasons, but it cannot be built and will fail if tried.
|
472
1187
|
Building from source requires many files across submodules and requires Rust to be installed. See the [Build](#build)
|
473
1188
|
section for how to build a the repository.
|
474
1189
|
|
475
|
-
The SDK works on Ruby 3.
|
1190
|
+
The SDK works on Ruby 3.2+, but due to [an issue](https://github.com/temporalio/sdk-ruby/issues/162), fibers (and
|
476
1191
|
`async` gem) are only supported on Ruby versions 3.3 and newer.
|
477
1192
|
|
478
1193
|
## Development
|
@@ -481,7 +1196,7 @@ The SDK works on Ruby 3.1+, but due to [an issue](https://github.com/temporalio/
|
|
481
1196
|
|
482
1197
|
Prerequisites:
|
483
1198
|
|
484
|
-
* [Ruby](https://www.ruby-lang.org/) >= 3.
|
1199
|
+
* [Ruby](https://www.ruby-lang.org/) >= 3.2 (i.e. `ruby` and `bundle` on the `PATH`)
|
485
1200
|
* [Rust](https://www.rust-lang.org/) latest stable (i.e. `cargo` on the `PATH`)
|
486
1201
|
* This repository, cloned recursively
|
487
1202
|
* Change to the `temporalio/` directory
|