temporalio 0.2.0-x86_64-darwin → 0.3.0-x86_64-darwin

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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -0
  3. data/Gemfile +3 -3
  4. data/Rakefile +10 -296
  5. data/lib/temporalio/activity/complete_async_error.rb +1 -1
  6. data/lib/temporalio/activity/context.rb +5 -2
  7. data/lib/temporalio/activity/definition.rb +163 -65
  8. data/lib/temporalio/activity/info.rb +22 -21
  9. data/lib/temporalio/activity.rb +2 -59
  10. data/lib/temporalio/api/activity/v1/message.rb +25 -0
  11. data/lib/temporalio/api/cloud/account/v1/message.rb +28 -0
  12. data/lib/temporalio/api/cloud/cloudservice/v1/request_response.rb +34 -1
  13. data/lib/temporalio/api/cloud/cloudservice/v1/service.rb +1 -1
  14. data/lib/temporalio/api/cloud/identity/v1/message.rb +6 -1
  15. data/lib/temporalio/api/cloud/namespace/v1/message.rb +8 -1
  16. data/lib/temporalio/api/cloud/nexus/v1/message.rb +31 -0
  17. data/lib/temporalio/api/cloud/operation/v1/message.rb +2 -1
  18. data/lib/temporalio/api/cloud/region/v1/message.rb +2 -1
  19. data/lib/temporalio/api/cloud/resource/v1/message.rb +23 -0
  20. data/lib/temporalio/api/cloud/sink/v1/message.rb +24 -0
  21. data/lib/temporalio/api/cloud/usage/v1/message.rb +31 -0
  22. data/lib/temporalio/api/common/v1/message.rb +7 -1
  23. data/lib/temporalio/api/enums/v1/event_type.rb +1 -1
  24. data/lib/temporalio/api/enums/v1/failed_cause.rb +1 -1
  25. data/lib/temporalio/api/enums/v1/reset.rb +1 -1
  26. data/lib/temporalio/api/history/v1/message.rb +1 -1
  27. data/lib/temporalio/api/nexus/v1/message.rb +2 -2
  28. data/lib/temporalio/api/operatorservice/v1/service.rb +1 -1
  29. data/lib/temporalio/api/payload_visitor.rb +1513 -0
  30. data/lib/temporalio/api/schedule/v1/message.rb +2 -1
  31. data/lib/temporalio/api/testservice/v1/request_response.rb +31 -0
  32. data/lib/temporalio/api/testservice/v1/service.rb +23 -0
  33. data/lib/temporalio/api/workflow/v1/message.rb +1 -1
  34. data/lib/temporalio/api/workflowservice/v1/request_response.rb +17 -2
  35. data/lib/temporalio/api/workflowservice/v1/service.rb +1 -1
  36. data/lib/temporalio/api.rb +1 -0
  37. data/lib/temporalio/cancellation.rb +34 -14
  38. data/lib/temporalio/client/async_activity_handle.rb +12 -37
  39. data/lib/temporalio/client/connection/cloud_service.rb +309 -231
  40. data/lib/temporalio/client/connection/operator_service.rb +36 -84
  41. data/lib/temporalio/client/connection/service.rb +6 -5
  42. data/lib/temporalio/client/connection/test_service.rb +111 -0
  43. data/lib/temporalio/client/connection/workflow_service.rb +264 -441
  44. data/lib/temporalio/client/connection.rb +90 -44
  45. data/lib/temporalio/client/interceptor.rb +160 -60
  46. data/lib/temporalio/client/schedule.rb +967 -0
  47. data/lib/temporalio/client/schedule_handle.rb +126 -0
  48. data/lib/temporalio/client/workflow_execution.rb +7 -10
  49. data/lib/temporalio/client/workflow_handle.rb +38 -95
  50. data/lib/temporalio/client/workflow_update_handle.rb +3 -5
  51. data/lib/temporalio/client.rb +122 -42
  52. data/lib/temporalio/common_enums.rb +17 -0
  53. data/lib/temporalio/converters/data_converter.rb +4 -7
  54. data/lib/temporalio/converters/failure_converter.rb +5 -3
  55. data/lib/temporalio/converters/payload_converter/composite.rb +4 -0
  56. data/lib/temporalio/converters/payload_converter.rb +6 -8
  57. data/lib/temporalio/converters/raw_value.rb +20 -0
  58. data/lib/temporalio/error/failure.rb +1 -1
  59. data/lib/temporalio/error.rb +10 -2
  60. data/lib/temporalio/internal/bridge/3.2/temporalio_bridge.bundle +0 -0
  61. data/lib/temporalio/internal/bridge/3.3/temporalio_bridge.bundle +0 -0
  62. data/lib/temporalio/internal/bridge/{3.1 → 3.4}/temporalio_bridge.bundle +0 -0
  63. data/lib/temporalio/internal/bridge/api/core_interface.rb +5 -1
  64. data/lib/temporalio/internal/bridge/api/nexus/nexus.rb +33 -0
  65. data/lib/temporalio/internal/bridge/api/workflow_activation/workflow_activation.rb +5 -1
  66. data/lib/temporalio/internal/bridge/api/workflow_commands/workflow_commands.rb +4 -1
  67. data/lib/temporalio/internal/bridge/client.rb +11 -6
  68. data/lib/temporalio/internal/bridge/testing.rb +20 -0
  69. data/lib/temporalio/internal/bridge/worker.rb +2 -0
  70. data/lib/temporalio/internal/bridge.rb +1 -1
  71. data/lib/temporalio/internal/client/implementation.rb +245 -70
  72. data/lib/temporalio/internal/metric.rb +122 -0
  73. data/lib/temporalio/internal/proto_utils.rb +86 -7
  74. data/lib/temporalio/internal/worker/activity_worker.rb +52 -24
  75. data/lib/temporalio/internal/worker/multi_runner.rb +51 -7
  76. data/lib/temporalio/internal/worker/workflow_instance/child_workflow_handle.rb +54 -0
  77. data/lib/temporalio/internal/worker/workflow_instance/context.rb +329 -0
  78. data/lib/temporalio/internal/worker/workflow_instance/details.rb +44 -0
  79. data/lib/temporalio/internal/worker/workflow_instance/external_workflow_handle.rb +32 -0
  80. data/lib/temporalio/internal/worker/workflow_instance/externally_immutable_hash.rb +22 -0
  81. data/lib/temporalio/internal/worker/workflow_instance/handler_execution.rb +25 -0
  82. data/lib/temporalio/internal/worker/workflow_instance/handler_hash.rb +41 -0
  83. data/lib/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rb +97 -0
  84. data/lib/temporalio/internal/worker/workflow_instance/inbound_implementation.rb +62 -0
  85. data/lib/temporalio/internal/worker/workflow_instance/outbound_implementation.rb +415 -0
  86. data/lib/temporalio/internal/worker/workflow_instance/replay_safe_logger.rb +37 -0
  87. data/lib/temporalio/internal/worker/workflow_instance/replay_safe_metric.rb +40 -0
  88. data/lib/temporalio/internal/worker/workflow_instance/scheduler.rb +163 -0
  89. data/lib/temporalio/internal/worker/workflow_instance.rb +730 -0
  90. data/lib/temporalio/internal/worker/workflow_worker.rb +196 -0
  91. data/lib/temporalio/metric.rb +109 -0
  92. data/lib/temporalio/retry_policy.rb +37 -14
  93. data/lib/temporalio/runtime.rb +118 -75
  94. data/lib/temporalio/search_attributes.rb +80 -37
  95. data/lib/temporalio/testing/activity_environment.rb +2 -2
  96. data/lib/temporalio/testing/workflow_environment.rb +251 -5
  97. data/lib/temporalio/version.rb +1 -1
  98. data/lib/temporalio/worker/activity_executor/thread_pool.rb +9 -217
  99. data/lib/temporalio/worker/activity_executor.rb +3 -3
  100. data/lib/temporalio/worker/interceptor.rb +340 -66
  101. data/lib/temporalio/worker/thread_pool.rb +237 -0
  102. data/lib/temporalio/worker/workflow_executor/thread_pool.rb +230 -0
  103. data/lib/temporalio/worker/workflow_executor.rb +26 -0
  104. data/lib/temporalio/worker.rb +201 -30
  105. data/lib/temporalio/workflow/activity_cancellation_type.rb +20 -0
  106. data/lib/temporalio/workflow/child_workflow_cancellation_type.rb +21 -0
  107. data/lib/temporalio/workflow/child_workflow_handle.rb +43 -0
  108. data/lib/temporalio/workflow/definition.rb +566 -0
  109. data/lib/temporalio/workflow/external_workflow_handle.rb +41 -0
  110. data/lib/temporalio/workflow/future.rb +151 -0
  111. data/lib/temporalio/workflow/handler_unfinished_policy.rb +13 -0
  112. data/lib/temporalio/workflow/info.rb +82 -0
  113. data/lib/temporalio/workflow/parent_close_policy.rb +19 -0
  114. data/lib/temporalio/workflow/update_info.rb +20 -0
  115. data/lib/temporalio/workflow.rb +523 -0
  116. data/lib/temporalio.rb +4 -0
  117. data/temporalio.gemspec +2 -2
  118. metadata +52 -6
@@ -0,0 +1,230 @@
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(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)
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) 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
+ worker_state.workflow_definitions[nil]
191
+ unless definition
192
+ raise Error::ApplicationError.new(
193
+ "Workflow type #{init_job.workflow_type} is not registered on this worker, available workflows: " +
194
+ worker_state.workflow_definitions.keys.compact.sort.join(', '),
195
+ type: 'NotFoundError'
196
+ )
197
+ end
198
+
199
+ Internal::Worker::WorkflowInstance.new(
200
+ Internal::Worker::WorkflowInstance::Details.new(
201
+ namespace: worker_state.namespace,
202
+ task_queue: worker_state.task_queue,
203
+ definition:,
204
+ initial_activation:,
205
+ logger: worker_state.logger,
206
+ metric_meter: worker_state.metric_meter,
207
+ payload_converter: worker_state.data_converter.payload_converter,
208
+ failure_converter: worker_state.data_converter.failure_converter,
209
+ interceptors: worker_state.workflow_interceptors,
210
+ disable_eager_activity_execution: worker_state.disable_eager_activity_execution,
211
+ illegal_calls: worker_state.illegal_calls,
212
+ workflow_failure_exception_types: worker_state.workflow_failure_exception_types
213
+ )
214
+ )
215
+ end
216
+
217
+ def evict(worker_state, run_id)
218
+ worker_state.evict_running_workflow(run_id)
219
+ @executor._remove_workflow(worker_state, run_id)
220
+ end
221
+ end
222
+
223
+ private_constant :Worker
224
+
225
+ # Error raised when a processing a workflow task takes more than the expected amount of time.
226
+ class DeadlockError < Exception; end # rubocop:disable Lint/InheritException
227
+ end
228
+ end
229
+ end
230
+ 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(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
@@ -8,9 +8,13 @@ require 'temporalio/internal/bridge'
8
8
  require 'temporalio/internal/bridge/worker'
9
9
  require 'temporalio/internal/worker/activity_worker'
10
10
  require 'temporalio/internal/worker/multi_runner'
11
+ require 'temporalio/internal/worker/workflow_instance'
12
+ require 'temporalio/internal/worker/workflow_worker'
11
13
  require 'temporalio/worker/activity_executor'
12
14
  require 'temporalio/worker/interceptor'
15
+ require 'temporalio/worker/thread_pool'
13
16
  require 'temporalio/worker/tuner'
17
+ require 'temporalio/worker/workflow_executor'
14
18
 
15
19
  module Temporalio
16
20
  # Worker for processing activities and workflows on a task queue.
@@ -19,13 +23,14 @@ module Temporalio
19
23
  # {run_all} is used for a collection of workers. These can wait until a block is complete or a {Cancellation} is
20
24
  # canceled.
21
25
  class Worker
22
- # Options as returned from {options} for `**to_h`` splat use in {initialize}. See {initialize} for details.
23
- Options = Struct.new(
26
+ Options = Data.define(
24
27
  :client,
25
28
  :task_queue,
26
29
  :activities,
27
- :activity_executors,
30
+ :workflows,
28
31
  :tuner,
32
+ :activity_executors,
33
+ :workflow_executor,
29
34
  :interceptors,
30
35
  :build_id,
31
36
  :identity,
@@ -42,9 +47,16 @@ module Temporalio
42
47
  :max_task_queue_activities_per_second,
43
48
  :graceful_shutdown_period,
44
49
  :use_worker_versioning,
45
- keyword_init: true
50
+ :disable_eager_activity_execution,
51
+ :illegal_workflow_calls,
52
+ :workflow_failure_exception_types,
53
+ :workflow_payload_codec_thread_pool,
54
+ :debug_mode
46
55
  )
47
56
 
57
+ # Options as returned from {options} for `**to_h` splat use in {initialize}. See {initialize} for details.
58
+ class Options; end # rubocop:disable Lint/EmptyClass
59
+
48
60
  # @return [String] Memoized default build ID. This default value is built as a checksum of all of the loaded Ruby
49
61
  # source files in `$LOADED_FEATURES`. Users may prefer to set the build ID to a better representation of the
50
62
  # source.
@@ -108,7 +120,7 @@ module Temporalio
108
120
  runner.apply_thread_or_fiber_block(&block)
109
121
 
110
122
  # Reuse first worker logger
111
- logger = workers.first&.options&.logger or raise # Help steep
123
+ logger = workers.first&.options&.logger or raise # Never nil
112
124
 
113
125
  # On cancel, initiate shutdown
114
126
  cancellation.add_cancel_callback do
@@ -121,16 +133,34 @@ module Temporalio
121
133
  block_result = nil
122
134
  loop do
123
135
  event = runner.next_event
136
+ # TODO(cretz): Consider improving performance instead of this case statement
124
137
  case event
125
138
  when Internal::Worker::MultiRunner::Event::PollSuccess
126
139
  # Successful poll
127
- event.worker._on_poll_bytes(event.worker_type, event.bytes)
140
+ event.worker._on_poll_bytes(runner, event.worker_type, event.bytes)
128
141
  when Internal::Worker::MultiRunner::Event::PollFailure
129
142
  # Poll failure, this causes shutdown of all workers
130
- logger.error('Poll failure (beginning worker shutdown if not alaredy occurring)')
143
+ logger.error('Poll failure (beginning worker shutdown if not already occurring)')
131
144
  logger.error(event.error)
132
145
  first_error ||= event.error
133
146
  runner.initiate_shutdown
147
+ when Internal::Worker::MultiRunner::Event::WorkflowActivationDecoded
148
+ # Came back from a codec as decoded
149
+ event.workflow_worker.handle_activation(runner:, activation: event.activation, decoded: true)
150
+ when Internal::Worker::MultiRunner::Event::WorkflowActivationComplete
151
+ # An activation is complete
152
+ event.workflow_worker.handle_activation_complete(
153
+ runner:,
154
+ activation_completion: event.activation_completion,
155
+ encoded: event.encoded,
156
+ completion_complete_queue: event.completion_complete_queue
157
+ )
158
+ when Internal::Worker::MultiRunner::Event::WorkflowActivationCompletionComplete
159
+ # Completion complete, only need to log error if it occurs here
160
+ if event.error
161
+ logger.error("Activation completion failed to record on run ID #{event.run_id}")
162
+ logger.error(event.error)
163
+ end
134
164
  when Internal::Worker::MultiRunner::Event::PollerShutDown
135
165
  # Individual poller shut down. Nothing to do here until we support
136
166
  # worker status or something.
@@ -186,6 +216,9 @@ module Temporalio
186
216
  end
187
217
  end
188
218
 
219
+ # Notify each worker we're done with it
220
+ workers.each(&:_on_shutdown_complete)
221
+
189
222
  # If there was an shutdown-causing error, we raise that
190
223
  if !first_error.nil?
191
224
  raise first_error
@@ -194,6 +227,53 @@ module Temporalio
194
227
  end
195
228
  end
196
229
 
230
+ # @return [Hash<String, [:all, Array<Symbol>]>] Default, immutable set illegal calls used for the
231
+ # `illegal_workflow_calls` worker option. See the documentation of that option for more details.
232
+ def self.default_illegal_workflow_calls
233
+ @default_illegal_workflow_calls ||= begin
234
+ hash = {
235
+ 'BasicSocket' => :all,
236
+ 'Date' => %i[initialize today],
237
+ 'DateTime' => %i[initialize now],
238
+ 'Dir' => :all,
239
+ 'Fiber' => [:set_scheduler],
240
+ 'File' => :all,
241
+ 'FileTest' => :all,
242
+ 'FileUtils' => :all,
243
+ 'Find' => :all,
244
+ 'GC' => :all,
245
+ 'IO' => [
246
+ :read
247
+ # Intentionally leaving out write so puts will work. We don't want to add heavy logic replacing stdout or
248
+ # trying to derive whether it's file vs stdout write.
249
+ #:write
250
+ ],
251
+ 'Kernel' => %i[abort at_exit autoload autoload? eval exec exit fork gets load open rand readline readlines
252
+ spawn srand system test trap],
253
+ 'Net::HTTP' => :all,
254
+ 'Pathname' => :all,
255
+ # TODO(cretz): Investigate why clock_gettime called from Timeout thread affects this code at all. Stack trace
256
+ # test executing activities inside a timeout will fail if clock_gettime is blocked.
257
+ 'Process' => %i[abort argv0 daemon detach exec exit exit! fork kill setpriority setproctitle setrlimit setsid
258
+ spawn times wait wait2 waitall warmup],
259
+ # TODO(cretz): Allow Ractor.current since exception formatting in error_highlight references it
260
+ # 'Ractor' => :all,
261
+ 'Random::Base' => [:initialize],
262
+ 'Resolv' => :all,
263
+ 'SecureRandom' => :all,
264
+ 'Signal' => :all,
265
+ 'Socket' => :all,
266
+ 'Tempfile' => :all,
267
+ 'Thread' => %i[abort_on_exception= exit fork handle_interrupt ignore_deadlock= kill new pass
268
+ pending_interrupt? report_on_exception= start stop initialize join name= priority= raise run
269
+ terminate thread_variable_set wakeup],
270
+ 'Time' => %i[initialize now]
271
+ } #: Hash[String, :all | Array[Symbol]]
272
+ hash.each_value(&:freeze)
273
+ hash.freeze
274
+ end
275
+ end
276
+
197
277
  # @return [Options] Frozen options for this client which has the same attributes as {initialize}.
198
278
  attr_reader :options
199
279
 
@@ -201,20 +281,26 @@ module Temporalio
201
281
  #
202
282
  # @param client [Client] Client for this worker.
203
283
  # @param task_queue [String] Task queue for this worker.
204
- # @param activities [Array<Activity, Class<Activity>, Activity::Definition>] Activities for this worker.
205
- # @param activity_executors [Hash<Symbol, Worker::ActivityExecutor>] Executors that activities can run within.
284
+ # @param activities [Array<Activity::Definition, Class<Activity::Definition>, Activity::Definition::Info>]
285
+ # Activities for this worker.
286
+ # @param workflows [Array<Class<Workflow::Definition>>] Workflows for this worker.
206
287
  # @param tuner [Tuner] Tuner that controls the amount of concurrent activities/workflows that run at a time.
207
- # @param interceptors [Array<Interceptor>] Interceptors specific to this worker. Note, interceptors set on the
208
- # client that include the {Interceptor} module are automatically included here, so no need to specify them again.
288
+ # @param activity_executors [Hash<Symbol, Worker::ActivityExecutor>] Executors that activities can run within.
289
+ # @param workflow_executor [WorkflowExecutor] Workflow executor that workflow tasks run within. This must be a
290
+ # {WorkflowExecutor::ThreadPool} currently.
291
+ # @param interceptors [Array<Interceptor::Activity, Interceptor::Workflow>] Interceptors specific to this worker.
292
+ # Note, interceptors set on the client that include the {Interceptor::Activity} or {Interceptor::Workflow} module
293
+ # are automatically included here, so no need to specify them again.
209
294
  # @param build_id [String] Unique identifier for the current runtime. This is best set as a unique value
210
295
  # representing all code and should change only when code does. This can be something like a git commit hash. If
211
296
  # unset, default is hash of known Ruby code.
212
297
  # @param identity [String, nil] Override the identity for this worker. If unset, client identity is used.
298
+ # @param logger [Logger] Logger to override client logger with. Default is the client logger.
213
299
  # @param max_cached_workflows [Integer] Number of workflows held in cache for use by sticky task queue. If set to 0,
214
300
  # workflow caching and sticky queuing are disabled.
215
301
  # @param max_concurrent_workflow_task_polls [Integer] Maximum number of concurrent poll workflow task requests we
216
302
  # will perform at a time on this worker's task queue.
217
- # @param nonsticky_to_sticky_poll_ratio [Float] `max_concurrent_workflow_task_polls`` * this number = the number of
303
+ # @param nonsticky_to_sticky_poll_ratio [Float] `max_concurrent_workflow_task_polls` * this number = the number of
218
304
  # max pollers that will be allowed for the nonsticky queue when sticky tasks are enabled. If both defaults are
219
305
  # used, the sticky queue will allow 4 max pollers while the nonsticky queue will allow one. The minimum for either
220
306
  # poller is 1, so if `max_concurrent_workflow_task_polls` is 1 and sticky queues are enabled, there will be 2
@@ -239,12 +325,35 @@ module Temporalio
239
325
  # @param use_worker_versioning [Boolean] If true, the `build_id` argument must be specified, and this worker opts
240
326
  # into the worker versioning feature. This ensures it only receives workflow tasks for workflows which it claims
241
327
  # to be compatible with. For more information, see https://docs.temporal.io/workers#worker-versioning.
328
+ # @param disable_eager_activity_execution [Boolean] If true, disables eager activity execution. Eager activity
329
+ # execution is an optimization on some servers that sends activities back to the same worker as the calling
330
+ # workflow if they can run there. This should be set to true for `max_task_queue_activities_per_second` to work
331
+ # and in a future version of this API may be implied as such (i.e. this setting will be ignored if that setting is
332
+ # set).
333
+ # @param illegal_workflow_calls [Hash<String, [:all, Array<Symbol>]>] Set of illegal workflow calls that are
334
+ # considered unsafe/non-deterministic and will raise if seen. The key of the hash is the fully qualified string
335
+ # class name (no leading `::`). The value is either `:all` which means any use of the class, or an array of
336
+ # symbols for methods on the class that cannot be used. The methods refer to either instance or class methods,
337
+ # there is no way to differentiate at this time.
338
+ # @param workflow_failure_exception_types [Array<Class<Exception>>] Workflow failure exception types. This is the
339
+ # set of exception types that, if a workflow-thrown exception extends, will cause the workflow/update to fail
340
+ # instead of suspending the workflow via task failure. These are applied in addition to the
341
+ # `workflow_failure_exception_type` on the workflow definition class itself. If {::Exception} is set, it
342
+ # effectively will fail a workflow/update in all user exception cases.
343
+ # @param workflow_payload_codec_thread_pool [ThreadPool, nil] Thread pool to run payload codec encode/decode within.
344
+ # This is required if a payload codec exists and the worker is not fiber based. Codecs can potentially block
345
+ # execution which is why they need to be run in the background.
346
+ # @param debug_mode [Boolean] If true, deadlock detection is disabled. Deadlock detection will fail workflow tasks
347
+ # if they block the thread for too long. This defaults to true if the `TEMPORAL_DEBUG` environment variable is
348
+ # `true` or `1`.
242
349
  def initialize(
243
350
  client:,
244
351
  task_queue:,
245
352
  activities: [],
246
- activity_executors: ActivityExecutor.defaults,
353
+ workflows: [],
247
354
  tuner: Tuner.create_fixed,
355
+ activity_executors: ActivityExecutor.defaults,
356
+ workflow_executor: WorkflowExecutor::ThreadPool.default,
248
357
  interceptors: [],
249
358
  build_id: Worker.default_build_id,
250
359
  identity: nil,
@@ -260,17 +369,23 @@ module Temporalio
260
369
  max_activities_per_second: nil,
261
370
  max_task_queue_activities_per_second: nil,
262
371
  graceful_shutdown_period: 0,
263
- use_worker_versioning: false
372
+ use_worker_versioning: false,
373
+ disable_eager_activity_execution: false,
374
+ illegal_workflow_calls: Worker.default_illegal_workflow_calls,
375
+ workflow_failure_exception_types: [],
376
+ workflow_payload_codec_thread_pool: nil,
377
+ debug_mode: %w[true 1].include?(ENV['TEMPORAL_DEBUG'].to_s.downcase)
264
378
  )
265
- # TODO(cretz): Remove when workflows come about
266
- raise ArgumentError, 'Must have at least one activity' if activities.empty?
379
+ raise ArgumentError, 'Must have at least one activity or workflow' if activities.empty? && workflows.empty?
267
380
 
268
381
  @options = Options.new(
269
382
  client:,
270
383
  task_queue:,
271
384
  activities:,
272
- activity_executors:,
385
+ workflows:,
273
386
  tuner:,
387
+ activity_executors:,
388
+ workflow_executor:,
274
389
  interceptors:,
275
390
  build_id:,
276
391
  identity:,
@@ -286,15 +401,36 @@ module Temporalio
286
401
  max_activities_per_second:,
287
402
  max_task_queue_activities_per_second:,
288
403
  graceful_shutdown_period:,
289
- use_worker_versioning:
404
+ use_worker_versioning:,
405
+ disable_eager_activity_execution:,
406
+ illegal_workflow_calls:,
407
+ workflow_failure_exception_types:,
408
+ workflow_payload_codec_thread_pool:,
409
+ debug_mode:
290
410
  ).freeze
291
411
 
412
+ # Preload workflow definitions and some workflow settings for the bridge
413
+ workflow_definitions = Internal::Worker::WorkflowWorker.workflow_definitions(workflows)
414
+ nondeterminism_as_workflow_fail = workflow_failure_exception_types.any? do |t|
415
+ t.is_a?(Class) && t >= Workflow::NondeterminismError
416
+ end
417
+ nondeterminism_as_workflow_fail_for_types = workflow_definitions.values.map do |defn|
418
+ next unless defn.failure_exception_types.any? { |t| t.is_a?(Class) && t >= Workflow::NondeterminismError }
419
+
420
+ # If they tried to do this on a dynamic workflow and haven't already set worker-level option, warn
421
+ unless defn.name || nondeterminism_as_workflow_fail
422
+ warn('Note, dynamic workflows cannot trap non-determinism errors, so worker-level ' \
423
+ 'workflow_failure_exception_types should be set to capture that if that is the intention')
424
+ end
425
+ defn.name
426
+ end.compact
427
+
292
428
  # Create the bridge worker
293
429
  @bridge_worker = Internal::Bridge::Worker.new(
294
430
  client.connection._core_client,
295
431
  Internal::Bridge::Worker::Options.new(
296
432
  activity: !activities.empty?,
297
- workflow: false,
433
+ workflow: !workflows.empty?,
298
434
  namespace: client.namespace,
299
435
  task_queue:,
300
436
  tuner: Internal::Bridge::Worker::TunerOptions.new(
@@ -308,26 +444,42 @@ module Temporalio
308
444
  max_concurrent_workflow_task_polls:,
309
445
  nonsticky_to_sticky_poll_ratio:,
310
446
  max_concurrent_activity_task_polls:,
311
- no_remote_activities:,
447
+ # For shutdown to work properly, we must disable remote activities
448
+ # ourselves if there are no activities
449
+ no_remote_activities: no_remote_activities || activities.empty?,
312
450
  sticky_queue_schedule_to_start_timeout:,
313
451
  max_heartbeat_throttle_interval:,
314
452
  default_heartbeat_throttle_interval:,
315
453
  max_worker_activities_per_second: max_activities_per_second,
316
454
  max_task_queue_activities_per_second:,
317
455
  graceful_shutdown_period:,
318
- use_worker_versioning:
456
+ use_worker_versioning:,
457
+ nondeterminism_as_workflow_fail:,
458
+ nondeterminism_as_workflow_fail_for_types:
319
459
  )
320
460
  )
321
461
 
322
462
  # Collect interceptors from client and params
323
- @all_interceptors = client.options.interceptors.select { |i| i.is_a?(Interceptor) } + interceptors
463
+ @activity_interceptors = (client.options.interceptors + interceptors).select do |i|
464
+ i.is_a?(Interceptor::Activity)
465
+ end
466
+ @workflow_interceptors = (client.options.interceptors + interceptors).select do |i|
467
+ i.is_a?(Interceptor::Workflow)
468
+ end
324
469
 
325
470
  # Cancellation for the whole worker
326
471
  @worker_shutdown_cancellation = Cancellation.new
327
472
 
328
473
  # Create workers
329
- # TODO(cretz): Make conditional when workflows appear
330
- @activity_worker = Internal::Worker::ActivityWorker.new(self, @bridge_worker)
474
+ unless activities.empty?
475
+ @activity_worker = Internal::Worker::ActivityWorker.new(worker: self,
476
+ bridge_worker: @bridge_worker)
477
+ end
478
+ unless workflows.empty?
479
+ @workflow_worker = Internal::Worker::WorkflowWorker.new(worker: self,
480
+ bridge_worker: @bridge_worker,
481
+ workflow_definitions:)
482
+ end
331
483
 
332
484
  # Validate worker
333
485
  @bridge_worker.validate
@@ -387,16 +539,35 @@ module Temporalio
387
539
  end
388
540
 
389
541
  # @!visibility private
390
- def _all_interceptors
391
- @all_interceptors
542
+ def _activity_interceptors
543
+ @activity_interceptors
392
544
  end
393
545
 
394
546
  # @!visibility private
395
- def _on_poll_bytes(worker_type, bytes)
396
- # TODO(cretz): Workflow workers
397
- raise "Unrecognized worker type #{worker_type}" unless worker_type == :activity
547
+ def _workflow_interceptors
548
+ @workflow_interceptors
549
+ end
550
+
551
+ # @!visibility private
552
+ def _on_poll_bytes(runner, worker_type, bytes)
553
+ case worker_type
554
+ when :activity
555
+ @activity_worker.handle_task(Internal::Bridge::Api::ActivityTask::ActivityTask.decode(bytes))
556
+ when :workflow
557
+ @workflow_worker.handle_activation(
558
+ runner:,
559
+ activation: Internal::Bridge::Api::WorkflowActivation::WorkflowActivation.decode(bytes),
560
+ decoded: false
561
+ )
562
+ else
563
+ raise "Unrecognized worker type #{worker_type}"
564
+ end
565
+ end
398
566
 
399
- @activity_worker.handle_task(Internal::Bridge::Api::ActivityTask::ActivityTask.decode(bytes))
567
+ # @!visibility private
568
+ def _on_shutdown_complete
569
+ @workflow_worker&.on_shutdown_complete
570
+ @workflow_worker = nil
400
571
  end
401
572
 
402
573
  private
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'temporalio/internal/bridge/api'
4
+
5
+ module Temporalio
6
+ module Workflow
7
+ # Cancellation types for activities.
8
+ module ActivityCancellationType
9
+ # Initiate a cancellation request and immediately report cancellation to the workflow.
10
+ TRY_CANCEL = Internal::Bridge::Api::WorkflowCommands::ActivityCancellationType::TRY_CANCEL
11
+ # Wait for activity cancellation completion. Note that activity must heartbeat to receive a cancellation
12
+ # notification. This can block the cancellation for a long time if activity doesn't heartbeat or chooses to ignore
13
+ # the cancellation request.
14
+ WAIT_CANCELLATION_COMPLETED =
15
+ Internal::Bridge::Api::WorkflowCommands::ActivityCancellationType::WAIT_CANCELLATION_COMPLETED
16
+ # Do not request cancellation of the activity and immediately report cancellation to the workflow.
17
+ ABANDON = Internal::Bridge::Api::WorkflowCommands::ActivityCancellationType::ABANDON
18
+ end
19
+ end
20
+ end