temporalio 0.3.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +503 -395
  3. data/Gemfile +4 -0
  4. data/README.md +183 -10
  5. data/Rakefile +1 -1
  6. data/ext/Cargo.toml +1 -0
  7. data/lib/temporalio/activity/context.rb +13 -0
  8. data/lib/temporalio/activity/definition.rb +22 -5
  9. data/lib/temporalio/activity/info.rb +3 -0
  10. data/lib/temporalio/api/batch/v1/message.rb +6 -1
  11. data/lib/temporalio/api/command/v1/message.rb +1 -1
  12. data/lib/temporalio/api/common/v1/message.rb +2 -1
  13. data/lib/temporalio/api/deployment/v1/message.rb +38 -0
  14. data/lib/temporalio/api/enums/v1/batch_operation.rb +1 -1
  15. data/lib/temporalio/api/enums/v1/common.rb +1 -1
  16. data/lib/temporalio/api/enums/v1/deployment.rb +23 -0
  17. data/lib/temporalio/api/enums/v1/event_type.rb +1 -1
  18. data/lib/temporalio/api/enums/v1/failed_cause.rb +1 -1
  19. data/lib/temporalio/api/enums/v1/nexus.rb +21 -0
  20. data/lib/temporalio/api/enums/v1/reset.rb +1 -1
  21. data/lib/temporalio/api/enums/v1/workflow.rb +2 -1
  22. data/lib/temporalio/api/errordetails/v1/message.rb +3 -1
  23. data/lib/temporalio/api/failure/v1/message.rb +3 -1
  24. data/lib/temporalio/api/history/v1/message.rb +3 -1
  25. data/lib/temporalio/api/nexus/v1/message.rb +2 -1
  26. data/lib/temporalio/api/payload_visitor.rb +75 -7
  27. data/lib/temporalio/api/query/v1/message.rb +2 -1
  28. data/lib/temporalio/api/taskqueue/v1/message.rb +4 -1
  29. data/lib/temporalio/api/workflow/v1/message.rb +9 -1
  30. data/lib/temporalio/api/workflowservice/v1/request_response.rb +40 -11
  31. data/lib/temporalio/api/workflowservice/v1/service.rb +1 -1
  32. data/lib/temporalio/api.rb +1 -0
  33. data/lib/temporalio/client/connection/workflow_service.rb +238 -28
  34. data/lib/temporalio/client/interceptor.rb +39 -0
  35. data/lib/temporalio/client/schedule.rb +25 -1
  36. data/lib/temporalio/client/with_start_workflow_operation.rb +115 -0
  37. data/lib/temporalio/client/workflow_execution.rb +19 -0
  38. data/lib/temporalio/client/workflow_handle.rb +3 -3
  39. data/lib/temporalio/client.rb +125 -2
  40. data/lib/temporalio/contrib/open_telemetry.rb +470 -0
  41. data/lib/temporalio/error.rb +1 -0
  42. data/lib/temporalio/internal/bridge/api/activity_task/activity_task.rb +1 -1
  43. data/lib/temporalio/internal/bridge/api/common/common.rb +2 -1
  44. data/lib/temporalio/internal/bridge/api/workflow_activation/workflow_activation.rb +1 -1
  45. data/lib/temporalio/internal/bridge/api/workflow_commands/workflow_commands.rb +1 -1
  46. data/lib/temporalio/internal/bridge/api/workflow_completion/workflow_completion.rb +2 -1
  47. data/lib/temporalio/internal/bridge/runtime.rb +3 -0
  48. data/lib/temporalio/internal/bridge/testing.rb +3 -0
  49. data/lib/temporalio/internal/client/implementation.rb +232 -10
  50. data/lib/temporalio/internal/proto_utils.rb +34 -2
  51. data/lib/temporalio/internal/worker/activity_worker.rb +20 -8
  52. data/lib/temporalio/internal/worker/multi_runner.rb +2 -2
  53. data/lib/temporalio/internal/worker/workflow_instance/context.rb +57 -3
  54. data/lib/temporalio/internal/worker/workflow_instance/details.rb +4 -2
  55. data/lib/temporalio/internal/worker/workflow_instance/outbound_implementation.rb +11 -26
  56. data/lib/temporalio/internal/worker/workflow_instance/scheduler.rb +22 -2
  57. data/lib/temporalio/internal/worker/workflow_instance.rb +76 -32
  58. data/lib/temporalio/internal/worker/workflow_worker.rb +62 -19
  59. data/lib/temporalio/runtime/metric_buffer.rb +94 -0
  60. data/lib/temporalio/runtime.rb +48 -10
  61. data/lib/temporalio/search_attributes.rb +13 -0
  62. data/lib/temporalio/testing/activity_environment.rb +42 -14
  63. data/lib/temporalio/testing/workflow_environment.rb +26 -3
  64. data/lib/temporalio/version.rb +1 -1
  65. data/lib/temporalio/worker/interceptor.rb +3 -0
  66. data/lib/temporalio/worker/thread_pool.rb +5 -5
  67. data/lib/temporalio/worker/tuner.rb +38 -0
  68. data/lib/temporalio/worker/workflow_executor/thread_pool.rb +13 -8
  69. data/lib/temporalio/worker/workflow_executor.rb +1 -1
  70. data/lib/temporalio/worker/workflow_replayer.rb +350 -0
  71. data/lib/temporalio/worker.rb +58 -52
  72. data/lib/temporalio/workflow/definition.rb +40 -8
  73. data/lib/temporalio/workflow/future.rb +2 -2
  74. data/lib/temporalio/workflow/info.rb +22 -0
  75. data/lib/temporalio/workflow.rb +60 -8
  76. data/lib/temporalio/workflow_history.rb +26 -1
  77. data/temporalio.gemspec +2 -1
  78. metadata +26 -5
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Much of this logic taken from
4
- # https://github.com/ruby-concurrency/concurrent-ruby/blob/044020f44b36930b863b930f3ee8fa1e9f750469/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb,
5
- # see MIT license at
6
- # https://github.com/ruby-concurrency/concurrent-ruby/blob/044020f44b36930b863b930f3ee8fa1e9f750469/LICENSE.txt
7
-
8
3
  module Temporalio
9
4
  class Worker
10
5
  # Implementation of a thread pool. This implementation is a stripped down form of Concurrent Ruby's
11
6
  # `CachedThreadPool`.
12
7
  class ThreadPool
8
+ # Much of this logic taken from
9
+ # https://github.com/ruby-concurrency/concurrent-ruby/blob/044020f44b36930b863b930f3ee8fa1e9f750469/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb,
10
+ # see MIT license at
11
+ # https://github.com/ruby-concurrency/concurrent-ruby/blob/044020f44b36930b863b930f3ee8fa1e9f750469/LICENSE.txt
12
+
13
13
  # @return [ThreadPool] Default/shared thread pool instance with unlimited max threads.
14
14
  def self.default
15
15
  @default ||= new
@@ -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
@@ -36,7 +36,7 @@ module Temporalio
36
36
  end
37
37
 
38
38
  # @!visibility private
39
- def _validate_worker(worker, worker_state)
39
+ def _validate_worker(workflow_worker, worker_state)
40
40
  # Do nothing
41
41
  end
42
42
 
@@ -137,7 +137,7 @@ module Temporalio
137
137
 
138
138
  # If it's eviction only, just evict inline and do nothing else
139
139
  if cache_remove_job && activation.jobs.size == 1
140
- evict(worker_state, activation.run_id)
140
+ evict(worker_state, activation.run_id, cache_remove_job)
141
141
  worker_state.logger.debug('Sending empty workflow completion') if LOG_ACTIVATIONS
142
142
  yield Internal::Bridge::Api::WorkflowCompletion::WorkflowActivationCompletion.new(
143
143
  run_id: activation.run_id,
@@ -173,7 +173,7 @@ module Temporalio
173
173
  end
174
174
 
175
175
  # Go ahead and evict if there is an eviction job
176
- evict(worker_state, activation.run_id) if cache_remove_job
176
+ evict(worker_state, activation.run_id, cache_remove_job) if cache_remove_job
177
177
 
178
178
  # Complete the activation
179
179
  worker_state.logger.debug("Sending workflow completion: #{completion}") if LOG_ACTIVATIONS
@@ -186,8 +186,12 @@ module Temporalio
186
186
  raise 'Missing initialize job in initial activation' unless init_job
187
187
 
188
188
  # Obtain definition
189
- definition = worker_state.workflow_definitions[init_job.workflow_type] ||
190
- worker_state.workflow_definitions[nil]
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
+
191
195
  unless definition
192
196
  raise Error::ApplicationError.new(
193
197
  "Workflow type #{init_job.workflow_type} is not registered on this worker, available workflows: " +
@@ -209,13 +213,14 @@ module Temporalio
209
213
  interceptors: worker_state.workflow_interceptors,
210
214
  disable_eager_activity_execution: worker_state.disable_eager_activity_execution,
211
215
  illegal_calls: worker_state.illegal_calls,
212
- workflow_failure_exception_types: worker_state.workflow_failure_exception_types
216
+ workflow_failure_exception_types: worker_state.workflow_failure_exception_types,
217
+ unsafe_workflow_io_enabled: worker_state.unsafe_workflow_io_enabled
213
218
  )
214
219
  )
215
220
  end
216
221
 
217
- def evict(worker_state, run_id)
218
- worker_state.evict_running_workflow(run_id)
222
+ def evict(worker_state, run_id, cache_remove_job)
223
+ worker_state.evict_running_workflow(run_id, cache_remove_job)
219
224
  @executor._remove_workflow(worker_state, run_id)
220
225
  end
221
226
  end
@@ -13,7 +13,7 @@ module Temporalio
13
13
  end
14
14
 
15
15
  # @!visibility private
16
- def _validate_worker(worker, worker_state)
16
+ def _validate_worker(workflow_worker, worker_state)
17
17
  raise NotImplementedError
18
18
  end
19
19
 
@@ -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