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,415 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'temporalio/activity/definition'
4
+ require 'temporalio/cancellation'
5
+ require 'temporalio/error'
6
+ require 'temporalio/internal/bridge/api'
7
+ require 'temporalio/internal/proto_utils'
8
+ require 'temporalio/internal/worker/workflow_instance'
9
+ require 'temporalio/worker/interceptor'
10
+ require 'temporalio/workflow'
11
+ require 'temporalio/workflow/child_workflow_handle'
12
+
13
+ module Temporalio
14
+ module Internal
15
+ module Worker
16
+ class WorkflowInstance
17
+ # Root implementation of the outbound interceptor.
18
+ class OutboundImplementation < Temporalio::Worker::Interceptor::Workflow::Outbound
19
+ def initialize(instance)
20
+ super(nil) # steep:ignore
21
+ @instance = instance
22
+ @activity_counter = 0
23
+ @timer_counter = 0
24
+ @child_counter = 0
25
+ @external_signal_counter = 0
26
+ @external_cancel_counter = 0
27
+ end
28
+
29
+ def cancel_external_workflow(input)
30
+ # Add command
31
+ seq = (@external_cancel_counter += 1)
32
+ cmd = Bridge::Api::WorkflowCommands::RequestCancelExternalWorkflowExecution.new(
33
+ seq:,
34
+ workflow_execution: Bridge::Api::Common::NamespacedWorkflowExecution.new(
35
+ namespace: @instance.info.namespace,
36
+ workflow_id: input.id,
37
+ run_id: input.run_id
38
+ )
39
+ )
40
+ @instance.add_command(
41
+ Bridge::Api::WorkflowCommands::WorkflowCommand.new(request_cancel_external_workflow_execution: cmd)
42
+ )
43
+ @instance.pending_external_cancels[seq] = Fiber.current
44
+
45
+ # Wait
46
+ resolution = Fiber.yield
47
+
48
+ # Raise if resolution has failure
49
+ return unless resolution.failure
50
+
51
+ raise @instance.failure_converter.from_failure(resolution.failure, @instance.payload_converter)
52
+ end
53
+
54
+ def execute_activity(input)
55
+ if input.schedule_to_close_timeout.nil? && input.start_to_close_timeout.nil?
56
+ raise ArgumentError, 'Activity must have schedule_to_close_timeout or start_to_close_timeout'
57
+ end
58
+
59
+ activity_type = case input.activity
60
+ when Class
61
+ Activity::Definition::Info.from_activity(input.activity).name
62
+ when Symbol, String
63
+ input.activity.to_s
64
+ else
65
+ raise ArgumentError, 'Activity must be a definition class, or a symbol/string'
66
+ end
67
+ raise 'Cannot invoke dynamic activities' unless activity_type
68
+
69
+ execute_activity_with_local_backoffs(local: false, cancellation: input.cancellation) do
70
+ seq = (@activity_counter += 1)
71
+ @instance.add_command(
72
+ Bridge::Api::WorkflowCommands::WorkflowCommand.new(
73
+ schedule_activity: Bridge::Api::WorkflowCommands::ScheduleActivity.new(
74
+ seq:,
75
+ activity_id: input.activity_id || seq.to_s,
76
+ activity_type:,
77
+ task_queue: input.task_queue,
78
+ headers: ProtoUtils.headers_to_proto_hash(input.headers, @instance.payload_converter),
79
+ arguments: ProtoUtils.convert_to_payload_array(@instance.payload_converter, input.args),
80
+ schedule_to_close_timeout: ProtoUtils.seconds_to_duration(input.schedule_to_close_timeout),
81
+ schedule_to_start_timeout: ProtoUtils.seconds_to_duration(input.schedule_to_start_timeout),
82
+ start_to_close_timeout: ProtoUtils.seconds_to_duration(input.start_to_close_timeout),
83
+ heartbeat_timeout: ProtoUtils.seconds_to_duration(input.heartbeat_timeout),
84
+ retry_policy: input.retry_policy&._to_proto,
85
+ cancellation_type: input.cancellation_type,
86
+ do_not_eagerly_execute: input.disable_eager_execution
87
+ )
88
+ )
89
+ )
90
+ seq
91
+ end
92
+ end
93
+
94
+ def execute_local_activity(input)
95
+ if input.schedule_to_close_timeout.nil? && input.start_to_close_timeout.nil?
96
+ raise ArgumentError, 'Activity must have schedule_to_close_timeout or start_to_close_timeout'
97
+ end
98
+
99
+ activity_type = case input.activity
100
+ when Class
101
+ Activity::Definition::Info.from_activity(input.activity).name
102
+ when Symbol, String
103
+ input.activity.to_s
104
+ else
105
+ raise ArgumentError, 'Activity must be a definition class, or a symbol/string'
106
+ end
107
+ raise 'Cannot invoke dynamic activities' unless activity_type
108
+
109
+ execute_activity_with_local_backoffs(local: true, cancellation: input.cancellation) do |do_backoff|
110
+ seq = (@activity_counter += 1)
111
+ @instance.add_command(
112
+ Bridge::Api::WorkflowCommands::WorkflowCommand.new(
113
+ schedule_local_activity: Bridge::Api::WorkflowCommands::ScheduleLocalActivity.new(
114
+ seq:,
115
+ activity_id: input.activity_id || seq.to_s,
116
+ activity_type:,
117
+ headers: ProtoUtils.headers_to_proto_hash(input.headers, @instance.payload_converter),
118
+ arguments: ProtoUtils.convert_to_payload_array(@instance.payload_converter, input.args),
119
+ schedule_to_close_timeout: ProtoUtils.seconds_to_duration(input.schedule_to_close_timeout),
120
+ schedule_to_start_timeout: ProtoUtils.seconds_to_duration(input.schedule_to_start_timeout),
121
+ start_to_close_timeout: ProtoUtils.seconds_to_duration(input.start_to_close_timeout),
122
+ retry_policy: input.retry_policy&._to_proto,
123
+ cancellation_type: input.cancellation_type,
124
+ local_retry_threshold: ProtoUtils.seconds_to_duration(input.local_retry_threshold),
125
+ attempt: do_backoff&.attempt || 0,
126
+ original_schedule_time: do_backoff&.original_schedule_time
127
+ )
128
+ )
129
+ )
130
+ seq
131
+ end
132
+ end
133
+
134
+ def execute_activity_with_local_backoffs(local:, cancellation:, &)
135
+ # We do not even want to schedule if the cancellation is already cancelled. We choose to use canceled
136
+ # failure instead of wrapping in activity failure which is similar to what other SDKs do, with the accepted
137
+ # tradeoff that it makes rescue more difficult (hence the presence of Error.canceled? helper).
138
+ raise Error::CanceledError, 'Activity canceled before scheduled' if cancellation.canceled?
139
+
140
+ # This has to be done in a loop for local activity backoff
141
+ last_local_backoff = nil
142
+ loop do
143
+ result = execute_activity_once(local:, cancellation:, last_local_backoff:, &)
144
+ return result unless result.is_a?(Bridge::Api::ActivityResult::DoBackoff)
145
+
146
+ # @type var result: untyped
147
+ last_local_backoff = result
148
+ # Have to sleep the amount of the backoff, which can be canceled with the same cancellation
149
+ # TODO(cretz): What should this cancellation raise?
150
+ Workflow.sleep(ProtoUtils.duration_to_seconds(result.backoff_duration), cancellation:)
151
+ end
152
+ end
153
+
154
+ # If this doesn't raise, it returns success | DoBackoff
155
+ def execute_activity_once(local:, cancellation:, last_local_backoff:, &)
156
+ # Add to pending activities (removed by the resolver)
157
+ seq = yield last_local_backoff
158
+ @instance.pending_activities[seq] = Fiber.current
159
+
160
+ # Add cancellation hook
161
+ cancel_callback_key = cancellation.add_cancel_callback do
162
+ # Only if the activity is present still
163
+ if @instance.pending_activities.include?(seq)
164
+ if local
165
+ @instance.add_command(
166
+ Bridge::Api::WorkflowCommands::WorkflowCommand.new(
167
+ request_cancel_local_activity: Bridge::Api::WorkflowCommands::RequestCancelLocalActivity.new(seq:)
168
+ )
169
+ )
170
+ else
171
+ @instance.add_command(
172
+ Bridge::Api::WorkflowCommands::WorkflowCommand.new(
173
+ request_cancel_activity: Bridge::Api::WorkflowCommands::RequestCancelActivity.new(seq:)
174
+ )
175
+ )
176
+ end
177
+ end
178
+ end
179
+
180
+ # Wait
181
+ resolution = Fiber.yield
182
+
183
+ # Remove cancellation callback
184
+ cancellation.remove_cancel_callback(cancel_callback_key)
185
+
186
+ case resolution.status
187
+ when :completed
188
+ @instance.payload_converter.from_payload(resolution.completed.result)
189
+ when :failed
190
+ raise @instance.failure_converter.from_failure(resolution.failed.failure, @instance.payload_converter)
191
+ when :cancelled
192
+ raise @instance.failure_converter.from_failure(resolution.cancelled.failure, @instance.payload_converter)
193
+ when :backoff
194
+ resolution.backoff
195
+ else
196
+ raise "Unrecognized resolution status: #{resolution.status}"
197
+ end
198
+ end
199
+
200
+ def initialize_continue_as_new_error(input)
201
+ # Do nothing
202
+ end
203
+
204
+ def signal_child_workflow(input)
205
+ _signal_external_workflow(
206
+ id: input.id,
207
+ run_id: nil,
208
+ child: true,
209
+ signal: input.signal,
210
+ args: input.args,
211
+ cancellation: input.cancellation,
212
+ headers: input.headers
213
+ )
214
+ end
215
+
216
+ def signal_external_workflow(input)
217
+ _signal_external_workflow(
218
+ id: input.id,
219
+ run_id: input.run_id,
220
+ child: false,
221
+ signal: input.signal,
222
+ args: input.args,
223
+ cancellation: input.cancellation,
224
+ headers: input.headers
225
+ )
226
+ end
227
+
228
+ def _signal_external_workflow(id:, run_id:, child:, signal:, args:, cancellation:, headers:)
229
+ raise Error::CanceledError, 'Signal canceled before scheduled' if cancellation.canceled?
230
+
231
+ # Add command
232
+ seq = (@external_signal_counter += 1)
233
+ cmd = Bridge::Api::WorkflowCommands::SignalExternalWorkflowExecution.new(
234
+ seq:,
235
+ signal_name: Workflow::Definition::Signal._name_from_parameter(signal),
236
+ args: ProtoUtils.convert_to_payload_array(@instance.payload_converter, args),
237
+ headers: ProtoUtils.headers_to_proto_hash(headers, @instance.payload_converter)
238
+ )
239
+ if child
240
+ cmd.child_workflow_id = id
241
+ else
242
+ cmd.workflow_execution = Bridge::Api::Common::NamespacedWorkflowExecution.new(
243
+ namespace: @instance.info.namespace,
244
+ workflow_id: id,
245
+ run_id:
246
+ )
247
+ end
248
+ @instance.add_command(
249
+ Bridge::Api::WorkflowCommands::WorkflowCommand.new(signal_external_workflow_execution: cmd)
250
+ )
251
+ @instance.pending_external_signals[seq] = Fiber.current
252
+
253
+ # Add a cancellation callback
254
+ cancel_callback_key = cancellation.add_cancel_callback do
255
+ # Add the command but do not raise, we will let resolution do that
256
+ @instance.add_command(
257
+ Bridge::Api::WorkflowCommands::WorkflowCommand.new(
258
+ cancel_signal_workflow: Bridge::Api::WorkflowCommands::CancelSignalWorkflow.new(seq:)
259
+ )
260
+ )
261
+ end
262
+
263
+ # Wait
264
+ resolution = Fiber.yield
265
+
266
+ # Remove cancellation callback
267
+ cancellation.remove_cancel_callback(cancel_callback_key)
268
+
269
+ # Raise if resolution has failure
270
+ return unless resolution.failure
271
+
272
+ raise @instance.failure_converter.from_failure(resolution.failure, @instance.payload_converter)
273
+ end
274
+
275
+ def sleep(input)
276
+ # If already cancelled, raise as such
277
+ if input.cancellation.canceled?
278
+ raise Error::CanceledError,
279
+ input.cancellation.canceled_reason || 'Timer canceled before started'
280
+ end
281
+
282
+ # Disallow negative durations
283
+ raise ArgumentError, 'Sleep duration cannot be less than 0' if input.duration&.negative?
284
+
285
+ # If the duration is infinite, just wait for cancellation
286
+ if input.duration.nil?
287
+ input.cancellation.wait
288
+ raise Error::CanceledError, input.cancellation.canceled_reason || 'Timer canceled'
289
+ end
290
+
291
+ # If duration is zero, we make it one millisecond. It was decided a 0 duration still makes a timer to ensure
292
+ # determinism if a timer's duration is altered from non-zero to zero or vice versa.
293
+ duration = input.duration
294
+ duration = 0.001 if duration.zero?
295
+
296
+ # Add command
297
+ seq = (@timer_counter += 1)
298
+ @instance.add_command(
299
+ Bridge::Api::WorkflowCommands::WorkflowCommand.new(
300
+ start_timer: Bridge::Api::WorkflowCommands::StartTimer.new(
301
+ seq:,
302
+ start_to_fire_timeout: ProtoUtils.seconds_to_duration(duration)
303
+ )
304
+ )
305
+ )
306
+ @instance.pending_timers[seq] = Fiber.current
307
+
308
+ # Add a cancellation callback
309
+ cancel_callback_key = input.cancellation.add_cancel_callback do
310
+ # Only if the timer is still present
311
+ fiber = @instance.pending_timers.delete(seq)
312
+ if fiber
313
+ # Add the command for cancel then raise
314
+ @instance.add_command(
315
+ Bridge::Api::WorkflowCommands::WorkflowCommand.new(
316
+ cancel_timer: Bridge::Api::WorkflowCommands::CancelTimer.new(seq:)
317
+ )
318
+ )
319
+ if fiber.alive?
320
+ fiber.raise(Error::CanceledError.new(input.cancellation.canceled_reason || 'Timer canceled'))
321
+ end
322
+ end
323
+ end
324
+
325
+ # Wait
326
+ Fiber.yield
327
+
328
+ # Remove cancellation callback (only needed on success)
329
+ input.cancellation.remove_cancel_callback(cancel_callback_key)
330
+ end
331
+
332
+ def start_child_workflow(input)
333
+ raise Error::CanceledError, 'Child canceled before scheduled' if input.cancellation.canceled?
334
+
335
+ # Add the command
336
+ seq = (@child_counter += 1)
337
+ @instance.add_command(
338
+ Bridge::Api::WorkflowCommands::WorkflowCommand.new(
339
+ start_child_workflow_execution: Bridge::Api::WorkflowCommands::StartChildWorkflowExecution.new(
340
+ seq:,
341
+ namespace: @instance.info.namespace,
342
+ workflow_id: input.id,
343
+ workflow_type: Workflow::Definition._workflow_type_from_workflow_parameter(input.workflow),
344
+ task_queue: input.task_queue,
345
+ input: ProtoUtils.convert_to_payload_array(@instance.payload_converter, input.args),
346
+ workflow_execution_timeout: ProtoUtils.seconds_to_duration(input.execution_timeout),
347
+ workflow_run_timeout: ProtoUtils.seconds_to_duration(input.run_timeout),
348
+ workflow_task_timeout: ProtoUtils.seconds_to_duration(input.task_timeout),
349
+ parent_close_policy: input.parent_close_policy,
350
+ workflow_id_reuse_policy: input.id_reuse_policy,
351
+ retry_policy: input.retry_policy&._to_proto,
352
+ cron_schedule: input.cron_schedule,
353
+ headers: ProtoUtils.headers_to_proto_hash(input.headers, @instance.payload_converter),
354
+ memo: ProtoUtils.memo_to_proto_hash(input.memo, @instance.payload_converter),
355
+ search_attributes: input.search_attributes&._to_proto_hash,
356
+ cancellation_type: input.cancellation_type
357
+ )
358
+ )
359
+ )
360
+
361
+ # Set as pending start and register cancel callback
362
+ @instance.pending_child_workflow_starts[seq] = Fiber.current
363
+ cancel_callback_key = input.cancellation.add_cancel_callback do
364
+ # Send cancel if in start or pending
365
+ if @instance.pending_child_workflow_starts.include?(seq) ||
366
+ @instance.pending_child_workflows.include?(seq)
367
+ @instance.add_command(
368
+ Bridge::Api::WorkflowCommands::WorkflowCommand.new(
369
+ cancel_child_workflow_execution: Bridge::Api::WorkflowCommands::CancelChildWorkflowExecution.new(
370
+ child_workflow_seq: seq
371
+ )
372
+ )
373
+ )
374
+ end
375
+ end
376
+
377
+ # Wait for start
378
+ resolution = Fiber.yield
379
+
380
+ case resolution.status
381
+ when :succeeded
382
+ # Create handle, passing along the cancel callback key, and set it as pending
383
+ handle = ChildWorkflowHandle.new(
384
+ id: input.id,
385
+ first_execution_run_id: resolution.succeeded.run_id,
386
+ instance: @instance,
387
+ cancellation: input.cancellation,
388
+ cancel_callback_key:
389
+ )
390
+ @instance.pending_child_workflows[seq] = handle
391
+ handle
392
+ when :failed
393
+ # Remove cancel callback and handle failure
394
+ input.cancellation.remove_cancel_callback(cancel_callback_key)
395
+ if resolution.failed.cause == :START_CHILD_WORKFLOW_EXECUTION_FAILED_CAUSE_WORKFLOW_ALREADY_EXISTS
396
+ raise Error::WorkflowAlreadyStartedError.new(
397
+ workflow_id: resolution.failed.workflow_id,
398
+ workflow_type: resolution.failed.workflow_type,
399
+ run_id: nil
400
+ )
401
+ end
402
+ raise "Unknown child start fail cause: #{resolution.failed.cause}"
403
+ when :cancelled
404
+ # Remove cancel callback and handle cancel
405
+ input.cancellation.remove_cancel_callback(cancel_callback_key)
406
+ raise @instance.failure_converter.from_failure(resolution.cancelled.failure, @instance.payload_converter)
407
+ else
408
+ raise "Unknown resolution status: #{resolution.status}"
409
+ end
410
+ end
411
+ end
412
+ end
413
+ end
414
+ end
415
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'temporalio/scoped_logger'
4
+ require 'temporalio/workflow'
5
+
6
+ module Temporalio
7
+ module Internal
8
+ module Worker
9
+ class WorkflowInstance
10
+ # Wrapper for a scoped logger that does not log on replay.
11
+ class ReplaySafeLogger < ScopedLogger
12
+ def initialize(logger:, instance:)
13
+ @instance = instance
14
+ @replay_safety_disabled = false
15
+ super(logger)
16
+ end
17
+
18
+ def replay_safety_disabled(&)
19
+ @replay_safety_disabled = true
20
+ yield
21
+ ensure
22
+ @replay_safety_disabled = false
23
+ end
24
+
25
+ def add(...)
26
+ if !@replay_safety_disabled && Temporalio::Workflow.in_workflow? && Temporalio::Workflow::Unsafe.replaying?
27
+ return true
28
+ end
29
+
30
+ # Disable illegal call tracing for the log call
31
+ @instance.illegal_call_tracing_disabled { super }
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'temporalio/scoped_logger'
4
+
5
+ module Temporalio
6
+ module Internal
7
+ module Worker
8
+ class WorkflowInstance
9
+ # Wrapper for a metric that does not log on replay.
10
+ class ReplaySafeMetric < SimpleDelegator
11
+ def record(value, additional_attributes: nil)
12
+ return if Temporalio::Workflow.in_workflow? && Temporalio::Workflow::Unsafe.replaying?
13
+
14
+ super
15
+ end
16
+
17
+ def with_additional_attributes(additional_attributes)
18
+ ReplaySafeMetric.new(super)
19
+ end
20
+
21
+ class Meter < SimpleDelegator
22
+ def create_metric(
23
+ metric_type,
24
+ name,
25
+ description: nil,
26
+ unit: nil,
27
+ value_type: :integer
28
+ )
29
+ ReplaySafeMetric.new(super)
30
+ end
31
+
32
+ def with_additional_attributes(additional_attributes)
33
+ Meter.new(super)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'temporalio'
4
+ require 'temporalio/cancellation'
5
+ require 'temporalio/error'
6
+ require 'temporalio/internal/worker/workflow_instance'
7
+ require 'temporalio/workflow'
8
+ require 'timeout'
9
+
10
+ module Temporalio
11
+ module Internal
12
+ module Worker
13
+ class WorkflowInstance
14
+ # Deterministic {::Fiber::Scheduler} implementation.
15
+ class Scheduler
16
+ def initialize(instance)
17
+ @instance = instance
18
+ @fibers = []
19
+ @ready = []
20
+ @wait_conditions = {}
21
+ @wait_condition_counter = 0
22
+ end
23
+
24
+ def context
25
+ @instance.context
26
+ end
27
+
28
+ def run_until_all_yielded
29
+ loop do
30
+ # Run all fibers until all yielded
31
+ while (fiber = @ready.shift)
32
+ fiber.resume
33
+ end
34
+
35
+ # Find the _first_ resolvable wait condition and if there, resolve it, and loop again, otherwise return.
36
+ # It is important that we both let fibers get all settled _before_ this and only allow a _single_ wait
37
+ # condition to be satisfied before looping. This allows wait condition users to trust that the line of
38
+ # code after the wait condition still has the condition satisfied.
39
+ # @type var cond_fiber: Fiber?
40
+ cond_fiber = nil
41
+ cond_result = nil
42
+ @wait_conditions.each do |seq, cond|
43
+ next unless (cond_result = cond.first.call)
44
+
45
+ cond_fiber = cond[1]
46
+ @wait_conditions.delete(seq)
47
+ break
48
+ end
49
+ return unless cond_fiber
50
+
51
+ cond_fiber.resume(cond_result)
52
+ end
53
+ end
54
+
55
+ def wait_condition(cancellation:, &block)
56
+ raise Workflow::InvalidWorkflowStateError, 'Cannot wait in this context' if @instance.context_frozen
57
+
58
+ if cancellation&.canceled?
59
+ raise Error::CanceledError,
60
+ cancellation.canceled_reason || 'Wait condition canceled before started'
61
+ end
62
+
63
+ seq = (@wait_condition_counter += 1)
64
+ @wait_conditions[seq] = [block, Fiber.current]
65
+
66
+ # Add a cancellation callback
67
+ cancel_callback_key = cancellation&.add_cancel_callback do
68
+ # Only if the condition is still present
69
+ cond = @wait_conditions.delete(seq)
70
+ if cond&.last&.alive?
71
+ cond&.last&.raise(Error::CanceledError.new(cancellation&.canceled_reason || 'Wait condition canceled'))
72
+ end
73
+ end
74
+
75
+ # This blocks until a resume is called on this fiber
76
+ result = Fiber.yield
77
+
78
+ # Remove cancellation callback (only needed on success)
79
+ cancellation&.remove_cancel_callback(cancel_callback_key) if cancel_callback_key
80
+
81
+ result
82
+ end
83
+
84
+ def stack_trace
85
+ # Collect backtraces of known fibers, separating with a blank line. We make sure to remove any lines that
86
+ # reference Temporal paths, and we remove any empty backtraces.
87
+ dir_path = @instance.illegal_call_tracing_disabled { File.dirname(Temporalio._root_file_path) }
88
+ @fibers.map do |fiber|
89
+ fiber.backtrace.reject { |s| s.start_with?(dir_path) }.join("\n")
90
+ end.reject(&:empty?).join("\n\n")
91
+ end
92
+
93
+ ###
94
+ # Fiber::Scheduler methods
95
+ #
96
+ # Note, we do not implement many methods here such as io_read and
97
+ # such. While it might seem to make sense to implement them and
98
+ # raise, we actually want to default to the blocking behavior of them
99
+ # not being present. This is so advanced things like logging still
100
+ # work inside of workflows. So we only implement the bare minimum.
101
+ ###
102
+
103
+ def block(_blocker, timeout = nil)
104
+ # TODO(cretz): Make the blocker visible in the stack trace?
105
+
106
+ # We just yield because unblock will resume this. We will just wrap in timeout if needed.
107
+ if timeout
108
+ begin
109
+ Timeout.timeout(timeout) { Fiber.yield }
110
+ true
111
+ rescue Timeout::Error
112
+ false
113
+ end
114
+ else
115
+ Fiber.yield
116
+ true
117
+ end
118
+ end
119
+
120
+ def close
121
+ # Nothing to do here, lifetime of scheduler is controlled by the instance
122
+ end
123
+
124
+ def fiber(&block)
125
+ if @instance.context_frozen
126
+ raise Workflow::InvalidWorkflowStateError, 'Cannot schedule fibers in this context'
127
+ end
128
+
129
+ fiber = Fiber.new do
130
+ block.call # steep:ignore
131
+ ensure
132
+ @fibers.delete(Fiber.current)
133
+ end
134
+ @fibers << fiber
135
+ @ready << fiber
136
+ fiber
137
+ end
138
+
139
+ def io_wait(io, events, timeout)
140
+ # TODO(cretz): This in a blocking fashion?
141
+ raise NotImplementedError, 'TODO'
142
+ end
143
+
144
+ def kernel_sleep(duration = nil)
145
+ Workflow.sleep(duration)
146
+ end
147
+
148
+ def process_wait(pid, flags)
149
+ raise NotImplementedError, 'Cannot wait on other processes in workflows'
150
+ end
151
+
152
+ def timeout_after(duration, exception_class, *exception_arguments, &)
153
+ context.timeout(duration, exception_class, *exception_arguments, summary: 'Timeout timer', &)
154
+ end
155
+
156
+ def unblock(_blocker, fiber)
157
+ @ready << fiber
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end