temporalio 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -0
  3. data/Cargo.lock +659 -370
  4. data/Cargo.toml +2 -2
  5. data/Gemfile +3 -3
  6. data/README.md +589 -47
  7. data/Rakefile +10 -296
  8. data/ext/Cargo.toml +1 -0
  9. data/lib/temporalio/activity/complete_async_error.rb +1 -1
  10. data/lib/temporalio/activity/context.rb +5 -2
  11. data/lib/temporalio/activity/definition.rb +163 -65
  12. data/lib/temporalio/activity/info.rb +22 -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/cloud/account/v1/message.rb +28 -0
  16. data/lib/temporalio/api/cloud/cloudservice/v1/request_response.rb +34 -1
  17. data/lib/temporalio/api/cloud/cloudservice/v1/service.rb +1 -1
  18. data/lib/temporalio/api/cloud/identity/v1/message.rb +6 -1
  19. data/lib/temporalio/api/cloud/namespace/v1/message.rb +8 -1
  20. data/lib/temporalio/api/cloud/nexus/v1/message.rb +31 -0
  21. data/lib/temporalio/api/cloud/operation/v1/message.rb +2 -1
  22. data/lib/temporalio/api/cloud/region/v1/message.rb +2 -1
  23. data/lib/temporalio/api/cloud/resource/v1/message.rb +23 -0
  24. data/lib/temporalio/api/cloud/sink/v1/message.rb +24 -0
  25. data/lib/temporalio/api/cloud/usage/v1/message.rb +31 -0
  26. data/lib/temporalio/api/common/v1/message.rb +7 -1
  27. data/lib/temporalio/api/enums/v1/event_type.rb +1 -1
  28. data/lib/temporalio/api/enums/v1/failed_cause.rb +1 -1
  29. data/lib/temporalio/api/enums/v1/reset.rb +1 -1
  30. data/lib/temporalio/api/history/v1/message.rb +1 -1
  31. data/lib/temporalio/api/nexus/v1/message.rb +2 -2
  32. data/lib/temporalio/api/operatorservice/v1/service.rb +1 -1
  33. data/lib/temporalio/api/payload_visitor.rb +1513 -0
  34. data/lib/temporalio/api/schedule/v1/message.rb +2 -1
  35. data/lib/temporalio/api/testservice/v1/request_response.rb +31 -0
  36. data/lib/temporalio/api/testservice/v1/service.rb +23 -0
  37. data/lib/temporalio/api/workflow/v1/message.rb +1 -1
  38. data/lib/temporalio/api/workflowservice/v1/request_response.rb +17 -2
  39. data/lib/temporalio/api/workflowservice/v1/service.rb +1 -1
  40. data/lib/temporalio/api.rb +1 -0
  41. data/lib/temporalio/cancellation.rb +34 -14
  42. data/lib/temporalio/client/async_activity_handle.rb +12 -37
  43. data/lib/temporalio/client/connection/cloud_service.rb +309 -231
  44. data/lib/temporalio/client/connection/operator_service.rb +36 -84
  45. data/lib/temporalio/client/connection/service.rb +6 -5
  46. data/lib/temporalio/client/connection/test_service.rb +111 -0
  47. data/lib/temporalio/client/connection/workflow_service.rb +264 -441
  48. data/lib/temporalio/client/connection.rb +90 -44
  49. data/lib/temporalio/client/interceptor.rb +160 -60
  50. data/lib/temporalio/client/schedule.rb +967 -0
  51. data/lib/temporalio/client/schedule_handle.rb +126 -0
  52. data/lib/temporalio/client/workflow_execution.rb +7 -10
  53. data/lib/temporalio/client/workflow_handle.rb +38 -95
  54. data/lib/temporalio/client/workflow_update_handle.rb +3 -5
  55. data/lib/temporalio/client.rb +122 -42
  56. data/lib/temporalio/common_enums.rb +17 -0
  57. data/lib/temporalio/converters/data_converter.rb +4 -7
  58. data/lib/temporalio/converters/failure_converter.rb +5 -3
  59. data/lib/temporalio/converters/payload_converter/composite.rb +4 -0
  60. data/lib/temporalio/converters/payload_converter.rb +6 -8
  61. data/lib/temporalio/converters/raw_value.rb +20 -0
  62. data/lib/temporalio/error/failure.rb +1 -1
  63. data/lib/temporalio/error.rb +10 -2
  64. data/lib/temporalio/internal/bridge/api/core_interface.rb +5 -1
  65. data/lib/temporalio/internal/bridge/api/nexus/nexus.rb +33 -0
  66. data/lib/temporalio/internal/bridge/api/workflow_activation/workflow_activation.rb +5 -1
  67. data/lib/temporalio/internal/bridge/api/workflow_commands/workflow_commands.rb +4 -1
  68. data/lib/temporalio/internal/bridge/client.rb +11 -6
  69. data/lib/temporalio/internal/bridge/testing.rb +20 -0
  70. data/lib/temporalio/internal/bridge/worker.rb +2 -0
  71. data/lib/temporalio/internal/bridge.rb +1 -1
  72. data/lib/temporalio/internal/client/implementation.rb +245 -70
  73. data/lib/temporalio/internal/metric.rb +122 -0
  74. data/lib/temporalio/internal/proto_utils.rb +86 -7
  75. data/lib/temporalio/internal/worker/activity_worker.rb +52 -24
  76. data/lib/temporalio/internal/worker/multi_runner.rb +51 -7
  77. data/lib/temporalio/internal/worker/workflow_instance/child_workflow_handle.rb +54 -0
  78. data/lib/temporalio/internal/worker/workflow_instance/context.rb +329 -0
  79. data/lib/temporalio/internal/worker/workflow_instance/details.rb +44 -0
  80. data/lib/temporalio/internal/worker/workflow_instance/external_workflow_handle.rb +32 -0
  81. data/lib/temporalio/internal/worker/workflow_instance/externally_immutable_hash.rb +22 -0
  82. data/lib/temporalio/internal/worker/workflow_instance/handler_execution.rb +25 -0
  83. data/lib/temporalio/internal/worker/workflow_instance/handler_hash.rb +41 -0
  84. data/lib/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rb +97 -0
  85. data/lib/temporalio/internal/worker/workflow_instance/inbound_implementation.rb +62 -0
  86. data/lib/temporalio/internal/worker/workflow_instance/outbound_implementation.rb +415 -0
  87. data/lib/temporalio/internal/worker/workflow_instance/replay_safe_logger.rb +37 -0
  88. data/lib/temporalio/internal/worker/workflow_instance/replay_safe_metric.rb +40 -0
  89. data/lib/temporalio/internal/worker/workflow_instance/scheduler.rb +163 -0
  90. data/lib/temporalio/internal/worker/workflow_instance.rb +730 -0
  91. data/lib/temporalio/internal/worker/workflow_worker.rb +196 -0
  92. data/lib/temporalio/metric.rb +109 -0
  93. data/lib/temporalio/retry_policy.rb +37 -14
  94. data/lib/temporalio/runtime.rb +118 -75
  95. data/lib/temporalio/search_attributes.rb +80 -37
  96. data/lib/temporalio/testing/activity_environment.rb +2 -2
  97. data/lib/temporalio/testing/workflow_environment.rb +251 -5
  98. data/lib/temporalio/version.rb +1 -1
  99. data/lib/temporalio/worker/activity_executor/thread_pool.rb +9 -217
  100. data/lib/temporalio/worker/activity_executor.rb +3 -3
  101. data/lib/temporalio/worker/interceptor.rb +340 -66
  102. data/lib/temporalio/worker/thread_pool.rb +237 -0
  103. data/lib/temporalio/worker/workflow_executor/thread_pool.rb +230 -0
  104. data/lib/temporalio/worker/workflow_executor.rb +26 -0
  105. data/lib/temporalio/worker.rb +201 -30
  106. data/lib/temporalio/workflow/activity_cancellation_type.rb +20 -0
  107. data/lib/temporalio/workflow/child_workflow_cancellation_type.rb +21 -0
  108. data/lib/temporalio/workflow/child_workflow_handle.rb +43 -0
  109. data/lib/temporalio/workflow/definition.rb +566 -0
  110. data/lib/temporalio/workflow/external_workflow_handle.rb +41 -0
  111. data/lib/temporalio/workflow/future.rb +151 -0
  112. data/lib/temporalio/workflow/handler_unfinished_policy.rb +13 -0
  113. data/lib/temporalio/workflow/info.rb +82 -0
  114. data/lib/temporalio/workflow/parent_close_policy.rb +19 -0
  115. data/lib/temporalio/workflow/update_info.rb +20 -0
  116. data/lib/temporalio/workflow.rb +523 -0
  117. data/lib/temporalio.rb +4 -0
  118. data/temporalio.gemspec +2 -2
  119. metadata +50 -8
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
 
@@ -12,18 +12,12 @@ execute asynchronous, long-running business logic in a scalable and resilient wa
12
12
  Also see:
13
13
 
14
14
  * [Ruby Samples](https://github.com/temporalio/samples-ruby)
15
- * [API Documentation](https://rubydoc.info/gems/temporalio)
15
+ * [API Documentation](https://rubydoc.info/gems/temporalio/0.2.0)
16
16
 
17
17
  ⚠️ UNDER ACTIVE DEVELOPMENT
18
18
 
19
19
  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:
25
-
26
- * Workflow workers
20
+ until the SDK is marked stable.
27
21
 
28
22
  **NOTE: This README is for the current branch and not necessarily what's released on RubyGems.**
29
23
 
@@ -35,15 +29,32 @@ Notably missing from this SDK:
35
29
 
36
30
  - [Quick Start](#quick-start)
37
31
  - [Installation](#installation)
38
- - [Implementing an Activity](#implementing-an-activity)
39
- - [Running a Workflow](#running-a-workflow)
32
+ - [Implementing a Workflow and Activity](#implementing-a-workflow-and-activity)
33
+ - [Running a Worker](#running-a-worker)
34
+ - [Executing a Workflow](#executing-a-workflow)
40
35
  - [Usage](#usage)
41
36
  - [Client](#client)
42
37
  - [Cloud Client Using mTLS](#cloud-client-using-mtls)
38
+ - [Cloud Client Using API Key](#cloud-client-using-api-key)
43
39
  - [Data Conversion](#data-conversion)
44
40
  - [ActiveRecord and ActiveModel](#activerecord-and-activemodel)
45
41
  - [Workers](#workers)
46
42
  - [Workflows](#workflows)
43
+ - [Workflow Definition](#workflow-definition)
44
+ - [Running Workflows](#running-workflows)
45
+ - [Invoking Activities](#invoking-activities)
46
+ - [Invoking Child Workflows](#invoking-child-workflows)
47
+ - [Timers and Conditions](#timers-and-conditions)
48
+ - [Workflow Fiber Scheduling and Cancellation](#workflow-fiber-scheduling-and-cancellation)
49
+ - [Workflow Futures](#workflow-futures)
50
+ - [Workflow Utilities](#workflow-utilities)
51
+ - [Workflow Exceptions](#workflow-exceptions)
52
+ - [Workflow Logic Constraints](#workflow-logic-constraints)
53
+ - [Workflow Testing](#workflow-testing)
54
+ - [Automatic Time Skipping](#automatic-time-skipping)
55
+ - [Manual Time Skipping](#manual-time-skipping)
56
+ - [Mocking Activities](#mocking-activities)
57
+ - [Workflow Replay](#workflow-replay)
47
58
  - [Activities](#activities)
48
59
  - [Activity Definition](#activity-definition)
49
60
  - [Activity Context](#activity-context)
@@ -65,6 +76,8 @@ Notably missing from this SDK:
65
76
 
66
77
  ### Installation
67
78
 
79
+ The Ruby SDK works with Ruby 3.2, 3.3, and 3.4.
80
+
68
81
  Can require in a Gemfile like:
69
82
 
70
83
  ```
@@ -85,58 +98,85 @@ information.
85
98
  **NOTE**: Due to [an issue](https://github.com/temporalio/sdk-ruby/issues/162), fibers (and `async` gem) are only
86
99
  supported on Ruby versions 3.3 and newer.
87
100
 
88
- ### Implementing an Activity
101
+ ### Implementing a Workflow and Activity
89
102
 
90
- Implementing workflows is not yet supported in the Ruby SDK, but implementing activities is.
103
+ Activities are classes. Here is an example of a simple activity that can be put in `say_hello_activity.rb`:
91
104
 
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
105
 
95
106
  ```ruby
96
107
  require 'temporalio/activity'
97
- require 'temporalio/cancellation'
98
- require 'temporalio/client'
99
- require 'temporalio/worker'
100
108
 
101
109
  # Implementation of a simple activity
102
- class SayHelloActivity < Temporalio::Activity
110
+ class SayHelloActivity < Temporalio::Activity::Definition
103
111
  def execute(name)
104
112
  "Hello, #{name}!"
105
113
  end
106
114
  end
115
+ ```
116
+
117
+ Workflows are also classes. To create the workflow, put the following in `say_hello_workflow.rb`:
118
+
119
+ ```ruby
120
+ require 'temporalio/workflow'
121
+ require_relative 'say_hello_activity'
122
+
123
+ class SayHelloWorkflow < Temporalio::Workflow::Definition
124
+ def execute(name)
125
+ Temporalio::Workflow.execute_activity(
126
+ SayHelloActivity,
127
+ name,
128
+ schedule_to_close_timeout: 300
129
+ )
130
+ end
131
+ end
132
+ ```
133
+
134
+ This is a simple workflow that executes the `SayHelloActivity` activity.
135
+
136
+ ### Running a Worker
137
+
138
+ To run this in a worker, put the following in `worker.rb`:
139
+
140
+ ```ruby
141
+ require 'temporalio/client'
142
+ require 'temporalio/worker'
143
+ require_relative 'say_hello_activity'
144
+ require_relative 'say_hello_workflow'
107
145
 
108
146
  # Create a client
109
147
  client = Temporalio::Client.connect('localhost:7233', 'my-namespace')
110
148
 
111
- # Create a worker with the client and activities
149
+ # Create a worker with the client, activities, and workflows
112
150
  worker = Temporalio::Worker.new(
113
151
  client:,
114
152
  task_queue: 'my-task-queue',
115
- # There are various forms an activity can take, see specific section for details.
153
+ workflows: [SayHelloWorkflow],
154
+ # There are various forms an activity can take, see "Activities" section for details
116
155
  activities: [SayHelloActivity]
117
156
  )
118
157
 
119
- # Run the worker until SIGINT. This can be done in many ways, see specific
120
- # section for details.
158
+ # Run the worker until SIGINT. This can be done in many ways, see "Workers" section for details.
121
159
  worker.run(shutdown_signals: ['SIGINT'])
122
160
  ```
123
161
 
124
- Running that will run the worker until Ctrl+C pressed.
162
+ Running that will run the worker until Ctrl+C is pressed.
125
163
 
126
- ### Running a Workflow
164
+ ### Executing a Workflow
127
165
 
128
- Assuming that `SayHelloWorkflow` just calls this activity, it can be run like so:
166
+ To start and wait on the workflow result, with the worker program running elsewhere, put the following in
167
+ `execute_workflow.rb`:
129
168
 
130
169
  ```ruby
131
170
  require 'temporalio/client'
171
+ require_relative 'say_hello_workflow'
132
172
 
133
173
  # Create a client
134
174
  client = Temporalio::Client.connect('localhost:7233', 'my-namespace')
135
175
 
136
176
  # Run workflow
137
177
  result = client.execute_workflow(
138
- 'SayHelloWorkflow',
139
- 'Temporal',
178
+ SayHelloWorkflow,
179
+ 'Temporal', # This is the input to the workflow
140
180
  id: 'my-workflow-id',
141
181
  task_queue: 'my-task-queue'
142
182
  )
@@ -163,8 +203,8 @@ client = Temporalio::Client.connect('localhost:7233', 'my-namespace')
163
203
 
164
204
  # Start a workflow
165
205
  handle = client.start_workflow(
166
- 'SayHelloWorkflow',
167
- 'Temporal',
206
+ MyWorkflow,
207
+ 'arg1', 'arg2',
168
208
  id: 'my-workflow-id',
169
209
  task_queue: 'my-task-queue'
170
210
  )
@@ -179,6 +219,8 @@ Notes about the above code:
179
219
  * Temporal clients are not explicitly closed.
180
220
  * To enable TLS, the `tls` option can be set to `true` or a `Temporalio::Client::Connection::TLSOptions` instance.
181
221
  * Instead of `start_workflow` + `result` above, `execute_workflow` shortcut can be used if the handle is not needed.
222
+ * Both `start_workflow` and `execute_workflow` accept either the workflow class or the string/symbol name of the
223
+ workflow.
182
224
  * The `handle` above is a `Temporalio::Client::WorkflowHandle` which has several other operations that can be performed
183
225
  on a workflow. To get a handle to an existing workflow, use `workflow_handle` on the client.
184
226
  * Clients are thread safe and are fiber-compatible (but fiber compatibility only supported for Ruby 3.3+ at this time).
@@ -201,6 +243,22 @@ client = Temporalio::Client.connect(
201
243
  ))
202
244
  ```
203
245
 
246
+ #### Cloud Client Using API Key
247
+
248
+ Assuming the API key is 'my-api-key', this is how to connect to Temporal cloud:
249
+
250
+ ```ruby
251
+ require 'temporalio/client'
252
+
253
+ # Create a client
254
+ client = Temporalio::Client.connect(
255
+ 'my-namespace.a1b2c.tmprl.cloud:7233',
256
+ 'my-namespace.a1b2c',
257
+ api_key: 'my-api-key'
258
+ tls: true
259
+ )
260
+ ```
261
+
204
262
  #### Data Conversion
205
263
 
206
264
  Data converters are used to convert raw Temporal payloads to/from actual Ruby types. A custom data converter can be set
@@ -215,11 +273,12 @@ which supports the following types:
215
273
  * `nil`
216
274
  * "bytes" (i.e. `String` with `Encoding::ASCII_8BIT` encoding)
217
275
  * `Google::Protobuf::MessageExts` instances
218
- * [`JSON` module](https://docs.ruby-lang.org/en/master/JSON.html) for everything else
276
+ * [JSON module](https://docs.ruby-lang.org/en/master/JSON.html) for everything else
219
277
 
220
278
  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.
279
+ `create_additions: true` set by default). So a Ruby object will often appear as a hash when deserialized. Also, hashes
280
+ that are passed in with symbol keys end up with string keys when deserialized. While "JSON Additions" are supported, it
281
+ is not cross-SDK-language compatible since this is a Ruby-specific construct.
223
282
 
224
283
  The default payload converter is a collection of "encoding payload converters". On serialize, each encoding converter
225
284
  will be tried in order until one accepts (default falls through to the JSON one). The encoding converter sets an
@@ -280,8 +339,7 @@ well.
280
339
 
281
340
  ### Workers
282
341
 
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:
342
+ Workers host workflows and/or activities. Here's how to run a worker:
285
343
 
286
344
  ```ruby
287
345
  require 'temporalio/client'
@@ -291,11 +349,12 @@ require 'my_module'
291
349
  # Create a client
292
350
  client = Temporalio::Client.connect('localhost:7233', 'my-namespace')
293
351
 
294
- # Create a worker with the client and activities
352
+ # Create a worker with the client, activities, and workflows
295
353
  worker = Temporalio::Worker.new(
296
354
  client:,
297
355
  task_queue: 'my-task-queue',
298
- # There are various forms an activity can take, see specific section for details.
356
+ workflows: [MyModule::MyWorkflow],
357
+ # There are various forms an activity can take, see "Activities" section for details
299
358
  activities: [MyModule::MyActivity]
300
359
  )
301
360
 
@@ -311,19 +370,490 @@ Notes about the above code:
311
370
  * This just shows providing an activity class, but there are other forms, see the "Activities" section for details.
312
371
  * The worker `run` method accepts an optional `Temporalio::Cancellation` object that can be used to cancel instead or in
313
372
  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
373
+ * The worker `run` method accepts a `shutdown_signals` array which will trap the signal and start shutdown when
315
374
  received.
316
375
  * Workers work with threads or fibers (but fiber compatibility only supported for Ruby 3.3+ at this time). Fiber-based
317
376
  activities (see "Activities" section) only work if the worker is created within a fiber.
318
377
  * The `run` method does not return until the worker is shut down. This means even if shutdown is triggered (e.g. via
319
378
  `Cancellation` or block completion), it may not return immediately. Activities not completing may hang worker
320
379
  shutdown, see the "Activities" section.
321
- * Workers can have many more options not shown here (e.g. data converters and interceptors).
380
+ * Workers can have many more options not shown here (e.g. tuners and interceptors).
322
381
  * The `Temporalio::Worker.run_all` class method is available for running multiple workers concurrently.
323
382
 
324
383
  ### Workflows
325
384
 
326
- ⚠️ Workflows cannot yet be implemented Ruby.
385
+ #### Workflow Definition
386
+
387
+ Workflows are defined as classes that extend `Temporalio::Workflow::Definition`. The entry point for a workflow is
388
+ `execute` and must be defined. Methods for handling signals, queries, and updates are marked with `workflow_signal`,
389
+ `workflow_query`, and `workflow_update` just before the method is defined. Here is an example of a workflow definition:
390
+
391
+ ```ruby
392
+ require 'temporalio/workflow'
393
+
394
+ class GreetingWorkflow < Temporalio::Workflow::Definition
395
+ workflow_query_attr_reader :current_greeting
396
+
397
+ def execute(params)
398
+ loop do
399
+ # Call activity called CreateGreeting to create greeting and store as attribute
400
+ @current_greeting = Temporalio::Workflow.execute_activity(
401
+ CreateGreeting,
402
+ params,
403
+ schedule_to_close_timeout: 300
404
+ )
405
+ Temporalio::Workflow.logger.debug("Greeting set to #{@current_greeting}")
406
+
407
+ # Wait for param update or complete signal. Note, cancellation can occur by default
408
+ # on wait_condition calls, so Cancellation object doesn't need to be passed
409
+ # explicitly.
410
+ Temporalio::Workflow.wait_condition { @greeting_params_update || @complete }
411
+
412
+ # If there was an update, exchange and rerun. If it's _only_ a complete, finish
413
+ # workflow with the greeting.
414
+ if @greeting_params_update
415
+ params, @greeting_params_update = @greeting_params_update, nil
416
+ else
417
+ return @current_greeting
418
+ end
419
+ end
420
+ end
421
+
422
+ workflow_update
423
+ def update_greeting_params(greeting_params_update)
424
+ @greeting_params_update = greeting_params_update
425
+ end
426
+
427
+ workflow_signal
428
+ def complete_with_greeting
429
+ @complete = true
430
+ end
431
+ end
432
+ ```
433
+
434
+ Notes about the above code:
435
+
436
+ * `execute` is the primary entrypoint and its result/exception represents the workflow result/failure.
437
+ * `workflow_signal`, `workflow_query` (and the shortcut seen above, `workflow_query_attr_reader`), and `workflow_update`
438
+ implicitly create class methods usable by callers/clients. A workflow definition with no methods actually implemented
439
+ can even be created for use by clients if the workflow is implemented elsewhere and/or in another language.
440
+ * Workflow code must be deterministic. See the "Workflow Logic Constraints" section below.
441
+ * `execute_activity` accepts either the activity class or the string/symbol for the name.
442
+
443
+ The following protected class methods are available on `Temporalio::Workflow::Definition` to customize the overall
444
+ workflow definition/behavior:
445
+
446
+ * `workflow_name` - Accepts a string or symbol to change the name. Otherwise the name is defaulted to the unqualified
447
+ class name.
448
+ * `workflow_dynamic` - Marks a workflow as dynamic. Dynamic workflows do not have names and handle any workflow that is
449
+ not otherwise registered. A worker can only have one dynamic workflow. It is often useful to use `workflow_raw_args`
450
+ with this.
451
+ * `workflow_raw_args` - Have workflow arguments delivered to `execute` (and `initialize` if `workflow_init` in use) as
452
+ `Temporalio::Converters::RawValue`s. These are wrappers for the raw payloads that have not been decoded. They can be
453
+ decoded with `Temporalio::Workflow.payload_converter`. Using this with `*args` splat can be helpful in dynamic
454
+ situations.
455
+ * `workflow_failure_exception_type` - Accepts one or more exception classes that will be considered workflow failure
456
+ instead of task failure. See the "Exceptions" section later on what this means. This can be called multiple times.
457
+ * `workflow_query_attr_reader` - Is a helper that accepts one or more symbols for attributes to expose as `attr_reader`
458
+ _and_ `workflow_query`. This means it is a superset of `attr_reader` and will not work if also using `attr_reader` or
459
+ `attr_accessor`. If a writer is needed alongside this, use `attr_writer`.
460
+
461
+ The following protected class methods can be called just before defining instance methods to customize the
462
+ definition/behavior of the method:
463
+
464
+ * `workflow_init` - Mark an `initialize` method as needing the workflow start arguments. Otherwise, `initialize` must
465
+ accept no required arguments. This must be placed above the `initialize` method or it will fail.
466
+ * `workflow_signal` - Mark the next method as a workflow signal. The signal name is defaulted to the method name but can
467
+ be customized by the `name` kwarg. See the API documentation for more kwargs that can be set. Return values for
468
+ signals are discarded and exceptions raised in signal handlers are treated as if they occurred in the primary workflow
469
+ method. This also defines a class method of the same name to return the definition for use by clients.
470
+ * `workflow_query` - Mark the next method as a workflow query. The query name is defaulted to the method name but can
471
+ be customized by the `name` kwarg. See the API documentation for more kwargs that can be set. The result of the method
472
+ is the result of the query. Queries must never have any side effects, meaning they should never mutate state or try to
473
+ wait on anything. This also defines a class method of the same name to return the definition for use by clients.
474
+ * `workflow_update` - Mark the next method as a workflow update. The update name is defaulted to the method name but can
475
+ be customized by the `name` kwarg. See the API documentation for more kwargs that can be set. The result of the method
476
+ is the result of the update. This also defines a class method of the same name to return the definition for use by
477
+ clients.
478
+ * `workflow_update_validator` - Mark the next method as a validator to an update. This accepts a symbol for the
479
+ `workflow_update` method it validates. Validators are used to do early rejection of updates and must never have any
480
+ side effects, meaning they should never mutate state or try to wait on anything.
481
+
482
+ Workflows can be inherited, but subclass workflow-level decorators override superclass ones, and the same method can't
483
+ be decorated with different handler types/names in the hierarchy.
484
+
485
+ #### Running Workflows
486
+
487
+ To start a workflow from a client, you can `start_workflow` and use the resulting handle:
488
+
489
+ ```ruby
490
+ # Start the workflow
491
+ handle = my_client.start_workflow(
492
+ GreetingWorkflow,
493
+ { salutation: 'Hello', name: 'Temporal' },
494
+ id: 'my-workflow-id',
495
+ task_queue: 'my-task-queue'
496
+ )
497
+
498
+ # Check current greeting via query
499
+ puts "Current greeting: #{handle.query(GreetingWorkflow.current_greeting)}"
500
+
501
+ # Change the params via update
502
+ handle.execute_update(
503
+ GreetingWorkflow.update_greeting_params,
504
+ { salutation: 'Aloha', name: 'John' }
505
+ )
506
+
507
+ # Tell it to complete via signal
508
+ handle.signal(GreetingWorkflow.complete_with_greeting)
509
+
510
+ # Wait for workflow result
511
+ puts "Final greeting: #{handle.result}"
512
+ ```
513
+
514
+ Some things to note about the above code:
515
+
516
+ * This uses the `GreetingWorkflow` workflow from the previous section.
517
+ * The output of this code is "Current greeting: Hello, Temporal!" and "Final greeting: Aloha, John!".
518
+ * ID and task queue are required for starting a workflow.
519
+ * Signal, query, and update calls here use the class methods created on the definition for safety. So if the
520
+ `update_greeting_params` method didn't exist or wasn't marked as an update, the code will fail client side before even
521
+ attempting the call. Static typing tooling may also take advantage of this for param/result type checking.
522
+ * A helper `execute_workflow` method is available on the client that is just `start_workflow` + handle `result`.
523
+
524
+ #### Invoking Activities
525
+
526
+ * Activities are executed with `Temporalio::Workflow.execute_activity`, which accepts the activity class or a
527
+ string/symbol activity name.
528
+ * Activity options are kwargs on the `execute_activity` method. Either `schedule_to_close_timeout` or
529
+ `start_to_close_timeout` must be set.
530
+ * Other options like `retry_policy`, `cancellation_type`, etc can also be set.
531
+ * The `cancellation` can be set to a `Cancellation` to send a cancel request to the activity. By default, the
532
+ `cancellation` is the overall `Temporalio::Workflow.cancellation` which is the overarching workflow cancellation.
533
+ * Activity failures are raised from the call as `Temporalio::Error::ActivityError`.
534
+ * `execute_local_activity` exists with mostly the same options for local activities.
535
+
536
+ #### Invoking Child Workflows
537
+
538
+ * Child workflows are started with `Temporalio::Workflow.start_child_workflow`, which accepts the workflow class or
539
+ string/symbol name, arguments, and other options.
540
+ * Result for `start_child_workflow` is a `Temporalio::Workflow::ChildWorkflowHandle` which has the `id`, the ability to
541
+ wait on the `result`, and the ability to `signal` the child.
542
+ * The `start_child_workflow` call does not complete until the start has been accepted by the server.
543
+ * A helper `execute_child_workflow` method is available that is just `start_child_workflow` + handle `result`.
544
+
545
+ #### Timers and Conditions
546
+
547
+ * A timer is represented by `Temporalio::Workflow.sleep`.
548
+ * Timers are also started on `Temporalio::Workflow.timeout`.
549
+ * _Technically_ `Kernel.sleep` and `Timeout.timeout` also delegate to the above calls, but the more explicit workflow
550
+ forms are encouraged because they accept more options and are not subject to Ruby standard library implementation
551
+ changes.
552
+ * Each timer accepts a `Cancellation`, but if none is given, it defaults to `Temporalio::Workflow.cancellation`.
553
+ * `Temporalio::Workflow.wait_condition` accepts a block that waits until the evaluated block result is truthy, then
554
+ returns the value.
555
+ * This function is invoked on each iteration of the internal event loop. This means it cannot have any side effects.
556
+ * This is commonly used for checking if a variable is changed from some other part of a workflow (e.g. a signal
557
+ handler).
558
+ * Each wait conditions accepts a `Cancellation`, but if none is given, it defaults to
559
+ `Temporalio::Workflow.cancellation`.
560
+
561
+ #### Workflow Fiber Scheduling and Cancellation
562
+
563
+ Workflows are backed by a custom, deterministic `Fiber::Scheduler`. All fiber calls inside a workflow use this scheduler
564
+ to ensure coroutines run deterministically.
565
+
566
+ Every workflow contains a `Temporalio::Cancellation` at `Temporalio::Workflow.cancellation`. This is canceled when the
567
+ workflow is canceled. For all workflow calls that accept a cancellation token, this is the default. So if a workflow is
568
+ waiting on `execute_activity` and the workflow is canceled, that cancellation will propagate to the waiting activity.
569
+
570
+ `Cancellation`s may be created to perform cancellation more specifically. A `Cancellation` token derived from the
571
+ workflow one can be created via `my_cancel, my_cancel_proc = Cancellation.new(Temporalio::Workflow.cancellation)`. Then
572
+ `my_cancel` can be passed as `cancellation` to cancel something more specifically when `my_cancel_proc.call` is invoked.
573
+
574
+ `Cancellation`s don't have to be derived from the workflow one, they can just be created standalone or "detached". This
575
+ is useful for executing, say, a cleanup activity in an `ensure` block that needs to run even on cancel. If the cleanup
576
+ activity had instead used the workflow cancellation or one derived from it, then on cancellation it would be cancelled
577
+ before it even started.
578
+
579
+ #### Workflow Futures
580
+
581
+ `Temporalio::Workflow::Future` can be used for running things in the background or concurrently. This is basically a
582
+ safe wrapper around `Fiber.schedule` for starting and `Workflow.wait_condition` for waiting.
583
+
584
+ Nothing uses futures by default, but they work with all workflow code/constructs. For instance, to run 3 activities and
585
+ wait for them all to complete, something like this can be written:
586
+
587
+ ```ruby
588
+ # Start 3 activities in background
589
+ fut1 = Temporalio::Workflow::Future.new do
590
+ Temporalio::Workflow.execute_activity(MyActivity1, schedule_to_close_timeout: 300)
591
+ end
592
+ fut2 = Temporalio::Workflow::Future.new do
593
+ Temporalio::Workflow.execute_activity(MyActivity2, schedule_to_close_timeout: 300)
594
+ end
595
+ fut3 = Temporalio::Workflow::Future.new do
596
+ Temporalio::Workflow.execute_activity(MyActivity3, schedule_to_close_timeout: 300)
597
+ end
598
+
599
+ # Wait for them all to complete
600
+ Temporalio::Workflow::Future.all_of(fut1, fut2, fut3).wait
601
+
602
+ Temporalio::Workflow.logger.debug("Got: #{fut1.result}, #{fut2.result}, #{fut3.result}")
603
+ ```
604
+
605
+ Or, say, to wait on the first of 5 activities or a timeout to complete:
606
+
607
+ ```ruby
608
+ # Start 5 activities
609
+ act_futs = 5.times.map do |i|
610
+ Temporalio::Workflow::Future.new do
611
+ Temporalio::Workflow.execute_activity(MyActivity, "my-arg-#{i}" schedule_to_close_timeout: 300)
612
+ end
613
+ end
614
+ # Start a timer
615
+ sleep_fut = Temporalio::Workflow::Future.new { Temporalio::Workflow.sleep(30) }
616
+
617
+ # Wait for first act result or sleep fut
618
+ act_result = Temporalio::Workflow::Future.any_of(sleep_fut, *act_futs).wait
619
+ # Fail if timer done first
620
+ raise Temporalio::Error::ApplicationError, 'Timer expired' if sleep_fut.done?
621
+ # Print act result otherwise
622
+ puts "Act result: #{act_result}"
623
+ ```
624
+
625
+ There are several other details not covered here about futures, such as how exceptions are handled, how to use a setter
626
+ proc instead of a block, etc. See the API documentation for details.
627
+
628
+ #### Workflow Utilities
629
+
630
+ In addition to the pieces documented above, additional methods are available on `Temporalio::Workflow` that can be used
631
+ from workflows including:
632
+
633
+ * `in_workflow?` - Returns `true` if in the workflow or `false` otherwise. This is the only method on the class that can
634
+ be called outside of a workflow without raising an exception.
635
+ * `info` - Immutable workflow information.
636
+ * `logger` - A Ruby logger that adds contextual information and takes care not to log on replay.
637
+ * `metric_meter` - A metric meter for making custom metrics that adds contextual information and takes care not to
638
+ record on replay.
639
+ * `random` - A deterministic `Random` instance.
640
+ * `memo` - A read-only hash of the memo (updated via `upsert_memo`).
641
+ * `search_attributes` - A read-only `SearchAttributes` collection (updated via `upsert_search_attributes`).
642
+ * `now` - Current, deterministic UTC time for the workflow.
643
+ * `all_handlers_finished?` - Returns true when all signal and update handlers are done. Useful as
644
+ `Temporalio::Workflow.wait_condition { Temporalio::Workflow.all_handlers_finished? }` for making sure not to return
645
+ from the primary workflow method until all handlers are done.
646
+ * `patched` and `deprecate_patch` - Support for patch-based versioning inside the workflow.
647
+ * `continue_as_new_suggested` - Returns true when the server recommends performing a continue as new.
648
+ * `current_update_info` - Returns `Temporalio::Workflow::UpdateInfo` if the current code is inside an update, or nil
649
+ otherwise.
650
+ * `external_workflow_handle` - Obtain an handle to an external workflow for signalling or cancelling.
651
+ * `payload_converter` - Payload converter if needed for converting raw args.
652
+ * `signal_handlers`, `query_handlers`, and `update_handlers` - Hashes for the current set of handlers keyed by name (or
653
+ nil key for dynamic). `[]=` or `store` can be called on these to update the handlers, though defined handlers are
654
+ encouraged over runtime-set ones.
655
+
656
+ `Temporalio::Workflow::ContinueAsNewError` can be raised to continue-as-new the workflow. It accepts positional args and
657
+ defaults the workflow to the same as the current, though it can be changed with the `workflow` kwarg. See API
658
+ documentation for other details.
659
+
660
+ #### Workflow Exceptions
661
+
662
+ * Workflows can raise exceptions to fail the workflow/update or the "workflow task" (i.e. suspend the workflow, retrying
663
+ until code update allows it to continue).
664
+ * By default, exceptions that are instances of `Temporalio::Error::Failure` (or `Timeout::Error`) will fail the
665
+ workflow/update with that exception.
666
+ * For failing the workflow/update explicitly with a user exception, explicitly raise
667
+ `Temporalio::Error::ApplicationError`. This can be marked non-retryable or include details as needed.
668
+ * Other exceptions that come from activity execution, child execution, cancellation, etc are already instances of
669
+ `Temporalio::Error::Failure` and will fail the workflow/update if uncaught.
670
+ * By default, all other exceptions fail the "workflow task" which means the workflow/update will continually retry until
671
+ the code is fixed. This is helpful for bad code or other non-predictable exceptions. To actually fail the
672
+ workflow/update, use `Temporalio::Error::ApplicationError` as mentioned above.
673
+ * By default, all non-deterministic exceptions that are detected internally fail the "workflow task".
674
+
675
+ The default behavior can be customized at the worker level for all workflows via the
676
+ `workflow_failure_exception_types` worker option or per workflow via the `workflow_failure_exception_type` definition
677
+ method on the workflow itself. When a workflow encounters a "workflow task" fail (i.e. suspend), it will first check
678
+ either of these collections to see if the exception is an instance of any of the types and if so, will turn into a
679
+ workflow/update failure. As a special case, when a non-deterministic exception occurs and
680
+ `Temporalio::Workflow::NondeterminismError` is assignable to any of the types in the collection, that too
681
+ will turn into a workflow/update failure. However unlike other exceptions, non-deterministic exceptions that match
682
+ during update handlers become workflow failures not update failures because a non-deterministic exception is an
683
+ entire-workflow-failure situation.
684
+
685
+ #### Workflow Logic Constraints
686
+
687
+ Temporal Workflows [must be deterministic](https://docs.temporal.io/workflows#deterministic-constraints), which includes
688
+ Ruby workflows. This means there are several things workflows cannot do such as:
689
+
690
+ * Perform IO (network, disk, stdio, etc)
691
+ * Access/alter external mutable state
692
+ * Do any threading
693
+ * Do anything using the system clock (e.g. `Time.Now`)
694
+ * Make any random calls
695
+ * Make any not-guaranteed-deterministic calls
696
+
697
+ #### Workflow Testing
698
+
699
+ Workflow testing can be done in an integration-test fashion against a real server. However, it is hard to simulate
700
+ timeouts and other long time-based code. Using the time-skipping workflow test environment can help there.
701
+
702
+ A non-time-skipping `Temporalio::Testing::WorkflowEnvironment` can be started via `start_local` which supports all
703
+ standard Temporal features. It is actually a real Temporal server lazily downloaded on first use and run as a
704
+ subprocess in the background.
705
+
706
+ A time-skipping `Temporalio::Testing::WorkflowEnvironment` can be started via `start_time_skipping` which is a
707
+ reimplementation of the Temporal server with special time skipping capabilities. This too lazily downloads the process
708
+ to run when first called. Note, this class is not thread safe nor safe for use with independent tests. It can be reused,
709
+ but only for one test at a time because time skipping is locked/unlocked at the environment level. Note, the
710
+ time-skipping test server does not work on ARM-based processors at this time, though macOS ARM users can use it via the
711
+ built-in x64 translation in macOS.
712
+
713
+ ##### Automatic Time Skipping
714
+
715
+ Anytime a workflow result is waited on, the time-skipping server automatically advances to the next event it can. To
716
+ manually advance time before waiting on the result of the workflow, the `WorkflowEnvironment.sleep` method can be used
717
+ on the environment itself. If an activity is running, time-skipping is disabled.
718
+
719
+ Here's a simple example of a workflow that sleeps for 24 hours:
720
+
721
+ ```ruby
722
+ require 'temporalio/workflow'
723
+
724
+ class WaitADayWorkflow < Temporalio::Workflow::Definition
725
+ def execute
726
+ Temporalio::Workflow.sleep(1 * 24 * 60 * 60)
727
+ 'all done'
728
+ end
729
+ end
730
+ ```
731
+
732
+ A regular integration test of this workflow on a normal server would be way too slow. However, the time-skipping server
733
+ automatically skips to the next event when we wait on the result. Here's a minitest for that workflow:
734
+
735
+ ```ruby
736
+ class MyTest < Minitest::Test
737
+ def test_wait_a_day
738
+ Temporalio::Testing::WorkflowEnvironment.start_time_skipping do |env|
739
+ worker = Temporalio::Worker.new(
740
+ client: env.client,
741
+ task_queue: "tq-#{SecureRandom.uuid}",
742
+ workflows: [WaitADayWorkflow],
743
+ workflow_executor: Temporalio::Worker::WorkflowExecutor::ThreadPool.default
744
+ )
745
+ worker.run do
746
+ result = env.client.execute_workflow(
747
+ WaitADayWorkflow,
748
+ id: "wf-#{SecureRandom.uuid}",
749
+ task_queue: worker.task_queue
750
+ )
751
+ assert_equal 'all done', result
752
+ end
753
+ end
754
+ end
755
+ end
756
+ ```
757
+
758
+ This test will run almost instantly. This is because by calling `execute_workflow` on our client, we are actually
759
+ calling `start_workflow` + handle `result`, and `result` automatically skips time as much as it can (basically until the
760
+ end of the workflow or until an activity is run).
761
+
762
+ To disable automatic time-skipping while waiting for a workflow result, run code inside a block passed to
763
+ `auto_time_skipping_disabled`.
764
+
765
+ ##### Manual Time Skipping
766
+
767
+ Until a workflow is waited on, all time skipping in the time-skipping environment is done manually via
768
+ `WorkflowEnvironment.sleep`.
769
+
770
+ Here's a workflow that waits for a signal or times out:
771
+
772
+ ```ruby
773
+ require 'temporalio/workflow'
774
+
775
+ class SignalWorkflow < Temporalio::Workflow::Definition
776
+ def execute
777
+ Temporalio::Workflow.timeout(45) do
778
+ Temporalio::Workflow.wait_condition { @signal_received }
779
+ 'got signal'
780
+ rescue Timeout::Error
781
+ 'got timeout'
782
+ end
783
+ end
784
+
785
+ workflow_signal
786
+ def some_signal
787
+ @signal_received = true
788
+ end
789
+ end
790
+ ```
791
+
792
+ To test a normal signal, you might:
793
+
794
+ ```ruby
795
+ class MyTest < Minitest::Test
796
+ def test_signal_workflow_success
797
+ Temporalio::Testing::WorkflowEnvironment.start_time_skipping do |env|
798
+ worker = Temporalio::Worker.new(
799
+ client: env.client,
800
+ task_queue: "tq-#{SecureRandom.uuid}",
801
+ workflows: [SignalWorkflow],
802
+ workflow_executor: Temporalio::Worker::WorkflowExecutor::ThreadPool.default
803
+ )
804
+ worker.run do
805
+ handle = env.client.start_workflow(
806
+ SignalWorkflow,
807
+ id: "wf-#{SecureRandom.uuid}",
808
+ task_queue: worker.task_queue
809
+ )
810
+ handle.signal(SignalWorkflow.some_signal)
811
+ assert_equal 'got signal', handle.result
812
+ end
813
+ end
814
+ end
815
+ end
816
+ ```
817
+
818
+ But how would you test the timeout part? Like so:
819
+
820
+ ```ruby
821
+ class MyTest < Minitest::Test
822
+ def test_signal_workflow_timeout
823
+ Temporalio::Testing::WorkflowEnvironment.start_time_skipping do |env|
824
+ worker = Temporalio::Worker.new(
825
+ client: env.client,
826
+ task_queue: "tq-#{SecureRandom.uuid}",
827
+ workflows: [SignalWorkflow],
828
+ workflow_executor: Temporalio::Worker::WorkflowExecutor::ThreadPool.default
829
+ )
830
+ worker.run do
831
+ handle = env.client.start_workflow(
832
+ SignalWorkflow,
833
+ id: "wf-#{SecureRandom.uuid}",
834
+ task_queue: worker.task_queue
835
+ )
836
+ env.sleep(50)
837
+ assert_equal 'got timeout', handle.result
838
+ end
839
+ end
840
+ end
841
+ end
842
+ ```
843
+
844
+ This test will run almost instantly. The `env.sleep(50)` manually skips 50 seconds of time, allowing the timeout to be
845
+ triggered without actually waiting the full 45 seconds to time out.
846
+
847
+ ##### Mocking Activities
848
+
849
+ When testing workflows, often you don't want to actually run the activities. Activities are just classes that extend
850
+ `Temporalio::Activity::Definition`. Simply write different/empty/fake/asserting ones and pass those to the worker to
851
+ have different activities called during the test. You may need to use `activity_name :MyRealActivityClassName` inside
852
+ the mock activity class to make it appear as the real name.
853
+
854
+ #### Workflow Replay
855
+
856
+ TODO: Workflow replayer not yet implemented
327
857
 
328
858
  ### Activities
329
859
 
@@ -334,16 +864,16 @@ Activities can be defined in a few different ways. They are usually classes, but
334
864
  Here is a common activity definition:
335
865
 
336
866
  ```ruby
337
- class FindUserActivity < Temporalio::Activity
867
+ class FindUserActivity < Temporalio::Activity::Definition
338
868
  def execute(user_id)
339
869
  User.find(user_id)
340
870
  end
341
871
  end
342
872
  ```
343
873
 
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:
874
+ Activities are defined as classes that extend `Temporalio::Activity::Definition` and provide an `execute` method. When
875
+ this activity is provided to the worker as a _class_ (e.g. `activities: [FindUserActivity]`), it will be instantiated
876
+ for _every attempt_. Many users may prefer using the same instance across activities, for example:
347
877
 
348
878
  ```ruby
349
879
  class FindUserActivity < Temporalio::Activity
@@ -367,8 +897,13 @@ Some notes about activity definition:
367
897
  * Long running activities should heartbeat regularly, see "Activity Heartbeating and Cancellation" later.
368
898
  * By default every activity attempt is executed in a thread on a thread pool, but fibers are also supported. See
369
899
  "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.
900
+ * Technically an activity definition can be created manually via `Temporalio::Activity::Definition::Info.new` that
901
+ accepts a proc or a block, but the class form is recommended.
902
+ * `activity_dynamic` can be used to mark an activity dynamic. Dynamic activities do not have names and handle any
903
+ activity that is not otherwise registered. A worker can only have one dynamic activity.
904
+ * `workflow_raw_args` can be used to have activity arguments delivered to `execute` as
905
+ `Temporalio::Converters::RawValue`s. These are wrappers for the raw payloads that have not been converted to types
906
+ (but they have been decoded by the codec if present). They can be converted with `payload_converter` on the context.
372
907
 
373
908
  #### Activity Context
374
909
 
@@ -452,6 +987,13 @@ it will raise the error raised in the activity.
452
987
  The constructor of the environment has multiple keyword arguments that can be set to affect the activity context for the
453
988
  activity.
454
989
 
990
+ ### Ractors
991
+
992
+ It was an original goal to have workflows actually be Ractors for deterministic state isolation and have the library
993
+ support Ractors in general. However, due to the SDK's heavy use of the Google Protobuf library which
994
+ [is not Ractor-safe](https://github.com/protocolbuffers/protobuf/issues/19321), the Temporal Ruby SDK does not currently
995
+ work with Ractors.
996
+
455
997
  ### Platform Support
456
998
 
457
999
  This SDK is backed by a Ruby C extension written in Rust leveraging the