temporalio 1.1.0 → 1.2.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +151 -192
  3. data/Cargo.toml +9 -1
  4. data/Gemfile +6 -4
  5. data/README.md +2 -2
  6. data/ext/Cargo.toml +1 -1
  7. data/lib/temporalio/activity/definition.rb +6 -1
  8. data/lib/temporalio/api/activity/v1/message.rb +11 -2
  9. data/lib/temporalio/api/command/v1/message.rb +1 -1
  10. data/lib/temporalio/api/common/v1/message.rb +2 -1
  11. data/lib/temporalio/api/deployment/v1/message.rb +2 -1
  12. data/lib/temporalio/api/enums/v1/activity.rb +23 -0
  13. data/lib/temporalio/api/enums/v1/event_type.rb +1 -1
  14. data/lib/temporalio/api/enums/v1/failed_cause.rb +1 -1
  15. data/lib/temporalio/api/enums/v1/task_queue.rb +2 -1
  16. data/lib/temporalio/api/enums/v1/workflow.rb +3 -1
  17. data/lib/temporalio/api/errordetails/v1/message.rb +2 -1
  18. data/lib/temporalio/api/history/v1/message.rb +3 -1
  19. data/lib/temporalio/api/namespace/v1/message.rb +2 -1
  20. data/lib/temporalio/api/nexus/v1/message.rb +1 -1
  21. data/lib/temporalio/api/operatorservice/v1/request_response.rb +1 -1
  22. data/lib/temporalio/api/payload_visitor.rb +64 -0
  23. data/lib/temporalio/api/taskqueue/v1/message.rb +1 -1
  24. data/lib/temporalio/api/worker/v1/message.rb +1 -1
  25. data/lib/temporalio/api/workflow/v1/message.rb +2 -1
  26. data/lib/temporalio/api/workflowservice/v1/request_response.rb +23 -1
  27. data/lib/temporalio/api/workflowservice/v1/service.rb +1 -1
  28. data/lib/temporalio/client/connection/workflow_service.rb +150 -0
  29. data/lib/temporalio/client/connection.rb +17 -3
  30. data/lib/temporalio/client/plugin.rb +42 -0
  31. data/lib/temporalio/client.rb +82 -13
  32. data/lib/temporalio/converters/payload_converter/binary_protobuf.rb +4 -1
  33. data/lib/temporalio/converters/payload_converter/json_protobuf.rb +4 -1
  34. data/lib/temporalio/env_config.rb +2 -11
  35. data/lib/temporalio/internal/bridge/api/core_interface.rb +3 -1
  36. data/lib/temporalio/internal/bridge/api/nexus/nexus.rb +2 -1
  37. data/lib/temporalio/internal/bridge/api/workflow_commands/workflow_commands.rb +1 -1
  38. data/lib/temporalio/internal/bridge/worker.rb +1 -0
  39. data/lib/temporalio/internal/worker/workflow_instance/context.rb +4 -0
  40. data/lib/temporalio/internal/worker/workflow_instance/externally_immutable_hash.rb +1 -1
  41. data/lib/temporalio/internal/worker/workflow_instance/handler_hash.rb +1 -1
  42. data/lib/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rb +8 -4
  43. data/lib/temporalio/internal/worker/workflow_instance/outbound_implementation.rb +32 -11
  44. data/lib/temporalio/internal/worker/workflow_instance/replay_safe_logger.rb +2 -1
  45. data/lib/temporalio/internal/worker/workflow_instance/replay_safe_metric.rb +2 -1
  46. data/lib/temporalio/internal/worker/workflow_instance/scheduler.rb +10 -1
  47. data/lib/temporalio/internal/worker/workflow_instance.rb +15 -11
  48. data/lib/temporalio/simple_plugin.rb +192 -0
  49. data/lib/temporalio/version.rb +1 -1
  50. data/lib/temporalio/worker/plugin.rb +88 -0
  51. data/lib/temporalio/worker/workflow_replayer.rb +28 -5
  52. data/lib/temporalio/worker.rb +116 -42
  53. data/lib/temporalio/workflow/definition.rb +3 -1
  54. data/lib/temporalio/workflow.rb +8 -1
  55. data/temporalio.gemspec +1 -1
  56. metadata +7 -3
@@ -43,7 +43,12 @@ module Temporalio
43
43
  @instance.pending_external_cancels[seq] = Fiber.current
44
44
 
45
45
  # Wait
46
- resolution = Fiber.yield
46
+ resolution = begin
47
+ Fiber.yield
48
+ ensure
49
+ # Remove pending
50
+ @instance.pending_external_cancels.delete(seq)
51
+ end
47
52
 
48
53
  # Raise if resolution has failure
49
54
  return unless resolution.failure
@@ -169,10 +174,13 @@ module Temporalio
169
174
  end
170
175
 
171
176
  # Wait
172
- resolution = Fiber.yield
173
-
174
- # Remove cancellation callback
175
- cancellation.remove_cancel_callback(cancel_callback_key)
177
+ resolution = begin
178
+ Fiber.yield
179
+ ensure
180
+ # Remove pending and cancel callback
181
+ @instance.pending_activities.delete(seq)
182
+ cancellation.remove_cancel_callback(cancel_callback_key)
183
+ end
176
184
 
177
185
  case resolution.status
178
186
  when :completed
@@ -254,10 +262,13 @@ module Temporalio
254
262
  end
255
263
 
256
264
  # Wait
257
- resolution = Fiber.yield
258
-
259
- # Remove cancellation callback
260
- cancellation.remove_cancel_callback(cancel_callback_key)
265
+ resolution = begin
266
+ Fiber.yield
267
+ ensure
268
+ # Remove pending and cancel callback
269
+ @instance.pending_external_signals.delete(seq)
270
+ cancellation.remove_cancel_callback(cancel_callback_key)
271
+ end
261
272
 
262
273
  # Raise if resolution has failure
263
274
  return unless resolution.failure
@@ -317,7 +328,12 @@ module Temporalio
317
328
  end
318
329
 
319
330
  # Wait
320
- Fiber.yield
331
+ begin
332
+ Fiber.yield
333
+ ensure
334
+ # Remove pending
335
+ @instance.pending_timers.delete(seq)
336
+ end
321
337
 
322
338
  # Remove cancellation callback (only needed on success)
323
339
  input.cancellation.remove_cancel_callback(cancel_callback_key)
@@ -374,7 +390,12 @@ module Temporalio
374
390
  end
375
391
 
376
392
  # Wait for start
377
- resolution = Fiber.yield
393
+ resolution = begin
394
+ Fiber.yield
395
+ ensure
396
+ # Remove pending
397
+ @instance.pending_child_workflow_starts.delete(seq)
398
+ end
378
399
 
379
400
  case resolution.status
380
401
  when :succeeded
@@ -23,7 +23,8 @@ module Temporalio
23
23
  end
24
24
 
25
25
  def add(...)
26
- if !@replay_safety_disabled && Temporalio::Workflow.in_workflow? && Temporalio::Workflow::Unsafe.replaying?
26
+ if !@replay_safety_disabled && Temporalio::Workflow.in_workflow? &&
27
+ Temporalio::Workflow::Unsafe.replaying_history_events?
27
28
  return true
28
29
  end
29
30
 
@@ -9,7 +9,8 @@ module Temporalio
9
9
  # Wrapper for a metric that does not log on replay.
10
10
  class ReplaySafeMetric < SimpleDelegator
11
11
  def record(value, additional_attributes: nil)
12
- return if Temporalio::Workflow.in_workflow? && Temporalio::Workflow::Unsafe.replaying?
12
+ return if Temporalio::Workflow.in_workflow? &&
13
+ Temporalio::Workflow::Unsafe.replaying_history_events?
13
14
 
14
15
  super
15
16
  end
@@ -78,7 +78,12 @@ module Temporalio
78
78
  end
79
79
 
80
80
  # This blocks until a resume is called on this fiber
81
- result = Fiber.yield
81
+ result = begin
82
+ Fiber.yield
83
+ ensure
84
+ # Remove pending
85
+ @wait_conditions.delete(seq)
86
+ end
82
87
 
83
88
  # Remove cancellation callback (only needed on success)
84
89
  cancellation&.remove_cancel_callback(cancel_callback_key) if cancel_callback_key
@@ -141,6 +146,10 @@ module Temporalio
141
146
  fiber
142
147
  end
143
148
 
149
+ def fiber_interrupt(fiber, exception)
150
+ fiber.raise(exception) if fiber.alive?
151
+ end
152
+
144
153
  def io_wait(io, events, timeout)
145
154
  # Do not allow if IO disabled
146
155
  unless @instance.io_enabled
@@ -57,7 +57,8 @@ module Temporalio
57
57
  :pending_external_signals, :pending_external_cancels, :in_progress_handlers, :payload_converter,
58
58
  :failure_converter, :cancellation, :continue_as_new_suggested, :current_deployment_version,
59
59
  :current_history_length, :current_history_size, :replaying, :random,
60
- :signal_handlers, :query_handlers, :update_handlers, :context_frozen, :assert_valid_local_activity
60
+ :signal_handlers, :query_handlers, :update_handlers, :context_frozen, :assert_valid_local_activity,
61
+ :in_query_or_validator
61
62
  attr_accessor :io_enabled, :current_details
62
63
 
63
64
  def initialize(details)
@@ -91,6 +92,7 @@ module Temporalio
91
92
  @current_history_length = 0
92
93
  @current_history_size = 0
93
94
  @replaying = false
95
+ @in_query_or_validator = false
94
96
  @workflow_failure_exception_types = details.workflow_failure_exception_types
95
97
  @signal_handlers = HandlerHash.new(
96
98
  details.definition.signals,
@@ -182,7 +184,7 @@ module Temporalio
182
184
  # Apply jobs and run event loop
183
185
  begin
184
186
  # Create instance if it doesn't already exist
185
- @instance ||= with_context_frozen { create_instance }
187
+ @instance ||= with_context_frozen(in_query_or_validator: false) { create_instance }
186
188
 
187
189
  # Apply jobs
188
190
  activation.jobs.each { |job| apply(job) }
@@ -345,7 +347,7 @@ module Temporalio
345
347
  when :initialize_workflow
346
348
  # Ignore
347
349
  when :fire_timer
348
- pending_timers.delete(job.fire_timer.seq)&.resume
350
+ pending_timers[job.fire_timer.seq]&.resume
349
351
  when :update_random_seed
350
352
  @random = illegal_call_tracing_disabled { Random.new(job.update_random_seed.randomness_seed) }
351
353
  when :query_workflow
@@ -356,23 +358,23 @@ module Temporalio
356
358
  when :signal_workflow
357
359
  apply_signal(job.signal_workflow)
358
360
  when :resolve_activity
359
- pending_activities.delete(job.resolve_activity.seq)&.resume(job.resolve_activity.result)
361
+ pending_activities[job.resolve_activity.seq]&.resume(job.resolve_activity.result)
360
362
  when :notify_has_patch
361
363
  @patches_notified << job.notify_has_patch.patch_id
362
364
  when :resolve_child_workflow_execution_start
363
- pending_child_workflow_starts.delete(job.resolve_child_workflow_execution_start.seq)&.resume(
365
+ pending_child_workflow_starts[job.resolve_child_workflow_execution_start.seq]&.resume(
364
366
  job.resolve_child_workflow_execution_start
365
367
  )
366
368
  when :resolve_child_workflow_execution
367
- pending_child_workflows.delete(job.resolve_child_workflow_execution.seq)&._resolve(
369
+ pending_child_workflows[job.resolve_child_workflow_execution.seq]&._resolve(
368
370
  job.resolve_child_workflow_execution.result
369
371
  )
370
372
  when :resolve_signal_external_workflow
371
- pending_external_signals.delete(job.resolve_signal_external_workflow.seq)&.resume(
373
+ pending_external_signals[job.resolve_signal_external_workflow.seq]&.resume(
372
374
  job.resolve_signal_external_workflow
373
375
  )
374
376
  when :resolve_request_cancel_external_workflow
375
- pending_external_cancels.delete(job.resolve_request_cancel_external_workflow.seq)&.resume(
377
+ pending_external_cancels[job.resolve_request_cancel_external_workflow.seq]&.resume(
376
378
  job.resolve_request_cancel_external_workflow
377
379
  )
378
380
  when :do_update
@@ -439,7 +441,7 @@ module Temporalio
439
441
  end
440
442
  result_hint = defn.result_hint
441
443
 
442
- with_context_frozen do
444
+ with_context_frozen(in_query_or_validator: true) do
443
445
  @inbound.handle_query(
444
446
  Temporalio::Worker::Interceptor::Workflow::HandleQueryInput.new(
445
447
  id: job.query_id,
@@ -502,7 +504,7 @@ module Temporalio
502
504
  # other SDKs, we are re-converting the args between validate and update to disallow user mutation in
503
505
  # validator/interceptor.
504
506
  if job.run_validator && defn.validator_to_invoke
505
- with_context_frozen do
507
+ with_context_frozen(in_query_or_validator: true) do
506
508
  @inbound.validate_update(
507
509
  Temporalio::Worker::Interceptor::Workflow::HandleUpdateInput.new(
508
510
  id: job.id,
@@ -663,11 +665,13 @@ module Temporalio
663
665
  @definition_options.failure_exception_types&.any? { |cls| err.is_a?(cls) }
664
666
  end
665
667
 
666
- def with_context_frozen(&)
668
+ def with_context_frozen(in_query_or_validator:, &)
667
669
  @context_frozen = true
670
+ @in_query_or_validator = in_query_or_validator
668
671
  yield
669
672
  ensure
670
673
  @context_frozen = false
674
+ @in_query_or_validator = false
671
675
  end
672
676
 
673
677
  def convert_handler_args(payload_array:, defn:)
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'temporalio/client'
4
+ require 'temporalio/worker'
5
+
6
+ module Temporalio
7
+ # Plugin that implements both {Client::Plugin} and {Worker::Plugin} and provides a simplified common set of settings
8
+ # for configuring both.
9
+ #
10
+ # WARNING: Plugins are experimental.
11
+ class SimplePlugin
12
+ include Client::Plugin
13
+ include Worker::Plugin
14
+
15
+ Options = Data.define(
16
+ :name,
17
+ :data_converter,
18
+ :client_interceptors,
19
+ :activities,
20
+ :workflows,
21
+ :worker_interceptors,
22
+ :workflow_failure_exception_types,
23
+ :run_context
24
+ )
25
+
26
+ # Options as returned from {options} representing the options passed to the constructor.
27
+ class Options; end # rubocop:disable Lint/EmptyClass
28
+
29
+ # @return [Options] Frozen options for this plugin which has the same attributes as {initialize}.
30
+ attr_reader :options
31
+
32
+ # Create a simple plugin.
33
+ #
34
+ # @param name [String] Required string name for this plugin.
35
+ # @param data_converter [Converters::DataConverter, Proc, nil] Data converter to apply to clients and workflow
36
+ # replayers. This can be a proc that accepts the existing data converter and returns a new one.
37
+ # @param client_interceptors [Array<Client::Integerceptor>, Proc, nil] Client interceptors that are appended to the
38
+ # existing client set (which means if they implement worker interceptors they are applied for the workers too). A
39
+ # proc can be provided that accepts the existing array and returns a new one.
40
+ # @param activities [Array<Activity::Definition, Class<Activity::Definition>, Activity::Definition::Info>, Proc,
41
+ # nil] Activities to append to each worker activity set. A proc can be provided that accepts the existing array
42
+ # and returns a new one.
43
+ # @param workflows [Array<Class<Workflow::Definition>>, Proc, nil] Workflows to append to each worker workflow set.
44
+ # A proc can be provided that accepts the existing array and returns a new one.
45
+ # @param worker_interceptors [Array<Interceptor::Activity, Interceptor::Workflow>, Proc, nil] Worker interceptors
46
+ # that are appended to the existing worker or workflow replayer set. A proc can be provided that accepts the
47
+ # existing array and returns a new one.
48
+ # @param workflow_failure_exception_types [Array<Class<Exception>>] Workflow failure exception types that are
49
+ # appended to the existing worker or workflow replayer set. A proc can be provided that accepts the existing array
50
+ # and returns a new one.
51
+ # @param run_context [Proc, nil] A proc that intercepts both {run_worker} or {with_workflow_replay_worker}. The proc
52
+ # should accept two positional parameters: options and next_call. The options are either
53
+ # {Worker::Plugin::RunWorkerOptions} or {Worker::Plugin::WithWorkflowReplayWorkerOptions}. The next_call is a proc
54
+ # itself that accepts the options and returns a value. This run_context proc should return the result of the
55
+ # next_call.
56
+ def initialize(
57
+ name:,
58
+ data_converter: nil,
59
+ client_interceptors: nil,
60
+ activities: nil,
61
+ workflows: nil,
62
+ worker_interceptors: nil,
63
+ workflow_failure_exception_types: nil,
64
+ run_context: nil
65
+ )
66
+ @options = Options.new(
67
+ name:,
68
+ data_converter:,
69
+ client_interceptors:,
70
+ activities:,
71
+ workflows:,
72
+ worker_interceptors:,
73
+ workflow_failure_exception_types:,
74
+ run_context:
75
+ ).freeze
76
+ end
77
+
78
+ # Implements {Client::Plugin#name} and {Worker::Plugin#name}.
79
+ def name
80
+ @options.name
81
+ end
82
+
83
+ # Implements {Client::Plugin#configure_client}.
84
+ def configure_client(options)
85
+ if (data_converter = _single_option(new: @options.data_converter, existing: options.data_converter,
86
+ type: Converters::DataConverter, name: 'data converter'))
87
+ options = options.with(data_converter:)
88
+ end
89
+ if (interceptors = _array_option(new: @options.client_interceptors, existing: options.interceptors,
90
+ name: 'client interceptor'))
91
+ options = options.with(interceptors:)
92
+ end
93
+ options
94
+ end
95
+
96
+ # Implements {Client::Plugin#connect_client}.
97
+ def connect_client(options, next_call)
98
+ next_call.call(options)
99
+ end
100
+
101
+ # Implements {Worker::Plugin#configure_worker}.
102
+ def configure_worker(options)
103
+ if (activities = _array_option(new: @options.activities, existing: options.activities, name: 'activity'))
104
+ options = options.with(activities:)
105
+ end
106
+ if (workflows = _array_option(new: @options.workflows, existing: options.workflows, name: 'workflow'))
107
+ options = options.with(workflows:)
108
+ end
109
+ if (interceptors = _array_option(new: @options.worker_interceptors, existing: options.interceptors,
110
+ name: 'worker interceptor'))
111
+ options = options.with(interceptors:)
112
+ end
113
+ if (workflow_failure_exception_types = _array_option(new: @options.workflow_failure_exception_types,
114
+ existing: options.workflow_failure_exception_types,
115
+ name: 'workflow failure exception types'))
116
+ options = options.with(workflow_failure_exception_types:)
117
+ end
118
+ options
119
+ end
120
+
121
+ # Implements {Worker::Plugin#run_worker}.
122
+ def run_worker(options, next_call)
123
+ if @options.run_context
124
+ @options.run_context.call(options, next_call) # steep:ignore NoMethod
125
+ else
126
+ next_call.call(options)
127
+ end
128
+ end
129
+
130
+ # Implements {Worker::Plugin#configure_workflow_replayer}.
131
+ def configure_workflow_replayer(options)
132
+ if (data_converter = _single_option(new: @options.data_converter, existing: options.data_converter,
133
+ type: Converters::DataConverter, name: 'data converter'))
134
+ options = options.with(data_converter:)
135
+ end
136
+ if (workflows = _array_option(new: @options.workflows, existing: options.workflows, name: 'workflow'))
137
+ options = options.with(workflows:)
138
+ end
139
+ if (interceptors = _array_option(new: @options.worker_interceptors, existing: options.interceptors,
140
+ name: 'worker interceptor'))
141
+ options = options.with(interceptors:)
142
+ end
143
+ if (workflow_failure_exception_types = _array_option(new: @options.workflow_failure_exception_types,
144
+ existing: options.workflow_failure_exception_types,
145
+ name: 'workflow failure exception types'))
146
+ options = options.with(workflow_failure_exception_types:)
147
+ end
148
+ options
149
+ end
150
+
151
+ # Implements {Worker::Plugin#with_workflow_replay_worker}.
152
+ def with_workflow_replay_worker(options, next_call)
153
+ if @options.run_context
154
+ @options.run_context.call(options, next_call) # steep:ignore NoMethod
155
+ else
156
+ next_call.call(options)
157
+ end
158
+ end
159
+
160
+ # @!visibility private
161
+ def _single_option(new:, existing:, type:, name:)
162
+ case new
163
+ when nil
164
+ nil
165
+ when Proc
166
+ new.call(existing).tap do |val| # steep:ignore NoMethod
167
+ raise "Instance of #{name} required" unless val.is_a?(type)
168
+ end
169
+ when type
170
+ new
171
+ else
172
+ raise "Unrecognized #{name} type #{new.class}"
173
+ end
174
+ end
175
+
176
+ # @!visibility private
177
+ def _array_option(new:, existing:, name:)
178
+ case new
179
+ when nil
180
+ nil
181
+ when Proc
182
+ new.call(existing).tap do |conv| # steep:ignore NoMethod
183
+ raise "Array for #{name} required" unless conv.is_a?(Array)
184
+ end
185
+ when Array
186
+ existing + new # steep:ignore NoMethod
187
+ else
188
+ raise "Unrecognized #{name} type #{new.class}"
189
+ end
190
+ end
191
+ end
192
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Temporalio
4
- VERSION = '1.1.0'
4
+ VERSION = '1.2.0'
5
5
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Temporalio
4
+ class Worker
5
+ # Plugin mixin to include for configuring workers and workflow replayers, and intercepting the running of them.
6
+ #
7
+ # This is a low-level implementation that requires abstract methods herein to be implemented. Many implementers may
8
+ # prefer {SimplePlugin} which includes this.
9
+ #
10
+ # WARNING: Plugins are experimental.
11
+ module Plugin
12
+ RunWorkerOptions = Data.define(
13
+ :worker,
14
+ :cancellation,
15
+ :shutdown_signals,
16
+ :raise_in_block_on_shutdown
17
+ )
18
+
19
+ # Options for {run_worker}.
20
+ #
21
+ # The options contain the worker and some other options from {Worker#run}/{Worker.run_all}. Unlike other memebers
22
+ # in this class, mutating the worker member before invoking the next call in the chain has no effect.
23
+ #
24
+ # @note Additional required attributes of this class may be added in the future. Users should never instantiate
25
+ # this class, but instead use `with` on it in {run_worker}.
26
+ class RunWorkerOptions; end # rubocop:disable Lint/EmptyClass
27
+
28
+ WithWorkflowReplayWorkerOptions = Data.define(
29
+ :worker
30
+ )
31
+
32
+ # Options for {with_workflow_replay_worker}.
33
+ #
34
+ # @note Additional required attributes of this class may be added in the future. Users should never instantiate
35
+ # this class, but instead use `with` on it in {with_workflow_replay_worker}.
36
+ #
37
+ # @!attribute worker
38
+ # @return [WorkflowReplayer::ReplayWorker] Replay worker.
39
+ class WithWorkflowReplayWorkerOptions; end # rubocop:disable Lint/EmptyClass
40
+
41
+ # @abstract
42
+ # @return [String] Name of the plugin.
43
+ def name
44
+ raise NotImplementedError
45
+ end
46
+
47
+ # Configure a worker.
48
+ #
49
+ # @abstract
50
+ # @param options [Options] Current immutable options set.
51
+ # @return [Options] Options to use, possibly updated from original.
52
+ def configure_worker(options)
53
+ raise NotImplementedError
54
+ end
55
+
56
+ # Run a worker.
57
+ #
58
+ # @abstract
59
+ # @param options [RunWorkerOptions] Current immutable options set.
60
+ # @param next_call [Proc] Proc for the next plugin in the chain to call. It accepts the options and returns an
61
+ # arbitrary object that should also be returned from this method.
62
+ # @return [Object] Result of next_call.
63
+ def run_worker(options, next_call)
64
+ raise NotImplementedError
65
+ end
66
+
67
+ # Configure a workflow replayer.
68
+ #
69
+ # @abstract
70
+ # @param options [WorkflowReplayer::Options] Current immutable options set.
71
+ # @return [WorkflowReplayer::Options] Options to use, possibly updated from original.
72
+ def configure_workflow_replayer(options)
73
+ raise NotImplementedError
74
+ end
75
+
76
+ # Run a replay worker.
77
+ #
78
+ # @abstract
79
+ # @param options [WithWorkflowReplayWorkerOptions] Current immutable options set.
80
+ # @param next_call [Proc] Proc for the next plugin in the chain to call. It accepts the options and returns an
81
+ # arbitrary object that should also be returned from this method.
82
+ # @return [Object] Result of next_call.
83
+ def with_workflow_replay_worker(options, next_call)
84
+ raise NotImplementedError
85
+ end
86
+ end
87
+ end
88
+ end
@@ -7,6 +7,7 @@ require 'temporalio/internal/bridge/worker'
7
7
  require 'temporalio/internal/worker/multi_runner'
8
8
  require 'temporalio/internal/worker/workflow_worker'
9
9
  require 'temporalio/worker/interceptor'
10
+ require 'temporalio/worker/plugin'
10
11
  require 'temporalio/worker/poller_behavior'
11
12
  require 'temporalio/worker/thread_pool'
12
13
  require 'temporalio/worker/tuner'
@@ -24,6 +25,7 @@ module Temporalio
24
25
  :task_queue,
25
26
  :data_converter,
26
27
  :workflow_executor,
28
+ :plugins,
27
29
  :interceptors,
28
30
  :identity,
29
31
  :logger,
@@ -50,6 +52,8 @@ module Temporalio
50
52
  # payloads.
51
53
  # @param workflow_executor [WorkflowExecutor] Workflow executor that workflow tasks run within. This must be a
52
54
  # {WorkflowExecutor::ThreadPool} currently.
55
+ # @param plugins [Array<Plugin>] Plugins to use for configuring replayer and intercepting replay. WARNING: Plugins
56
+ # are experimental.
53
57
  # @param interceptors [Array<Interceptor::Workflow>] Workflow interceptors.
54
58
  # @param identity [String, nil] Override the identity for this replater.
55
59
  # @param logger [Logger] Logger to use. Defaults to stdout with warn level. Callers setting this logger are
@@ -83,6 +87,7 @@ module Temporalio
83
87
  task_queue: 'ReplayTaskQueue',
84
88
  data_converter: Converters::DataConverter.default,
85
89
  workflow_executor: WorkflowExecutor::ThreadPool.default,
90
+ plugins: [],
86
91
  interceptors: [],
87
92
  identity: nil,
88
93
  logger: Logger.new($stdout, level: Logger::WARN),
@@ -100,6 +105,7 @@ module Temporalio
100
105
  task_queue:,
101
106
  data_converter:,
102
107
  workflow_executor:,
108
+ plugins:,
103
109
  interceptors:,
104
110
  identity:,
105
111
  logger:,
@@ -110,13 +116,18 @@ module Temporalio
110
116
  debug_mode:,
111
117
  runtime:
112
118
  ).freeze
119
+ # Apply plugins
120
+ Worker._validate_plugins!(plugins)
121
+ @options = plugins.reduce(@options) { |options, plugin| plugin.configure_workflow_replayer(options) }
122
+
113
123
  # Preload definitions and other settings
114
124
  @workflow_definitions = Internal::Worker::WorkflowWorker.workflow_definitions(
115
- workflows, should_enforce_versioning_behavior: false
125
+ @options.workflows, should_enforce_versioning_behavior: false
116
126
  )
117
127
  @nondeterminism_as_workflow_fail, @nondeterminism_as_workflow_fail_for_types =
118
128
  Internal::Worker::WorkflowWorker.bridge_workflow_failure_exception_type_options(
119
- workflow_failure_exception_types:, workflow_definitions: @workflow_definitions
129
+ workflow_failure_exception_types: @options.workflow_failure_exception_types,
130
+ workflow_definitions: @workflow_definitions
120
131
  )
121
132
  # If there is a block, we'll go ahead and assume it's for with_replay_worker
122
133
  with_replay_worker(&) if block_given? # steep:ignore
@@ -154,7 +165,18 @@ module Temporalio
154
165
  # @yield Block of code to run with a replay worker.
155
166
  # @yieldparam [ReplayWorker] Worker to run replays on. Note, only one workflow can replay at a time.
156
167
  # @yieldreturn [Object] Result of the block.
157
- def with_replay_worker(&)
168
+ def with_replay_worker(&block)
169
+ # Apply plugins
170
+ run_block = proc do |options|
171
+ # @type var options: Plugin::WithWorkflowReplayWorkerOptions
172
+ block.call(options.worker)
173
+ end
174
+ run_block = options.plugins.reverse_each.reduce(run_block) do |next_call, plugin|
175
+ proc do |options|
176
+ plugin.with_workflow_replay_worker(options, next_call) # steep:ignore
177
+ end
178
+ end
179
+
158
180
  worker = ReplayWorker.new(
159
181
  options:,
160
182
  workflow_definitions: @workflow_definitions,
@@ -162,7 +184,7 @@ module Temporalio
162
184
  nondeterminism_as_workflow_fail_for_types: @nondeterminism_as_workflow_fail_for_types
163
185
  )
164
186
  begin
165
- yield worker
187
+ run_block.call(Plugin::WithWorkflowReplayWorkerOptions.new(worker:))
166
188
  ensure
167
189
  worker._shutdown
168
190
  end
@@ -221,7 +243,8 @@ module Temporalio
221
243
  graceful_shutdown_period: 0.0,
222
244
  nondeterminism_as_workflow_fail:,
223
245
  nondeterminism_as_workflow_fail_for_types:,
224
- deployment_options: Worker.default_deployment_options._to_bridge_options
246
+ deployment_options: Worker.default_deployment_options._to_bridge_options,
247
+ plugins: options.plugins.map(&:name).uniq.sort
225
248
  )
226
249
  )
227
250