temporalio 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.yardopts +2 -0
- data/Cargo.lock +980 -583
- data/Cargo.toml +2 -2
- data/Gemfile +7 -3
- data/README.md +769 -54
- data/Rakefile +10 -296
- data/ext/Cargo.toml +2 -0
- data/lib/temporalio/activity/complete_async_error.rb +1 -1
- data/lib/temporalio/activity/context.rb +18 -2
- data/lib/temporalio/activity/definition.rb +180 -65
- data/lib/temporalio/activity/info.rb +25 -21
- data/lib/temporalio/activity.rb +2 -59
- data/lib/temporalio/api/activity/v1/message.rb +25 -0
- data/lib/temporalio/api/batch/v1/message.rb +6 -1
- 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/command/v1/message.rb +1 -1
- data/lib/temporalio/api/common/v1/message.rb +8 -1
- data/lib/temporalio/api/deployment/v1/message.rb +38 -0
- data/lib/temporalio/api/enums/v1/batch_operation.rb +1 -1
- data/lib/temporalio/api/enums/v1/common.rb +1 -1
- data/lib/temporalio/api/enums/v1/deployment.rb +23 -0
- 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/nexus.rb +21 -0
- data/lib/temporalio/api/enums/v1/reset.rb +1 -1
- data/lib/temporalio/api/enums/v1/workflow.rb +2 -1
- data/lib/temporalio/api/errordetails/v1/message.rb +3 -1
- data/lib/temporalio/api/failure/v1/message.rb +3 -1
- data/lib/temporalio/api/history/v1/message.rb +3 -1
- data/lib/temporalio/api/nexus/v1/message.rb +3 -2
- data/lib/temporalio/api/operatorservice/v1/service.rb +1 -1
- data/lib/temporalio/api/payload_visitor.rb +1581 -0
- data/lib/temporalio/api/query/v1/message.rb +2 -1
- data/lib/temporalio/api/schedule/v1/message.rb +2 -1
- data/lib/temporalio/api/taskqueue/v1/message.rb +4 -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 +9 -1
- data/lib/temporalio/api/workflowservice/v1/request_response.rb +46 -2
- data/lib/temporalio/api/workflowservice/v1/service.rb +1 -1
- data/lib/temporalio/api.rb +2 -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 +474 -441
- data/lib/temporalio/client/connection.rb +90 -44
- data/lib/temporalio/client/interceptor.rb +199 -60
- data/lib/temporalio/client/schedule.rb +991 -0
- data/lib/temporalio/client/schedule_handle.rb +126 -0
- data/lib/temporalio/client/with_start_workflow_operation.rb +115 -0
- data/lib/temporalio/client/workflow_execution.rb +26 -10
- data/lib/temporalio/client/workflow_handle.rb +41 -98
- data/lib/temporalio/client/workflow_update_handle.rb +3 -5
- data/lib/temporalio/client.rb +247 -44
- data/lib/temporalio/common_enums.rb +17 -0
- data/lib/temporalio/contrib/open_telemetry.rb +470 -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 +11 -2
- data/lib/temporalio/internal/bridge/api/activity_task/activity_task.rb +1 -1
- data/lib/temporalio/internal/bridge/api/common/common.rb +2 -1
- 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/api/workflow_completion/workflow_completion.rb +2 -1
- data/lib/temporalio/internal/bridge/client.rb +11 -6
- data/lib/temporalio/internal/bridge/runtime.rb +3 -0
- data/lib/temporalio/internal/bridge/testing.rb +23 -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 +468 -71
- data/lib/temporalio/internal/metric.rb +122 -0
- data/lib/temporalio/internal/proto_utils.rb +118 -7
- data/lib/temporalio/internal/worker/activity_worker.rb +69 -29
- data/lib/temporalio/internal/worker/multi_runner.rb +53 -9
- data/lib/temporalio/internal/worker/workflow_instance/child_workflow_handle.rb +54 -0
- data/lib/temporalio/internal/worker/workflow_instance/context.rb +383 -0
- data/lib/temporalio/internal/worker/workflow_instance/details.rb +46 -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 +400 -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 +183 -0
- data/lib/temporalio/internal/worker/workflow_instance.rb +774 -0
- data/lib/temporalio/internal/worker/workflow_worker.rb +239 -0
- data/lib/temporalio/metric.rb +109 -0
- data/lib/temporalio/retry_policy.rb +37 -14
- data/lib/temporalio/runtime/metric_buffer.rb +94 -0
- data/lib/temporalio/runtime.rb +160 -79
- data/lib/temporalio/search_attributes.rb +93 -37
- data/lib/temporalio/testing/activity_environment.rb +44 -16
- data/lib/temporalio/testing/workflow_environment.rb +276 -7
- 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 +343 -66
- data/lib/temporalio/worker/thread_pool.rb +237 -0
- data/lib/temporalio/worker/tuner.rb +38 -0
- data/lib/temporalio/worker/workflow_executor/thread_pool.rb +235 -0
- data/lib/temporalio/worker/workflow_executor.rb +26 -0
- data/lib/temporalio/worker/workflow_replayer.rb +350 -0
- data/lib/temporalio/worker.rb +235 -58
- 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 +598 -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 +104 -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 +575 -0
- data/lib/temporalio/workflow_history.rb +26 -1
- data/lib/temporalio.rb +4 -0
- data/temporalio.gemspec +4 -3
- metadata +73 -10
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'temporalio/internal/bridge/worker'
|
4
|
+
|
3
5
|
module Temporalio
|
4
6
|
class Worker
|
5
7
|
# Worker tuner that allows for dynamic customization of some aspects of worker configuration.
|
@@ -18,6 +20,14 @@ module Temporalio
|
|
18
20
|
def initialize(slots) # rubocop:disable Lint/MissingSuper
|
19
21
|
@slots = slots
|
20
22
|
end
|
23
|
+
|
24
|
+
# @!visibility private
|
25
|
+
def _to_bridge_options
|
26
|
+
Internal::Bridge::Worker::TunerSlotSupplierOptions.new(
|
27
|
+
fixed_size: slots,
|
28
|
+
resource_based: nil
|
29
|
+
)
|
30
|
+
end
|
21
31
|
end
|
22
32
|
|
23
33
|
# A slot supplier that will dynamically adjust the number of slots based on resource usage.
|
@@ -34,6 +44,25 @@ module Temporalio
|
|
34
44
|
@tuner_options = tuner_options
|
35
45
|
@slot_options = slot_options
|
36
46
|
end
|
47
|
+
|
48
|
+
# @!visibility private
|
49
|
+
def _to_bridge_options
|
50
|
+
Internal::Bridge::Worker::TunerSlotSupplierOptions.new(
|
51
|
+
fixed_size: nil,
|
52
|
+
resource_based: Internal::Bridge::Worker::TunerResourceBasedSlotSupplierOptions.new(
|
53
|
+
target_mem_usage: tuner_options.target_memory_usage,
|
54
|
+
target_cpu_usage: tuner_options.target_cpu_usage,
|
55
|
+
min_slots: slot_options.min_slots,
|
56
|
+
max_slots: slot_options.max_slots,
|
57
|
+
ramp_throttle: slot_options.ramp_throttle
|
58
|
+
)
|
59
|
+
)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# @!visibility private
|
64
|
+
def _to_bridge_options
|
65
|
+
raise ArgumentError, 'Tuner slot suppliers must be instances of Fixed or ResourceBased'
|
37
66
|
end
|
38
67
|
end
|
39
68
|
|
@@ -146,6 +175,15 @@ module Temporalio
|
|
146
175
|
@activity_slot_supplier = activity_slot_supplier
|
147
176
|
@local_activity_slot_supplier = local_activity_slot_supplier
|
148
177
|
end
|
178
|
+
|
179
|
+
# @!visibility private
|
180
|
+
def _to_bridge_options
|
181
|
+
Internal::Bridge::Worker::TunerOptions.new(
|
182
|
+
workflow_slot_supplier: workflow_slot_supplier._to_bridge_options,
|
183
|
+
activity_slot_supplier: activity_slot_supplier._to_bridge_options,
|
184
|
+
local_activity_slot_supplier: local_activity_slot_supplier._to_bridge_options
|
185
|
+
)
|
186
|
+
end
|
149
187
|
end
|
150
188
|
end
|
151
189
|
end
|
@@ -0,0 +1,235 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'etc'
|
4
|
+
require 'temporalio/internal/bridge/api'
|
5
|
+
require 'temporalio/internal/proto_utils'
|
6
|
+
require 'temporalio/internal/worker/workflow_instance'
|
7
|
+
require 'temporalio/scoped_logger'
|
8
|
+
require 'temporalio/worker/thread_pool'
|
9
|
+
require 'temporalio/worker/workflow_executor'
|
10
|
+
require 'temporalio/workflow'
|
11
|
+
require 'temporalio/workflow/definition'
|
12
|
+
require 'timeout'
|
13
|
+
|
14
|
+
module Temporalio
|
15
|
+
class Worker
|
16
|
+
class WorkflowExecutor
|
17
|
+
# Thread pool implementation of {WorkflowExecutor}.
|
18
|
+
#
|
19
|
+
# Users should use {default} unless they have specific needs to change the thread pool or max threads.
|
20
|
+
class ThreadPool < WorkflowExecutor
|
21
|
+
# @return [ThreadPool] Default executor that lazily constructs an instance with default values.
|
22
|
+
def self.default
|
23
|
+
@default ||= ThreadPool.new
|
24
|
+
end
|
25
|
+
|
26
|
+
# Create a thread pool executor. Most users may prefer {default}.
|
27
|
+
#
|
28
|
+
# @param max_threads [Integer] Maximum number of threads to use concurrently.
|
29
|
+
# @param thread_pool [Worker::ThreadPool] Thread pool to use.
|
30
|
+
def initialize(max_threads: [4, Etc.nprocessors].max, thread_pool: Temporalio::Worker::ThreadPool.default) # rubocop:disable Lint/MissingSuper
|
31
|
+
@max_threads = max_threads
|
32
|
+
@thread_pool = thread_pool
|
33
|
+
@workers_mutex = Mutex.new
|
34
|
+
@workers = []
|
35
|
+
@workers_by_worker_state_and_run_id = {}
|
36
|
+
end
|
37
|
+
|
38
|
+
# @!visibility private
|
39
|
+
def _validate_worker(workflow_worker, worker_state)
|
40
|
+
# Do nothing
|
41
|
+
end
|
42
|
+
|
43
|
+
# @!visibility private
|
44
|
+
def _activate(activation, worker_state, &)
|
45
|
+
# Get applicable worker
|
46
|
+
worker = @workers_mutex.synchronize do
|
47
|
+
run_key = [worker_state, activation.run_id]
|
48
|
+
@workers_by_worker_state_and_run_id.fetch(run_key) do
|
49
|
+
# If not found, get a new one either by creating if not enough or find the one with the fewest.
|
50
|
+
new_worker = if @workers.size < @max_threads
|
51
|
+
created_worker = Worker.new(self)
|
52
|
+
@workers << Worker.new(self)
|
53
|
+
created_worker
|
54
|
+
else
|
55
|
+
@workers.min_by(&:workflow_count)
|
56
|
+
end
|
57
|
+
@workers_by_worker_state_and_run_id[run_key] = new_worker
|
58
|
+
new_worker.workflow_count += 1
|
59
|
+
new_worker
|
60
|
+
end
|
61
|
+
end
|
62
|
+
raise "No worker for run ID #{activation.run_id}" unless worker
|
63
|
+
|
64
|
+
# Enqueue activation
|
65
|
+
worker.enqueue_activation(activation, worker_state, &)
|
66
|
+
end
|
67
|
+
|
68
|
+
# @!visibility private
|
69
|
+
def _thread_pool
|
70
|
+
@thread_pool
|
71
|
+
end
|
72
|
+
|
73
|
+
# @!visibility private
|
74
|
+
def _remove_workflow(worker_state, run_id)
|
75
|
+
@workers_mutex.synchronize do
|
76
|
+
worker = @workers_by_worker_state_and_run_id.delete([worker_state, run_id])
|
77
|
+
if worker
|
78
|
+
worker.workflow_count -= 1
|
79
|
+
# Remove worker from array if done. The array should be small enough that the delete being O(N) is not
|
80
|
+
# worth using a set or a map.
|
81
|
+
if worker.workflow_count.zero?
|
82
|
+
@workers.delete(worker)
|
83
|
+
worker.shutdown
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# @!visibility private
|
90
|
+
class Worker
|
91
|
+
LOG_ACTIVATIONS = false
|
92
|
+
|
93
|
+
attr_accessor :workflow_count
|
94
|
+
|
95
|
+
def initialize(executor)
|
96
|
+
@executor = executor
|
97
|
+
@workflow_count = 0
|
98
|
+
@queue = Queue.new
|
99
|
+
executor._thread_pool.execute { run }
|
100
|
+
end
|
101
|
+
|
102
|
+
# @!visibility private
|
103
|
+
def enqueue_activation(activation, worker_state, &completion_block)
|
104
|
+
@queue << [:activate, activation, worker_state, completion_block]
|
105
|
+
end
|
106
|
+
|
107
|
+
# @!visibility private
|
108
|
+
def shutdown
|
109
|
+
@queue << [:shutdown]
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def run
|
115
|
+
loop do
|
116
|
+
work = @queue.pop
|
117
|
+
if work.is_a?(Exception)
|
118
|
+
Warning.warn("Failed activation: #{work}")
|
119
|
+
elsif work.is_a?(Array)
|
120
|
+
case work.first
|
121
|
+
when :shutdown
|
122
|
+
return
|
123
|
+
when :activate
|
124
|
+
activate(work[1], work[2], &work[3])
|
125
|
+
end
|
126
|
+
end
|
127
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
128
|
+
Warning.warn("Unexpected failure during run: #{e.full_message}")
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def activate(activation, worker_state, &)
|
133
|
+
worker_state.logger.debug("Received workflow activation: #{activation}") if LOG_ACTIVATIONS
|
134
|
+
|
135
|
+
# Check whether it has eviction
|
136
|
+
cache_remove_job = activation.jobs.find { |j| !j.remove_from_cache.nil? }&.remove_from_cache
|
137
|
+
|
138
|
+
# If it's eviction only, just evict inline and do nothing else
|
139
|
+
if cache_remove_job && activation.jobs.size == 1
|
140
|
+
evict(worker_state, activation.run_id, cache_remove_job)
|
141
|
+
worker_state.logger.debug('Sending empty workflow completion') if LOG_ACTIVATIONS
|
142
|
+
yield Internal::Bridge::Api::WorkflowCompletion::WorkflowActivationCompletion.new(
|
143
|
+
run_id: activation.run_id,
|
144
|
+
successful: Internal::Bridge::Api::WorkflowCompletion::Success.new
|
145
|
+
)
|
146
|
+
return
|
147
|
+
end
|
148
|
+
|
149
|
+
completion = Timeout.timeout(
|
150
|
+
worker_state.deadlock_timeout,
|
151
|
+
DeadlockError,
|
152
|
+
# TODO(cretz): Document that this affects all running workflows on this worker
|
153
|
+
# and maybe test to see how that is mitigated
|
154
|
+
"[TMPRL1101] Potential deadlock detected: workflow didn't yield " \
|
155
|
+
"within #{worker_state.deadlock_timeout} second(s)."
|
156
|
+
) do
|
157
|
+
# Get or create workflow
|
158
|
+
instance = worker_state.get_or_create_running_workflow(activation.run_id) do
|
159
|
+
create_instance(activation, worker_state)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Activate. We expect most errors in here to have been captured inside.
|
163
|
+
instance.activate(activation)
|
164
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
165
|
+
worker_state.logger.error("Failed activation on workflow run ID: #{activation.run_id}")
|
166
|
+
worker_state.logger.error(e)
|
167
|
+
Internal::Worker::WorkflowInstance.new_completion_with_failure(
|
168
|
+
run_id: activation.run_id,
|
169
|
+
error: e,
|
170
|
+
failure_converter: worker_state.data_converter.failure_converter,
|
171
|
+
payload_converter: worker_state.data_converter.payload_converter
|
172
|
+
)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Go ahead and evict if there is an eviction job
|
176
|
+
evict(worker_state, activation.run_id, cache_remove_job) if cache_remove_job
|
177
|
+
|
178
|
+
# Complete the activation
|
179
|
+
worker_state.logger.debug("Sending workflow completion: #{completion}") if LOG_ACTIVATIONS
|
180
|
+
yield completion
|
181
|
+
end
|
182
|
+
|
183
|
+
def create_instance(initial_activation, worker_state)
|
184
|
+
# Extract start job
|
185
|
+
init_job = initial_activation.jobs.find { |j| !j.initialize_workflow.nil? }&.initialize_workflow
|
186
|
+
raise 'Missing initialize job in initial activation' unless init_job
|
187
|
+
|
188
|
+
# Obtain definition
|
189
|
+
definition = worker_state.workflow_definitions[init_job.workflow_type]
|
190
|
+
# If not present and not reserved, try dynamic
|
191
|
+
if !definition && !Internal::ProtoUtils.reserved_name?(init_job.workflow_type)
|
192
|
+
definition = worker_state.workflow_definitions[nil]
|
193
|
+
end
|
194
|
+
|
195
|
+
unless definition
|
196
|
+
raise Error::ApplicationError.new(
|
197
|
+
"Workflow type #{init_job.workflow_type} is not registered on this worker, available workflows: " +
|
198
|
+
worker_state.workflow_definitions.keys.compact.sort.join(', '),
|
199
|
+
type: 'NotFoundError'
|
200
|
+
)
|
201
|
+
end
|
202
|
+
|
203
|
+
Internal::Worker::WorkflowInstance.new(
|
204
|
+
Internal::Worker::WorkflowInstance::Details.new(
|
205
|
+
namespace: worker_state.namespace,
|
206
|
+
task_queue: worker_state.task_queue,
|
207
|
+
definition:,
|
208
|
+
initial_activation:,
|
209
|
+
logger: worker_state.logger,
|
210
|
+
metric_meter: worker_state.metric_meter,
|
211
|
+
payload_converter: worker_state.data_converter.payload_converter,
|
212
|
+
failure_converter: worker_state.data_converter.failure_converter,
|
213
|
+
interceptors: worker_state.workflow_interceptors,
|
214
|
+
disable_eager_activity_execution: worker_state.disable_eager_activity_execution,
|
215
|
+
illegal_calls: worker_state.illegal_calls,
|
216
|
+
workflow_failure_exception_types: worker_state.workflow_failure_exception_types,
|
217
|
+
unsafe_workflow_io_enabled: worker_state.unsafe_workflow_io_enabled
|
218
|
+
)
|
219
|
+
)
|
220
|
+
end
|
221
|
+
|
222
|
+
def evict(worker_state, run_id, cache_remove_job)
|
223
|
+
worker_state.evict_running_workflow(run_id, cache_remove_job)
|
224
|
+
@executor._remove_workflow(worker_state, run_id)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
private_constant :Worker
|
229
|
+
|
230
|
+
# Error raised when a processing a workflow task takes more than the expected amount of time.
|
231
|
+
class DeadlockError < Exception; end # rubocop:disable Lint/InheritException
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'temporalio/worker/workflow_executor/thread_pool'
|
4
|
+
|
5
|
+
module Temporalio
|
6
|
+
class Worker
|
7
|
+
# Workflow executor that executes workflow tasks. Unlike {ActivityExecutor}, this class is not meant for user
|
8
|
+
# implementation. The only implementation that is currently accepted is {WorkflowExecutor::ThreadPool}.
|
9
|
+
class WorkflowExecutor
|
10
|
+
# @!visibility private
|
11
|
+
def initialize
|
12
|
+
raise 'Cannot create custom executors'
|
13
|
+
end
|
14
|
+
|
15
|
+
# @!visibility private
|
16
|
+
def _validate_worker(workflow_worker, worker_state)
|
17
|
+
raise NotImplementedError
|
18
|
+
end
|
19
|
+
|
20
|
+
# @!visibility private
|
21
|
+
def _activate(activation, worker_state, &)
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,350 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'temporalio/api'
|
4
|
+
require 'temporalio/converters'
|
5
|
+
require 'temporalio/internal/bridge'
|
6
|
+
require 'temporalio/internal/bridge/worker'
|
7
|
+
require 'temporalio/internal/worker/multi_runner'
|
8
|
+
require 'temporalio/internal/worker/workflow_worker'
|
9
|
+
require 'temporalio/worker/interceptor'
|
10
|
+
require 'temporalio/worker/thread_pool'
|
11
|
+
require 'temporalio/worker/tuner'
|
12
|
+
require 'temporalio/worker/workflow_executor'
|
13
|
+
require 'temporalio/workflow'
|
14
|
+
require 'temporalio/workflow_history'
|
15
|
+
|
16
|
+
module Temporalio
|
17
|
+
class Worker
|
18
|
+
# Replayer to replay workflows from existing history.
|
19
|
+
class WorkflowReplayer
|
20
|
+
Options = Data.define(
|
21
|
+
:workflows,
|
22
|
+
:namespace,
|
23
|
+
:task_queue,
|
24
|
+
:data_converter,
|
25
|
+
:workflow_executor,
|
26
|
+
:interceptors,
|
27
|
+
:build_id,
|
28
|
+
:identity,
|
29
|
+
:logger,
|
30
|
+
:illegal_workflow_calls,
|
31
|
+
:workflow_failure_exception_types,
|
32
|
+
:workflow_payload_codec_thread_pool,
|
33
|
+
:unsafe_workflow_io_enabled,
|
34
|
+
:debug_mode,
|
35
|
+
:runtime
|
36
|
+
)
|
37
|
+
|
38
|
+
# Options as returned from {options} representing the options passed to the constructor.
|
39
|
+
class Options; end # rubocop:disable Lint/EmptyClass
|
40
|
+
|
41
|
+
# @return [Options] Options for this replayer which has the same attributes as {initialize}.
|
42
|
+
attr_reader :options
|
43
|
+
|
44
|
+
# Create a new replayer. This combines some options from both {Worker.initialize} and {Client.initialize}.
|
45
|
+
#
|
46
|
+
# @param workflows [Array<Class<Workflow::Definition>>] Workflows for this replayer.
|
47
|
+
# @param namespace [String] Namespace as set in the workflow info.
|
48
|
+
# @param task_queue [String] Task queue as set in the workflow info.
|
49
|
+
# @param data_converter [Converters::DataConverter] Data converter to use for all data conversions to/from
|
50
|
+
# payloads.
|
51
|
+
# @param workflow_executor [WorkflowExecutor] Workflow executor that workflow tasks run within. This must be a
|
52
|
+
# {WorkflowExecutor::ThreadPool} currently.
|
53
|
+
# @param interceptors [Array<Interceptor::Workflow>] Workflow interceptors.
|
54
|
+
# @param build_id [String] Unique identifier for the current runtime. This is best set as a unique value
|
55
|
+
# representing all code and should change only when code does. This can be something like a git commit hash. If
|
56
|
+
# unset, default is hash of known Ruby code.
|
57
|
+
# @param identity [String, nil] Override the identity for this replater.
|
58
|
+
# @param logger [Logger] Logger to use. Defaults to stdout with warn level. Callers setting this logger are
|
59
|
+
# responsible for closing it.
|
60
|
+
# @param illegal_workflow_calls [Hash<String, [:all, Array<Symbol>]>] Set of illegal workflow calls that are
|
61
|
+
# considered unsafe/non-deterministic and will raise if seen. The key of the hash is the fully qualified string
|
62
|
+
# class name (no leading `::`). The value is either `:all` which means any use of the class, or an array of
|
63
|
+
# symbols for methods on the class that cannot be used. The methods refer to either instance or class methods,
|
64
|
+
# there is no way to differentiate at this time.
|
65
|
+
# @param workflow_failure_exception_types [Array<Class<Exception>>] Workflow failure exception types. This is the
|
66
|
+
# set of exception types that, if a workflow-thrown exception extends, will cause the workflow/update to fail
|
67
|
+
# instead of suspending the workflow via task failure. These are applied in addition to the
|
68
|
+
# `workflow_failure_exception_type` on the workflow definition class itself. If {::Exception} is set, it
|
69
|
+
# effectively will fail a workflow/update in all user exception cases.
|
70
|
+
# @param workflow_payload_codec_thread_pool [ThreadPool, nil] Thread pool to run payload codec encode/decode
|
71
|
+
# within. This is required if a payload codec exists and the worker is not fiber based. Codecs can potentially
|
72
|
+
# block execution which is why they need to be run in the background.
|
73
|
+
# @param unsafe_workflow_io_enabled [Boolean] If false, the default, workflow code that invokes io_wait on the
|
74
|
+
# fiber scheduler will fail. Instead of setting this to true, users are encouraged to use
|
75
|
+
# {Workflow::Unsafe.io_enabled} with a block for narrower enabling of IO.
|
76
|
+
# @param debug_mode [Boolean] If true, deadlock detection is disabled. Deadlock detection will fail workflow tasks
|
77
|
+
# if they block the thread for too long. This defaults to true if the `TEMPORAL_DEBUG` environment variable is
|
78
|
+
# `true` or `1`.
|
79
|
+
# @param runtime [Runtime] Runtime for this replayer.
|
80
|
+
#
|
81
|
+
# @yield If a block is present, this is the equivalent of calling {with_replay_worker} with the block and
|
82
|
+
# discarding the result.
|
83
|
+
def initialize(
|
84
|
+
workflows:,
|
85
|
+
namespace: 'ReplayNamespace',
|
86
|
+
task_queue: 'ReplayTaskQueue',
|
87
|
+
data_converter: Converters::DataConverter.default,
|
88
|
+
workflow_executor: WorkflowExecutor::ThreadPool.default,
|
89
|
+
interceptors: [],
|
90
|
+
build_id: Worker.default_build_id,
|
91
|
+
identity: nil,
|
92
|
+
logger: Logger.new($stdout, level: Logger::WARN),
|
93
|
+
illegal_workflow_calls: Worker.default_illegal_workflow_calls,
|
94
|
+
workflow_failure_exception_types: [],
|
95
|
+
workflow_payload_codec_thread_pool: nil,
|
96
|
+
unsafe_workflow_io_enabled: false,
|
97
|
+
debug_mode: %w[true 1].include?(ENV['TEMPORAL_DEBUG'].to_s.downcase),
|
98
|
+
runtime: Runtime.default,
|
99
|
+
&
|
100
|
+
)
|
101
|
+
@options = Options.new(
|
102
|
+
workflows:,
|
103
|
+
namespace:,
|
104
|
+
task_queue:,
|
105
|
+
data_converter:,
|
106
|
+
workflow_executor:,
|
107
|
+
interceptors:,
|
108
|
+
build_id:,
|
109
|
+
identity:,
|
110
|
+
logger:,
|
111
|
+
illegal_workflow_calls:,
|
112
|
+
workflow_failure_exception_types:,
|
113
|
+
workflow_payload_codec_thread_pool:,
|
114
|
+
unsafe_workflow_io_enabled:,
|
115
|
+
debug_mode:,
|
116
|
+
runtime:
|
117
|
+
).freeze
|
118
|
+
# Preload definitions and other settings
|
119
|
+
@workflow_definitions = Internal::Worker::WorkflowWorker.workflow_definitions(workflows)
|
120
|
+
@nondeterminism_as_workflow_fail, @nondeterminism_as_workflow_fail_for_types =
|
121
|
+
Internal::Worker::WorkflowWorker.bridge_workflow_failure_exception_type_options(
|
122
|
+
workflow_failure_exception_types:, workflow_definitions: @workflow_definitions
|
123
|
+
)
|
124
|
+
# If there is a block, we'll go ahead and assume it's for with_replay_worker
|
125
|
+
with_replay_worker(&) if block_given? # steep:ignore
|
126
|
+
end
|
127
|
+
|
128
|
+
# Replay a workflow history.
|
129
|
+
#
|
130
|
+
# If doing multiple histories, it is better to use {replay_workflows} or {with_replay_worker} since they create
|
131
|
+
# a replay worker just once instead of each time like this call does.
|
132
|
+
#
|
133
|
+
# @param history [WorkflowHistory] History to replay.
|
134
|
+
# @param raise_on_replay_failure [Boolean] If true, the default, this will raise an exception on any replay
|
135
|
+
# failure. If false and the replay fails, the failure will be available in {ReplayResult.replay_failure}.
|
136
|
+
#
|
137
|
+
# @return [ReplayResult] Result of the replay.
|
138
|
+
def replay_workflow(history, raise_on_replay_failure: true)
|
139
|
+
with_replay_worker { |worker| worker.replay_workflow(history, raise_on_replay_failure:) }
|
140
|
+
end
|
141
|
+
|
142
|
+
# Replay multiple workflow histories.
|
143
|
+
#
|
144
|
+
# @param histories [Enumerable<WorkflowHistory>] Histories to replay.
|
145
|
+
# @param raise_on_replay_failure [Boolean] If true, this will raise an exception on any replay failure. If false,
|
146
|
+
# the default, and the replay fails, the failure will be available in {ReplayResult.replay_failure}.
|
147
|
+
#
|
148
|
+
# @return [Array<ReplayResult>] Results of the replay.
|
149
|
+
def replay_workflows(histories, raise_on_replay_failure: false)
|
150
|
+
with_replay_worker do |worker|
|
151
|
+
histories.map { |h| worker.replay_workflow(h, raise_on_replay_failure:) }
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Run a block of code with a {ReplayWorker} to execute replays.
|
156
|
+
#
|
157
|
+
# @yield Block of code to run with a replay worker.
|
158
|
+
# @yieldparam [ReplayWorker] Worker to run replays on. Note, only one workflow can replay at a time.
|
159
|
+
# @yieldreturn [Object] Result of the block.
|
160
|
+
def with_replay_worker(&)
|
161
|
+
worker = ReplayWorker.new(
|
162
|
+
options:,
|
163
|
+
workflow_definitions: @workflow_definitions,
|
164
|
+
nondeterminism_as_workflow_fail: @nondeterminism_as_workflow_fail,
|
165
|
+
nondeterminism_as_workflow_fail_for_types: @nondeterminism_as_workflow_fail_for_types
|
166
|
+
)
|
167
|
+
begin
|
168
|
+
yield worker
|
169
|
+
ensure
|
170
|
+
worker._shutdown
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# Result of a single workflow replay run.
|
175
|
+
class ReplayResult
|
176
|
+
# @return [WorkflowHistory] History originally passed in to the replayer.
|
177
|
+
attr_reader :history
|
178
|
+
|
179
|
+
# @return [Exception, nil] Failure during replay if any.
|
180
|
+
attr_reader :replay_failure
|
181
|
+
|
182
|
+
# @!visibility private
|
183
|
+
def initialize(history:, replay_failure:)
|
184
|
+
@history = history
|
185
|
+
@replay_failure = replay_failure
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# Replay worker that can be used to replay individual workflow runs. Only one call to {replay_workflow} can be
|
190
|
+
# made at a time.
|
191
|
+
class ReplayWorker
|
192
|
+
# @!visibility private
|
193
|
+
def initialize(
|
194
|
+
options:,
|
195
|
+
workflow_definitions:,
|
196
|
+
nondeterminism_as_workflow_fail:,
|
197
|
+
nondeterminism_as_workflow_fail_for_types:
|
198
|
+
)
|
199
|
+
# Create the bridge worker and the replayer
|
200
|
+
@bridge_replayer, @bridge_worker = Internal::Bridge::Worker::WorkflowReplayer.new(
|
201
|
+
options.runtime._core_runtime,
|
202
|
+
Internal::Bridge::Worker::Options.new(
|
203
|
+
activity: false,
|
204
|
+
workflow: true,
|
205
|
+
namespace: options.namespace,
|
206
|
+
task_queue: options.task_queue,
|
207
|
+
tuner: Tuner.create_fixed(
|
208
|
+
workflow_slots: 2, activity_slots: 1, local_activity_slots: 1
|
209
|
+
)._to_bridge_options,
|
210
|
+
build_id: options.build_id,
|
211
|
+
identity_override: options.identity,
|
212
|
+
max_cached_workflows: 2,
|
213
|
+
max_concurrent_workflow_task_polls: 1,
|
214
|
+
nonsticky_to_sticky_poll_ratio: 1.0,
|
215
|
+
max_concurrent_activity_task_polls: 1,
|
216
|
+
no_remote_activities: true,
|
217
|
+
sticky_queue_schedule_to_start_timeout: 1.0,
|
218
|
+
max_heartbeat_throttle_interval: 1.0,
|
219
|
+
default_heartbeat_throttle_interval: 1.0,
|
220
|
+
max_worker_activities_per_second: nil,
|
221
|
+
max_task_queue_activities_per_second: nil,
|
222
|
+
graceful_shutdown_period: 0.0,
|
223
|
+
use_worker_versioning: false,
|
224
|
+
nondeterminism_as_workflow_fail:,
|
225
|
+
nondeterminism_as_workflow_fail_for_types:
|
226
|
+
)
|
227
|
+
)
|
228
|
+
|
229
|
+
# Create the workflow worker
|
230
|
+
@workflow_worker = Internal::Worker::WorkflowWorker.new(
|
231
|
+
bridge_worker: @bridge_worker,
|
232
|
+
namespace: options.namespace,
|
233
|
+
task_queue: options.task_queue,
|
234
|
+
workflow_definitions:,
|
235
|
+
workflow_executor: options.workflow_executor,
|
236
|
+
logger: options.logger,
|
237
|
+
data_converter: options.data_converter,
|
238
|
+
metric_meter: options.runtime.metric_meter,
|
239
|
+
workflow_interceptors: options.interceptors.select do |i|
|
240
|
+
i.is_a?(Interceptor::Workflow)
|
241
|
+
end,
|
242
|
+
disable_eager_activity_execution: false,
|
243
|
+
illegal_workflow_calls: options.illegal_workflow_calls,
|
244
|
+
workflow_failure_exception_types: options.workflow_failure_exception_types,
|
245
|
+
workflow_payload_codec_thread_pool: options.workflow_payload_codec_thread_pool,
|
246
|
+
unsafe_workflow_io_enabled: options.unsafe_workflow_io_enabled,
|
247
|
+
debug_mode: options.debug_mode,
|
248
|
+
on_eviction: proc { |_, remove_job| @last_workflow_remove_job = remove_job } # steep:ignore
|
249
|
+
)
|
250
|
+
|
251
|
+
# Create the runner
|
252
|
+
@runner = Internal::Worker::MultiRunner.new(workers: [self], shutdown_signals: [])
|
253
|
+
end
|
254
|
+
|
255
|
+
# Replay a workflow history.
|
256
|
+
#
|
257
|
+
# @param history [WorkflowHistory] History to replay.
|
258
|
+
# @param raise_on_replay_failure [Boolean] If true, the default, this will raise an exception on any replay
|
259
|
+
# failure. If false and the replay fails, the failure will be available in {ReplayResult.replay_failure}.
|
260
|
+
#
|
261
|
+
# @return [ReplayResult] Result of the replay.
|
262
|
+
def replay_workflow(history, raise_on_replay_failure: true)
|
263
|
+
raise ArgumentError, 'Expected history as WorkflowHistory' unless history.is_a?(WorkflowHistory)
|
264
|
+
# Due to our event processing model, only one can run at a time
|
265
|
+
raise 'Already running' if @running
|
266
|
+
raise 'Replayer shutdown' if @shutdown
|
267
|
+
|
268
|
+
# Push history proto
|
269
|
+
# TODO(cretz): Unset this
|
270
|
+
@running = true
|
271
|
+
@last_workflow_remove_job = nil
|
272
|
+
begin
|
273
|
+
@bridge_replayer.push_history(
|
274
|
+
history.workflow_id, Api::History::V1::History.new(events: history.events).to_proto
|
275
|
+
)
|
276
|
+
|
277
|
+
# Process events until workflow complete
|
278
|
+
until @last_workflow_remove_job
|
279
|
+
event = @runner.next_event
|
280
|
+
case event
|
281
|
+
when Internal::Worker::MultiRunner::Event::PollSuccess
|
282
|
+
@workflow_worker.handle_activation(
|
283
|
+
runner: @runner,
|
284
|
+
activation: Internal::Bridge::Api::WorkflowActivation::WorkflowActivation.decode(event.bytes),
|
285
|
+
decoded: false
|
286
|
+
)
|
287
|
+
when Internal::Worker::MultiRunner::Event::WorkflowActivationDecoded
|
288
|
+
@workflow_worker.handle_activation(runner: @runner, activation: event.activation, decoded: true)
|
289
|
+
when Internal::Worker::MultiRunner::Event::WorkflowActivationComplete
|
290
|
+
@workflow_worker.handle_activation_complete(
|
291
|
+
runner: @runner,
|
292
|
+
activation_completion: event.activation_completion,
|
293
|
+
encoded: event.encoded,
|
294
|
+
completion_complete_queue: event.completion_complete_queue
|
295
|
+
)
|
296
|
+
when Internal::Worker::MultiRunner::Event::WorkflowActivationCompletionComplete
|
297
|
+
# Ignore
|
298
|
+
else
|
299
|
+
raise "Unexpected event: #{event}"
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
# Create exception if removal is due to error
|
304
|
+
err = if @last_workflow_remove_job.reason == :NONDETERMINISM
|
305
|
+
Workflow::NondeterminismError.new(
|
306
|
+
"#{@last_workflow_remove_job.reason}: #{@last_workflow_remove_job.message}"
|
307
|
+
)
|
308
|
+
elsif !%i[CACHE_FULL LANG_REQUESTED].include?(@last_workflow_remove_job.reason)
|
309
|
+
Workflow::InvalidWorkflowStateError.new(
|
310
|
+
"#{@last_workflow_remove_job.reason}: #{@last_workflow_remove_job.message}"
|
311
|
+
)
|
312
|
+
end
|
313
|
+
# Raise if wanting to raise, otherwise return result
|
314
|
+
raise err if raise_on_replay_failure && err
|
315
|
+
|
316
|
+
ReplayResult.new(history:, replay_failure: err)
|
317
|
+
ensure
|
318
|
+
@running = false
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
# @!visibility private
|
323
|
+
def _shutdown
|
324
|
+
@shutdown = true
|
325
|
+
@runner.initiate_shutdown
|
326
|
+
# Wait for all-pollers-shutdown before finalizing
|
327
|
+
until @runner.next_event.is_a?(Internal::Worker::MultiRunner::Event::AllPollersShutDown); end
|
328
|
+
@runner.wait_complete_and_finalize_shutdown
|
329
|
+
@workflow_worker.on_shutdown_complete
|
330
|
+
@workflow_worker = nil
|
331
|
+
end
|
332
|
+
|
333
|
+
# @!visibility private
|
334
|
+
def _bridge_worker
|
335
|
+
@bridge_worker
|
336
|
+
end
|
337
|
+
|
338
|
+
# @!visibility private
|
339
|
+
def _initiate_shutdown
|
340
|
+
_bridge_worker.initiate_shutdown
|
341
|
+
end
|
342
|
+
|
343
|
+
# @!visibility private
|
344
|
+
def _wait_all_complete
|
345
|
+
# Do nothing
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|