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
@@ -2,85 +2,359 @@
2
2
 
3
3
  module Temporalio
4
4
  class Worker
5
- # Mixin for intercepting worker work. Clases that `include` may implement their own {intercept_activity} that
6
- # returns their own instance of {ActivityInbound}.
7
- #
8
- # @note Input classes herein may get new required fields added and therefore the constructors of the Input classes
9
- # may change in backwards incompatible ways. Users should not try to construct Input classes themselves.
10
5
  module Interceptor
11
- # Method called when intercepting an activity. This is called when starting an activity attempt.
6
+ # Mixin for intercepting activity worker work. Clases that `include` may implement their own {intercept_activity}
7
+ # that returns their own instance of {Inbound}.
12
8
  #
13
- # @param next_interceptor [ActivityInbound] Next interceptor in the chain that should be called. This is usually
14
- # passed to {ActivityInbound} constructor.
15
- # @return [ActivityInbound] Interceptor to be called for activity calls.
16
- def intercept_activity(next_interceptor)
17
- next_interceptor
18
- end
19
-
20
- # Input for {ActivityInbound.execute}.
21
- ExecuteActivityInput = Struct.new(
22
- :proc,
23
- :args,
24
- :headers,
25
- keyword_init: true
26
- )
27
-
28
- # Input for {ActivityOutbound.heartbeat}.
29
- HeartbeatActivityInput = Struct.new(
30
- :details,
31
- keyword_init: true
32
- )
33
-
34
- # Inbound interceptor for intercepting inbound activity calls. This should be extended by users needing to
35
- # intercept activities.
36
- class ActivityInbound
37
- # @return [ActivityInbound] Next interceptor in the chain.
38
- attr_reader :next_interceptor
39
-
40
- # Initialize inbound with the next interceptor in the chain.
9
+ # @note Input classes herein may get new required fields added and therefore the constructors of the Input classes
10
+ # may change in backwards incompatible ways. Users should not try to construct Input classes themselves.
11
+ module Activity
12
+ # Method called when intercepting an activity. This is called when starting an activity attempt.
41
13
  #
42
- # @param next_interceptor [ActivityInbound] Next interceptor in the chain.
43
- def initialize(next_interceptor)
44
- @next_interceptor = next_interceptor
14
+ # @param next_interceptor [Inbound] Next interceptor in the chain that should be called. This is usually passed
15
+ # to {Inbound} constructor.
16
+ # @return [Inbound] Interceptor to be called for activity calls.
17
+ def intercept_activity(next_interceptor)
18
+ next_interceptor
45
19
  end
46
20
 
47
- # Initialize the outbound interceptor. This should be extended by users to return their own {ActivityOutbound}
48
- # implementation that wraps the parameter here.
49
- #
50
- # @param outbound [ActivityOutbound] Next outbound interceptor in the chain.
51
- # @return [ActivityOutbound] Outbound activity interceptor.
52
- def init(outbound)
53
- @next_interceptor.init(outbound)
21
+ # Input for {Inbound.execute}.
22
+ ExecuteInput = Data.define(
23
+ :proc,
24
+ :args,
25
+ :headers
26
+ )
27
+
28
+ # Inbound interceptor for intercepting inbound activity calls. This should be extended by users needing to
29
+ # intercept activities.
30
+ class Inbound
31
+ # @return [Inbound] Next interceptor in the chain.
32
+ attr_reader :next_interceptor
33
+
34
+ # Initialize inbound with the next interceptor in the chain.
35
+ #
36
+ # @param next_interceptor [Inbound] Next interceptor in the chain.
37
+ def initialize(next_interceptor)
38
+ @next_interceptor = next_interceptor
39
+ end
40
+
41
+ # Initialize the outbound interceptor. This should be extended by users to return their own {Outbound}
42
+ # implementation that wraps the parameter here.
43
+ #
44
+ # @param outbound [Outbound] Next outbound interceptor in the chain.
45
+ # @return [Outbound] Outbound activity interceptor.
46
+ def init(outbound)
47
+ @next_interceptor.init(outbound)
48
+ end
49
+
50
+ # Execute an activity and return result or raise exception. Next interceptor in chain (i.e. `super`) will
51
+ # perform the execution.
52
+ #
53
+ # @param input [ExecuteInput] Input information.
54
+ # @return [Object] Activity result.
55
+ def execute(input)
56
+ @next_interceptor.execute(input)
57
+ end
54
58
  end
55
59
 
56
- # Execute an activity and return result or raise exception. Next interceptor in chain (i.e. `super`) will
57
- # perform the execution.
58
- #
59
- # @param input [ExecuteActivityInput] Input information.
60
- # @return [Object] Activity result.
61
- def execute(input)
62
- @next_interceptor.execute(input)
60
+ # Input for {Outbound.heartbeat}.
61
+ HeartbeatInput = Data.define(
62
+ :details
63
+ )
64
+
65
+ # Outbound interceptor for intercepting outbound activity calls. This should be extended by users needing to
66
+ # intercept activity calls.
67
+ class Outbound
68
+ # @return [Outbound] Next interceptor in the chain.
69
+ attr_reader :next_interceptor
70
+
71
+ # Initialize outbound with the next interceptor in the chain.
72
+ #
73
+ # @param next_interceptor [Outbound] Next interceptor in the chain.
74
+ def initialize(next_interceptor)
75
+ @next_interceptor = next_interceptor
76
+ end
77
+
78
+ # Issue a heartbeat.
79
+ #
80
+ # @param input [HeartbeatInput] Input information.
81
+ def heartbeat(input)
82
+ @next_interceptor.heartbeat(input)
83
+ end
63
84
  end
64
85
  end
65
86
 
66
- # Outbound interceptor for intercepting outbound activity calls. This should be extended by users needing to
67
- # intercept activity calls.
68
- class ActivityOutbound
69
- # @return [ActivityInbound] Next interceptor in the chain.
70
- attr_reader :next_interceptor
71
-
72
- # Initialize outbound with the next interceptor in the chain.
87
+ # Mixin for intercepting workflow worker work. Classes that `include` may implement their own {intercept_workflow}
88
+ # that returns their own instance of {Inbound}.
89
+ #
90
+ # @note Input classes herein may get new required fields added and therefore the constructors of the Input classes
91
+ # may change in backwards incompatible ways. Users should not try to construct Input classes themselves.
92
+ module Workflow
93
+ # Method called when intercepting a workflow. This is called when creating a workflow instance.
73
94
  #
74
- # @param next_interceptor [ActivityOutbound] Next interceptor in the chain.
75
- def initialize(next_interceptor)
76
- @next_interceptor = next_interceptor
95
+ # @param next_interceptor [Inbound] Next interceptor in the chain that should be called. This is usually passed
96
+ # to {Inbound} constructor.
97
+ # @return [Inbound] Interceptor to be called for workflow calls.
98
+ def intercept_workflow(next_interceptor)
99
+ next_interceptor
77
100
  end
78
101
 
79
- # Issue a heartbeat.
80
- #
81
- # @param input [HeartbeatActivityInput] Input information.
82
- def heartbeat(input)
83
- @next_interceptor.heartbeat(input)
102
+ # Input for {Inbound.execute}.
103
+ ExecuteInput = Data.define(
104
+ :args,
105
+ :headers
106
+ )
107
+
108
+ # Input for {Inbound.handle_signal}.
109
+ HandleSignalInput = Data.define(
110
+ :signal,
111
+ :args,
112
+ :definition,
113
+ :headers
114
+ )
115
+
116
+ # Input for {Inbound.handle_query}.
117
+ HandleQueryInput = Data.define(
118
+ :id,
119
+ :query,
120
+ :args,
121
+ :definition,
122
+ :headers
123
+ )
124
+
125
+ # Input for {Inbound.validate_update} and {Inbound.handle_update}.
126
+ HandleUpdateInput = Data.define(
127
+ :id,
128
+ :update,
129
+ :args,
130
+ :definition,
131
+ :headers
132
+ )
133
+
134
+ # Inbound interceptor for intercepting inbound workflow calls. This should be extended by users needing to
135
+ # intercept workflows.
136
+ class Inbound
137
+ # @return [Inbound] Next interceptor in the chain.
138
+ attr_reader :next_interceptor
139
+
140
+ # Initialize inbound with the next interceptor in the chain.
141
+ #
142
+ # @param next_interceptor [Inbound] Next interceptor in the chain.
143
+ def initialize(next_interceptor)
144
+ @next_interceptor = next_interceptor
145
+ end
146
+
147
+ # Initialize the outbound interceptor. This should be extended by users to return their own {Outbound}
148
+ # implementation that wraps the parameter here.
149
+ #
150
+ # @param outbound [Outbound] Next outbound interceptor in the chain.
151
+ # @return [Outbound] Outbound workflow interceptor.
152
+ def init(outbound)
153
+ @next_interceptor.init(outbound)
154
+ end
155
+
156
+ # Execute a workflow and return result or raise exception. Next interceptor in chain (i.e. `super`) will
157
+ # perform the execution.
158
+ #
159
+ # @param input [ExecuteInput] Input information.
160
+ # @return [Object] Workflow result.
161
+ def execute(input)
162
+ @next_interceptor.execute(input)
163
+ end
164
+
165
+ # Handle a workflow signal. Next interceptor in chain (i.e. `super`) will perform the handling.
166
+ #
167
+ # @param input [HandleSignalInput] Input information.
168
+ def handle_signal(input)
169
+ @next_interceptor.handle_signal(input)
170
+ end
171
+
172
+ # Handle a workflow query and return result or raise exception. Next interceptor in chain (i.e. `super`) will
173
+ # perform the handling.
174
+ #
175
+ # @param input [HandleQueryInput] Input information.
176
+ # @return [Object] Query result.
177
+ def handle_query(input)
178
+ @next_interceptor.handle_query(input)
179
+ end
180
+
181
+ # Validate a workflow update. Next interceptor in chain (i.e. `super`) will perform the validation.
182
+ #
183
+ # @param input [HandleUpdateInput] Input information.
184
+ def validate_update(input)
185
+ @next_interceptor.validate_update(input)
186
+ end
187
+
188
+ # Handle a workflow update and return result or raise exception. Next interceptor in chain (i.e. `super`) will
189
+ # perform the handling.
190
+ #
191
+ # @param input [HandleUpdateInput] Input information.
192
+ # @return [Object] Update result.
193
+ def handle_update(input)
194
+ @next_interceptor.handle_update(input)
195
+ end
196
+ end
197
+
198
+ # Input for {Outbound.cancel_external_workflow}.
199
+ CancelExternalWorkflowInput = Data.define(
200
+ :id,
201
+ :run_id
202
+ )
203
+
204
+ # Input for {Outbound.execute_activity}.
205
+ ExecuteActivityInput = Data.define(
206
+ :activity,
207
+ :args,
208
+ :task_queue,
209
+ :schedule_to_close_timeout,
210
+ :schedule_to_start_timeout,
211
+ :start_to_close_timeout,
212
+ :heartbeat_timeout,
213
+ :retry_policy,
214
+ :cancellation,
215
+ :cancellation_type,
216
+ :activity_id,
217
+ :disable_eager_execution,
218
+ :headers
219
+ )
220
+
221
+ # Input for {Outbound.execute_local_activity}.
222
+ ExecuteLocalActivityInput = Data.define(
223
+ :activity,
224
+ :args,
225
+ :schedule_to_close_timeout,
226
+ :schedule_to_start_timeout,
227
+ :start_to_close_timeout,
228
+ :retry_policy,
229
+ :local_retry_threshold,
230
+ :cancellation,
231
+ :cancellation_type,
232
+ :activity_id,
233
+ :headers
234
+ )
235
+
236
+ # Input for {Outbound.initialize_continue_as_new_error}.
237
+ InitializeContinueAsNewErrorInput = Data.define(
238
+ :error
239
+ )
240
+
241
+ # Input for {Outbound.signal_child_workflow}.
242
+ SignalChildWorkflowInput = Data.define(
243
+ :id,
244
+ :signal,
245
+ :args,
246
+ :cancellation,
247
+ :headers
248
+ )
249
+
250
+ # Input for {Outbound.signal_external_workflow}.
251
+ SignalExternalWorkflowInput = Data.define(
252
+ :id,
253
+ :run_id,
254
+ :signal,
255
+ :args,
256
+ :cancellation,
257
+ :headers
258
+ )
259
+
260
+ # Input for {Outbound.sleep}.
261
+ SleepInput = Data.define(
262
+ :duration,
263
+ :summary,
264
+ :cancellation
265
+ )
266
+
267
+ # Input for {Outbound.start_child_workflow}.
268
+ StartChildWorkflowInput = Data.define(
269
+ :workflow,
270
+ :args,
271
+ :id,
272
+ :task_queue,
273
+ :cancellation,
274
+ :cancellation_type,
275
+ :parent_close_policy,
276
+ :execution_timeout,
277
+ :run_timeout,
278
+ :task_timeout,
279
+ :id_reuse_policy,
280
+ :retry_policy,
281
+ :cron_schedule,
282
+ :memo,
283
+ :search_attributes,
284
+ :headers
285
+ )
286
+
287
+ # Outbound interceptor for intercepting outbound workflow calls. This should be extended by users needing to
288
+ # intercept workflow calls.
289
+ class Outbound
290
+ # @return [Outbound] Next interceptor in the chain.
291
+ attr_reader :next_interceptor
292
+
293
+ # Initialize outbound with the next interceptor in the chain.
294
+ #
295
+ # @param next_interceptor [Outbound] Next interceptor in the chain.
296
+ def initialize(next_interceptor)
297
+ @next_interceptor = next_interceptor
298
+ end
299
+
300
+ # Cancel external workflow.
301
+ #
302
+ # @param input [CancelExternalWorkflowInput] Input.
303
+ def cancel_external_workflow(input)
304
+ @next_interceptor.cancel_external_workflow(input)
305
+ end
306
+
307
+ # Execute activity.
308
+ #
309
+ # @param input [ExecuteActivityInput] Input.
310
+ # @return [Object] Activity result.
311
+ def execute_activity(input)
312
+ @next_interceptor.execute_activity(input)
313
+ end
314
+
315
+ # Execute local activity.
316
+ #
317
+ # @param input [ExecuteLocalActivityInput] Input.
318
+ # @return [Object] Activity result.
319
+ def execute_local_activity(input)
320
+ @next_interceptor.execute_local_activity(input)
321
+ end
322
+
323
+ # Initialize continue as new error.
324
+ #
325
+ # @param input [InitializeContinueAsNewErrorInput] Input.
326
+ def initialize_continue_as_new_error(input)
327
+ @next_interceptor.initialize_continue_as_new_error(input)
328
+ end
329
+
330
+ # Signal child workflow.
331
+ #
332
+ # @param input [SignalChildWorkflowInput] Input.
333
+ def signal_child_workflow(input)
334
+ @next_interceptor.signal_child_workflow(input)
335
+ end
336
+
337
+ # Signal external workflow.
338
+ #
339
+ # @param input [SignalExternalWorkflowInput] Input.
340
+ def signal_external_workflow(input)
341
+ @next_interceptor.signal_external_workflow(input)
342
+ end
343
+
344
+ # Sleep.
345
+ #
346
+ # @param input [SleepInput] Input.
347
+ def sleep(input)
348
+ @next_interceptor.sleep(input)
349
+ end
350
+
351
+ # Start child workflow.
352
+ #
353
+ # @param input [StartChildWorkflowInput] Input.
354
+ # @return [Workflow::ChildWorkflowHandle] Child workflow handle.
355
+ def start_child_workflow(input)
356
+ @next_interceptor.start_child_workflow(input)
357
+ end
84
358
  end
85
359
  end
86
360
  end
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Much of this logic taken from
4
+ # https://github.com/ruby-concurrency/concurrent-ruby/blob/044020f44b36930b863b930f3ee8fa1e9f750469/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb,
5
+ # see MIT license at
6
+ # https://github.com/ruby-concurrency/concurrent-ruby/blob/044020f44b36930b863b930f3ee8fa1e9f750469/LICENSE.txt
7
+
8
+ module Temporalio
9
+ class Worker
10
+ # Implementation of a thread pool. This implementation is a stripped down form of Concurrent Ruby's
11
+ # `CachedThreadPool`.
12
+ class ThreadPool
13
+ # @return [ThreadPool] Default/shared thread pool instance with unlimited max threads.
14
+ def self.default
15
+ @default ||= new
16
+ end
17
+
18
+ # @!visibility private
19
+ def self._monotonic_time
20
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
21
+ end
22
+
23
+ # Create a new thread pool that creates threads as needed.
24
+ #
25
+ # @param max_threads [Integer, nil] Maximum number of thread workers to create, or nil for unlimited max.
26
+ # @param idle_timeout [Float] Number of seconds before a thread worker with no work should be stopped. Note,
27
+ # the check of whether a thread worker is idle is only done on each new {execute} call.
28
+ def initialize(max_threads: nil, idle_timeout: 20)
29
+ @max_threads = max_threads
30
+ @idle_timeout = idle_timeout
31
+
32
+ @mutex = Mutex.new
33
+ @pool = []
34
+ @ready = []
35
+ @queue = []
36
+ @scheduled_task_count = 0
37
+ @completed_task_count = 0
38
+ @largest_length = 0
39
+ @workers_counter = 0
40
+ @prune_interval = @idle_timeout / 2
41
+ @next_prune_time = ThreadPool._monotonic_time + @prune_interval
42
+ end
43
+
44
+ # Execute the given block in a thread. The block should be built to never raise and need no arguments.
45
+ #
46
+ # @yield Block to execute.
47
+ def execute(&block)
48
+ @mutex.synchronize do
49
+ locked_assign_worker(&block) || locked_enqueue(&block)
50
+ @scheduled_task_count += 1
51
+ locked_prune_pool if @next_prune_time < ThreadPool._monotonic_time
52
+ end
53
+ end
54
+
55
+ # @return [Integer] The largest number of threads that have been created in the pool since construction.
56
+ def largest_length
57
+ @mutex.synchronize { @largest_length }
58
+ end
59
+
60
+ # @return [Integer] The number of tasks that have been scheduled for execution on the pool since construction.
61
+ def scheduled_task_count
62
+ @mutex.synchronize { @scheduled_task_count }
63
+ end
64
+
65
+ # @return [Integer] The number of tasks that have been completed by the pool since construction.
66
+ def completed_task_count
67
+ @mutex.synchronize { @completed_task_count }
68
+ end
69
+
70
+ # @return [Integer] The number of threads that are actively executing tasks.
71
+ def active_count
72
+ @mutex.synchronize { @pool.length - @ready.length }
73
+ end
74
+
75
+ # @return [Integer] The number of threads currently in the pool.
76
+ def length
77
+ @mutex.synchronize { @pool.length }
78
+ end
79
+
80
+ # @return [Integer] The number of tasks in the queue awaiting execution.
81
+ def queue_length
82
+ @mutex.synchronize { @queue.length }
83
+ end
84
+
85
+ # Gracefully shutdown each thread when it is done with its current task. This should not be called until all
86
+ # workers using this executor are complete. This does not need to be called at all on program exit (e.g. for the
87
+ # global default).
88
+ def shutdown
89
+ @mutex.synchronize do
90
+ # Stop all workers
91
+ @pool.each(&:stop)
92
+ end
93
+ end
94
+
95
+ # Kill each thread. This should not be called until all workers using this executor are complete. This does not
96
+ # need to be called at all on program exit (e.g. for the global default).
97
+ def kill
98
+ @mutex.synchronize do
99
+ # Kill all workers
100
+ @pool.each(&:kill)
101
+ @pool.clear
102
+ @ready.clear
103
+ end
104
+ end
105
+
106
+ # @!visibility private
107
+ def _remove_busy_worker(worker)
108
+ @mutex.synchronize { locked_remove_busy_worker(worker) }
109
+ end
110
+
111
+ # @!visibility private
112
+ def _ready_worker(worker, last_message)
113
+ @mutex.synchronize { locked_ready_worker(worker, last_message) }
114
+ end
115
+
116
+ # @!visibility private
117
+ def _worker_died(worker)
118
+ @mutex.synchronize { locked_worker_died(worker) }
119
+ end
120
+
121
+ # @!visibility private
122
+ def _worker_task_completed
123
+ @mutex.synchronize { @completed_task_count += 1 }
124
+ end
125
+
126
+ private
127
+
128
+ def locked_assign_worker(&block)
129
+ # keep growing if the pool is not at the minimum yet
130
+ worker, = @ready.pop || locked_add_busy_worker
131
+ if worker
132
+ worker << block
133
+ true
134
+ else
135
+ false
136
+ end
137
+ end
138
+
139
+ def locked_enqueue(&block)
140
+ @queue << block
141
+ end
142
+
143
+ def locked_add_busy_worker
144
+ return if @max_threads && @pool.size >= @max_threads
145
+
146
+ @workers_counter += 1
147
+ @pool << (worker = Worker.new(self, @workers_counter))
148
+ @largest_length = @pool.length if @pool.length > @largest_length
149
+ worker
150
+ end
151
+
152
+ def locked_prune_pool
153
+ now = ThreadPool._monotonic_time
154
+ stopped_workers = 0
155
+ while !@ready.empty? && (@pool.size - stopped_workers).positive?
156
+ worker, last_message = @ready.first
157
+ break unless now - last_message > @idle_timeout
158
+
159
+ stopped_workers += 1
160
+ @ready.shift
161
+ worker << :stop
162
+
163
+ end
164
+
165
+ @next_prune_time = ThreadPool._monotonic_time + @prune_interval
166
+ end
167
+
168
+ def locked_remove_busy_worker(worker)
169
+ @pool.delete(worker)
170
+ end
171
+
172
+ def locked_ready_worker(worker, last_message)
173
+ block = @queue.shift
174
+ if block
175
+ worker << block
176
+ else
177
+ @ready.push([worker, last_message])
178
+ end
179
+ end
180
+
181
+ def locked_worker_died(worker)
182
+ locked_remove_busy_worker(worker)
183
+ replacement_worker = locked_add_busy_worker
184
+ locked_ready_worker(replacement_worker, ThreadPool._monotonic_time) if replacement_worker
185
+ end
186
+
187
+ # @!visibility private
188
+ class Worker
189
+ def initialize(pool, id)
190
+ @queue = Queue.new
191
+ @thread = Thread.new(@queue, pool) do |my_queue, my_pool|
192
+ catch(:stop) do
193
+ loop do
194
+ case block = my_queue.pop
195
+ when :stop
196
+ pool._remove_busy_worker(self)
197
+ throw :stop
198
+ else
199
+ begin
200
+ block.call
201
+ my_pool._worker_task_completed
202
+ my_pool._ready_worker(self, ThreadPool._monotonic_time)
203
+ rescue StandardError => e
204
+ # Ignore
205
+ warn("Unexpected execute block error: #{e.full_message}")
206
+ rescue Exception => e # rubocop:disable Lint/RescueException
207
+ warn("Unexpected execute block exception: #{e.full_message}")
208
+ my_pool._worker_died(self)
209
+ throw :stop
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
215
+ @thread.name = "temporal-thread-#{id}"
216
+ end
217
+
218
+ # @!visibility private
219
+ def <<(block)
220
+ @queue << block
221
+ end
222
+
223
+ # @!visibility private
224
+ def stop
225
+ @queue << :stop
226
+ end
227
+
228
+ # @!visibility private
229
+ def kill
230
+ @thread.kill
231
+ end
232
+ end
233
+
234
+ private_constant :Worker
235
+ end
236
+ end
237
+ end