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.
Files changed (141) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -0
  3. data/Cargo.lock +980 -583
  4. data/Cargo.toml +2 -2
  5. data/Gemfile +7 -3
  6. data/README.md +769 -54
  7. data/Rakefile +10 -296
  8. data/ext/Cargo.toml +2 -0
  9. data/lib/temporalio/activity/complete_async_error.rb +1 -1
  10. data/lib/temporalio/activity/context.rb +18 -2
  11. data/lib/temporalio/activity/definition.rb +180 -65
  12. data/lib/temporalio/activity/info.rb +25 -21
  13. data/lib/temporalio/activity.rb +2 -59
  14. data/lib/temporalio/api/activity/v1/message.rb +25 -0
  15. data/lib/temporalio/api/batch/v1/message.rb +6 -1
  16. data/lib/temporalio/api/cloud/account/v1/message.rb +28 -0
  17. data/lib/temporalio/api/cloud/cloudservice/v1/request_response.rb +34 -1
  18. data/lib/temporalio/api/cloud/cloudservice/v1/service.rb +1 -1
  19. data/lib/temporalio/api/cloud/identity/v1/message.rb +6 -1
  20. data/lib/temporalio/api/cloud/namespace/v1/message.rb +8 -1
  21. data/lib/temporalio/api/cloud/nexus/v1/message.rb +31 -0
  22. data/lib/temporalio/api/cloud/operation/v1/message.rb +2 -1
  23. data/lib/temporalio/api/cloud/region/v1/message.rb +2 -1
  24. data/lib/temporalio/api/cloud/resource/v1/message.rb +23 -0
  25. data/lib/temporalio/api/cloud/sink/v1/message.rb +24 -0
  26. data/lib/temporalio/api/cloud/usage/v1/message.rb +31 -0
  27. data/lib/temporalio/api/command/v1/message.rb +1 -1
  28. data/lib/temporalio/api/common/v1/message.rb +8 -1
  29. data/lib/temporalio/api/deployment/v1/message.rb +38 -0
  30. data/lib/temporalio/api/enums/v1/batch_operation.rb +1 -1
  31. data/lib/temporalio/api/enums/v1/common.rb +1 -1
  32. data/lib/temporalio/api/enums/v1/deployment.rb +23 -0
  33. data/lib/temporalio/api/enums/v1/event_type.rb +1 -1
  34. data/lib/temporalio/api/enums/v1/failed_cause.rb +1 -1
  35. data/lib/temporalio/api/enums/v1/nexus.rb +21 -0
  36. data/lib/temporalio/api/enums/v1/reset.rb +1 -1
  37. data/lib/temporalio/api/enums/v1/workflow.rb +2 -1
  38. data/lib/temporalio/api/errordetails/v1/message.rb +3 -1
  39. data/lib/temporalio/api/failure/v1/message.rb +3 -1
  40. data/lib/temporalio/api/history/v1/message.rb +3 -1
  41. data/lib/temporalio/api/nexus/v1/message.rb +3 -2
  42. data/lib/temporalio/api/operatorservice/v1/service.rb +1 -1
  43. data/lib/temporalio/api/payload_visitor.rb +1581 -0
  44. data/lib/temporalio/api/query/v1/message.rb +2 -1
  45. data/lib/temporalio/api/schedule/v1/message.rb +2 -1
  46. data/lib/temporalio/api/taskqueue/v1/message.rb +4 -1
  47. data/lib/temporalio/api/testservice/v1/request_response.rb +31 -0
  48. data/lib/temporalio/api/testservice/v1/service.rb +23 -0
  49. data/lib/temporalio/api/workflow/v1/message.rb +9 -1
  50. data/lib/temporalio/api/workflowservice/v1/request_response.rb +46 -2
  51. data/lib/temporalio/api/workflowservice/v1/service.rb +1 -1
  52. data/lib/temporalio/api.rb +2 -0
  53. data/lib/temporalio/cancellation.rb +34 -14
  54. data/lib/temporalio/client/async_activity_handle.rb +12 -37
  55. data/lib/temporalio/client/connection/cloud_service.rb +309 -231
  56. data/lib/temporalio/client/connection/operator_service.rb +36 -84
  57. data/lib/temporalio/client/connection/service.rb +6 -5
  58. data/lib/temporalio/client/connection/test_service.rb +111 -0
  59. data/lib/temporalio/client/connection/workflow_service.rb +474 -441
  60. data/lib/temporalio/client/connection.rb +90 -44
  61. data/lib/temporalio/client/interceptor.rb +199 -60
  62. data/lib/temporalio/client/schedule.rb +991 -0
  63. data/lib/temporalio/client/schedule_handle.rb +126 -0
  64. data/lib/temporalio/client/with_start_workflow_operation.rb +115 -0
  65. data/lib/temporalio/client/workflow_execution.rb +26 -10
  66. data/lib/temporalio/client/workflow_handle.rb +41 -98
  67. data/lib/temporalio/client/workflow_update_handle.rb +3 -5
  68. data/lib/temporalio/client.rb +247 -44
  69. data/lib/temporalio/common_enums.rb +17 -0
  70. data/lib/temporalio/contrib/open_telemetry.rb +470 -0
  71. data/lib/temporalio/converters/data_converter.rb +4 -7
  72. data/lib/temporalio/converters/failure_converter.rb +5 -3
  73. data/lib/temporalio/converters/payload_converter/composite.rb +4 -0
  74. data/lib/temporalio/converters/payload_converter.rb +6 -8
  75. data/lib/temporalio/converters/raw_value.rb +20 -0
  76. data/lib/temporalio/error/failure.rb +1 -1
  77. data/lib/temporalio/error.rb +11 -2
  78. data/lib/temporalio/internal/bridge/api/activity_task/activity_task.rb +1 -1
  79. data/lib/temporalio/internal/bridge/api/common/common.rb +2 -1
  80. data/lib/temporalio/internal/bridge/api/core_interface.rb +5 -1
  81. data/lib/temporalio/internal/bridge/api/nexus/nexus.rb +33 -0
  82. data/lib/temporalio/internal/bridge/api/workflow_activation/workflow_activation.rb +5 -1
  83. data/lib/temporalio/internal/bridge/api/workflow_commands/workflow_commands.rb +4 -1
  84. data/lib/temporalio/internal/bridge/api/workflow_completion/workflow_completion.rb +2 -1
  85. data/lib/temporalio/internal/bridge/client.rb +11 -6
  86. data/lib/temporalio/internal/bridge/runtime.rb +3 -0
  87. data/lib/temporalio/internal/bridge/testing.rb +23 -0
  88. data/lib/temporalio/internal/bridge/worker.rb +2 -0
  89. data/lib/temporalio/internal/bridge.rb +1 -1
  90. data/lib/temporalio/internal/client/implementation.rb +468 -71
  91. data/lib/temporalio/internal/metric.rb +122 -0
  92. data/lib/temporalio/internal/proto_utils.rb +118 -7
  93. data/lib/temporalio/internal/worker/activity_worker.rb +69 -29
  94. data/lib/temporalio/internal/worker/multi_runner.rb +53 -9
  95. data/lib/temporalio/internal/worker/workflow_instance/child_workflow_handle.rb +54 -0
  96. data/lib/temporalio/internal/worker/workflow_instance/context.rb +383 -0
  97. data/lib/temporalio/internal/worker/workflow_instance/details.rb +46 -0
  98. data/lib/temporalio/internal/worker/workflow_instance/external_workflow_handle.rb +32 -0
  99. data/lib/temporalio/internal/worker/workflow_instance/externally_immutable_hash.rb +22 -0
  100. data/lib/temporalio/internal/worker/workflow_instance/handler_execution.rb +25 -0
  101. data/lib/temporalio/internal/worker/workflow_instance/handler_hash.rb +41 -0
  102. data/lib/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rb +97 -0
  103. data/lib/temporalio/internal/worker/workflow_instance/inbound_implementation.rb +62 -0
  104. data/lib/temporalio/internal/worker/workflow_instance/outbound_implementation.rb +400 -0
  105. data/lib/temporalio/internal/worker/workflow_instance/replay_safe_logger.rb +37 -0
  106. data/lib/temporalio/internal/worker/workflow_instance/replay_safe_metric.rb +40 -0
  107. data/lib/temporalio/internal/worker/workflow_instance/scheduler.rb +183 -0
  108. data/lib/temporalio/internal/worker/workflow_instance.rb +774 -0
  109. data/lib/temporalio/internal/worker/workflow_worker.rb +239 -0
  110. data/lib/temporalio/metric.rb +109 -0
  111. data/lib/temporalio/retry_policy.rb +37 -14
  112. data/lib/temporalio/runtime/metric_buffer.rb +94 -0
  113. data/lib/temporalio/runtime.rb +160 -79
  114. data/lib/temporalio/search_attributes.rb +93 -37
  115. data/lib/temporalio/testing/activity_environment.rb +44 -16
  116. data/lib/temporalio/testing/workflow_environment.rb +276 -7
  117. data/lib/temporalio/version.rb +1 -1
  118. data/lib/temporalio/worker/activity_executor/thread_pool.rb +9 -217
  119. data/lib/temporalio/worker/activity_executor.rb +3 -3
  120. data/lib/temporalio/worker/interceptor.rb +343 -66
  121. data/lib/temporalio/worker/thread_pool.rb +237 -0
  122. data/lib/temporalio/worker/tuner.rb +38 -0
  123. data/lib/temporalio/worker/workflow_executor/thread_pool.rb +235 -0
  124. data/lib/temporalio/worker/workflow_executor.rb +26 -0
  125. data/lib/temporalio/worker/workflow_replayer.rb +350 -0
  126. data/lib/temporalio/worker.rb +235 -58
  127. data/lib/temporalio/workflow/activity_cancellation_type.rb +20 -0
  128. data/lib/temporalio/workflow/child_workflow_cancellation_type.rb +21 -0
  129. data/lib/temporalio/workflow/child_workflow_handle.rb +43 -0
  130. data/lib/temporalio/workflow/definition.rb +598 -0
  131. data/lib/temporalio/workflow/external_workflow_handle.rb +41 -0
  132. data/lib/temporalio/workflow/future.rb +151 -0
  133. data/lib/temporalio/workflow/handler_unfinished_policy.rb +13 -0
  134. data/lib/temporalio/workflow/info.rb +104 -0
  135. data/lib/temporalio/workflow/parent_close_policy.rb +19 -0
  136. data/lib/temporalio/workflow/update_info.rb +20 -0
  137. data/lib/temporalio/workflow.rb +575 -0
  138. data/lib/temporalio/workflow_history.rb +26 -1
  139. data/lib/temporalio.rb +4 -0
  140. data/temporalio.gemspec +4 -3
  141. metadata +73 -10
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
- ![Temporal Ruby SDK](https://raw.githubusercontent.com/temporalio/assets/main/files/w/ruby.png)
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
- ![Ruby 3.1 | 3.2 | 3.3](https://img.shields.io/badge/ruby-3.1%20%7C%203.2%20%7C%203.3-blue.svg?style=for-the-badge)
3
+ ![Ruby 3.2 | 3.3 | 3.4](https://img.shields.io/badge/ruby-3.2%20|%203.3%20|%203.4-blue.svg?style=for-the-badge)
4
4
  [![MIT](https://img.shields.io/github/license/temporalio/sdk-ruby.svg?style=for-the-badge)](LICENSE)
5
5
  [![Gem](https://img.shields.io/gem/v/temporalio?style=for-the-badge)](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://rubydoc.info/gems/temporalio)
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. The SDK has undergone a refresh from a previous unstable version. The last tag before
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
- * Workflow workers
27
-
28
- **NOTE: This README is for the current branch and not necessarily what's released on RubyGems.**
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 an Activity](#implementing-an-activity)
39
- - [Running a Workflow](#running-a-workflow)
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 and Linux MUSL do not have gems. See the [Platform Support](#platform-support) section for more
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 an Activity
111
+ ### Implementing a Workflow and Activity
89
112
 
90
- Implementing workflows is not yet supported in the Ruby SDK, but implementing activities is.
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 activities
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
- # There are various forms an activity can take, see specific section for details.
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 specific
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
- ### Running a Workflow
174
+ ### Executing a Workflow
127
175
 
128
- Assuming that `SayHelloWorkflow` just calls this activity, it can be run like so:
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
- 'SayHelloWorkflow',
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
- 'SayHelloWorkflow',
167
- 'Temporal',
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
- * [`JSON` module](https://docs.ruby-lang.org/en/master/JSON.html) for everything else
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. While
222
- "JSON Additions" are supported, it is not cross-SDK-language compatible since this is a Ruby-specific construct.
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. Workflows cannot yet be written in Ruby, but activities can. Here's how to run
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 activities
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
- # There are various forms an activity can take, see specific section for details.
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 an `shutdown_signals` array which will trap the signal and start shutdown when
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. data converters and interceptors).
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
- ⚠️ Workflows cannot yet be implemented Ruby.
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 this activity
345
- is provided to the worker as a _class_ (e.g. `activities: [FindUserActivity]`), it will be instantiated for
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 accepts a
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. Currently, a gem is not published for
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.1+, but due to [an issue](https://github.com/temporalio/sdk-ruby/issues/162), fibers (and
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.1 (i.e. `ruby` and `bundle` on the `PATH`)
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