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.
Files changed (141) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -0
  3. data/Cargo.lock +980 -583
  4. data/Cargo.toml +2 -2
  5. data/Gemfile +7 -3
  6. data/README.md +769 -54
  7. data/Rakefile +10 -296
  8. data/ext/Cargo.toml +2 -0
  9. data/lib/temporalio/activity/complete_async_error.rb +1 -1
  10. data/lib/temporalio/activity/context.rb +18 -2
  11. data/lib/temporalio/activity/definition.rb +180 -65
  12. data/lib/temporalio/activity/info.rb +25 -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/batch/v1/message.rb +6 -1
  16. data/lib/temporalio/api/cloud/account/v1/message.rb +28 -0
  17. data/lib/temporalio/api/cloud/cloudservice/v1/request_response.rb +34 -1
  18. data/lib/temporalio/api/cloud/cloudservice/v1/service.rb +1 -1
  19. data/lib/temporalio/api/cloud/identity/v1/message.rb +6 -1
  20. data/lib/temporalio/api/cloud/namespace/v1/message.rb +8 -1
  21. data/lib/temporalio/api/cloud/nexus/v1/message.rb +31 -0
  22. data/lib/temporalio/api/cloud/operation/v1/message.rb +2 -1
  23. data/lib/temporalio/api/cloud/region/v1/message.rb +2 -1
  24. data/lib/temporalio/api/cloud/resource/v1/message.rb +23 -0
  25. data/lib/temporalio/api/cloud/sink/v1/message.rb +24 -0
  26. data/lib/temporalio/api/cloud/usage/v1/message.rb +31 -0
  27. data/lib/temporalio/api/command/v1/message.rb +1 -1
  28. data/lib/temporalio/api/common/v1/message.rb +8 -1
  29. data/lib/temporalio/api/deployment/v1/message.rb +38 -0
  30. data/lib/temporalio/api/enums/v1/batch_operation.rb +1 -1
  31. data/lib/temporalio/api/enums/v1/common.rb +1 -1
  32. data/lib/temporalio/api/enums/v1/deployment.rb +23 -0
  33. data/lib/temporalio/api/enums/v1/event_type.rb +1 -1
  34. data/lib/temporalio/api/enums/v1/failed_cause.rb +1 -1
  35. data/lib/temporalio/api/enums/v1/nexus.rb +21 -0
  36. data/lib/temporalio/api/enums/v1/reset.rb +1 -1
  37. data/lib/temporalio/api/enums/v1/workflow.rb +2 -1
  38. data/lib/temporalio/api/errordetails/v1/message.rb +3 -1
  39. data/lib/temporalio/api/failure/v1/message.rb +3 -1
  40. data/lib/temporalio/api/history/v1/message.rb +3 -1
  41. data/lib/temporalio/api/nexus/v1/message.rb +3 -2
  42. data/lib/temporalio/api/operatorservice/v1/service.rb +1 -1
  43. data/lib/temporalio/api/payload_visitor.rb +1581 -0
  44. data/lib/temporalio/api/query/v1/message.rb +2 -1
  45. data/lib/temporalio/api/schedule/v1/message.rb +2 -1
  46. data/lib/temporalio/api/taskqueue/v1/message.rb +4 -1
  47. data/lib/temporalio/api/testservice/v1/request_response.rb +31 -0
  48. data/lib/temporalio/api/testservice/v1/service.rb +23 -0
  49. data/lib/temporalio/api/workflow/v1/message.rb +9 -1
  50. data/lib/temporalio/api/workflowservice/v1/request_response.rb +46 -2
  51. data/lib/temporalio/api/workflowservice/v1/service.rb +1 -1
  52. data/lib/temporalio/api.rb +2 -0
  53. data/lib/temporalio/cancellation.rb +34 -14
  54. data/lib/temporalio/client/async_activity_handle.rb +12 -37
  55. data/lib/temporalio/client/connection/cloud_service.rb +309 -231
  56. data/lib/temporalio/client/connection/operator_service.rb +36 -84
  57. data/lib/temporalio/client/connection/service.rb +6 -5
  58. data/lib/temporalio/client/connection/test_service.rb +111 -0
  59. data/lib/temporalio/client/connection/workflow_service.rb +474 -441
  60. data/lib/temporalio/client/connection.rb +90 -44
  61. data/lib/temporalio/client/interceptor.rb +199 -60
  62. data/lib/temporalio/client/schedule.rb +991 -0
  63. data/lib/temporalio/client/schedule_handle.rb +126 -0
  64. data/lib/temporalio/client/with_start_workflow_operation.rb +115 -0
  65. data/lib/temporalio/client/workflow_execution.rb +26 -10
  66. data/lib/temporalio/client/workflow_handle.rb +41 -98
  67. data/lib/temporalio/client/workflow_update_handle.rb +3 -5
  68. data/lib/temporalio/client.rb +247 -44
  69. data/lib/temporalio/common_enums.rb +17 -0
  70. data/lib/temporalio/contrib/open_telemetry.rb +470 -0
  71. data/lib/temporalio/converters/data_converter.rb +4 -7
  72. data/lib/temporalio/converters/failure_converter.rb +5 -3
  73. data/lib/temporalio/converters/payload_converter/composite.rb +4 -0
  74. data/lib/temporalio/converters/payload_converter.rb +6 -8
  75. data/lib/temporalio/converters/raw_value.rb +20 -0
  76. data/lib/temporalio/error/failure.rb +1 -1
  77. data/lib/temporalio/error.rb +11 -2
  78. data/lib/temporalio/internal/bridge/api/activity_task/activity_task.rb +1 -1
  79. data/lib/temporalio/internal/bridge/api/common/common.rb +2 -1
  80. data/lib/temporalio/internal/bridge/api/core_interface.rb +5 -1
  81. data/lib/temporalio/internal/bridge/api/nexus/nexus.rb +33 -0
  82. data/lib/temporalio/internal/bridge/api/workflow_activation/workflow_activation.rb +5 -1
  83. data/lib/temporalio/internal/bridge/api/workflow_commands/workflow_commands.rb +4 -1
  84. data/lib/temporalio/internal/bridge/api/workflow_completion/workflow_completion.rb +2 -1
  85. data/lib/temporalio/internal/bridge/client.rb +11 -6
  86. data/lib/temporalio/internal/bridge/runtime.rb +3 -0
  87. data/lib/temporalio/internal/bridge/testing.rb +23 -0
  88. data/lib/temporalio/internal/bridge/worker.rb +2 -0
  89. data/lib/temporalio/internal/bridge.rb +1 -1
  90. data/lib/temporalio/internal/client/implementation.rb +468 -71
  91. data/lib/temporalio/internal/metric.rb +122 -0
  92. data/lib/temporalio/internal/proto_utils.rb +118 -7
  93. data/lib/temporalio/internal/worker/activity_worker.rb +69 -29
  94. data/lib/temporalio/internal/worker/multi_runner.rb +53 -9
  95. data/lib/temporalio/internal/worker/workflow_instance/child_workflow_handle.rb +54 -0
  96. data/lib/temporalio/internal/worker/workflow_instance/context.rb +383 -0
  97. data/lib/temporalio/internal/worker/workflow_instance/details.rb +46 -0
  98. data/lib/temporalio/internal/worker/workflow_instance/external_workflow_handle.rb +32 -0
  99. data/lib/temporalio/internal/worker/workflow_instance/externally_immutable_hash.rb +22 -0
  100. data/lib/temporalio/internal/worker/workflow_instance/handler_execution.rb +25 -0
  101. data/lib/temporalio/internal/worker/workflow_instance/handler_hash.rb +41 -0
  102. data/lib/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rb +97 -0
  103. data/lib/temporalio/internal/worker/workflow_instance/inbound_implementation.rb +62 -0
  104. data/lib/temporalio/internal/worker/workflow_instance/outbound_implementation.rb +400 -0
  105. data/lib/temporalio/internal/worker/workflow_instance/replay_safe_logger.rb +37 -0
  106. data/lib/temporalio/internal/worker/workflow_instance/replay_safe_metric.rb +40 -0
  107. data/lib/temporalio/internal/worker/workflow_instance/scheduler.rb +183 -0
  108. data/lib/temporalio/internal/worker/workflow_instance.rb +774 -0
  109. data/lib/temporalio/internal/worker/workflow_worker.rb +239 -0
  110. data/lib/temporalio/metric.rb +109 -0
  111. data/lib/temporalio/retry_policy.rb +37 -14
  112. data/lib/temporalio/runtime/metric_buffer.rb +94 -0
  113. data/lib/temporalio/runtime.rb +160 -79
  114. data/lib/temporalio/search_attributes.rb +93 -37
  115. data/lib/temporalio/testing/activity_environment.rb +44 -16
  116. data/lib/temporalio/testing/workflow_environment.rb +276 -7
  117. data/lib/temporalio/version.rb +1 -1
  118. data/lib/temporalio/worker/activity_executor/thread_pool.rb +9 -217
  119. data/lib/temporalio/worker/activity_executor.rb +3 -3
  120. data/lib/temporalio/worker/interceptor.rb +343 -66
  121. data/lib/temporalio/worker/thread_pool.rb +237 -0
  122. data/lib/temporalio/worker/tuner.rb +38 -0
  123. data/lib/temporalio/worker/workflow_executor/thread_pool.rb +235 -0
  124. data/lib/temporalio/worker/workflow_executor.rb +26 -0
  125. data/lib/temporalio/worker/workflow_replayer.rb +350 -0
  126. data/lib/temporalio/worker.rb +235 -58
  127. data/lib/temporalio/workflow/activity_cancellation_type.rb +20 -0
  128. data/lib/temporalio/workflow/child_workflow_cancellation_type.rb +21 -0
  129. data/lib/temporalio/workflow/child_workflow_handle.rb +43 -0
  130. data/lib/temporalio/workflow/definition.rb +598 -0
  131. data/lib/temporalio/workflow/external_workflow_handle.rb +41 -0
  132. data/lib/temporalio/workflow/future.rb +151 -0
  133. data/lib/temporalio/workflow/handler_unfinished_policy.rb +13 -0
  134. data/lib/temporalio/workflow/info.rb +104 -0
  135. data/lib/temporalio/workflow/parent_close_policy.rb +19 -0
  136. data/lib/temporalio/workflow/update_info.rb +20 -0
  137. data/lib/temporalio/workflow.rb +575 -0
  138. data/lib/temporalio/workflow_history.rb +26 -1
  139. data/lib/temporalio.rb +4 -0
  140. data/temporalio.gemspec +4 -3
  141. 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