temporalio 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardopts +2 -0
- data/Cargo.lock +659 -370
- data/Cargo.toml +2 -2
- data/Gemfile +3 -3
- data/README.md +589 -47
- data/Rakefile +10 -296
- data/ext/Cargo.toml +1 -0
- data/lib/temporalio/activity/complete_async_error.rb +1 -1
- data/lib/temporalio/activity/context.rb +5 -2
- data/lib/temporalio/activity/definition.rb +163 -65
- data/lib/temporalio/activity/info.rb +22 -21
- data/lib/temporalio/activity.rb +2 -59
- data/lib/temporalio/api/activity/v1/message.rb +25 -0
- data/lib/temporalio/api/cloud/account/v1/message.rb +28 -0
- data/lib/temporalio/api/cloud/cloudservice/v1/request_response.rb +34 -1
- data/lib/temporalio/api/cloud/cloudservice/v1/service.rb +1 -1
- data/lib/temporalio/api/cloud/identity/v1/message.rb +6 -1
- data/lib/temporalio/api/cloud/namespace/v1/message.rb +8 -1
- data/lib/temporalio/api/cloud/nexus/v1/message.rb +31 -0
- data/lib/temporalio/api/cloud/operation/v1/message.rb +2 -1
- data/lib/temporalio/api/cloud/region/v1/message.rb +2 -1
- data/lib/temporalio/api/cloud/resource/v1/message.rb +23 -0
- data/lib/temporalio/api/cloud/sink/v1/message.rb +24 -0
- data/lib/temporalio/api/cloud/usage/v1/message.rb +31 -0
- data/lib/temporalio/api/common/v1/message.rb +7 -1
- data/lib/temporalio/api/enums/v1/event_type.rb +1 -1
- data/lib/temporalio/api/enums/v1/failed_cause.rb +1 -1
- data/lib/temporalio/api/enums/v1/reset.rb +1 -1
- data/lib/temporalio/api/history/v1/message.rb +1 -1
- data/lib/temporalio/api/nexus/v1/message.rb +2 -2
- data/lib/temporalio/api/operatorservice/v1/service.rb +1 -1
- data/lib/temporalio/api/payload_visitor.rb +1513 -0
- data/lib/temporalio/api/schedule/v1/message.rb +2 -1
- data/lib/temporalio/api/testservice/v1/request_response.rb +31 -0
- data/lib/temporalio/api/testservice/v1/service.rb +23 -0
- data/lib/temporalio/api/workflow/v1/message.rb +1 -1
- data/lib/temporalio/api/workflowservice/v1/request_response.rb +17 -2
- data/lib/temporalio/api/workflowservice/v1/service.rb +1 -1
- data/lib/temporalio/api.rb +1 -0
- data/lib/temporalio/cancellation.rb +34 -14
- data/lib/temporalio/client/async_activity_handle.rb +12 -37
- data/lib/temporalio/client/connection/cloud_service.rb +309 -231
- data/lib/temporalio/client/connection/operator_service.rb +36 -84
- data/lib/temporalio/client/connection/service.rb +6 -5
- data/lib/temporalio/client/connection/test_service.rb +111 -0
- data/lib/temporalio/client/connection/workflow_service.rb +264 -441
- data/lib/temporalio/client/connection.rb +90 -44
- data/lib/temporalio/client/interceptor.rb +160 -60
- data/lib/temporalio/client/schedule.rb +967 -0
- data/lib/temporalio/client/schedule_handle.rb +126 -0
- data/lib/temporalio/client/workflow_execution.rb +7 -10
- data/lib/temporalio/client/workflow_handle.rb +38 -95
- data/lib/temporalio/client/workflow_update_handle.rb +3 -5
- data/lib/temporalio/client.rb +122 -42
- data/lib/temporalio/common_enums.rb +17 -0
- data/lib/temporalio/converters/data_converter.rb +4 -7
- data/lib/temporalio/converters/failure_converter.rb +5 -3
- data/lib/temporalio/converters/payload_converter/composite.rb +4 -0
- data/lib/temporalio/converters/payload_converter.rb +6 -8
- data/lib/temporalio/converters/raw_value.rb +20 -0
- data/lib/temporalio/error/failure.rb +1 -1
- data/lib/temporalio/error.rb +10 -2
- data/lib/temporalio/internal/bridge/api/core_interface.rb +5 -1
- data/lib/temporalio/internal/bridge/api/nexus/nexus.rb +33 -0
- data/lib/temporalio/internal/bridge/api/workflow_activation/workflow_activation.rb +5 -1
- data/lib/temporalio/internal/bridge/api/workflow_commands/workflow_commands.rb +4 -1
- data/lib/temporalio/internal/bridge/client.rb +11 -6
- data/lib/temporalio/internal/bridge/testing.rb +20 -0
- data/lib/temporalio/internal/bridge/worker.rb +2 -0
- data/lib/temporalio/internal/bridge.rb +1 -1
- data/lib/temporalio/internal/client/implementation.rb +245 -70
- data/lib/temporalio/internal/metric.rb +122 -0
- data/lib/temporalio/internal/proto_utils.rb +86 -7
- data/lib/temporalio/internal/worker/activity_worker.rb +52 -24
- data/lib/temporalio/internal/worker/multi_runner.rb +51 -7
- data/lib/temporalio/internal/worker/workflow_instance/child_workflow_handle.rb +54 -0
- data/lib/temporalio/internal/worker/workflow_instance/context.rb +329 -0
- data/lib/temporalio/internal/worker/workflow_instance/details.rb +44 -0
- data/lib/temporalio/internal/worker/workflow_instance/external_workflow_handle.rb +32 -0
- data/lib/temporalio/internal/worker/workflow_instance/externally_immutable_hash.rb +22 -0
- data/lib/temporalio/internal/worker/workflow_instance/handler_execution.rb +25 -0
- data/lib/temporalio/internal/worker/workflow_instance/handler_hash.rb +41 -0
- data/lib/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rb +97 -0
- data/lib/temporalio/internal/worker/workflow_instance/inbound_implementation.rb +62 -0
- data/lib/temporalio/internal/worker/workflow_instance/outbound_implementation.rb +415 -0
- data/lib/temporalio/internal/worker/workflow_instance/replay_safe_logger.rb +37 -0
- data/lib/temporalio/internal/worker/workflow_instance/replay_safe_metric.rb +40 -0
- data/lib/temporalio/internal/worker/workflow_instance/scheduler.rb +163 -0
- data/lib/temporalio/internal/worker/workflow_instance.rb +730 -0
- data/lib/temporalio/internal/worker/workflow_worker.rb +196 -0
- data/lib/temporalio/metric.rb +109 -0
- data/lib/temporalio/retry_policy.rb +37 -14
- data/lib/temporalio/runtime.rb +118 -75
- data/lib/temporalio/search_attributes.rb +80 -37
- data/lib/temporalio/testing/activity_environment.rb +2 -2
- data/lib/temporalio/testing/workflow_environment.rb +251 -5
- data/lib/temporalio/version.rb +1 -1
- data/lib/temporalio/worker/activity_executor/thread_pool.rb +9 -217
- data/lib/temporalio/worker/activity_executor.rb +3 -3
- data/lib/temporalio/worker/interceptor.rb +340 -66
- data/lib/temporalio/worker/thread_pool.rb +237 -0
- data/lib/temporalio/worker/workflow_executor/thread_pool.rb +230 -0
- data/lib/temporalio/worker/workflow_executor.rb +26 -0
- data/lib/temporalio/worker.rb +201 -30
- data/lib/temporalio/workflow/activity_cancellation_type.rb +20 -0
- data/lib/temporalio/workflow/child_workflow_cancellation_type.rb +21 -0
- data/lib/temporalio/workflow/child_workflow_handle.rb +43 -0
- data/lib/temporalio/workflow/definition.rb +566 -0
- data/lib/temporalio/workflow/external_workflow_handle.rb +41 -0
- data/lib/temporalio/workflow/future.rb +151 -0
- data/lib/temporalio/workflow/handler_unfinished_policy.rb +13 -0
- data/lib/temporalio/workflow/info.rb +82 -0
- data/lib/temporalio/workflow/parent_close_policy.rb +19 -0
- data/lib/temporalio/workflow/update_info.rb +20 -0
- data/lib/temporalio/workflow.rb +523 -0
- data/lib/temporalio.rb +4 -0
- data/temporalio.gemspec +2 -2
- 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
|