@@ -23,12 +23,17 @@ At this point the SDK only supports the **Temporal Client** capabilities:
23
23
- gRPC access to Temporal Server
24
24
- Temporal Cloud is not yet supported due to the lack of TLS support, but it's coming soon
25
25
26
+ As well as ** Activity Worker** capabilities:
27
+
28
+ - Definiting activities
29
+ - Activity heartbeats/cancellations
30
+ - Running activity workers
26
31
27
32
## Quick Start
28
33
29
34
### Installation
30
35
31
- Add the [ ` temporalio ` gem] ( https://rubygems.org/gems/temporalio ) to your Gemfile:
36
+ Add the [ temporalio gem] ( https://rubygems.org/gems/temporalio ) to your Gemfile:
32
37
33
38
``` ruby
34
39
gem ' temporalio'
@@ -82,12 +87,164 @@ The default data converter supports converting multiple types including:
82
87
83
88
- ` nil `
84
89
- bytes (` String ` with ` Encoding::ASCII_8BIT ` )
85
- - Anything that [ ` JSON.generate ` ] ( https://ruby-doc.org/stdlib-3.0.0/libdoc/json/rdoc/JSON.html#method-i-generate )
90
+ - Anything that [ JSON.generate] ( https://ruby-doc.org/stdlib-3.0.0/libdoc/json/rdoc/JSON.html#method-i-generate )
86
91
supports
87
92
88
93
This notably doesn't include any ` Date ` , ` Time ` , or ` DateTime ` objects as they may not work across
89
94
different SDKs. A custom payload converter can be implemented to support these.
90
95
96
+ ### Workers
97
+
98
+ Workers host workflows (coming soon) and/or activities. Here's how to run a worker:
99
+
100
+ ``` ruby
101
+ require ' temporal'
102
+
103
+ # Establish a gRPC connection to the server
104
+ connection = Temporal ::Connection .new (' localhost:7233' )
105
+
106
+ # Initialize a new worker with your activities
107
+ worker = Temporal ::Worker .new (connection, ' my-namespace' , ' my-task-queue' , activities: [MyActivity ])
108
+
109
+ # Occupy the thread by running the worker
110
+ worker.run
111
+ ```
112
+
113
+ Some things to note about the above code:
114
+
115
+ - This creates/uses the same connection that is used for initializing a client
116
+ - Workers can have many more options not shown here (e.g. data converters and interceptors)
117
+
118
+ In order to have more control over running of a worker you can provide a block of code by the end of
119
+ which the worker will shut itself down:
120
+
121
+ ``` ruby
122
+ # Initialize worker_1, worker_2 and worker_3 as in the example above
123
+
124
+ # Run the worker for 5 seconds, then shut down
125
+ worker_1.run { sleep 5 }
126
+
127
+ # Or shut the worker down when a workflow completes (very useful for running specs):
128
+ client = Temporal ::Client .new (connection, ' my-namespace' )
129
+ handle = client.start_workflow(' MyWorkflow' , ' some input' , id: ' my-id' , task_queue: ' my-task-queue' )
130
+ worker_2.run { handle.result }
131
+
132
+ # Or wait for some external signal to stop the worker
133
+ stop_queue = Queue .new
134
+ Signal .trap (' USR1' ) { stop_queue.close }
135
+ worker_3.run { stop_queue.pop }
136
+ ```
137
+
138
+ You can also shut down a running worker by calling ` Temporal::Worker#shutdown ` from a separate
139
+ thread at any time.
140
+
141
+ #### Running multiple workers
142
+
143
+ In order to run multiple workers in the same thread you can use the ` Temporal::Worker.run ` method:
144
+
145
+ ``` ruby
146
+ # Initialize workers
147
+ worker_1 = Temporal ::Worker .new (connection, ' my-namespace-1' , ' my-task-queue-1' , activities: [MyActivity1 , MyActivity2 ])
148
+ worker_2 = Temporal ::Worker .new (connection, ' my-namespace-2' , ' my-task-queue-1' , activities: [MyActivity3 ])
149
+ worker_3 = Temporal ::Worker .new (connection, ' my-namespace-1' , ' my-task-queue-2' , activities: [MyActivity4 ])
150
+
151
+ Temporal ::Worker .run(worker_1, worker_2, worker_3)
152
+ ```
153
+
154
+ Please note that similar to ` Temporal::Worker#run ` , ` Temporal::Worker.run ` accepts a block that
155
+ behaves the same way — the workers will be shut down when the block finishes.
156
+
157
+ #### Worker Shutdown
158
+
159
+ The ` Temporal::Worker#run ` (as well as ` Temporal::Worker#shutdown ` ) invocation will wait on all
160
+ activities to complete, so if a long-running activity does not at least respect cancellation, the
161
+ shutdown may never complete.
162
+
163
+ ### Activities
164
+
165
+ #### Definition
166
+
167
+ Activities are defined by subclassing ` Temporal::Activity ` class:
168
+
169
+ ``` ruby
170
+ class SayHelloActivity < Temporal ::Activity
171
+ # Optionally specify a custom activity name:
172
+ # (The class name `SayHelloActivity` will be used by default)
173
+ activity_name ' say-hello'
174
+
175
+ def execute (name )
176
+ return " Hello, #{ name } !"
177
+ end
178
+ end
179
+ ```
180
+
181
+ Some things to note about activity definitions:
182
+
183
+ - Long running activities should regularly heartbeat and handle cancellation
184
+ - Activities can only have positional arguments. Best practice is to only take a single argument
185
+ that is an object/dataclass of fields that can be added to as needed.
186
+
187
+ #### Activity Context
188
+
189
+ Activity classes have access to ` Temporal::Activity::Context ` via the ` activity ` method. Which
190
+ itself provides access to useful methods, specifically:
191
+
192
+ - ` info ` - Returns the immutable info of the currently running activity
193
+ - ` heartbeat(*details) ` - Record a heartbeat
194
+ - ` cancelled? ` - Whether a cancellation has been requested on this activity
195
+ - ` shield ` - Prevent cancel exception from being thrown during the provided block of code
196
+
197
+ ##### Heartbeating and Cancellation
198
+
199
+ In order for a non-local activity to be notified of cancellation requests, it must call
200
+ ` activity.heartbeat ` . It is strongly recommended that all but the fastest executing activities call
201
+ this method regularly.
202
+
203
+ In addition to obtaining cancellation information, heartbeats also support detail data that is
204
+ persisted on the server for retrieval during activity retry. If an activity calls
205
+ ` activity.heartbeat(123, 456) ` and then fails and is retried, ` activity.info.heartbeat_details ` will
206
+ return an array containing ` 123 ` and ` 456 ` on the next run.
207
+
208
+ A cancellation is implemented using the ` Thread#raise ` method, which will raise a
209
+ ` Temporal::Error::CancelledError ` during the execution of an activity. This means that your code
210
+ might get interrupted at any point and never complete. In order to protect critical parts of your
211
+ activities wrap them in ` activity.shield ` :
212
+
213
+ ``` ruby
214
+ class ActivityWithCriticalLogic < Temporal ::Activity
215
+ def execute
216
+ activity.shield do
217
+ run_business_critical_logic_1
218
+ end
219
+
220
+ run_non_critical_logic
221
+
222
+ activity.shield do
223
+ run_business_critical_logic_2
224
+ end
225
+ end
226
+ end
227
+ ```
228
+
229
+ This will ensure that a cancellation request received while inside the ` activity.shield ` block will
230
+ not raise an exception until that block finishes.
231
+
232
+ In case the entire activity is considered critical, you can mark it as ` shielded! ` and ignore
233
+ cancellation requests altogether:
234
+
235
+ ``` ruby
236
+ class CriticalActivity < Temporal ::Activity
237
+ shielded!
238
+
239
+ def execute
240
+ ...
241
+ end
242
+ end
243
+ ```
244
+
245
+ For any long-running activity using this approach it is recommended to periodically check
246
+ ` activity.cancelled? ` flag and respond accordingly.
247
+
91
248
92
249
## Dev Setup
93
250
0 commit comments