amit-temporalio 0.3.1

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 (179) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +2 -0
  3. data/Cargo.lock +4325 -0
  4. data/Cargo.toml +25 -0
  5. data/Gemfile +23 -0
  6. data/LICENSE +21 -0
  7. data/README.md +1148 -0
  8. data/Rakefile +101 -0
  9. data/ext/Cargo.toml +27 -0
  10. data/lib/temporalio/activity/complete_async_error.rb +11 -0
  11. data/lib/temporalio/activity/context.rb +116 -0
  12. data/lib/temporalio/activity/definition.rb +189 -0
  13. data/lib/temporalio/activity/info.rb +64 -0
  14. data/lib/temporalio/activity.rb +12 -0
  15. data/lib/temporalio/api/activity/v1/message.rb +25 -0
  16. data/lib/temporalio/api/batch/v1/message.rb +31 -0
  17. data/lib/temporalio/api/cloud/account/v1/message.rb +28 -0
  18. data/lib/temporalio/api/cloud/cloudservice/v1/request_response.rb +126 -0
  19. data/lib/temporalio/api/cloud/cloudservice/v1/service.rb +25 -0
  20. data/lib/temporalio/api/cloud/cloudservice.rb +3 -0
  21. data/lib/temporalio/api/cloud/identity/v1/message.rb +41 -0
  22. data/lib/temporalio/api/cloud/namespace/v1/message.rb +42 -0
  23. data/lib/temporalio/api/cloud/nexus/v1/message.rb +31 -0
  24. data/lib/temporalio/api/cloud/operation/v1/message.rb +28 -0
  25. data/lib/temporalio/api/cloud/region/v1/message.rb +24 -0
  26. data/lib/temporalio/api/cloud/resource/v1/message.rb +23 -0
  27. data/lib/temporalio/api/cloud/sink/v1/message.rb +24 -0
  28. data/lib/temporalio/api/cloud/usage/v1/message.rb +31 -0
  29. data/lib/temporalio/api/command/v1/message.rb +46 -0
  30. data/lib/temporalio/api/common/v1/grpc_status.rb +23 -0
  31. data/lib/temporalio/api/common/v1/message.rb +47 -0
  32. data/lib/temporalio/api/enums/v1/batch_operation.rb +22 -0
  33. data/lib/temporalio/api/enums/v1/command_type.rb +21 -0
  34. data/lib/temporalio/api/enums/v1/common.rb +26 -0
  35. data/lib/temporalio/api/enums/v1/event_type.rb +21 -0
  36. data/lib/temporalio/api/enums/v1/failed_cause.rb +26 -0
  37. data/lib/temporalio/api/enums/v1/namespace.rb +23 -0
  38. data/lib/temporalio/api/enums/v1/query.rb +22 -0
  39. data/lib/temporalio/api/enums/v1/reset.rb +23 -0
  40. data/lib/temporalio/api/enums/v1/schedule.rb +21 -0
  41. data/lib/temporalio/api/enums/v1/task_queue.rb +25 -0
  42. data/lib/temporalio/api/enums/v1/update.rb +22 -0
  43. data/lib/temporalio/api/enums/v1/workflow.rb +30 -0
  44. data/lib/temporalio/api/errordetails/v1/message.rb +42 -0
  45. data/lib/temporalio/api/export/v1/message.rb +24 -0
  46. data/lib/temporalio/api/failure/v1/message.rb +35 -0
  47. data/lib/temporalio/api/filter/v1/message.rb +27 -0
  48. data/lib/temporalio/api/history/v1/message.rb +90 -0
  49. data/lib/temporalio/api/namespace/v1/message.rb +31 -0
  50. data/lib/temporalio/api/nexus/v1/message.rb +40 -0
  51. data/lib/temporalio/api/operatorservice/v1/request_response.rb +49 -0
  52. data/lib/temporalio/api/operatorservice/v1/service.rb +23 -0
  53. data/lib/temporalio/api/operatorservice.rb +3 -0
  54. data/lib/temporalio/api/payload_visitor.rb +1513 -0
  55. data/lib/temporalio/api/protocol/v1/message.rb +23 -0
  56. data/lib/temporalio/api/query/v1/message.rb +27 -0
  57. data/lib/temporalio/api/replication/v1/message.rb +26 -0
  58. data/lib/temporalio/api/schedule/v1/message.rb +43 -0
  59. data/lib/temporalio/api/sdk/v1/enhanced_stack_trace.rb +25 -0
  60. data/lib/temporalio/api/sdk/v1/task_complete_metadata.rb +21 -0
  61. data/lib/temporalio/api/sdk/v1/user_metadata.rb +23 -0
  62. data/lib/temporalio/api/sdk/v1/workflow_metadata.rb +23 -0
  63. data/lib/temporalio/api/taskqueue/v1/message.rb +45 -0
  64. data/lib/temporalio/api/testservice/v1/request_response.rb +31 -0
  65. data/lib/temporalio/api/testservice/v1/service.rb +23 -0
  66. data/lib/temporalio/api/update/v1/message.rb +33 -0
  67. data/lib/temporalio/api/version/v1/message.rb +26 -0
  68. data/lib/temporalio/api/workflow/v1/message.rb +43 -0
  69. data/lib/temporalio/api/workflowservice/v1/request_response.rb +204 -0
  70. data/lib/temporalio/api/workflowservice/v1/service.rb +23 -0
  71. data/lib/temporalio/api/workflowservice.rb +3 -0
  72. data/lib/temporalio/api.rb +14 -0
  73. data/lib/temporalio/cancellation.rb +170 -0
  74. data/lib/temporalio/client/activity_id_reference.rb +32 -0
  75. data/lib/temporalio/client/async_activity_handle.rb +85 -0
  76. data/lib/temporalio/client/connection/cloud_service.rb +726 -0
  77. data/lib/temporalio/client/connection/operator_service.rb +201 -0
  78. data/lib/temporalio/client/connection/service.rb +42 -0
  79. data/lib/temporalio/client/connection/test_service.rb +111 -0
  80. data/lib/temporalio/client/connection/workflow_service.rb +1041 -0
  81. data/lib/temporalio/client/connection.rb +316 -0
  82. data/lib/temporalio/client/interceptor.rb +416 -0
  83. data/lib/temporalio/client/schedule.rb +967 -0
  84. data/lib/temporalio/client/schedule_handle.rb +126 -0
  85. data/lib/temporalio/client/workflow_execution.rb +100 -0
  86. data/lib/temporalio/client/workflow_execution_count.rb +36 -0
  87. data/lib/temporalio/client/workflow_execution_status.rb +18 -0
  88. data/lib/temporalio/client/workflow_handle.rb +389 -0
  89. data/lib/temporalio/client/workflow_query_reject_condition.rb +14 -0
  90. data/lib/temporalio/client/workflow_update_handle.rb +65 -0
  91. data/lib/temporalio/client/workflow_update_wait_stage.rb +17 -0
  92. data/lib/temporalio/client.rb +484 -0
  93. data/lib/temporalio/common_enums.rb +41 -0
  94. data/lib/temporalio/converters/data_converter.rb +99 -0
  95. data/lib/temporalio/converters/failure_converter.rb +202 -0
  96. data/lib/temporalio/converters/payload_codec.rb +26 -0
  97. data/lib/temporalio/converters/payload_converter/binary_null.rb +34 -0
  98. data/lib/temporalio/converters/payload_converter/binary_plain.rb +35 -0
  99. data/lib/temporalio/converters/payload_converter/binary_protobuf.rb +42 -0
  100. data/lib/temporalio/converters/payload_converter/composite.rb +66 -0
  101. data/lib/temporalio/converters/payload_converter/encoding.rb +35 -0
  102. data/lib/temporalio/converters/payload_converter/json_plain.rb +44 -0
  103. data/lib/temporalio/converters/payload_converter/json_protobuf.rb +41 -0
  104. data/lib/temporalio/converters/payload_converter.rb +71 -0
  105. data/lib/temporalio/converters/raw_value.rb +20 -0
  106. data/lib/temporalio/converters.rb +9 -0
  107. data/lib/temporalio/error/failure.rb +219 -0
  108. data/lib/temporalio/error.rb +155 -0
  109. data/lib/temporalio/internal/bridge/api/activity_result/activity_result.rb +34 -0
  110. data/lib/temporalio/internal/bridge/api/activity_task/activity_task.rb +31 -0
  111. data/lib/temporalio/internal/bridge/api/child_workflow/child_workflow.rb +33 -0
  112. data/lib/temporalio/internal/bridge/api/common/common.rb +26 -0
  113. data/lib/temporalio/internal/bridge/api/core_interface.rb +40 -0
  114. data/lib/temporalio/internal/bridge/api/external_data/external_data.rb +27 -0
  115. data/lib/temporalio/internal/bridge/api/nexus/nexus.rb +33 -0
  116. data/lib/temporalio/internal/bridge/api/workflow_activation/workflow_activation.rb +56 -0
  117. data/lib/temporalio/internal/bridge/api/workflow_commands/workflow_commands.rb +57 -0
  118. data/lib/temporalio/internal/bridge/api/workflow_completion/workflow_completion.rb +30 -0
  119. data/lib/temporalio/internal/bridge/api.rb +3 -0
  120. data/lib/temporalio/internal/bridge/client.rb +95 -0
  121. data/lib/temporalio/internal/bridge/runtime.rb +53 -0
  122. data/lib/temporalio/internal/bridge/testing.rb +66 -0
  123. data/lib/temporalio/internal/bridge/worker.rb +85 -0
  124. data/lib/temporalio/internal/bridge.rb +36 -0
  125. data/lib/temporalio/internal/client/implementation.rb +700 -0
  126. data/lib/temporalio/internal/metric.rb +122 -0
  127. data/lib/temporalio/internal/proto_utils.rb +133 -0
  128. data/lib/temporalio/internal/worker/activity_worker.rb +376 -0
  129. data/lib/temporalio/internal/worker/multi_runner.rb +213 -0
  130. data/lib/temporalio/internal/worker/workflow_instance/child_workflow_handle.rb +54 -0
  131. data/lib/temporalio/internal/worker/workflow_instance/context.rb +333 -0
  132. data/lib/temporalio/internal/worker/workflow_instance/details.rb +44 -0
  133. data/lib/temporalio/internal/worker/workflow_instance/external_workflow_handle.rb +32 -0
  134. data/lib/temporalio/internal/worker/workflow_instance/externally_immutable_hash.rb +22 -0
  135. data/lib/temporalio/internal/worker/workflow_instance/handler_execution.rb +25 -0
  136. data/lib/temporalio/internal/worker/workflow_instance/handler_hash.rb +41 -0
  137. data/lib/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rb +97 -0
  138. data/lib/temporalio/internal/worker/workflow_instance/inbound_implementation.rb +62 -0
  139. data/lib/temporalio/internal/worker/workflow_instance/outbound_implementation.rb +415 -0
  140. data/lib/temporalio/internal/worker/workflow_instance/replay_safe_logger.rb +37 -0
  141. data/lib/temporalio/internal/worker/workflow_instance/replay_safe_metric.rb +40 -0
  142. data/lib/temporalio/internal/worker/workflow_instance/scheduler.rb +163 -0
  143. data/lib/temporalio/internal/worker/workflow_instance.rb +730 -0
  144. data/lib/temporalio/internal/worker/workflow_worker.rb +236 -0
  145. data/lib/temporalio/internal.rb +7 -0
  146. data/lib/temporalio/metric.rb +109 -0
  147. data/lib/temporalio/retry_policy.rb +74 -0
  148. data/lib/temporalio/runtime.rb +314 -0
  149. data/lib/temporalio/scoped_logger.rb +96 -0
  150. data/lib/temporalio/search_attributes.rb +343 -0
  151. data/lib/temporalio/testing/activity_environment.rb +136 -0
  152. data/lib/temporalio/testing/workflow_environment.rb +383 -0
  153. data/lib/temporalio/testing.rb +10 -0
  154. data/lib/temporalio/version.rb +5 -0
  155. data/lib/temporalio/worker/activity_executor/fiber.rb +49 -0
  156. data/lib/temporalio/worker/activity_executor/thread_pool.rb +46 -0
  157. data/lib/temporalio/worker/activity_executor.rb +55 -0
  158. data/lib/temporalio/worker/interceptor.rb +362 -0
  159. data/lib/temporalio/worker/thread_pool.rb +237 -0
  160. data/lib/temporalio/worker/tuner.rb +189 -0
  161. data/lib/temporalio/worker/workflow_executor/thread_pool.rb +230 -0
  162. data/lib/temporalio/worker/workflow_executor.rb +26 -0
  163. data/lib/temporalio/worker/workflow_replayer.rb +343 -0
  164. data/lib/temporalio/worker.rb +569 -0
  165. data/lib/temporalio/workflow/activity_cancellation_type.rb +20 -0
  166. data/lib/temporalio/workflow/child_workflow_cancellation_type.rb +21 -0
  167. data/lib/temporalio/workflow/child_workflow_handle.rb +43 -0
  168. data/lib/temporalio/workflow/definition.rb +566 -0
  169. data/lib/temporalio/workflow/external_workflow_handle.rb +41 -0
  170. data/lib/temporalio/workflow/future.rb +151 -0
  171. data/lib/temporalio/workflow/handler_unfinished_policy.rb +13 -0
  172. data/lib/temporalio/workflow/info.rb +82 -0
  173. data/lib/temporalio/workflow/parent_close_policy.rb +19 -0
  174. data/lib/temporalio/workflow/update_info.rb +20 -0
  175. data/lib/temporalio/workflow.rb +529 -0
  176. data/lib/temporalio/workflow_history.rb +47 -0
  177. data/lib/temporalio.rb +11 -0
  178. data/temporalio.gemspec +28 -0
  179. metadata +234 -0
data/README.md ADDED
@@ -0,0 +1,1148 @@
1
+ <div style="overflow: hidden"><img src="https://raw.githubusercontent.com/temporalio/assets/main/files/w/ruby.png" alt="Temporal Ruby SDK" /></div>
2
+
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
+ [![MIT](https://img.shields.io/github/license/temporalio/sdk-ruby.svg?style=for-the-badge)](LICENSE)
5
+ [![Gem](https://img.shields.io/gem/v/temporalio?style=for-the-badge)](https://rubygems.org/gems/temporalio)
6
+
7
+ [Temporal](https://temporal.io/) is a distributed, scalable, durable, and highly available orchestration engine used to
8
+ execute asynchronous, long-running business logic in a scalable and resilient way.
9
+
10
+ **Temporal Ruby SDK** is the framework for authoring workflows and activities using the Ruby programming language.
11
+
12
+ Also see:
13
+
14
+ * [Ruby SDK](https://github.com/temporalio/sdk-ruby)
15
+ * [Ruby Samples](https://github.com/temporalio/samples-ruby)
16
+ * [API Documentation](https://ruby.temporal.io)
17
+
18
+ ⚠️ UNDER ACTIVE DEVELOPMENT
19
+
20
+ This SDK is under active development and has not released a stable version yet. APIs may change in incompatible ways
21
+ until the SDK is marked stable.
22
+
23
+ **NOTE: This README is for the current branch and not necessarily what's released on RubyGems.**
24
+
25
+ ---
26
+
27
+ <!-- START doctoc generated TOC please keep comment here to allow auto update -->
28
+ <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
29
+ **Contents**
30
+
31
+ - [Quick Start](#quick-start)
32
+ - [Installation](#installation)
33
+ - [Implementing a Workflow and Activity](#implementing-a-workflow-and-activity)
34
+ - [Running a Worker](#running-a-worker)
35
+ - [Executing a Workflow](#executing-a-workflow)
36
+ - [Usage](#usage)
37
+ - [Client](#client)
38
+ - [Cloud Client Using mTLS](#cloud-client-using-mtls)
39
+ - [Cloud Client Using API Key](#cloud-client-using-api-key)
40
+ - [Data Conversion](#data-conversion)
41
+ - [ActiveRecord and ActiveModel](#activerecord-and-activemodel)
42
+ - [Workers](#workers)
43
+ - [Workflows](#workflows)
44
+ - [Workflow Definition](#workflow-definition)
45
+ - [Running Workflows](#running-workflows)
46
+ - [Invoking Activities](#invoking-activities)
47
+ - [Invoking Child Workflows](#invoking-child-workflows)
48
+ - [Timers and Conditions](#timers-and-conditions)
49
+ - [Workflow Fiber Scheduling and Cancellation](#workflow-fiber-scheduling-and-cancellation)
50
+ - [Workflow Futures](#workflow-futures)
51
+ - [Workflow Utilities](#workflow-utilities)
52
+ - [Workflow Exceptions](#workflow-exceptions)
53
+ - [Workflow Logic Constraints](#workflow-logic-constraints)
54
+ - [Workflow Testing](#workflow-testing)
55
+ - [Automatic Time Skipping](#automatic-time-skipping)
56
+ - [Manual Time Skipping](#manual-time-skipping)
57
+ - [Mocking Activities](#mocking-activities)
58
+ - [Workflow Replay](#workflow-replay)
59
+ - [Activities](#activities)
60
+ - [Activity Definition](#activity-definition)
61
+ - [Activity Context](#activity-context)
62
+ - [Activity Heartbeating and Cancellation](#activity-heartbeating-and-cancellation)
63
+ - [Activity Worker Shutdown](#activity-worker-shutdown)
64
+ - [Activity Concurrency and Executors](#activity-concurrency-and-executors)
65
+ - [Activity Testing](#activity-testing)
66
+ - [Ractors](#ractors)
67
+ - [Platform Support](#platform-support)
68
+ - [Development](#development)
69
+ - [Build](#build)
70
+ - [Build Platform-specific Gem](#build-platform-specific-gem)
71
+ - [Testing](#testing)
72
+ - [Code Formatting and Type Checking](#code-formatting-and-type-checking)
73
+ - [Proto Generation](#proto-generation)
74
+
75
+ <!-- END doctoc generated TOC please keep comment here to allow auto update -->
76
+
77
+ ## Quick Start
78
+
79
+ ### Installation
80
+
81
+ The Ruby SDK works with Ruby 3.2, 3.3, and 3.4.
82
+
83
+ Can require in a Gemfile like:
84
+
85
+ ```
86
+ gem 'temporalio'
87
+ ```
88
+
89
+ Or via `gem install` like:
90
+
91
+ ```
92
+ gem install temporalio
93
+ ```
94
+
95
+ **NOTE**: Only macOS ARM/x64 and Linux ARM/x64 are supported, and the platform-specific gem chosen is based on when the
96
+ gem/bundle install is performed. A source gem is published but cannot be used directly and will fail to build if tried.
97
+ MinGW-based Windows and Linux MUSL do not have gems. See the [Platform Support](#platform-support) section for more
98
+ information.
99
+
100
+ **NOTE**: Due to [an issue](https://github.com/temporalio/sdk-ruby/issues/162), fibers (and `async` gem) are only
101
+ supported on Ruby versions 3.3 and newer.
102
+
103
+ ### Implementing a Workflow and Activity
104
+
105
+ Activities are classes. Here is an example of a simple activity that can be put in `say_hello_activity.rb`:
106
+
107
+
108
+ ```ruby
109
+ require 'temporalio/activity'
110
+
111
+ # Implementation of a simple activity
112
+ class SayHelloActivity < Temporalio::Activity::Definition
113
+ def execute(name)
114
+ "Hello, #{name}!"
115
+ end
116
+ end
117
+ ```
118
+
119
+ Workflows are also classes. To create the workflow, put the following in `say_hello_workflow.rb`:
120
+
121
+ ```ruby
122
+ require 'temporalio/workflow'
123
+ require_relative 'say_hello_activity'
124
+
125
+ class SayHelloWorkflow < Temporalio::Workflow::Definition
126
+ def execute(name)
127
+ Temporalio::Workflow.execute_activity(
128
+ SayHelloActivity,
129
+ name,
130
+ schedule_to_close_timeout: 300
131
+ )
132
+ end
133
+ end
134
+ ```
135
+
136
+ This is a simple workflow that executes the `SayHelloActivity` activity.
137
+
138
+ ### Running a Worker
139
+
140
+ To run this in a worker, put the following in `worker.rb`:
141
+
142
+ ```ruby
143
+ require 'temporalio/client'
144
+ require 'temporalio/worker'
145
+ require_relative 'say_hello_activity'
146
+ require_relative 'say_hello_workflow'
147
+
148
+ # Create a client
149
+ client = Temporalio::Client.connect('localhost:7233', 'my-namespace')
150
+
151
+ # Create a worker with the client, activities, and workflows
152
+ worker = Temporalio::Worker.new(
153
+ client:,
154
+ task_queue: 'my-task-queue',
155
+ workflows: [SayHelloWorkflow],
156
+ # There are various forms an activity can take, see "Activities" section for details
157
+ activities: [SayHelloActivity]
158
+ )
159
+
160
+ # Run the worker until SIGINT. This can be done in many ways, see "Workers" section for details.
161
+ worker.run(shutdown_signals: ['SIGINT'])
162
+ ```
163
+
164
+ Running that will run the worker until Ctrl+C is pressed.
165
+
166
+ ### Executing a Workflow
167
+
168
+ To start and wait on the workflow result, with the worker program running elsewhere, put the following in
169
+ `execute_workflow.rb`:
170
+
171
+ ```ruby
172
+ require 'temporalio/client'
173
+ require_relative 'say_hello_workflow'
174
+
175
+ # Create a client
176
+ client = Temporalio::Client.connect('localhost:7233', 'my-namespace')
177
+
178
+ # Run workflow
179
+ result = client.execute_workflow(
180
+ SayHelloWorkflow,
181
+ 'Temporal', # This is the input to the workflow
182
+ id: 'my-workflow-id',
183
+ task_queue: 'my-task-queue'
184
+ )
185
+ puts "Result: #{result}"
186
+ ```
187
+
188
+ This will output:
189
+
190
+ ```
191
+ Result: Hello, Temporal!
192
+ ```
193
+
194
+ ## Usage
195
+
196
+ ### Client
197
+
198
+ A client can be created and used to start a workflow or otherwise interact with Temporal. For example:
199
+
200
+ ```ruby
201
+ require 'temporalio/client'
202
+
203
+ # Create a client
204
+ client = Temporalio::Client.connect('localhost:7233', 'my-namespace')
205
+
206
+ # Start a workflow
207
+ handle = client.start_workflow(
208
+ MyWorkflow,
209
+ 'arg1', 'arg2',
210
+ id: 'my-workflow-id',
211
+ task_queue: 'my-task-queue'
212
+ )
213
+
214
+ # Wait for result
215
+ result = handle.result
216
+ puts "Result: #{result}"
217
+ ```
218
+
219
+ Notes about the above code:
220
+
221
+ * Temporal clients are not explicitly closed.
222
+ * To enable TLS, the `tls` option can be set to `true` or a `Temporalio::Client::Connection::TLSOptions` instance.
223
+ * Instead of `start_workflow` + `result` above, `execute_workflow` shortcut can be used if the handle is not needed.
224
+ * Both `start_workflow` and `execute_workflow` accept either the workflow class or the string/symbol name of the
225
+ workflow.
226
+ * The `handle` above is a `Temporalio::Client::WorkflowHandle` which has several other operations that can be performed
227
+ on a workflow. To get a handle to an existing workflow, use `workflow_handle` on the client.
228
+ * Clients are thread safe and are fiber-compatible (but fiber compatibility only supported for Ruby 3.3+ at this time).
229
+
230
+ #### Cloud Client Using mTLS
231
+
232
+ Assuming a client certificate is present at `my-cert.pem` and a client key is present at `my-key.pem`, this is how to
233
+ connect to Temporal Cloud:
234
+
235
+ ```ruby
236
+ require 'temporalio/client'
237
+
238
+ # Create a client
239
+ client = Temporalio::Client.connect(
240
+ 'my-namespace.a1b2c.tmprl.cloud:7233',
241
+ 'my-namespace.a1b2c',
242
+ tls: Temporalio::Client::Connection::TLSOptions.new(
243
+ client_cert: File.read('my-cert.pem'),
244
+ client_private_key: File.read('my-key.pem')
245
+ ))
246
+ ```
247
+
248
+ #### Cloud Client Using API Key
249
+
250
+ Assuming the API key is 'my-api-key', this is how to connect to Temporal cloud:
251
+
252
+ ```ruby
253
+ require 'temporalio/client'
254
+
255
+ # Create a client
256
+ client = Temporalio::Client.connect(
257
+ 'my-namespace.a1b2c.tmprl.cloud:7233',
258
+ 'my-namespace.a1b2c',
259
+ api_key: 'my-api-key'
260
+ tls: true
261
+ )
262
+ ```
263
+
264
+ #### Data Conversion
265
+
266
+ Data converters are used to convert raw Temporal payloads to/from actual Ruby types. A custom data converter can be set
267
+ via the `data_converter` keyword argument when creating a client. Data converters are a combination of payload
268
+ converters, payload codecs, and failure converters. Payload converters convert Ruby values to/from serialized bytes.
269
+ Payload codecs convert bytes to bytes (e.g. for compression or encryption). Failure converters convert exceptions
270
+ to/from serialized failures.
271
+
272
+ Data converters are in the `Temporalio::Converters` module. The default data converter uses a default payload converter,
273
+ which supports the following types:
274
+
275
+ * `nil`
276
+ * "bytes" (i.e. `String` with `Encoding::ASCII_8BIT` encoding)
277
+ * `Google::Protobuf::MessageExts` instances
278
+ * [JSON module](https://docs.ruby-lang.org/en/master/JSON.html) for everything else
279
+
280
+ This means that normal Ruby objects will use `JSON.generate` when serializing and `JSON.parse` when deserializing (with
281
+ `create_additions: true` set by default). So a Ruby object will often appear as a hash when deserialized. Also, hashes
282
+ that are passed in with symbol keys end up with string keys when deserialized. While "JSON Additions" are supported, it
283
+ is not cross-SDK-language compatible since this is a Ruby-specific construct.
284
+
285
+ The default payload converter is a collection of "encoding payload converters". On serialize, each encoding converter
286
+ will be tried in order until one accepts (default falls through to the JSON one). The encoding converter sets an
287
+ `encoding` metadata value which is used to know which converter to use on deserialize. Custom encoding converters can be
288
+ created, or even the entire payload converter can be replaced with a different implementation.
289
+
290
+ ##### ActiveRecord and ActiveModel
291
+
292
+ By default, `ActiveRecord` and `ActiveModel` objects do not natively support the `JSON` module. A mixin can be created
293
+ to add this support for `ActiveRecord`, for example:
294
+
295
+ ```ruby
296
+ module ActiveRecordJSONSupport
297
+ extend ActiveSupport::Concern
298
+ include ActiveModel::Serializers::JSON
299
+
300
+ included do
301
+ def to_json(*args)
302
+ hash = as_json
303
+ hash[::JSON.create_id] = self.class.name
304
+ hash.to_json(*args)
305
+ end
306
+
307
+ def self.json_create(object)
308
+ object.delete(::JSON.create_id)
309
+ ret = new
310
+ ret.attributes = object
311
+ ret
312
+ end
313
+ end
314
+ end
315
+ ```
316
+
317
+ Similarly, a mixin for `ActiveModel` that adds `attributes` accessors can leverage this same mixin, for example:
318
+
319
+ ```ruby
320
+ module ActiveModelJSONSupport
321
+ extend ActiveSupport::Concern
322
+ include ActiveRecordJSONSupport
323
+
324
+ included do
325
+ def attributes=(hash)
326
+ hash.each do |key, value|
327
+ send("#{key}=", value)
328
+ end
329
+ end
330
+
331
+ def attributes
332
+ instance_values
333
+ end
334
+ end
335
+ end
336
+ ```
337
+
338
+ Now `include ActiveRecordJSONSupport` or `include ActiveModelJSONSupport` will make the models work with Ruby `JSON`
339
+ module and therefore Temporal. Of course any other approach to make the models work with the `JSON` module will work as
340
+ well.
341
+
342
+ ### Workers
343
+
344
+ Workers host workflows and/or activities. Here's how to run a worker:
345
+
346
+ ```ruby
347
+ require 'temporalio/client'
348
+ require 'temporalio/worker'
349
+ require 'my_module'
350
+
351
+ # Create a client
352
+ client = Temporalio::Client.connect('localhost:7233', 'my-namespace')
353
+
354
+ # Create a worker with the client, activities, and workflows
355
+ worker = Temporalio::Worker.new(
356
+ client:,
357
+ task_queue: 'my-task-queue',
358
+ workflows: [MyModule::MyWorkflow],
359
+ # There are various forms an activity can take, see "Activities" section for details
360
+ activities: [MyModule::MyActivity]
361
+ )
362
+
363
+ # Run the worker until block complete
364
+ worker.run do
365
+ something_that_waits_for_completion
366
+ end
367
+ ```
368
+
369
+ Notes about the above code:
370
+
371
+ * A worker uses the same client that is used for other Temporal things.
372
+ * This just shows providing an activity class, but there are other forms, see the "Activities" section for details.
373
+ * The worker `run` method accepts an optional `Temporalio::Cancellation` object that can be used to cancel instead or in
374
+ addition to providing a block that waits for completion.
375
+ * The worker `run` method accepts a `shutdown_signals` array which will trap the signal and start shutdown when
376
+ received.
377
+ * Workers work with threads or fibers (but fiber compatibility only supported for Ruby 3.3+ at this time). Fiber-based
378
+ activities (see "Activities" section) only work if the worker is created within a fiber.
379
+ * The `run` method does not return until the worker is shut down. This means even if shutdown is triggered (e.g. via
380
+ `Cancellation` or block completion), it may not return immediately. Activities not completing may hang worker
381
+ shutdown, see the "Activities" section.
382
+ * Workers can have many more options not shown here (e.g. tuners and interceptors).
383
+ * The `Temporalio::Worker.run_all` class method is available for running multiple workers concurrently.
384
+
385
+ ### Workflows
386
+
387
+ #### Workflow Definition
388
+
389
+ Workflows are defined as classes that extend `Temporalio::Workflow::Definition`. The entry point for a workflow is
390
+ `execute` and must be defined. Methods for handling signals, queries, and updates are marked with `workflow_signal`,
391
+ `workflow_query`, and `workflow_update` just before the method is defined. Here is an example of a workflow definition:
392
+
393
+ ```ruby
394
+ require 'temporalio/workflow'
395
+
396
+ class GreetingWorkflow < Temporalio::Workflow::Definition
397
+ workflow_query_attr_reader :current_greeting
398
+
399
+ def execute(params)
400
+ loop do
401
+ # Call activity called CreateGreeting to create greeting and store as attribute
402
+ @current_greeting = Temporalio::Workflow.execute_activity(
403
+ CreateGreeting,
404
+ params,
405
+ schedule_to_close_timeout: 300
406
+ )
407
+ Temporalio::Workflow.logger.debug("Greeting set to #{@current_greeting}")
408
+
409
+ # Wait for param update or complete signal. Note, cancellation can occur by default
410
+ # on wait_condition calls, so Cancellation object doesn't need to be passed
411
+ # explicitly.
412
+ Temporalio::Workflow.wait_condition { @greeting_params_update || @complete }
413
+
414
+ # If there was an update, exchange and rerun. If it's _only_ a complete, finish
415
+ # workflow with the greeting.
416
+ if @greeting_params_update
417
+ params, @greeting_params_update = @greeting_params_update, nil
418
+ else
419
+ return @current_greeting
420
+ end
421
+ end
422
+ end
423
+
424
+ workflow_update
425
+ def update_greeting_params(greeting_params_update)
426
+ @greeting_params_update = greeting_params_update
427
+ end
428
+
429
+ workflow_signal
430
+ def complete_with_greeting
431
+ @complete = true
432
+ end
433
+ end
434
+ ```
435
+
436
+ Notes about the above code:
437
+
438
+ * `execute` is the primary entrypoint and its result/exception represents the workflow result/failure.
439
+ * `workflow_signal`, `workflow_query` (and the shortcut seen above, `workflow_query_attr_reader`), and `workflow_update`
440
+ implicitly create class methods usable by callers/clients. A workflow definition with no methods actually implemented
441
+ can even be created for use by clients if the workflow is implemented elsewhere and/or in another language.
442
+ * Workflow code must be deterministic. See the "Workflow Logic Constraints" section below.
443
+ * `execute_activity` accepts either the activity class or the string/symbol for the name.
444
+
445
+ The following protected class methods are available on `Temporalio::Workflow::Definition` to customize the overall
446
+ workflow definition/behavior:
447
+
448
+ * `workflow_name` - Accepts a string or symbol to change the name. Otherwise the name is defaulted to the unqualified
449
+ class name.
450
+ * `workflow_dynamic` - Marks a workflow as dynamic. Dynamic workflows do not have names and handle any workflow that is
451
+ not otherwise registered. A worker can only have one dynamic workflow. It is often useful to use `workflow_raw_args`
452
+ with this.
453
+ * `workflow_raw_args` - Have workflow arguments delivered to `execute` (and `initialize` if `workflow_init` in use) as
454
+ `Temporalio::Converters::RawValue`s. These are wrappers for the raw payloads that have not been decoded. They can be
455
+ decoded with `Temporalio::Workflow.payload_converter`. Using this with `*args` splat can be helpful in dynamic
456
+ situations.
457
+ * `workflow_failure_exception_type` - Accepts one or more exception classes that will be considered workflow failure
458
+ instead of task failure. See the "Exceptions" section later on what this means. This can be called multiple times.
459
+ * `workflow_query_attr_reader` - Is a helper that accepts one or more symbols for attributes to expose as `attr_reader`
460
+ _and_ `workflow_query`. This means it is a superset of `attr_reader` and will not work if also using `attr_reader` or
461
+ `attr_accessor`. If a writer is needed alongside this, use `attr_writer`.
462
+
463
+ The following protected class methods can be called just before defining instance methods to customize the
464
+ definition/behavior of the method:
465
+
466
+ * `workflow_init` - Mark an `initialize` method as needing the workflow start arguments. Otherwise, `initialize` must
467
+ accept no required arguments. This must be placed above the `initialize` method or it will fail.
468
+ * `workflow_signal` - Mark the next method as a workflow signal. The signal name is defaulted to the method name but can
469
+ be customized by the `name` kwarg. See the API documentation for more kwargs that can be set. Return values for
470
+ signals are discarded and exceptions raised in signal handlers are treated as if they occurred in the primary workflow
471
+ method. This also defines a class method of the same name to return the definition for use by clients.
472
+ * `workflow_query` - Mark the next method as a workflow query. The query name is defaulted to the method name but can
473
+ be customized by the `name` kwarg. See the API documentation for more kwargs that can be set. The result of the method
474
+ is the result of the query. Queries must never have any side effects, meaning they should never mutate state or try to
475
+ wait on anything. This also defines a class method of the same name to return the definition for use by clients.
476
+ * `workflow_update` - Mark the next method as a workflow update. The update 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. The result of the method
478
+ is the result of the update. This also defines a class method of the same name to return the definition for use by
479
+ clients.
480
+ * `workflow_update_validator` - Mark the next method as a validator to an update. This accepts a symbol for the
481
+ `workflow_update` method it validates. Validators are used to do early rejection of updates and must never have any
482
+ side effects, meaning they should never mutate state or try to wait on anything.
483
+
484
+ Workflows can be inherited, but subclass workflow-level decorators override superclass ones, and the same method can't
485
+ be decorated with different handler types/names in the hierarchy.
486
+
487
+ #### Running Workflows
488
+
489
+ To start a workflow from a client, you can `start_workflow` and use the resulting handle:
490
+
491
+ ```ruby
492
+ # Start the workflow
493
+ handle = my_client.start_workflow(
494
+ GreetingWorkflow,
495
+ { salutation: 'Hello', name: 'Temporal' },
496
+ id: 'my-workflow-id',
497
+ task_queue: 'my-task-queue'
498
+ )
499
+
500
+ # Check current greeting via query
501
+ puts "Current greeting: #{handle.query(GreetingWorkflow.current_greeting)}"
502
+
503
+ # Change the params via update
504
+ handle.execute_update(
505
+ GreetingWorkflow.update_greeting_params,
506
+ { salutation: 'Aloha', name: 'John' }
507
+ )
508
+
509
+ # Tell it to complete via signal
510
+ handle.signal(GreetingWorkflow.complete_with_greeting)
511
+
512
+ # Wait for workflow result
513
+ puts "Final greeting: #{handle.result}"
514
+ ```
515
+
516
+ Some things to note about the above code:
517
+
518
+ * This uses the `GreetingWorkflow` workflow from the previous section.
519
+ * The output of this code is "Current greeting: Hello, Temporal!" and "Final greeting: Aloha, John!".
520
+ * ID and task queue are required for starting a workflow.
521
+ * Signal, query, and update calls here use the class methods created on the definition for safety. So if the
522
+ `update_greeting_params` method didn't exist or wasn't marked as an update, the code will fail client side before even
523
+ attempting the call. Static typing tooling may also take advantage of this for param/result type checking.
524
+ * A helper `execute_workflow` method is available on the client that is just `start_workflow` + handle `result`.
525
+
526
+ #### Invoking Activities
527
+
528
+ * Activities are executed with `Temporalio::Workflow.execute_activity`, which accepts the activity class or a
529
+ string/symbol activity name.
530
+ * Activity options are kwargs on the `execute_activity` method. Either `schedule_to_close_timeout` or
531
+ `start_to_close_timeout` must be set.
532
+ * Other options like `retry_policy`, `cancellation_type`, etc can also be set.
533
+ * The `cancellation` can be set to a `Cancellation` to send a cancel request to the activity. By default, the
534
+ `cancellation` is the overall `Temporalio::Workflow.cancellation` which is the overarching workflow cancellation.
535
+ * Activity failures are raised from the call as `Temporalio::Error::ActivityError`.
536
+ * `execute_local_activity` exists with mostly the same options for local activities.
537
+
538
+ #### Invoking Child Workflows
539
+
540
+ * Child workflows are started with `Temporalio::Workflow.start_child_workflow`, which accepts the workflow class or
541
+ string/symbol name, arguments, and other options.
542
+ * Result for `start_child_workflow` is a `Temporalio::Workflow::ChildWorkflowHandle` which has the `id`, the ability to
543
+ wait on the `result`, and the ability to `signal` the child.
544
+ * The `start_child_workflow` call does not complete until the start has been accepted by the server.
545
+ * A helper `execute_child_workflow` method is available that is just `start_child_workflow` + handle `result`.
546
+
547
+ #### Timers and Conditions
548
+
549
+ * A timer is represented by `Temporalio::Workflow.sleep`.
550
+ * Timers are also started on `Temporalio::Workflow.timeout`.
551
+ * _Technically_ `Kernel.sleep` and `Timeout.timeout` also delegate to the above calls, but the more explicit workflow
552
+ forms are encouraged because they accept more options and are not subject to Ruby standard library implementation
553
+ changes.
554
+ * Each timer accepts a `Cancellation`, but if none is given, it defaults to `Temporalio::Workflow.cancellation`.
555
+ * `Temporalio::Workflow.wait_condition` accepts a block that waits until the evaluated block result is truthy, then
556
+ returns the value.
557
+ * This function is invoked on each iteration of the internal event loop. This means it cannot have any side effects.
558
+ * This is commonly used for checking if a variable is changed from some other part of a workflow (e.g. a signal
559
+ handler).
560
+ * Each wait conditions accepts a `Cancellation`, but if none is given, it defaults to
561
+ `Temporalio::Workflow.cancellation`.
562
+
563
+ #### Workflow Fiber Scheduling and Cancellation
564
+
565
+ Workflows are backed by a custom, deterministic `Fiber::Scheduler`. All fiber calls inside a workflow use this scheduler
566
+ to ensure coroutines run deterministically.
567
+
568
+ Every workflow contains a `Temporalio::Cancellation` at `Temporalio::Workflow.cancellation`. This is canceled when the
569
+ workflow is canceled. For all workflow calls that accept a cancellation token, this is the default. So if a workflow is
570
+ waiting on `execute_activity` and the workflow is canceled, that cancellation will propagate to the waiting activity.
571
+
572
+ `Cancellation`s may be created to perform cancellation more specifically. A `Cancellation` token derived from the
573
+ workflow one can be created via `my_cancel, my_cancel_proc = Cancellation.new(Temporalio::Workflow.cancellation)`. Then
574
+ `my_cancel` can be passed as `cancellation` to cancel something more specifically when `my_cancel_proc.call` is invoked.
575
+
576
+ `Cancellation`s don't have to be derived from the workflow one, they can just be created standalone or "detached". This
577
+ is useful for executing, say, a cleanup activity in an `ensure` block that needs to run even on cancel. If the cleanup
578
+ activity had instead used the workflow cancellation or one derived from it, then on cancellation it would be cancelled
579
+ before it even started.
580
+
581
+ #### Workflow Futures
582
+
583
+ `Temporalio::Workflow::Future` can be used for running things in the background or concurrently. This is basically a
584
+ safe wrapper around `Fiber.schedule` for starting and `Workflow.wait_condition` for waiting.
585
+
586
+ Nothing uses futures by default, but they work with all workflow code/constructs. For instance, to run 3 activities and
587
+ wait for them all to complete, something like this can be written:
588
+
589
+ ```ruby
590
+ # Start 3 activities in background
591
+ fut1 = Temporalio::Workflow::Future.new do
592
+ Temporalio::Workflow.execute_activity(MyActivity1, schedule_to_close_timeout: 300)
593
+ end
594
+ fut2 = Temporalio::Workflow::Future.new do
595
+ Temporalio::Workflow.execute_activity(MyActivity2, schedule_to_close_timeout: 300)
596
+ end
597
+ fut3 = Temporalio::Workflow::Future.new do
598
+ Temporalio::Workflow.execute_activity(MyActivity3, schedule_to_close_timeout: 300)
599
+ end
600
+
601
+ # Wait for them all to complete
602
+ Temporalio::Workflow::Future.all_of(fut1, fut2, fut3).wait
603
+
604
+ Temporalio::Workflow.logger.debug("Got: #{fut1.result}, #{fut2.result}, #{fut3.result}")
605
+ ```
606
+
607
+ Or, say, to wait on the first of 5 activities or a timeout to complete:
608
+
609
+ ```ruby
610
+ # Start 5 activities
611
+ act_futs = 5.times.map do |i|
612
+ Temporalio::Workflow::Future.new do
613
+ Temporalio::Workflow.execute_activity(MyActivity, "my-arg-#{i}", schedule_to_close_timeout: 300)
614
+ end
615
+ end
616
+ # Start a timer
617
+ sleep_fut = Temporalio::Workflow::Future.new { Temporalio::Workflow.sleep(30) }
618
+
619
+ # Wait for first act result or sleep fut
620
+ act_result = Temporalio::Workflow::Future.any_of(sleep_fut, *act_futs).wait
621
+ # Fail if timer done first
622
+ raise Temporalio::Error::ApplicationError, 'Timer expired' if sleep_fut.done?
623
+ # Print act result otherwise
624
+ puts "Act result: #{act_result}"
625
+ ```
626
+
627
+ There are several other details not covered here about futures, such as how exceptions are handled, how to use a setter
628
+ proc instead of a block, etc. See the API documentation for details.
629
+
630
+ #### Workflow Utilities
631
+
632
+ In addition to the pieces documented above, additional methods are available on `Temporalio::Workflow` that can be used
633
+ from workflows including:
634
+
635
+ * `in_workflow?` - Returns `true` if in the workflow or `false` otherwise. This is the only method on the class that can
636
+ be called outside of a workflow without raising an exception.
637
+ * `info` - Immutable workflow information.
638
+ * `logger` - A Ruby logger that adds contextual information and takes care not to log on replay.
639
+ * `metric_meter` - A metric meter for making custom metrics that adds contextual information and takes care not to
640
+ record on replay.
641
+ * `random` - A deterministic `Random` instance.
642
+ * `memo` - A read-only hash of the memo (updated via `upsert_memo`).
643
+ * `search_attributes` - A read-only `SearchAttributes` collection (updated via `upsert_search_attributes`).
644
+ * `now` - Current, deterministic UTC time for the workflow.
645
+ * `all_handlers_finished?` - Returns true when all signal and update handlers are done. Useful as
646
+ `Temporalio::Workflow.wait_condition { Temporalio::Workflow.all_handlers_finished? }` for making sure not to return
647
+ from the primary workflow method until all handlers are done.
648
+ * `patched` and `deprecate_patch` - Support for patch-based versioning inside the workflow.
649
+ * `continue_as_new_suggested` - Returns true when the server recommends performing a continue as new.
650
+ * `current_update_info` - Returns `Temporalio::Workflow::UpdateInfo` if the current code is inside an update, or nil
651
+ otherwise.
652
+ * `external_workflow_handle` - Obtain an handle to an external workflow for signalling or cancelling.
653
+ * `payload_converter` - Payload converter if needed for converting raw args.
654
+ * `signal_handlers`, `query_handlers`, and `update_handlers` - Hashes for the current set of handlers keyed by name (or
655
+ nil key for dynamic). `[]=` or `store` can be called on these to update the handlers, though defined handlers are
656
+ encouraged over runtime-set ones.
657
+
658
+ `Temporalio::Workflow::ContinueAsNewError` can be raised to continue-as-new the workflow. It accepts positional args and
659
+ defaults the workflow to the same as the current, though it can be changed with the `workflow` kwarg. See API
660
+ documentation for other details.
661
+
662
+ #### Workflow Exceptions
663
+
664
+ * Workflows can raise exceptions to fail the workflow/update or the "workflow task" (i.e. suspend the workflow, retrying
665
+ until code update allows it to continue).
666
+ * By default, exceptions that are instances of `Temporalio::Error::Failure` (or `Timeout::Error`) will fail the
667
+ workflow/update with that exception.
668
+ * For failing the workflow/update explicitly with a user exception, explicitly raise
669
+ `Temporalio::Error::ApplicationError`. This can be marked non-retryable or include details as needed.
670
+ * Other exceptions that come from activity execution, child execution, cancellation, etc are already instances of
671
+ `Temporalio::Error::Failure` and will fail the workflow/update if uncaught.
672
+ * By default, all other exceptions fail the "workflow task" which means the workflow/update will continually retry until
673
+ the code is fixed. This is helpful for bad code or other non-predictable exceptions. To actually fail the
674
+ workflow/update, use `Temporalio::Error::ApplicationError` as mentioned above.
675
+ * By default, all non-deterministic exceptions that are detected internally fail the "workflow task".
676
+
677
+ The default behavior can be customized at the worker level for all workflows via the
678
+ `workflow_failure_exception_types` worker option or per workflow via the `workflow_failure_exception_type` definition
679
+ method on the workflow itself. When a workflow encounters a "workflow task" fail (i.e. suspend), it will first check
680
+ either of these collections to see if the exception is an instance of any of the types and if so, will turn into a
681
+ workflow/update failure. As a special case, when a non-deterministic exception occurs and
682
+ `Temporalio::Workflow::NondeterminismError` is assignable to any of the types in the collection, that too
683
+ will turn into a workflow/update failure. However unlike other exceptions, non-deterministic exceptions that match
684
+ during update handlers become workflow failures not update failures because a non-deterministic exception is an
685
+ entire-workflow-failure situation.
686
+
687
+ #### Workflow Logic Constraints
688
+
689
+ Temporal Workflows [must be deterministic](https://docs.temporal.io/workflows#deterministic-constraints), which includes
690
+ Ruby workflows. This means there are several things workflows cannot do such as:
691
+
692
+ * Perform IO (network, disk, stdio, etc)
693
+ * Access/alter external mutable state
694
+ * Do any threading
695
+ * Do anything using the system clock (e.g. `Time.Now`)
696
+ * Make any random calls
697
+ * Make any not-guaranteed-deterministic calls
698
+
699
+ #### Workflow Testing
700
+
701
+ Workflow testing can be done in an integration-test fashion against a real server. However, it is hard to simulate
702
+ timeouts and other long time-based code. Using the time-skipping workflow test environment can help there.
703
+
704
+ A non-time-skipping `Temporalio::Testing::WorkflowEnvironment` can be started via `start_local` which supports all
705
+ standard Temporal features. It is actually a real Temporal server lazily downloaded on first use and run as a
706
+ subprocess in the background.
707
+
708
+ A time-skipping `Temporalio::Testing::WorkflowEnvironment` can be started via `start_time_skipping` which is a
709
+ reimplementation of the Temporal server with special time skipping capabilities. This too lazily downloads the process
710
+ to run when first called. Note, this class is not thread safe nor safe for use with independent tests. It can be reused,
711
+ but only for one test at a time because time skipping is locked/unlocked at the environment level. Note, the
712
+ time-skipping test server does not work on ARM-based processors at this time, though macOS ARM users can use it via the
713
+ built-in x64 translation in macOS.
714
+
715
+ ##### Automatic Time Skipping
716
+
717
+ Anytime a workflow result is waited on, the time-skipping server automatically advances to the next event it can. To
718
+ manually advance time before waiting on the result of the workflow, the `WorkflowEnvironment.sleep` method can be used
719
+ on the environment itself. If an activity is running, time-skipping is disabled.
720
+
721
+ Here's a simple example of a workflow that sleeps for 24 hours:
722
+
723
+ ```ruby
724
+ require 'temporalio/workflow'
725
+
726
+ class WaitADayWorkflow < Temporalio::Workflow::Definition
727
+ def execute
728
+ Temporalio::Workflow.sleep(1 * 24 * 60 * 60)
729
+ 'all done'
730
+ end
731
+ end
732
+ ```
733
+
734
+ A regular integration test of this workflow on a normal server would be way too slow. However, the time-skipping server
735
+ automatically skips to the next event when we wait on the result. Here's a minitest for that workflow:
736
+
737
+ ```ruby
738
+ class MyTest < Minitest::Test
739
+ def test_wait_a_day
740
+ Temporalio::Testing::WorkflowEnvironment.start_time_skipping do |env|
741
+ worker = Temporalio::Worker.new(
742
+ client: env.client,
743
+ task_queue: "tq-#{SecureRandom.uuid}",
744
+ workflows: [WaitADayWorkflow],
745
+ workflow_executor: Temporalio::Worker::WorkflowExecutor::ThreadPool.default
746
+ )
747
+ worker.run do
748
+ result = env.client.execute_workflow(
749
+ WaitADayWorkflow,
750
+ id: "wf-#{SecureRandom.uuid}",
751
+ task_queue: worker.task_queue
752
+ )
753
+ assert_equal 'all done', result
754
+ end
755
+ end
756
+ end
757
+ end
758
+ ```
759
+
760
+ This test will run almost instantly. This is because by calling `execute_workflow` on our client, we are actually
761
+ calling `start_workflow` + handle `result`, and `result` automatically skips time as much as it can (basically until the
762
+ end of the workflow or until an activity is run).
763
+
764
+ To disable automatic time-skipping while waiting for a workflow result, run code inside a block passed to
765
+ `auto_time_skipping_disabled`.
766
+
767
+ ##### Manual Time Skipping
768
+
769
+ Until a workflow is waited on, all time skipping in the time-skipping environment is done manually via
770
+ `WorkflowEnvironment.sleep`.
771
+
772
+ Here's a workflow that waits for a signal or times out:
773
+
774
+ ```ruby
775
+ require 'temporalio/workflow'
776
+
777
+ class SignalWorkflow < Temporalio::Workflow::Definition
778
+ def execute
779
+ Temporalio::Workflow.timeout(45) do
780
+ Temporalio::Workflow.wait_condition { @signal_received }
781
+ 'got signal'
782
+ rescue Timeout::Error
783
+ 'got timeout'
784
+ end
785
+ end
786
+
787
+ workflow_signal
788
+ def some_signal
789
+ @signal_received = true
790
+ end
791
+ end
792
+ ```
793
+
794
+ To test a normal signal, you might:
795
+
796
+ ```ruby
797
+ class MyTest < Minitest::Test
798
+ def test_signal_workflow_success
799
+ Temporalio::Testing::WorkflowEnvironment.start_time_skipping do |env|
800
+ worker = Temporalio::Worker.new(
801
+ client: env.client,
802
+ task_queue: "tq-#{SecureRandom.uuid}",
803
+ workflows: [SignalWorkflow],
804
+ workflow_executor: Temporalio::Worker::WorkflowExecutor::ThreadPool.default
805
+ )
806
+ worker.run do
807
+ handle = env.client.start_workflow(
808
+ SignalWorkflow,
809
+ id: "wf-#{SecureRandom.uuid}",
810
+ task_queue: worker.task_queue
811
+ )
812
+ handle.signal(SignalWorkflow.some_signal)
813
+ assert_equal 'got signal', handle.result
814
+ end
815
+ end
816
+ end
817
+ end
818
+ ```
819
+
820
+ But how would you test the timeout part? Like so:
821
+
822
+ ```ruby
823
+ class MyTest < Minitest::Test
824
+ def test_signal_workflow_timeout
825
+ Temporalio::Testing::WorkflowEnvironment.start_time_skipping do |env|
826
+ worker = Temporalio::Worker.new(
827
+ client: env.client,
828
+ task_queue: "tq-#{SecureRandom.uuid}",
829
+ workflows: [SignalWorkflow],
830
+ workflow_executor: Temporalio::Worker::WorkflowExecutor::ThreadPool.default
831
+ )
832
+ worker.run do
833
+ handle = env.client.start_workflow(
834
+ SignalWorkflow,
835
+ id: "wf-#{SecureRandom.uuid}",
836
+ task_queue: worker.task_queue
837
+ )
838
+ env.sleep(50)
839
+ assert_equal 'got timeout', handle.result
840
+ end
841
+ end
842
+ end
843
+ end
844
+ ```
845
+
846
+ This test will run almost instantly. The `env.sleep(50)` manually skips 50 seconds of time, allowing the timeout to be
847
+ triggered without actually waiting the full 45 seconds to time out.
848
+
849
+ ##### Mocking Activities
850
+
851
+ When testing workflows, often you don't want to actually run the activities. Activities are just classes that extend
852
+ `Temporalio::Activity::Definition`. Simply write different/empty/fake/asserting ones and pass those to the worker to
853
+ have different activities called during the test. You may need to use `activity_name :MyRealActivityClassName` inside
854
+ the mock activity class to make it appear as the real name.
855
+
856
+ #### Workflow Replay
857
+
858
+ Given a workflow's history, it can be replayed locally to check for things like non-determinism errors. For example,
859
+ assuming the `history_json` parameter below is given a JSON string of history exported from the CLI or web UI, the
860
+ following function will replay it:
861
+
862
+ ```ruby
863
+ def replay_from_json(history_json)
864
+ replayer = Temporalio::Worker::WorkflowReplayer.new(workflows: [MyWorkflow])
865
+ replayer.replay_workflow(Temporalio::WorkflowHistory.from_history_json(history_json))
866
+ end
867
+ ```
868
+
869
+ If there is a non-determinism, this will raise an exception by default.
870
+
871
+ Workflow history can be loaded from more than just JSON. It can be fetched individually from a workflow handle, or even
872
+ in a list. For example, the following code will check that all workflow histories for a certain workflow type (i.e.
873
+ workflow class) are safe with the current workflow code.
874
+
875
+ ```ruby
876
+ def check_past_histories(client)
877
+ replayer = Temporalio::Worker::WorkflowReplayer.new(workflows: [MyWorkflow])
878
+ results = replayer.replay_workflows(client.list_workflows("WorkflowType = 'MyWorkflow'").map do |desc|
879
+ client.workflow_handle(desc.id, run_id: desc.run_id).fetch_history
880
+ end)
881
+ results.each { |res| raise res.replay_failure if res.replay_failure }
882
+ end
883
+ ```
884
+
885
+ But this only raises at the end because by default `replay_workflows` does not raise on failure like `replay_workflow`
886
+ does. The `raise_on_replay_failure: true` parameter could be set, or the replay worker can be used to process each one
887
+ like so:
888
+
889
+ ```ruby
890
+ def check_past_histories(client)
891
+ Temporalio::Worker::WorkflowReplayer.new(workflows: [MyWorkflow]) do |worker|
892
+ client.list_workflows("WorkflowType = 'MyWorkflow'").each do |desc|
893
+ worker.replay_workflow(client.workflow_handle(desc.id, run_id: desc.run_id).fetch_history)
894
+ end
895
+ end
896
+ end
897
+ ```
898
+
899
+ See the `WorkflowReplayer` API documentation for more details.
900
+
901
+ ### Activities
902
+
903
+ #### Activity Definition
904
+
905
+ Activities can be defined in a few different ways. They are usually classes, but manual definitions are supported too.
906
+
907
+ Here is a common activity definition:
908
+
909
+ ```ruby
910
+ class FindUserActivity < Temporalio::Activity::Definition
911
+ def execute(user_id)
912
+ User.find(user_id)
913
+ end
914
+ end
915
+ ```
916
+
917
+ Activities are defined as classes that extend `Temporalio::Activity::Definition` and provide an `execute` method. When
918
+ this activity is provided to the worker as a _class_ (e.g. `activities: [FindUserActivity]`), it will be instantiated
919
+ for _every attempt_. Many users may prefer using the same instance across activities, for example:
920
+
921
+ ```ruby
922
+ class FindUserActivity < Temporalio::Activity
923
+ def initialize(db)
924
+ @db = db
925
+ end
926
+
927
+ def execute(user_id)
928
+ @db[:users].first(id: user_id)
929
+ end
930
+ end
931
+ ```
932
+
933
+ When this is provided to a worker as an instance of the activity (e.g. `activities: [FindUserActivity.new(my_db)]`) then
934
+ the same instance is reused for each activity.
935
+
936
+ Some notes about activity definition:
937
+
938
+ * Temporal activities are identified by their name (or sometimes referred to as "activity type"). This defaults to the
939
+ unqualified class name of the activity, but can be customized by calling the `activity_name` class method.
940
+ * Long running activities should heartbeat regularly, see "Activity Heartbeating and Cancellation" later.
941
+ * By default every activity attempt is executed in a thread on a thread pool, but fibers are also supported. See
942
+ "Activity Concurrency and Executors" section later for more details.
943
+ * Technically an activity definition can be created manually via `Temporalio::Activity::Definition::Info.new` that
944
+ accepts a proc or a block, but the class form is recommended.
945
+ * `activity_dynamic` can be used to mark an activity dynamic. Dynamic activities do not have names and handle any
946
+ activity that is not otherwise registered. A worker can only have one dynamic activity.
947
+ * `workflow_raw_args` can be used to have activity arguments delivered to `execute` as
948
+ `Temporalio::Converters::RawValue`s. These are wrappers for the raw payloads that have not been converted to types
949
+ (but they have been decoded by the codec if present). They can be converted with `payload_converter` on the context.
950
+
951
+ #### Activity Context
952
+
953
+ When running in an activity, the `Temporalio::Activity::Context` is available via
954
+ `Temporalio::Activity::Context.current` which is backed by a thread/fiber local. In addition to other more advanced
955
+ things, this context provides:
956
+
957
+ * `info` - Information about the running activity.
958
+ * `heartbeat` - Method to call to issue an activity heartbeat (see "Activity Heartbeating and Cancellation" later).
959
+ * `cancellation` - Instance of `Temporalio::Cancellation` canceled when an activity is canceled (see
960
+ "Activity Heartbeating and Cancellation" later).
961
+ * `worker_shutdown_cancellation` - Instance of `Temporalio::Cancellation` canceled when worker is shutting down (see
962
+ "Activity Worker Shutdown" later).
963
+ * `logger` - Logger that automatically appends a hash with some activity info to every message.
964
+
965
+ #### Activity Heartbeating and Cancellation
966
+
967
+ In order for a non-local activity to be notified of server-side cancellation requests, it must regularly invoke
968
+ `heartbeat` on the `Temporalio::Activity::Context` instance (available via `Temporalio::Activity::Context.current`). It
969
+ is strongly recommended that all but the fastest executing activities call this function regularly.
970
+
971
+ In addition to obtaining cancellation information, heartbeats also support detail data that is persisted on the server
972
+ for retrieval during activity retry. If an activity calls `heartbeat(123)` and then fails and is retried,
973
+ `Temporalio::Activity::Context.current.info.heartbeat_details.first` will be `123`.
974
+
975
+ An activity can be canceled for multiple reasons, some server-side and some worker side. Server side cancellation
976
+ reasons include workflow canceling the activity, workflow completing, or activity timing out. On the worker side, the
977
+ activity can be canceled on worker shutdown (see next section). By default cancellation is relayed two ways - by marking
978
+ the `cancellation` on `Temporalio::Activity::Context` as canceled, and by issuing a `Thread.raise` or `Fiber.raise` with
979
+ the `Temporalio::Error::CanceledError`.
980
+
981
+ The `raise`-by-default approach was chosen because it is dangerous to the health of the system and the continued use of
982
+ worker slots to require activities opt-in to checking for cancellation by default. But if this behavior is not wanted,
983
+ `activity_cancel_raise false` class method can be called at the top of the activity which will disable the `raise`
984
+ behavior and just set the `cancellation` as canceled.
985
+
986
+ If needing to shield work from being canceled, the `shield` call on the `Temporalio::Cancellation` object can be used
987
+ with a block for the code to be shielded. The cancellation will not take effect on the cancellation object nor the raise
988
+ call while the work is shielded (regardless of nested depth). Once the shielding is complete, the cancellation will take
989
+ effect, including `Thread.raise`/`Fiber.raise` if that remains enabled.
990
+
991
+ #### Activity Worker Shutdown
992
+
993
+ An activity can react to a worker shutdown specifically and also a normal cancellation will be sent. A worker will not
994
+ complete its shutdown while an activity is in progress.
995
+
996
+ Upon worker shutdown, the `worker_shutdown_cancellation` cancellation on `Temporalio::Activity::Context` will be
997
+ canceled. Then the worker will wait a for a grace period set by the `graceful_shutdown_period` worker option (default 0)
998
+ before issuing actual cancellation to all still-running activities.
999
+
1000
+ Worker shutdown will then wait on all activities to complete. If a long-running activity does not respect cancellation,
1001
+ the shutdown may never complete.
1002
+
1003
+ #### Activity Concurrency and Executors
1004
+
1005
+ By default, activities run in the "thread pool executor" (i.e. `Temporalio::Worker::ActivityExecutor::ThreadPool`). This
1006
+ default is shared across all workers and is a naive thread pool that continually makes threads as needed when none are
1007
+ idle/available to handle incoming work. If a thread sits idle long enough, it will be killed.
1008
+
1009
+ The maximum number of concurrent activities a worker will run at a time is configured via its `tuner` option. The
1010
+ default is `Temporalio::Worker::Tuner.create_fixed` which defaults to 100 activities at a time for that worker. When
1011
+ this value is reached, the worker will stop asking for work from the server until there are slots available again.
1012
+
1013
+ In addition to the thread pool executor, there is also a fiber executor in the default executor set. To use fibers, call
1014
+ `activity_executor :fiber` class method at the top of the activity class (the default of this value is `:default` which
1015
+ is the thread pool executor). Activities can only choose the fiber executor if the worker has been created and run in a
1016
+ fiber, but thread pool executor is always available. Currently due to
1017
+ [an issue](https://github.com/temporalio/sdk-ruby/issues/162), workers can only run in a fiber on Ruby versions 3.3 and
1018
+ newer.
1019
+
1020
+ Technically the executor can be customized. The `activity_executors` worker option accepts a hash with the key as the
1021
+ symbol and the value as a `Temporalio::Worker::ActivityExecutor` implementation. Users should usually not need to
1022
+ customize this. If general code is needed to run around activities, users should use interceptors instead.
1023
+
1024
+ #### Activity Testing
1025
+
1026
+ Unit testing an activity can be done via the `Temporalio::Testing::ActivityEnvironment` class. Simply instantiate the
1027
+ class, then invoke `run` with the activity to test and the arguments to give. The result will be the activity result or
1028
+ it will raise the error raised in the activity.
1029
+
1030
+ The constructor of the environment has multiple keyword arguments that can be set to affect the activity context for the
1031
+ activity.
1032
+
1033
+ ### Ractors
1034
+
1035
+ It was an original goal to have workflows actually be Ractors for deterministic state isolation and have the library
1036
+ support Ractors in general. However, due to the SDK's heavy use of the Google Protobuf library which
1037
+ [is not Ractor-safe](https://github.com/protocolbuffers/protobuf/issues/19321), the Temporal Ruby SDK does not currently
1038
+ work with Ractors.
1039
+
1040
+ ### Platform Support
1041
+
1042
+ This SDK is backed by a Ruby C extension written in Rust leveraging the
1043
+ [Temporal Rust Core](https://github.com/temporalio/sdk-core). Gems are currently published for the following platforms:
1044
+
1045
+ * `aarch64-linux`
1046
+ * `x86_64-linux`
1047
+ * `arm64-darwin`
1048
+ * `x86_64-darwin`
1049
+
1050
+ This means Linux and macOS for ARM and x64 have published gems. Currently, a gem is not published for
1051
+ `aarch64-linux-musl` so Alpine Linux users may need to build from scratch or use a libc-based distro.
1052
+
1053
+ Due to [an issue](https://github.com/temporalio/sdk-ruby/issues/172) with Windows and multi-threaded Rust, MinGW-based
1054
+ Windows (i.e. `x64-mingw-ucrt`) is not supported. But WSL is supported using the normal Linux gem.
1055
+
1056
+ At this time a pure source gem is published for documentation reasons, but it cannot be built and will fail if tried.
1057
+ Building from source requires many files across submodules and requires Rust to be installed. See the [Build](#build)
1058
+ section for how to build a the repository.
1059
+
1060
+ The SDK works on Ruby 3.1+, but due to [an issue](https://github.com/temporalio/sdk-ruby/issues/162), fibers (and
1061
+ `async` gem) are only supported on Ruby versions 3.3 and newer.
1062
+
1063
+ ## Development
1064
+
1065
+ ### Build
1066
+
1067
+ Prerequisites:
1068
+
1069
+ * [Ruby](https://www.ruby-lang.org/) >= 3.1 (i.e. `ruby` and `bundle` on the `PATH`)
1070
+ * [Rust](https://www.rust-lang.org/) latest stable (i.e. `cargo` on the `PATH`)
1071
+ * This repository, cloned recursively
1072
+ * Change to the `temporalio/` directory
1073
+
1074
+ First, install dependencies:
1075
+
1076
+ bundle install
1077
+
1078
+ To build shared library for development use:
1079
+
1080
+ bundle exec rake compile
1081
+
1082
+ **NOTE**: This will make the current directory usable for the current Ruby version by putting the shared library
1083
+ `lib/temporalio/internal/bridge/temporalio_bridge.<ext>` in the proper place. But this development shared library may
1084
+ not work for other Ruby versions or other OS/arch combinations. For that, see "Build Platform-specific Gem" below.
1085
+
1086
+ **NOTE**: This is not `compile:dev` because debug-mode in Rust has
1087
+ [an issue](https://github.com/rust-lang/rust/issues/34283) that causes runtime stack size problems.
1088
+
1089
+ To lint, build, and test:
1090
+
1091
+ bundle exec rake
1092
+
1093
+ #### Build Platform-specific Gem
1094
+
1095
+ The standard `bundle exec rake build` will produce a gem in the `pkg` directory, but that gem will not be usable because
1096
+ the shared library is not present (neither the Rust code nor the compiled form). To create a platform-specific gem that
1097
+ can be used, `rb-sys-dock` must be run. See the
1098
+ [Cross-Compilation documentation](https://oxidize-rb.github.io/rb-sys/tutorial/publishing/cross-compilation.html) in the
1099
+ `rb-sys` repository. For example, running:
1100
+
1101
+ bundle exec rb-sys-dock --platform x86_64-linux --ruby-versions 3.2,3.3 --build
1102
+
1103
+ Will create a `pkg/temporalio-<version>-x86_64-linux.gem` file that can be used in x64 Linux environments on both Ruby
1104
+ 3.2 and Ruby 3.3 because it contains the shared libraries. For this specific example, the shared libraries are inside
1105
+ the gem at `lib/temporalio/internal/bridge/3.2/temporalio_bridge.so` and
1106
+ `lib/temporalio/internal/bridge/3.3/temporalio_bridge.so`.
1107
+
1108
+ ### Testing
1109
+
1110
+ This project uses `minitest`. To test:
1111
+
1112
+ bundle exec rake test
1113
+
1114
+ Can add options via `TESTOPTS`. E.g. single test:
1115
+
1116
+ bundle exec rake test TESTOPTS="--name=test_some_method"
1117
+
1118
+ E.g. all starting with prefix:
1119
+
1120
+ bundle exec rake test TESTOPTS="--name=/^test_some_method_prefix/"
1121
+
1122
+ E.g. all for a class:
1123
+
1124
+ bundle exec rake test TESTOPTS="--name=/SomeClassName/"
1125
+
1126
+ E.g. show all test names while executing:
1127
+
1128
+ bundle exec rake test TESTOPTS="--verbose"
1129
+
1130
+ ### Code Formatting and Type Checking
1131
+
1132
+ This project uses `rubocop`:
1133
+
1134
+ bundle exec rake rubocop:autocorrect
1135
+
1136
+ This project uses `steep`. First may need the RBS collection:
1137
+
1138
+ bundle exec rake rbs:install_collection
1139
+
1140
+ Now can run `steep`:
1141
+
1142
+ bundle exec rake steep
1143
+
1144
+ ### Proto Generation
1145
+
1146
+ Run:
1147
+
1148
+ bundle exec rake proto:generate