clara-temporalio 0.4.3-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 (190) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +2 -0
  3. data/Gemfile +27 -0
  4. data/Rakefile +101 -0
  5. data/lib/temporalio/activity/cancellation_details.rb +58 -0
  6. data/lib/temporalio/activity/complete_async_error.rb +11 -0
  7. data/lib/temporalio/activity/context.rb +131 -0
  8. data/lib/temporalio/activity/definition.rb +197 -0
  9. data/lib/temporalio/activity/info.rb +70 -0
  10. data/lib/temporalio/activity.rb +14 -0
  11. data/lib/temporalio/api/activity/v1/message.rb +25 -0
  12. data/lib/temporalio/api/batch/v1/message.rb +38 -0
  13. data/lib/temporalio/api/cloud/account/v1/message.rb +28 -0
  14. data/lib/temporalio/api/cloud/cloudservice/v1/request_response.rb +135 -0
  15. data/lib/temporalio/api/cloud/cloudservice/v1/service.rb +25 -0
  16. data/lib/temporalio/api/cloud/cloudservice.rb +3 -0
  17. data/lib/temporalio/api/cloud/identity/v1/message.rb +46 -0
  18. data/lib/temporalio/api/cloud/namespace/v1/message.rb +46 -0
  19. data/lib/temporalio/api/cloud/nexus/v1/message.rb +32 -0
  20. data/lib/temporalio/api/cloud/operation/v1/message.rb +28 -0
  21. data/lib/temporalio/api/cloud/region/v1/message.rb +24 -0
  22. data/lib/temporalio/api/cloud/resource/v1/message.rb +23 -0
  23. data/lib/temporalio/api/cloud/sink/v1/message.rb +24 -0
  24. data/lib/temporalio/api/cloud/usage/v1/message.rb +31 -0
  25. data/lib/temporalio/api/command/v1/message.rb +46 -0
  26. data/lib/temporalio/api/common/v1/grpc_status.rb +23 -0
  27. data/lib/temporalio/api/common/v1/message.rb +49 -0
  28. data/lib/temporalio/api/deployment/v1/message.rb +39 -0
  29. data/lib/temporalio/api/enums/v1/batch_operation.rb +22 -0
  30. data/lib/temporalio/api/enums/v1/command_type.rb +21 -0
  31. data/lib/temporalio/api/enums/v1/common.rb +28 -0
  32. data/lib/temporalio/api/enums/v1/deployment.rb +23 -0
  33. data/lib/temporalio/api/enums/v1/event_type.rb +21 -0
  34. data/lib/temporalio/api/enums/v1/failed_cause.rb +26 -0
  35. data/lib/temporalio/api/enums/v1/namespace.rb +23 -0
  36. data/lib/temporalio/api/enums/v1/nexus.rb +21 -0
  37. data/lib/temporalio/api/enums/v1/query.rb +22 -0
  38. data/lib/temporalio/api/enums/v1/reset.rb +23 -0
  39. data/lib/temporalio/api/enums/v1/schedule.rb +21 -0
  40. data/lib/temporalio/api/enums/v1/task_queue.rb +25 -0
  41. data/lib/temporalio/api/enums/v1/update.rb +22 -0
  42. data/lib/temporalio/api/enums/v1/workflow.rb +31 -0
  43. data/lib/temporalio/api/errordetails/v1/message.rb +44 -0
  44. data/lib/temporalio/api/export/v1/message.rb +24 -0
  45. data/lib/temporalio/api/failure/v1/message.rb +38 -0
  46. data/lib/temporalio/api/filter/v1/message.rb +27 -0
  47. data/lib/temporalio/api/history/v1/message.rb +94 -0
  48. data/lib/temporalio/api/namespace/v1/message.rb +31 -0
  49. data/lib/temporalio/api/nexus/v1/message.rb +41 -0
  50. data/lib/temporalio/api/operatorservice/v1/request_response.rb +49 -0
  51. data/lib/temporalio/api/operatorservice/v1/service.rb +23 -0
  52. data/lib/temporalio/api/operatorservice.rb +3 -0
  53. data/lib/temporalio/api/payload_visitor.rb +1668 -0
  54. data/lib/temporalio/api/protocol/v1/message.rb +23 -0
  55. data/lib/temporalio/api/query/v1/message.rb +28 -0
  56. data/lib/temporalio/api/replication/v1/message.rb +26 -0
  57. data/lib/temporalio/api/rules/v1/message.rb +27 -0
  58. data/lib/temporalio/api/schedule/v1/message.rb +43 -0
  59. data/lib/temporalio/api/sdk/v1/enhanced_stack_trace.rb +25 -0
  60. data/lib/temporalio/api/sdk/v1/task_complete_metadata.rb +21 -0
  61. data/lib/temporalio/api/sdk/v1/user_metadata.rb +23 -0
  62. data/lib/temporalio/api/sdk/v1/workflow_metadata.rb +23 -0
  63. data/lib/temporalio/api/taskqueue/v1/message.rb +48 -0
  64. data/lib/temporalio/api/testservice/v1/request_response.rb +31 -0
  65. data/lib/temporalio/api/testservice/v1/service.rb +23 -0
  66. data/lib/temporalio/api/update/v1/message.rb +33 -0
  67. data/lib/temporalio/api/version/v1/message.rb +26 -0
  68. data/lib/temporalio/api/workflow/v1/message.rb +63 -0
  69. data/lib/temporalio/api/workflowservice/v1/request_response.rb +244 -0
  70. data/lib/temporalio/api/workflowservice/v1/service.rb +23 -0
  71. data/lib/temporalio/api/workflowservice.rb +3 -0
  72. data/lib/temporalio/api.rb +15 -0
  73. data/lib/temporalio/cancellation.rb +170 -0
  74. data/lib/temporalio/client/activity_id_reference.rb +32 -0
  75. data/lib/temporalio/client/async_activity_handle.rb +85 -0
  76. data/lib/temporalio/client/connection/cloud_service.rb +786 -0
  77. data/lib/temporalio/client/connection/operator_service.rb +201 -0
  78. data/lib/temporalio/client/connection/service.rb +42 -0
  79. data/lib/temporalio/client/connection/test_service.rb +111 -0
  80. data/lib/temporalio/client/connection/workflow_service.rb +1326 -0
  81. data/lib/temporalio/client/connection.rb +316 -0
  82. data/lib/temporalio/client/interceptor.rb +457 -0
  83. data/lib/temporalio/client/schedule.rb +991 -0
  84. data/lib/temporalio/client/schedule_handle.rb +126 -0
  85. data/lib/temporalio/client/with_start_workflow_operation.rb +115 -0
  86. data/lib/temporalio/client/workflow_execution.rb +119 -0
  87. data/lib/temporalio/client/workflow_execution_count.rb +36 -0
  88. data/lib/temporalio/client/workflow_execution_status.rb +18 -0
  89. data/lib/temporalio/client/workflow_handle.rb +389 -0
  90. data/lib/temporalio/client/workflow_query_reject_condition.rb +14 -0
  91. data/lib/temporalio/client/workflow_update_handle.rb +65 -0
  92. data/lib/temporalio/client/workflow_update_wait_stage.rb +17 -0
  93. data/lib/temporalio/client.rb +625 -0
  94. data/lib/temporalio/common_enums.rb +55 -0
  95. data/lib/temporalio/contrib/open_telemetry.rb +469 -0
  96. data/lib/temporalio/converters/data_converter.rb +99 -0
  97. data/lib/temporalio/converters/failure_converter.rb +205 -0
  98. data/lib/temporalio/converters/payload_codec.rb +26 -0
  99. data/lib/temporalio/converters/payload_converter/binary_null.rb +34 -0
  100. data/lib/temporalio/converters/payload_converter/binary_plain.rb +35 -0
  101. data/lib/temporalio/converters/payload_converter/binary_protobuf.rb +42 -0
  102. data/lib/temporalio/converters/payload_converter/composite.rb +66 -0
  103. data/lib/temporalio/converters/payload_converter/encoding.rb +35 -0
  104. data/lib/temporalio/converters/payload_converter/json_plain.rb +44 -0
  105. data/lib/temporalio/converters/payload_converter/json_protobuf.rb +41 -0
  106. data/lib/temporalio/converters/payload_converter.rb +71 -0
  107. data/lib/temporalio/converters/raw_value.rb +20 -0
  108. data/lib/temporalio/converters.rb +9 -0
  109. data/lib/temporalio/error/failure.rb +237 -0
  110. data/lib/temporalio/error.rb +156 -0
  111. data/lib/temporalio/internal/bridge/3.2/temporalio_bridge.bundle +0 -0
  112. data/lib/temporalio/internal/bridge/3.3/temporalio_bridge.bundle +0 -0
  113. data/lib/temporalio/internal/bridge/3.4/temporalio_bridge.bundle +0 -0
  114. data/lib/temporalio/internal/bridge/api/activity_result/activity_result.rb +34 -0
  115. data/lib/temporalio/internal/bridge/api/activity_task/activity_task.rb +32 -0
  116. data/lib/temporalio/internal/bridge/api/child_workflow/child_workflow.rb +33 -0
  117. data/lib/temporalio/internal/bridge/api/common/common.rb +27 -0
  118. data/lib/temporalio/internal/bridge/api/core_interface.rb +40 -0
  119. data/lib/temporalio/internal/bridge/api/external_data/external_data.rb +27 -0
  120. data/lib/temporalio/internal/bridge/api/nexus/nexus.rb +34 -0
  121. data/lib/temporalio/internal/bridge/api/workflow_activation/workflow_activation.rb +56 -0
  122. data/lib/temporalio/internal/bridge/api/workflow_commands/workflow_commands.rb +58 -0
  123. data/lib/temporalio/internal/bridge/api/workflow_completion/workflow_completion.rb +31 -0
  124. data/lib/temporalio/internal/bridge/api.rb +3 -0
  125. data/lib/temporalio/internal/bridge/client.rb +95 -0
  126. data/lib/temporalio/internal/bridge/runtime.rb +56 -0
  127. data/lib/temporalio/internal/bridge/testing.rb +69 -0
  128. data/lib/temporalio/internal/bridge/worker.rb +109 -0
  129. data/lib/temporalio/internal/bridge.rb +36 -0
  130. data/lib/temporalio/internal/client/implementation.rb +926 -0
  131. data/lib/temporalio/internal/metric.rb +122 -0
  132. data/lib/temporalio/internal/proto_utils.rb +165 -0
  133. data/lib/temporalio/internal/worker/activity_worker.rb +448 -0
  134. data/lib/temporalio/internal/worker/multi_runner.rb +213 -0
  135. data/lib/temporalio/internal/worker/workflow_instance/child_workflow_handle.rb +54 -0
  136. data/lib/temporalio/internal/worker/workflow_instance/context.rb +391 -0
  137. data/lib/temporalio/internal/worker/workflow_instance/details.rb +49 -0
  138. data/lib/temporalio/internal/worker/workflow_instance/external_workflow_handle.rb +32 -0
  139. data/lib/temporalio/internal/worker/workflow_instance/externally_immutable_hash.rb +22 -0
  140. data/lib/temporalio/internal/worker/workflow_instance/handler_execution.rb +25 -0
  141. data/lib/temporalio/internal/worker/workflow_instance/handler_hash.rb +41 -0
  142. data/lib/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rb +97 -0
  143. data/lib/temporalio/internal/worker/workflow_instance/inbound_implementation.rb +62 -0
  144. data/lib/temporalio/internal/worker/workflow_instance/outbound_implementation.rb +404 -0
  145. data/lib/temporalio/internal/worker/workflow_instance/replay_safe_logger.rb +37 -0
  146. data/lib/temporalio/internal/worker/workflow_instance/replay_safe_metric.rb +40 -0
  147. data/lib/temporalio/internal/worker/workflow_instance/scheduler.rb +183 -0
  148. data/lib/temporalio/internal/worker/workflow_instance.rb +800 -0
  149. data/lib/temporalio/internal/worker/workflow_worker.rb +249 -0
  150. data/lib/temporalio/internal.rb +7 -0
  151. data/lib/temporalio/metric.rb +109 -0
  152. data/lib/temporalio/priority.rb +59 -0
  153. data/lib/temporalio/retry_policy.rb +74 -0
  154. data/lib/temporalio/runtime/metric_buffer.rb +94 -0
  155. data/lib/temporalio/runtime.rb +352 -0
  156. data/lib/temporalio/scoped_logger.rb +96 -0
  157. data/lib/temporalio/search_attributes.rb +356 -0
  158. data/lib/temporalio/testing/activity_environment.rb +175 -0
  159. data/lib/temporalio/testing/workflow_environment.rb +406 -0
  160. data/lib/temporalio/testing.rb +10 -0
  161. data/lib/temporalio/version.rb +5 -0
  162. data/lib/temporalio/versioning_override.rb +55 -0
  163. data/lib/temporalio/worker/activity_executor/fiber.rb +49 -0
  164. data/lib/temporalio/worker/activity_executor/thread_pool.rb +46 -0
  165. data/lib/temporalio/worker/activity_executor.rb +55 -0
  166. data/lib/temporalio/worker/deployment_options.rb +45 -0
  167. data/lib/temporalio/worker/interceptor.rb +367 -0
  168. data/lib/temporalio/worker/poller_behavior.rb +61 -0
  169. data/lib/temporalio/worker/thread_pool.rb +237 -0
  170. data/lib/temporalio/worker/tuner.rb +189 -0
  171. data/lib/temporalio/worker/workflow_executor/thread_pool.rb +236 -0
  172. data/lib/temporalio/worker/workflow_executor.rb +26 -0
  173. data/lib/temporalio/worker/workflow_replayer.rb +349 -0
  174. data/lib/temporalio/worker.rb +633 -0
  175. data/lib/temporalio/worker_deployment_version.rb +67 -0
  176. data/lib/temporalio/workflow/activity_cancellation_type.rb +20 -0
  177. data/lib/temporalio/workflow/child_workflow_cancellation_type.rb +21 -0
  178. data/lib/temporalio/workflow/child_workflow_handle.rb +43 -0
  179. data/lib/temporalio/workflow/definition.rb +680 -0
  180. data/lib/temporalio/workflow/external_workflow_handle.rb +41 -0
  181. data/lib/temporalio/workflow/future.rb +151 -0
  182. data/lib/temporalio/workflow/handler_unfinished_policy.rb +13 -0
  183. data/lib/temporalio/workflow/info.rb +107 -0
  184. data/lib/temporalio/workflow/parent_close_policy.rb +19 -0
  185. data/lib/temporalio/workflow/update_info.rb +20 -0
  186. data/lib/temporalio/workflow.rb +594 -0
  187. data/lib/temporalio/workflow_history.rb +47 -0
  188. data/lib/temporalio.rb +12 -0
  189. data/temporalio.gemspec +31 -0
  190. metadata +267 -0
@@ -0,0 +1,800 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'temporalio'
5
+ require 'temporalio/activity/definition'
6
+ require 'temporalio/api'
7
+ require 'temporalio/converters/payload_converter'
8
+ require 'temporalio/converters/raw_value'
9
+ require 'temporalio/error'
10
+ require 'temporalio/internal/bridge/api'
11
+ require 'temporalio/internal/proto_utils'
12
+ require 'temporalio/internal/worker/workflow_instance/child_workflow_handle'
13
+ require 'temporalio/internal/worker/workflow_instance/context'
14
+ require 'temporalio/internal/worker/workflow_instance/details'
15
+ require 'temporalio/internal/worker/workflow_instance/externally_immutable_hash'
16
+ require 'temporalio/internal/worker/workflow_instance/handler_execution'
17
+ require 'temporalio/internal/worker/workflow_instance/handler_hash'
18
+ require 'temporalio/internal/worker/workflow_instance/illegal_call_tracer'
19
+ require 'temporalio/internal/worker/workflow_instance/inbound_implementation'
20
+ require 'temporalio/internal/worker/workflow_instance/outbound_implementation'
21
+ require 'temporalio/internal/worker/workflow_instance/replay_safe_logger'
22
+ require 'temporalio/internal/worker/workflow_instance/replay_safe_metric'
23
+ require 'temporalio/internal/worker/workflow_instance/scheduler'
24
+ require 'temporalio/retry_policy'
25
+ require 'temporalio/scoped_logger'
26
+ require 'temporalio/worker/interceptor'
27
+ require 'temporalio/worker_deployment_version'
28
+ require 'temporalio/workflow/info'
29
+ require 'temporalio/workflow/update_info'
30
+ require 'timeout'
31
+
32
+ module Temporalio
33
+ module Internal
34
+ module Worker
35
+ # Instance of a user workflow. This is the instance with all state needed to run the workflow and is expected to
36
+ # be cached by the worker for sticky execution.
37
+ class WorkflowInstance
38
+ def self.new_completion_with_failure(run_id:, error:, failure_converter:, payload_converter:)
39
+ Bridge::Api::WorkflowCompletion::WorkflowActivationCompletion.new(
40
+ run_id: run_id,
41
+ failed: Bridge::Api::WorkflowCompletion::Failure.new(
42
+ failure: begin
43
+ failure_converter.to_failure(error, payload_converter)
44
+ rescue Exception => e # rubocop:disable Lint/RescueException
45
+ Api::Failure::V1::Failure.new(
46
+ message: "Failed converting error to failure: #{e.message}, " \
47
+ "original error message: #{error.message}",
48
+ application_failure_info: Api::Failure::V1::ApplicationFailureInfo.new
49
+ )
50
+ end
51
+ )
52
+ )
53
+ end
54
+
55
+ attr_reader :context, :logger, :info, :scheduler, :disable_eager_activity_execution, :pending_activities,
56
+ :pending_timers, :pending_child_workflow_starts, :pending_child_workflows,
57
+ :pending_external_signals, :pending_external_cancels, :in_progress_handlers, :payload_converter,
58
+ :failure_converter, :cancellation, :continue_as_new_suggested, :current_deployment_version,
59
+ :current_history_length, :current_history_size, :replaying, :random,
60
+ :signal_handlers, :query_handlers, :update_handlers, :context_frozen, :assert_valid_local_activity
61
+ attr_accessor :io_enabled, :current_details
62
+
63
+ def initialize(details)
64
+ # Initialize general state
65
+ @context = Context.new(self)
66
+ if details.illegal_calls && !details.illegal_calls.empty?
67
+ @tracer = IllegalCallTracer.new(details.illegal_calls)
68
+ end
69
+ @logger = ReplaySafeLogger.new(logger: details.logger, instance: self)
70
+ @logger.scoped_values_getter = proc { scoped_logger_info }
71
+ @runtime_metric_meter = details.metric_meter
72
+ @io_enabled = details.unsafe_workflow_io_enabled
73
+ @scheduler = Scheduler.new(self)
74
+ @payload_converter = details.payload_converter
75
+ @failure_converter = details.failure_converter
76
+ @disable_eager_activity_execution = details.disable_eager_activity_execution
77
+ @pending_activities = {} # Keyed by sequence, value is fiber to resume with proto result
78
+ @pending_timers = {} # Keyed by sequence, value is fiber to resume with proto result
79
+ @pending_child_workflow_starts = {} # Keyed by sequence, value is fiber to resume with proto result
80
+ @pending_child_workflows = {} # Keyed by sequence, value is ChildWorkflowHandle to resolve with proto result
81
+ @pending_external_signals = {} # Keyed by sequence, value is fiber to resume with proto result
82
+ @pending_external_cancels = {} # Keyed by sequence, value is fiber to resume with proto result
83
+ @buffered_signals = {} # Keyed by signal name, value is array of signal jobs
84
+ # TODO(cretz): Should these be sets instead? Both should be fairly low counts.
85
+ @in_progress_handlers = [] # Value is HandlerExecution
86
+ @patches_notified = []
87
+ @definition = details.definition
88
+ @interceptors = details.interceptors
89
+ @cancellation, @cancellation_proc = Cancellation.new
90
+ @continue_as_new_suggested = false
91
+ @current_history_length = 0
92
+ @current_history_size = 0
93
+ @replaying = false
94
+ @workflow_failure_exception_types = details.workflow_failure_exception_types
95
+ @signal_handlers = HandlerHash.new(
96
+ details.definition.signals,
97
+ Workflow::Definition::Signal
98
+ ) do |defn|
99
+ # New definition, drain buffer. If it's dynamic (i.e. no name) drain them all.
100
+ to_drain = if defn.name.nil?
101
+ all_signals = @buffered_signals.values.flatten
102
+ @buffered_signals.clear
103
+ all_signals
104
+ else
105
+ @buffered_signals.delete(defn.name)
106
+ end
107
+ to_drain&.each { |job| apply_signal(job) }
108
+ end
109
+ @query_handlers = HandlerHash.new(details.definition.queries, Workflow::Definition::Query)
110
+ @update_handlers = HandlerHash.new(details.definition.updates, Workflow::Definition::Update)
111
+ @definition_options = Workflow::DefinitionOptions.new(
112
+ failure_exception_types: details.definition.failure_exception_types,
113
+ versioning_behavior: details.definition.versioning_behavior
114
+ )
115
+
116
+ @assert_valid_local_activity = details.assert_valid_local_activity
117
+
118
+ # Create all things needed from initial job
119
+ @init_job = details.initial_activation.jobs.find { |j| !j.initialize_workflow.nil? }&.initialize_workflow
120
+ raise 'Missing init job from first activation' unless @init_job
121
+
122
+ illegal_call_tracing_disabled do
123
+ @info = Workflow::Info.new(
124
+ attempt: @init_job.attempt,
125
+ continued_run_id: ProtoUtils.string_or(@init_job.continued_from_execution_run_id),
126
+ cron_schedule: ProtoUtils.string_or(@init_job.cron_schedule),
127
+ execution_timeout: ProtoUtils.duration_to_seconds(@init_job.workflow_execution_timeout),
128
+ headers: ProtoUtils.headers_from_proto_map(@init_job.headers, @payload_converter) || {},
129
+ last_failure: if @init_job.continued_failure
130
+ @failure_converter.from_failure(@init_job.continued_failure, @payload_converter)
131
+ end,
132
+ last_result: if @init_job.last_completion_result
133
+ @payload_converter.from_payloads(@init_job.last_completion_result).first
134
+ end,
135
+ namespace: details.namespace,
136
+ parent: if @init_job.parent_workflow_info
137
+ Workflow::Info::ParentInfo.new(
138
+ namespace: @init_job.parent_workflow_info.namespace,
139
+ run_id: @init_job.parent_workflow_info.run_id,
140
+ workflow_id: @init_job.parent_workflow_info.workflow_id
141
+ )
142
+ end,
143
+ priority: Priority._from_proto(@init_job.priority),
144
+ retry_policy: (RetryPolicy._from_proto(@init_job.retry_policy) if @init_job.retry_policy),
145
+ root: if @init_job.root_workflow
146
+ Workflow::Info::RootInfo.new(
147
+ run_id: @init_job.root_workflow.run_id,
148
+ workflow_id: @init_job.root_workflow.workflow_id
149
+ )
150
+ end,
151
+ run_id: details.initial_activation.run_id,
152
+ run_timeout: ProtoUtils.duration_to_seconds(@init_job.workflow_run_timeout),
153
+ start_time: ProtoUtils.timestamp_to_time(@init_job.start_time) || raise,
154
+ task_queue: details.task_queue,
155
+ task_timeout: ProtoUtils.duration_to_seconds(@init_job.workflow_task_timeout) || raise,
156
+ workflow_id: @init_job.workflow_id,
157
+ workflow_type: @init_job.workflow_type
158
+ ).freeze
159
+
160
+ @random = Random.new(@init_job.randomness_seed)
161
+ end
162
+ end
163
+
164
+ def activate(activation)
165
+ # Run inside of scheduler
166
+ run_in_scheduler { activate_internal(activation) }
167
+ end
168
+
169
+ def add_command(command)
170
+ raise Workflow::InvalidWorkflowStateError, 'Cannot add commands in this context' if @context_frozen
171
+
172
+ @commands << command
173
+ end
174
+
175
+ def instance
176
+ @instance or raise 'Instance accessed before created'
177
+ end
178
+
179
+ def search_attributes
180
+ # Lazy on first access
181
+ @search_attributes ||= SearchAttributes._from_proto(
182
+ @init_job.search_attributes, disable_mutations: true, never_nil: true
183
+ ) || raise
184
+ end
185
+
186
+ def memo
187
+ # Lazy on first access
188
+ @memo ||= ExternallyImmutableHash.new(ProtoUtils.memo_from_proto(@init_job.memo, payload_converter) || {})
189
+ end
190
+
191
+ def now
192
+ # Create each time
193
+ ProtoUtils.timestamp_to_time(@now_timestamp) or raise 'Time unexpectedly not present'
194
+ end
195
+
196
+ def illegal_call_tracing_disabled(&)
197
+ @tracer.disable(&)
198
+ end
199
+
200
+ def patch(patch_id:, deprecated:)
201
+ # Use memoized result if present. If this is being deprecated, we can still use memoized result and skip the
202
+ # command.
203
+ patch_id = patch_id.to_s
204
+ @patches_memoized ||= {}
205
+ @patches_memoized.fetch(patch_id) do
206
+ patched = !replaying || @patches_notified.include?(patch_id)
207
+ @patches_memoized[patch_id] = patched
208
+ if patched
209
+ add_command(
210
+ Bridge::Api::WorkflowCommands::WorkflowCommand.new(
211
+ set_patch_marker: Bridge::Api::WorkflowCommands::SetPatchMarker.new(patch_id:, deprecated:)
212
+ )
213
+ )
214
+ end
215
+ patched
216
+ end
217
+ end
218
+
219
+ def metric_meter
220
+ @metric_meter ||= ReplaySafeMetric::Meter.new(
221
+ @runtime_metric_meter.with_additional_attributes(
222
+ {
223
+ namespace: info.namespace,
224
+ task_queue: info.task_queue,
225
+ workflow_type: info.workflow_type
226
+ }
227
+ )
228
+ )
229
+ end
230
+
231
+ private
232
+
233
+ def run_in_scheduler(&)
234
+ Fiber.set_scheduler(@scheduler)
235
+ if @tracer
236
+ @tracer.enable(&)
237
+ else
238
+ yield
239
+ end
240
+ ensure
241
+ Fiber.set_scheduler(nil)
242
+ end
243
+
244
+ def activate_internal(activation)
245
+ # Reset some activation state
246
+ @commands = []
247
+ @current_activation_error = nil
248
+ @continue_as_new_suggested = activation.continue_as_new_suggested
249
+ @current_deployment_version = WorkerDeploymentVersion._from_bridge(
250
+ activation.deployment_version_for_current_task
251
+ )
252
+ @current_history_length = activation.history_length
253
+ @current_history_size = activation.history_size_bytes
254
+ @replaying = activation.is_replaying
255
+ @now_timestamp = activation.timestamp
256
+
257
+ # Apply jobs and run event loop
258
+ begin
259
+ # Create instance if it doesn't already exist
260
+ @instance ||= with_context_frozen { create_instance }
261
+
262
+ # Apply jobs
263
+ activation.jobs.each { |job| apply(job) }
264
+
265
+ # Schedule primary 'execute' if not already running (i.e. this is
266
+ # the first activation)
267
+ @primary_fiber ||= schedule(top_level: true) { run_workflow }
268
+
269
+ # Run the event loop
270
+ @scheduler.run_until_all_yielded
271
+ rescue Exception => e # rubocop:disable Lint/RescueException
272
+ on_top_level_exception(e)
273
+ end
274
+
275
+ # If we are not replaying and workflow is complete but not a
276
+ # failure (i.e. success, continue as new, or cancel), we warn for
277
+ # any unfinished handlers.
278
+ if !@replaying && @commands.any? do |c|
279
+ !c.complete_workflow_execution.nil? ||
280
+ !c.continue_as_new_workflow_execution.nil? ||
281
+ !c.cancel_workflow_execution.nil?
282
+ end
283
+ warn_on_any_unfinished_handlers
284
+ end
285
+
286
+ # Return success or failure
287
+ if @current_activation_error
288
+ @logger.replay_safety_disabled do
289
+ @logger.warn('Failed activation')
290
+ @logger.warn(@current_activation_error)
291
+ end
292
+ WorkflowInstance.new_completion_with_failure(
293
+ run_id: activation.run_id,
294
+ error: @current_activation_error,
295
+ failure_converter: @failure_converter,
296
+ payload_converter: @payload_converter
297
+ )
298
+ else
299
+ Bridge::Api::WorkflowCompletion::WorkflowActivationCompletion.new(
300
+ run_id: activation.run_id,
301
+ successful: Bridge::Api::WorkflowCompletion::Success.new(
302
+ commands: @commands, versioning_behavior: @definition_options.versioning_behavior
303
+ )
304
+ )
305
+ end
306
+ ensure
307
+ @commands = nil
308
+ @current_activation_error = nil
309
+ end
310
+
311
+ def create_instance
312
+ # Convert workflow arguments
313
+ @workflow_arguments = convert_args(payload_array: @init_job.arguments,
314
+ method_name: :execute,
315
+ raw_args: @definition.raw_args)
316
+
317
+ # Initialize interceptors
318
+ @inbound = @interceptors.reverse_each.reduce(InboundImplementation.new(self)) do |acc, int|
319
+ int.intercept_workflow(acc)
320
+ end
321
+ @inbound.init(OutboundImplementation.new(self))
322
+
323
+ # Create the user instance
324
+ instance = if @definition.init
325
+ @definition.workflow_class.new(*@workflow_arguments)
326
+ else
327
+ @definition.workflow_class.new
328
+ end
329
+
330
+ # Run Dynamic config getter
331
+ if @definition.dynamic_options_method
332
+ dynamic_options = instance.send(@definition.dynamic_options_method)
333
+ if dynamic_options&.versioning_behavior
334
+ @definition_options.versioning_behavior = dynamic_options.versioning_behavior
335
+ end
336
+ if dynamic_options&.failure_exception_types
337
+ @definition_options.failure_exception_types = dynamic_options.failure_exception_types
338
+ end
339
+ end
340
+
341
+ instance
342
+ end
343
+
344
+ def apply(job)
345
+ case job.variant
346
+ when :initialize_workflow
347
+ # Ignore
348
+ when :fire_timer
349
+ pending_timers.delete(job.fire_timer.seq)&.resume
350
+ when :update_random_seed
351
+ @random = illegal_call_tracing_disabled { Random.new(job.update_random_seed.randomness_seed) }
352
+ when :query_workflow
353
+ apply_query(job.query_workflow)
354
+ when :cancel_workflow
355
+ # TODO(cretz): Use the details somehow?
356
+ @cancellation_proc.call(reason: 'Workflow canceled')
357
+ when :signal_workflow
358
+ apply_signal(job.signal_workflow)
359
+ when :resolve_activity
360
+ pending_activities.delete(job.resolve_activity.seq)&.resume(job.resolve_activity.result)
361
+ when :notify_has_patch
362
+ @patches_notified << job.notify_has_patch.patch_id
363
+ when :resolve_child_workflow_execution_start
364
+ pending_child_workflow_starts.delete(job.resolve_child_workflow_execution_start.seq)&.resume(
365
+ job.resolve_child_workflow_execution_start
366
+ )
367
+ when :resolve_child_workflow_execution
368
+ pending_child_workflows.delete(job.resolve_child_workflow_execution.seq)&._resolve(
369
+ job.resolve_child_workflow_execution.result
370
+ )
371
+ when :resolve_signal_external_workflow
372
+ pending_external_signals.delete(job.resolve_signal_external_workflow.seq)&.resume(
373
+ job.resolve_signal_external_workflow
374
+ )
375
+ when :resolve_request_cancel_external_workflow
376
+ pending_external_cancels.delete(job.resolve_request_cancel_external_workflow.seq)&.resume(
377
+ job.resolve_request_cancel_external_workflow
378
+ )
379
+ when :do_update
380
+ apply_update(job.do_update)
381
+ else
382
+ raise "Unrecognized activation job variant: #{job.variant}"
383
+ end
384
+ end
385
+
386
+ def apply_signal(job)
387
+ # Get signal definition, falling back to dynamic if not present and not reserved
388
+ defn = signal_handlers[job.signal_name]
389
+ defn = signal_handlers[nil] if !defn && !Internal::ProtoUtils.reserved_name?(job.signal_name)
390
+
391
+ handler_exec =
392
+ if defn
393
+ HandlerExecution.new(name: job.signal_name, update_id: nil, unfinished_policy: defn.unfinished_policy)
394
+ end
395
+ # Process as a top level handler so that errors are treated as if in primary workflow method
396
+ schedule(top_level: true, handler_exec:) do
397
+ # Send to interceptor if there is a definition, buffer otherwise
398
+ if defn
399
+ @inbound.handle_signal(
400
+ Temporalio::Worker::Interceptor::Workflow::HandleSignalInput.new(
401
+ signal: job.signal_name,
402
+ args: begin
403
+ convert_handler_args(payload_array: job.input, defn:)
404
+ rescue StandardError => e
405
+ # Signals argument conversion failure must not fail task
406
+ @logger.error("Failed converting signal input arguments for #{job.signal_name}, dropping signal")
407
+ @logger.error(e)
408
+ next
409
+ end,
410
+ definition: defn,
411
+ headers: ProtoUtils.headers_from_proto_map(job.headers, @payload_converter) || {}
412
+ )
413
+ )
414
+ else
415
+ buffered = @buffered_signals[job.signal_name]
416
+ buffered = @buffered_signals[job.signal_name] = [] if buffered.nil?
417
+ buffered << job
418
+ end
419
+ end
420
+ end
421
+
422
+ def apply_query(job)
423
+ schedule do
424
+ # If it's a built-in, run it without interceptors, otherwise do normal behavior
425
+ result = if job.query_type == '__stack_trace'
426
+ # Use raw value built from default converter because we don't want to use user-conversion
427
+ Converters::RawValue.new(Converters::PayloadConverter.default.to_payload(scheduler.stack_trace))
428
+ elsif job.query_type == '__temporal_workflow_metadata'
429
+ # Use raw value built from default converter because we don't want to use user-conversion
430
+ Converters::RawValue.new(Converters::PayloadConverter.default.to_payload(workflow_metadata))
431
+ else
432
+ # Get query definition, falling back to dynamic if not present and not reserved
433
+ defn = query_handlers[job.query_type]
434
+ defn = query_handlers[nil] if !defn && !Internal::ProtoUtils.reserved_name?(job.query_type)
435
+
436
+ unless defn
437
+ raise "Query handler for #{job.query_type} expected but not found, " \
438
+ "known queries: [#{query_handlers.keys.compact.sort.join(', ')}]"
439
+ end
440
+
441
+ with_context_frozen do
442
+ @inbound.handle_query(
443
+ Temporalio::Worker::Interceptor::Workflow::HandleQueryInput.new(
444
+ id: job.query_id,
445
+ query: job.query_type,
446
+ args: begin
447
+ convert_handler_args(payload_array: job.arguments, defn:)
448
+ rescue StandardError => e
449
+ raise "Failed converting query input arguments: #{e}"
450
+ end,
451
+ definition: defn,
452
+ headers: ProtoUtils.headers_from_proto_map(job.headers, @payload_converter) || {}
453
+ )
454
+ )
455
+ end
456
+ end
457
+
458
+ add_command(
459
+ Bridge::Api::WorkflowCommands::WorkflowCommand.new(
460
+ respond_to_query: Bridge::Api::WorkflowCommands::QueryResult.new(
461
+ query_id: job.query_id,
462
+ succeeded: Bridge::Api::WorkflowCommands::QuerySuccess.new(
463
+ response: @payload_converter.to_payload(result)
464
+ )
465
+ )
466
+ )
467
+ )
468
+ rescue Exception => e # rubocop:disable Lint/RescueException
469
+ add_command(
470
+ Bridge::Api::WorkflowCommands::WorkflowCommand.new(
471
+ respond_to_query: Bridge::Api::WorkflowCommands::QueryResult.new(
472
+ query_id: job.query_id,
473
+ failed: @failure_converter.to_failure(e, @payload_converter)
474
+ )
475
+ )
476
+ )
477
+ end
478
+ end
479
+
480
+ def apply_update(job)
481
+ # Get update definition, falling back to dynamic if not present and not reserved
482
+ defn = update_handlers[job.name]
483
+ defn = update_handlers[nil] if !defn && !Internal::ProtoUtils.reserved_name?(job.name)
484
+
485
+ handler_exec =
486
+ (HandlerExecution.new(name: job.name, update_id: job.id, unfinished_policy: defn.unfinished_policy) if defn)
487
+ schedule(handler_exec:) do
488
+ # Until this is accepted, all errors are rejections
489
+ accepted = false
490
+
491
+ # Set update info
492
+ Fiber[:__temporal_update_info] = Workflow::UpdateInfo.new(id: job.id, name: job.name).freeze
493
+
494
+ # Reject if not present
495
+ unless defn
496
+ raise "Update handler for #{job.name} expected but not found, " \
497
+ "known updates: [#{update_handlers.keys.compact.sort.join(', ')}]"
498
+ end
499
+
500
+ # To match other SDKs, we are only calling the validation interceptor if there is a validator. Also to match
501
+ # other SDKs, we are re-converting the args between validate and update to disallow user mutation in
502
+ # validator/interceptor.
503
+ if job.run_validator && defn.validator_to_invoke
504
+ with_context_frozen do
505
+ @inbound.validate_update(
506
+ Temporalio::Worker::Interceptor::Workflow::HandleUpdateInput.new(
507
+ id: job.id,
508
+ update: job.name,
509
+ args: begin
510
+ convert_handler_args(payload_array: job.input, defn:)
511
+ rescue StandardError => e
512
+ raise "Failed converting update input arguments: #{e}"
513
+ end,
514
+ definition: defn,
515
+ headers: ProtoUtils.headers_from_proto_map(job.headers, @payload_converter) || {}
516
+ )
517
+ )
518
+ end
519
+ end
520
+
521
+ # We build the input before marking accepted so the exception can reject instead of fail task
522
+ input = Temporalio::Worker::Interceptor::Workflow::HandleUpdateInput.new(
523
+ id: job.id,
524
+ update: job.name,
525
+ args: begin
526
+ convert_handler_args(payload_array: job.input, defn:)
527
+ rescue StandardError => e
528
+ raise "Failed converting update input arguments: #{e}"
529
+ end,
530
+ definition: defn,
531
+ headers: ProtoUtils.headers_from_proto_map(job.headers, @payload_converter) || {}
532
+ )
533
+
534
+ # Accept
535
+ add_command(
536
+ Bridge::Api::WorkflowCommands::WorkflowCommand.new(
537
+ update_response: Bridge::Api::WorkflowCommands::UpdateResponse.new(
538
+ protocol_instance_id: job.protocol_instance_id,
539
+ accepted: Google::Protobuf::Empty.new
540
+ )
541
+ )
542
+ )
543
+ accepted = true
544
+
545
+ # Issue update
546
+ result = @inbound.handle_update(input)
547
+
548
+ add_command(
549
+ Bridge::Api::WorkflowCommands::WorkflowCommand.new(
550
+ update_response: Bridge::Api::WorkflowCommands::UpdateResponse.new(
551
+ protocol_instance_id: job.protocol_instance_id,
552
+ completed: @payload_converter.to_payload(result)
553
+ )
554
+ )
555
+ )
556
+ rescue Exception => e # rubocop:disable Lint/RescueException
557
+ # Re-raise to cause task failure if this is accepted but this is not a failure exception
558
+ raise if accepted && !failure_exception?(e)
559
+
560
+ # Reject
561
+ add_command(
562
+ Bridge::Api::WorkflowCommands::WorkflowCommand.new(
563
+ update_response: Bridge::Api::WorkflowCommands::UpdateResponse.new(
564
+ protocol_instance_id: job.protocol_instance_id,
565
+ rejected: @failure_converter.to_failure(e, @payload_converter)
566
+ )
567
+ )
568
+ )
569
+ end
570
+ end
571
+
572
+ def run_workflow
573
+ result = @inbound.execute(
574
+ Temporalio::Worker::Interceptor::Workflow::ExecuteInput.new(
575
+ args: @workflow_arguments,
576
+ headers: @info.headers
577
+ )
578
+ )
579
+ add_command(
580
+ Bridge::Api::WorkflowCommands::WorkflowCommand.new(
581
+ complete_workflow_execution: Bridge::Api::WorkflowCommands::CompleteWorkflowExecution.new(
582
+ result: @payload_converter.to_payload(result)
583
+ )
584
+ )
585
+ )
586
+ end
587
+
588
+ def schedule(
589
+ top_level: false,
590
+ handler_exec: nil,
591
+ &
592
+ )
593
+ in_progress_handlers << handler_exec if handler_exec
594
+ Fiber.schedule do
595
+ yield
596
+ rescue Exception => e # rubocop:disable Lint/RescueException
597
+ if top_level
598
+ on_top_level_exception(e)
599
+ else
600
+ @current_activation_error ||= e
601
+ end
602
+ ensure
603
+ in_progress_handlers.delete(handler_exec) if handler_exec
604
+ end
605
+ end
606
+
607
+ def on_top_level_exception(err)
608
+ if err.is_a?(Workflow::ContinueAsNewError)
609
+ @logger.debug('Workflow requested continue as new')
610
+ add_command(
611
+ Bridge::Api::WorkflowCommands::WorkflowCommand.new(
612
+ continue_as_new_workflow_execution: Bridge::Api::WorkflowCommands::ContinueAsNewWorkflowExecution.new(
613
+ workflow_type: if err.workflow
614
+ Workflow::Definition._workflow_type_from_workflow_parameter(err.workflow)
615
+ end,
616
+ task_queue: err.task_queue,
617
+ arguments: ProtoUtils.convert_to_payload_array(payload_converter, err.args),
618
+ workflow_run_timeout: ProtoUtils.seconds_to_duration(err.run_timeout),
619
+ workflow_task_timeout: ProtoUtils.seconds_to_duration(err.task_timeout),
620
+ memo: ProtoUtils.memo_to_proto_hash(err.memo, payload_converter),
621
+ headers: ProtoUtils.headers_to_proto_hash(err.headers, payload_converter),
622
+ search_attributes: err.search_attributes&._to_proto,
623
+ retry_policy: err.retry_policy&._to_proto
624
+ )
625
+ )
626
+ )
627
+ elsif @cancellation.canceled? && Error.canceled?(err)
628
+ # If cancel was ever requested and this is a cancellation or an activity/child cancellation, we add a
629
+ # cancel command. Technically this means that a swallowed cancel followed by, say, an activity cancel
630
+ # later on will show the workflow as cancelled. But this is a Temporal limitation in that cancellation is
631
+ # a state not an event.
632
+ @logger.debug('Workflow requested to cancel and properly raised cancel')
633
+ @logger.debug(err)
634
+ add_command(
635
+ Bridge::Api::WorkflowCommands::WorkflowCommand.new(
636
+ cancel_workflow_execution: Bridge::Api::WorkflowCommands::CancelWorkflowExecution.new
637
+ )
638
+ )
639
+ elsif failure_exception?(err)
640
+ @logger.debug('Workflow raised failure')
641
+ @logger.debug(err)
642
+ add_command(
643
+ Bridge::Api::WorkflowCommands::WorkflowCommand.new(
644
+ fail_workflow_execution: Bridge::Api::WorkflowCommands::FailWorkflowExecution.new(
645
+ failure: @failure_converter.to_failure(err, @payload_converter)
646
+ )
647
+ )
648
+ )
649
+ else
650
+ @current_activation_error ||= err
651
+ end
652
+ end
653
+
654
+ def failure_exception?(err)
655
+ err.is_a?(Error::Failure) || err.is_a?(Timeout::Error) ||
656
+ @workflow_failure_exception_types&.any? { |cls| err.is_a?(cls) } ||
657
+ @definition_options.failure_exception_types&.any? { |cls| err.is_a?(cls) }
658
+ end
659
+
660
+ def with_context_frozen(&)
661
+ @context_frozen = true
662
+ yield
663
+ ensure
664
+ @context_frozen = false
665
+ end
666
+
667
+ def convert_handler_args(payload_array:, defn:)
668
+ convert_args(
669
+ payload_array:,
670
+ method_name: defn.to_invoke.is_a?(Symbol) ? defn.to_invoke : nil,
671
+ raw_args: defn.raw_args,
672
+ ignore_first_param: defn.name.nil? # Dynamic
673
+ )
674
+ end
675
+
676
+ def convert_args(payload_array:, method_name:, raw_args:, ignore_first_param: false)
677
+ # Just in case it is not an array
678
+ payload_array = payload_array.to_ary
679
+
680
+ # We want to discard extra arguments if we can. If there is a method
681
+ # name, try to look it up. Then, assuming there's no :rest, trim args
682
+ # to the amount of :req or :opt there are.
683
+ if method_name && @definition.workflow_class.method_defined?(method_name)
684
+ count = 0
685
+ req_count = 0
686
+ @definition.workflow_class.instance_method(method_name).parameters.each do |(type, _)|
687
+ if type == :rest
688
+ count = nil
689
+ break
690
+ elsif %i[req opt].include?(type)
691
+ count += 1
692
+ req_count += 1 if type == :req
693
+ end
694
+ end
695
+ # Fail if too few required param values, trim off excess if too many. If count is nil, it has a splat.
696
+ if count
697
+ if ignore_first_param
698
+ count -= 1
699
+ req_count -= 1
700
+ end
701
+ if req_count > payload_array.size
702
+ # We have to fail here instead of let Ruby fail the invocation because some handlers, such as signals,
703
+ # want to log and ignore invalid arguments instead of fail and if we used Ruby failure, we can't
704
+ # differentiate between too-few-param caused by us or somewhere else by a user.
705
+ raise ArgumentError, "wrong number of required arguments for #{method_name} " \
706
+ "(given #{payload_array.size}, expected #{req_count})"
707
+ end
708
+ payload_array = payload_array.take(count)
709
+ end
710
+ end
711
+
712
+ # Convert
713
+ if raw_args
714
+ payload_array.map { |p| Converters::RawValue.new(p) }
715
+ else
716
+ ProtoUtils.convert_from_payload_array(@payload_converter, payload_array)
717
+ end
718
+ end
719
+
720
+ def workflow_metadata
721
+ Temporalio::Api::Sdk::V1::WorkflowMetadata.new(
722
+ definition: Temporalio::Api::Sdk::V1::WorkflowDefinition.new(
723
+ type: info.workflow_type,
724
+ query_definitions: query_handlers.values.map do |defn|
725
+ Temporalio::Api::Sdk::V1::WorkflowInteractionDefinition.new(
726
+ name: defn.name || '', description: defn.description || ''
727
+ )
728
+ end,
729
+ signal_definitions: signal_handlers.values.map do |defn|
730
+ Temporalio::Api::Sdk::V1::WorkflowInteractionDefinition.new(
731
+ name: defn.name || '', description: defn.description || ''
732
+ )
733
+ end,
734
+ update_definitions: update_handlers.values.map do |defn|
735
+ Temporalio::Api::Sdk::V1::WorkflowInteractionDefinition.new(
736
+ name: defn.name || '', description: defn.description || ''
737
+ )
738
+ end
739
+ ),
740
+ current_details: current_details || ''
741
+ )
742
+ end
743
+
744
+ def scoped_logger_info
745
+ @scoped_logger_info ||= {
746
+ attempt: info.attempt,
747
+ namespace: info.namespace,
748
+ run_id: info.run_id,
749
+ task_queue: info.task_queue,
750
+ workflow_id: info.workflow_id,
751
+ workflow_type: info.workflow_type
752
+ }
753
+ # Append update info if there is any
754
+ update_info = Fiber[:__temporal_update_info]
755
+ return @scoped_logger_info unless update_info
756
+
757
+ @scoped_logger_info.merge({ update_id: update_info.id, update_name: update_info.name })
758
+ end
759
+
760
+ def warn_on_any_unfinished_handlers
761
+ updates, signals = in_progress_handlers.select do |h|
762
+ h.unfinished_policy == Workflow::HandlerUnfinishedPolicy::WARN_AND_ABANDON
763
+ end.partition(&:update_id)
764
+
765
+ unless updates.empty?
766
+ updates_str = JSON.generate(updates.map { |u| { name: u.name, id: u.update_id } })
767
+ warn(
768
+ "[TMPRL1102] Workflow #{info.workflow_id} finished while update handlers are still running. This may " \
769
+ 'have interrupted work that the update handler was doing, and the client that sent the update will ' \
770
+ "receive a 'workflow execution already completed' RPCError instead of the update result. You can wait " \
771
+ 'for all update and signal handlers to complete by using ' \
772
+ '`Temporalio::Workflow.wait_condition { Temporalio::Workflow.handlers_finished? }`. ' \
773
+ 'Alternatively, if both you and the clients sending the update are okay with interrupting running ' \
774
+ 'handlers when the workflow finishes, and causing clients to receive errors, then you can disable this ' \
775
+ 'warning via the update handler definition: ' \
776
+ '`workflow_update unfinished_policy: Temporalio::Workflow::HandlerUnfinishedPolicy.ABANDON`. ' \
777
+ "The following updates were unfinished (and warnings were not disabled for their handler): #{updates_str}"
778
+ )
779
+ end
780
+
781
+ return if signals.empty?
782
+
783
+ signals_str = JSON.generate(signals.group_by(&:name)
784
+ .transform_values(&:size).sort_by { |_, v| -v }.map { |name, count| { name:, count: } })
785
+ warn(
786
+ "[TMPRL1102] Workflow #{info.workflow_id} finished while signal handlers are still running. This may " \
787
+ 'have interrupted work that the signal handler was doing. You can wait for all update and signal ' \
788
+ 'handlers to complete by using ' \
789
+ '`Temporalio::Workflow.wait_condition { Temporalio::Workflow.handlers_finished? }`. ' \
790
+ 'Alternatively, if both you and the clients sending the signal are okay with interrupting running ' \
791
+ 'handlers when the workflow finishes, then you can disable this warning via the signal handler ' \
792
+ 'definition: ' \
793
+ '`workflow_signal unfinished_policy: Temporalio::Workflow::HandlerUnfinishedPolicy.ABANDON`. ' \
794
+ "The following signals were unfinished (and warnings were not disabled for their handler): #{signals_str}"
795
+ )
796
+ end
797
+ end
798
+ end
799
+ end
800
+ end