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
@@ -0,0 +1,329 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'temporalio/cancellation'
4
+ require 'temporalio/error'
5
+ require 'temporalio/internal/bridge/api'
6
+ require 'temporalio/internal/proto_utils'
7
+ require 'temporalio/internal/worker/workflow_instance'
8
+ require 'temporalio/internal/worker/workflow_instance/external_workflow_handle'
9
+ require 'temporalio/worker/interceptor'
10
+ require 'temporalio/workflow'
11
+
12
+ module Temporalio
13
+ module Internal
14
+ module Worker
15
+ class WorkflowInstance
16
+ # Context for all workflow calls. All calls in the {Workflow} class should call a method on this class and then
17
+ # this class can delegate the call as needed to other parts of the workflow instance system.
18
+ class Context
19
+ def initialize(instance)
20
+ @instance = instance
21
+ end
22
+
23
+ def all_handlers_finished?
24
+ @instance.in_progress_handlers.empty?
25
+ end
26
+
27
+ def cancellation
28
+ @instance.cancellation
29
+ end
30
+
31
+ def continue_as_new_suggested
32
+ @instance.continue_as_new_suggested
33
+ end
34
+
35
+ def current_history_length
36
+ @instance.current_history_length
37
+ end
38
+
39
+ def current_history_size
40
+ @instance.current_history_size
41
+ end
42
+
43
+ def current_update_info
44
+ Fiber[:__temporal_update_info]
45
+ end
46
+
47
+ def deprecate_patch(patch_id)
48
+ @instance.patch(patch_id:, deprecated: true)
49
+ end
50
+
51
+ def execute_activity(
52
+ activity,
53
+ *args,
54
+ task_queue:,
55
+ schedule_to_close_timeout:,
56
+ schedule_to_start_timeout:,
57
+ start_to_close_timeout:,
58
+ heartbeat_timeout:,
59
+ retry_policy:,
60
+ cancellation:,
61
+ cancellation_type:,
62
+ activity_id:,
63
+ disable_eager_execution:
64
+ )
65
+ @outbound.execute_activity(
66
+ Temporalio::Worker::Interceptor::Workflow::ExecuteActivityInput.new(
67
+ activity:,
68
+ args:,
69
+ task_queue: task_queue || info.task_queue,
70
+ schedule_to_close_timeout:,
71
+ schedule_to_start_timeout:,
72
+ start_to_close_timeout:,
73
+ heartbeat_timeout:,
74
+ retry_policy:,
75
+ cancellation:,
76
+ cancellation_type:,
77
+ activity_id:,
78
+ disable_eager_execution: disable_eager_execution || @instance.disable_eager_activity_execution,
79
+ headers: {}
80
+ )
81
+ )
82
+ end
83
+
84
+ def execute_local_activity(
85
+ activity,
86
+ *args,
87
+ schedule_to_close_timeout:,
88
+ schedule_to_start_timeout:,
89
+ start_to_close_timeout:,
90
+ retry_policy:,
91
+ local_retry_threshold:,
92
+ cancellation:,
93
+ cancellation_type:,
94
+ activity_id:
95
+ )
96
+ @outbound.execute_local_activity(
97
+ Temporalio::Worker::Interceptor::Workflow::ExecuteLocalActivityInput.new(
98
+ activity:,
99
+ args:,
100
+ schedule_to_close_timeout:,
101
+ schedule_to_start_timeout:,
102
+ start_to_close_timeout:,
103
+ retry_policy:,
104
+ local_retry_threshold:,
105
+ cancellation:,
106
+ cancellation_type:,
107
+ activity_id:,
108
+ headers: {}
109
+ )
110
+ )
111
+ end
112
+
113
+ def external_workflow_handle(workflow_id, run_id: nil)
114
+ ExternalWorkflowHandle.new(id: workflow_id, run_id:, instance: @instance)
115
+ end
116
+
117
+ def illegal_call_tracing_disabled(&)
118
+ @instance.illegal_call_tracing_disabled(&)
119
+ end
120
+
121
+ def info
122
+ @instance.info
123
+ end
124
+
125
+ def initialize_continue_as_new_error(error)
126
+ @outbound.initialize_continue_as_new_error(
127
+ Temporalio::Worker::Interceptor::Workflow::InitializeContinueAsNewErrorInput.new(error:)
128
+ )
129
+ end
130
+
131
+ def logger
132
+ @instance.logger
133
+ end
134
+
135
+ def memo
136
+ @instance.memo
137
+ end
138
+
139
+ def metric_meter
140
+ @instance.metric_meter
141
+ end
142
+
143
+ def now
144
+ @instance.now
145
+ end
146
+
147
+ def patched(patch_id)
148
+ @instance.patch(patch_id:, deprecated: false)
149
+ end
150
+
151
+ def payload_converter
152
+ @instance.payload_converter
153
+ end
154
+
155
+ def query_handlers
156
+ @instance.query_handlers
157
+ end
158
+
159
+ def random
160
+ @instance.random
161
+ end
162
+
163
+ def replaying?
164
+ @instance.replaying
165
+ end
166
+
167
+ def search_attributes
168
+ @instance.search_attributes
169
+ end
170
+
171
+ def signal_handlers
172
+ @instance.signal_handlers
173
+ end
174
+
175
+ def sleep(duration, summary:, cancellation:)
176
+ @outbound.sleep(
177
+ Temporalio::Worker::Interceptor::Workflow::SleepInput.new(
178
+ duration:,
179
+ summary:,
180
+ cancellation:
181
+ )
182
+ )
183
+ end
184
+
185
+ def start_child_workflow(
186
+ workflow,
187
+ *args,
188
+ id:,
189
+ task_queue:,
190
+ cancellation:,
191
+ cancellation_type:,
192
+ parent_close_policy:,
193
+ execution_timeout:,
194
+ run_timeout:,
195
+ task_timeout:,
196
+ id_reuse_policy:,
197
+ retry_policy:,
198
+ cron_schedule:,
199
+ memo:,
200
+ search_attributes:
201
+ )
202
+ @outbound.start_child_workflow(
203
+ Temporalio::Worker::Interceptor::Workflow::StartChildWorkflowInput.new(
204
+ workflow:,
205
+ args:,
206
+ id:,
207
+ task_queue:,
208
+ cancellation:,
209
+ cancellation_type:,
210
+ parent_close_policy:,
211
+ execution_timeout:,
212
+ run_timeout:,
213
+ task_timeout:,
214
+ id_reuse_policy:,
215
+ retry_policy:,
216
+ cron_schedule:,
217
+ memo:,
218
+ search_attributes:,
219
+ headers: {}
220
+ )
221
+ )
222
+ end
223
+
224
+ def timeout(duration, exception_class, *exception_args, summary:, &)
225
+ raise 'Block required for timeout' unless block_given?
226
+
227
+ # Run timer in background and block in foreground. This gives better stack traces than a future any-of race.
228
+ # We make a detached cancellation because we don't want to link to workflow cancellation.
229
+ sleep_cancel, sleep_cancel_proc = Cancellation.new
230
+ fiber = Fiber.current
231
+ Workflow::Future.new do
232
+ Workflow.sleep(duration, summary:, cancellation: sleep_cancel)
233
+ fiber.raise(exception_class, *exception_args) if fiber.alive? # steep:ignore
234
+ rescue Exception => e # rubocop:disable Lint/RescueException
235
+ # Re-raise in fiber
236
+ fiber.raise(e) if fiber.alive?
237
+ end
238
+
239
+ begin
240
+ yield
241
+ ensure
242
+ sleep_cancel_proc.call
243
+ end
244
+ end
245
+
246
+ def update_handlers
247
+ @instance.update_handlers
248
+ end
249
+
250
+ def upsert_memo(hash)
251
+ # Convert to memo, apply updates, then add the command (so command adding is post validation)
252
+ upserted_memo = ProtoUtils.memo_to_proto(hash, payload_converter)
253
+ memo._update do |new_hash|
254
+ hash.each do |key, val|
255
+ # Nil means delete
256
+ if val.nil?
257
+ new_hash.delete(key.to_s)
258
+ else
259
+ new_hash[key.to_s] = val
260
+ end
261
+ end
262
+ end
263
+ @instance.add_command(
264
+ Bridge::Api::WorkflowCommands::WorkflowCommand.new(
265
+ modify_workflow_properties: Bridge::Api::WorkflowCommands::ModifyWorkflowProperties.new(
266
+ upserted_memo:
267
+ )
268
+ )
269
+ )
270
+ end
271
+
272
+ def upsert_search_attributes(*updates)
273
+ # Apply updates then add the command (so command adding is post validation)
274
+ search_attributes._disable_mutations = false
275
+ search_attributes.update!(*updates)
276
+ @instance.add_command(
277
+ Bridge::Api::WorkflowCommands::WorkflowCommand.new(
278
+ upsert_workflow_search_attributes: Bridge::Api::WorkflowCommands::UpsertWorkflowSearchAttributes.new(
279
+ search_attributes: updates.to_h(&:_to_proto_pair)
280
+ )
281
+ )
282
+ )
283
+ ensure
284
+ search_attributes._disable_mutations = true
285
+ end
286
+
287
+ def wait_condition(cancellation:, &)
288
+ @instance.scheduler.wait_condition(cancellation:, &)
289
+ end
290
+
291
+ def _cancel_external_workflow(id:, run_id:)
292
+ @outbound.cancel_external_workflow(
293
+ Temporalio::Worker::Interceptor::Workflow::CancelExternalWorkflowInput.new(id:, run_id:)
294
+ )
295
+ end
296
+
297
+ def _outbound=(outbound)
298
+ @outbound = outbound
299
+ end
300
+
301
+ def _signal_child_workflow(id:, signal:, args:, cancellation:)
302
+ @outbound.signal_child_workflow(
303
+ Temporalio::Worker::Interceptor::Workflow::SignalChildWorkflowInput.new(
304
+ id:,
305
+ signal:,
306
+ args:,
307
+ cancellation:,
308
+ headers: {}
309
+ )
310
+ )
311
+ end
312
+
313
+ def _signal_external_workflow(id:, run_id:, signal:, args:, cancellation:)
314
+ @outbound.signal_external_workflow(
315
+ Temporalio::Worker::Interceptor::Workflow::SignalExternalWorkflowInput.new(
316
+ id:,
317
+ run_id:,
318
+ signal:,
319
+ args:,
320
+ cancellation:,
321
+ headers: {}
322
+ )
323
+ )
324
+ end
325
+ end
326
+ end
327
+ end
328
+ end
329
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Temporalio
4
+ module Internal
5
+ module Worker
6
+ class WorkflowInstance
7
+ # Details needed to instantiate a {WorkflowInstance}.
8
+ class Details
9
+ attr_reader :namespace, :task_queue, :definition, :initial_activation, :logger, :metric_meter,
10
+ :payload_converter, :failure_converter, :interceptors, :disable_eager_activity_execution,
11
+ :illegal_calls, :workflow_failure_exception_types
12
+
13
+ def initialize(
14
+ namespace:,
15
+ task_queue:,
16
+ definition:,
17
+ initial_activation:,
18
+ logger:,
19
+ metric_meter:,
20
+ payload_converter:,
21
+ failure_converter:,
22
+ interceptors:,
23
+ disable_eager_activity_execution:,
24
+ illegal_calls:,
25
+ workflow_failure_exception_types:
26
+ )
27
+ @namespace = namespace
28
+ @task_queue = task_queue
29
+ @definition = definition
30
+ @initial_activation = initial_activation
31
+ @logger = logger
32
+ @metric_meter = metric_meter
33
+ @payload_converter = payload_converter
34
+ @failure_converter = failure_converter
35
+ @interceptors = interceptors
36
+ @disable_eager_activity_execution = disable_eager_activity_execution
37
+ @illegal_calls = illegal_calls
38
+ @workflow_failure_exception_types = workflow_failure_exception_types
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'temporalio/cancellation'
4
+ require 'temporalio/workflow'
5
+ require 'temporalio/workflow/external_workflow_handle'
6
+
7
+ module Temporalio
8
+ module Internal
9
+ module Worker
10
+ class WorkflowInstance
11
+ # Implementation of the external workflow handle.
12
+ class ExternalWorkflowHandle < Workflow::ExternalWorkflowHandle
13
+ attr_reader :id, :run_id
14
+
15
+ def initialize(id:, run_id:, instance:) # rubocop:disable Lint/MissingSuper
16
+ @id = id
17
+ @run_id = run_id
18
+ @instance = instance
19
+ end
20
+
21
+ def signal(signal, *args, cancellation: Workflow.cancellation)
22
+ @instance.context._signal_external_workflow(id:, run_id:, signal:, args:, cancellation:)
23
+ end
24
+
25
+ def cancel
26
+ @instance.context._cancel_external_workflow(id:, run_id:)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Temporalio
4
+ module Internal
5
+ module Worker
6
+ class WorkflowInstance
7
+ # Delegator to a hash that does not allow external mutations. Used for memo.
8
+ class ExternallyImmutableHash < SimpleDelegator
9
+ def initialize(initial_hash)
10
+ super(initial_hash.freeze)
11
+ end
12
+
13
+ def _update(&)
14
+ new_hash = __getobj__.dup
15
+ yield new_hash
16
+ __setobj__(new_hash.freeze)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Temporalio
4
+ module Internal
5
+ module Worker
6
+ class WorkflowInstance
7
+ # Representation of a currently-executing handler. Used to track whether any handlers are still running and warn
8
+ # on workflow complete as needed.
9
+ class HandlerExecution
10
+ attr_reader :name, :update_id, :unfinished_policy
11
+
12
+ def initialize(
13
+ name:,
14
+ update_id:,
15
+ unfinished_policy:
16
+ )
17
+ @name = name
18
+ @update_id = update_id
19
+ @unfinished_policy = unfinished_policy
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Temporalio
4
+ module Internal
5
+ module Worker
6
+ class WorkflowInstance
7
+ # Hash for handlers that notifies when one is added. Only `[]=` and `store` can be used to mutate it.
8
+ class HandlerHash < SimpleDelegator
9
+ def initialize(initial_frozen_hash, definition_class, &on_new_definition)
10
+ super(initial_frozen_hash)
11
+ @definition_class = definition_class
12
+ @on_new_definition = on_new_definition
13
+ end
14
+
15
+ def []=(name, definition)
16
+ store(name, definition)
17
+ end
18
+
19
+ # steep:ignore:start
20
+ def store(name, definition)
21
+ raise ArgumentError, 'Name must be a string or nil' unless name.nil? || name.is_a?(String)
22
+
23
+ unless definition.nil? || definition.is_a?(@definition_class)
24
+ raise ArgumentError,
25
+ "Value must be a #{@definition_class.name} or nil"
26
+ end
27
+ raise ArgumentError, 'Name does not match one in definition' if definition && name != definition.name
28
+
29
+ # Do a copy-on-write op on the underlying frozen hash
30
+ new_hash = __getobj__.dup
31
+ new_hash[name] = definition
32
+ __setobj__(new_hash.freeze)
33
+ @on_new_definition&.call(definition) unless definition.nil?
34
+ definition
35
+ end
36
+ # steep:ignore:end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'temporalio/workflow'
4
+
5
+ module Temporalio
6
+ module Internal
7
+ module Worker
8
+ class WorkflowInstance
9
+ # Class that installs {::TracePoint} to disallow illegal calls.
10
+ class IllegalCallTracer
11
+ def self.frozen_validated_illegal_calls(illegal_calls)
12
+ illegal_calls.to_h do |key, val|
13
+ raise TypeError, 'Invalid illegal call map, top-level key must be a String' unless key.is_a?(String)
14
+
15
+ # @type var fixed_val: :all | Hash[Symbol, bool]
16
+ fixed_val = case val
17
+ when Array
18
+ val.to_h do |sub_val|
19
+ unless sub_val.is_a?(Symbol)
20
+ raise TypeError,
21
+ 'Invalid illegal call map, each value must be a Symbol'
22
+ end
23
+
24
+ [sub_val, true]
25
+ end.freeze
26
+ when :all
27
+ :all
28
+ else
29
+ raise TypeError, 'Invalid illegal call map, top-level value must be an Array or :all'
30
+ end
31
+
32
+ [key.frozen? ? key : key.dup.freeze, fixed_val]
33
+ end.freeze
34
+ end
35
+
36
+ # Illegal calls are Hash[String, :all | Hash[Symbol, Bool]]
37
+ def initialize(illegal_calls)
38
+ @tracepoint = TracePoint.new(:call, :c_call) do |tp|
39
+ # Manual check for proper thread since we have seen issues in Ruby 3.2 where it leaks
40
+ next unless Thread.current == @enabled_thread
41
+
42
+ cls = tp.defined_class
43
+ next unless cls.is_a?(Module)
44
+
45
+ # Extract the class name from the defined class. This is more difficult than it seems because you have to
46
+ # resolve the attached object of the singleton class. But in older Ruby (at least <= 3.1), the singleton
47
+ # class of things like `Date` does not have `attached_object` so you have to fall back in these rare cases
48
+ # to parsing the string output. Reaching the string parsing component is rare, so this should not have
49
+ # significant performance impact.
50
+ cls_name = if cls.singleton_class?
51
+ if cls.respond_to?(:attached_object)
52
+ cls = cls.attached_object # steep:ignore
53
+ next unless cls.is_a?(Module)
54
+
55
+ cls.name.to_s
56
+ else
57
+ cls.to_s.delete_prefix('#<Class:').delete_suffix('>')
58
+ end
59
+ else
60
+ cls.name.to_s
61
+ end
62
+
63
+ # Check if the call is considered illegal
64
+ vals = illegal_calls[cls_name]
65
+ if vals == :all || vals&.[](tp.callee_id) # steep:ignore
66
+ raise Workflow::NondeterminismError,
67
+ "Cannot access #{cls_name} #{tp.callee_id} from inside a " \
68
+ 'workflow. If this is known to be safe, the code can be run in ' \
69
+ 'a Temporalio::Workflow::Unsafe.illegal_call_tracing_disabled block.'
70
+ end
71
+ end
72
+ end
73
+
74
+ def enable(&block)
75
+ # We've seen leaking issues in Ruby 3.2 where the TracePoint inadvertently remains enabled even for threads
76
+ # that it was not started on. So we will check the thread ourselves.
77
+ @enabled_thread = Thread.current
78
+ @tracepoint.enable do
79
+ block.call
80
+ ensure
81
+ @enabled_thread = nil
82
+ end
83
+ end
84
+
85
+ def disable(&block)
86
+ previous_thread = @enabled_thread
87
+ @tracepoint.disable do
88
+ block.call
89
+ ensure
90
+ @enabled_thread = previous_thread
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'temporalio/internal/worker/workflow_instance'
4
+ require 'temporalio/worker/interceptor'
5
+ require 'temporalio/workflow'
6
+
7
+ module Temporalio
8
+ module Internal
9
+ module Worker
10
+ class WorkflowInstance
11
+ # Root implementation of the inbound interceptor.
12
+ class InboundImplementation < Temporalio::Worker::Interceptor::Workflow::Inbound
13
+ def initialize(instance)
14
+ super(nil) # steep:ignore
15
+ @instance = instance
16
+ end
17
+
18
+ def init(outbound)
19
+ @instance.context._outbound = outbound
20
+ end
21
+
22
+ def execute(input)
23
+ @instance.instance.execute(*input.args)
24
+ end
25
+
26
+ def handle_signal(input)
27
+ invoke_handler(input.signal, input)
28
+ end
29
+
30
+ def handle_query(input)
31
+ invoke_handler(input.query, input)
32
+ end
33
+
34
+ def validate_update(input)
35
+ invoke_handler(input.update, input, to_invoke: input.definition.validator_to_invoke)
36
+ end
37
+
38
+ def handle_update(input)
39
+ invoke_handler(input.update, input)
40
+ end
41
+
42
+ private
43
+
44
+ def invoke_handler(name, input, to_invoke: input.definition.to_invoke)
45
+ args = input.args
46
+ # Add name as first param if dynamic
47
+ args = [name] + args if input.definition.name.nil?
48
+ # Assume symbol or proc
49
+ case to_invoke
50
+ when Symbol
51
+ @instance.instance.send(to_invoke, *args)
52
+ when Proc
53
+ to_invoke.call(*args)
54
+ else
55
+ raise "Unrecognized invocation type #{to_invoke.class}"
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end