temporalio 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +503 -395
  3. data/Gemfile +4 -0
  4. data/README.md +183 -10
  5. data/Rakefile +1 -1
  6. data/ext/Cargo.toml +1 -0
  7. data/lib/temporalio/activity/context.rb +13 -0
  8. data/lib/temporalio/activity/definition.rb +22 -5
  9. data/lib/temporalio/activity/info.rb +3 -0
  10. data/lib/temporalio/api/batch/v1/message.rb +6 -1
  11. data/lib/temporalio/api/command/v1/message.rb +1 -1
  12. data/lib/temporalio/api/common/v1/message.rb +2 -1
  13. data/lib/temporalio/api/deployment/v1/message.rb +38 -0
  14. data/lib/temporalio/api/enums/v1/batch_operation.rb +1 -1
  15. data/lib/temporalio/api/enums/v1/common.rb +1 -1
  16. data/lib/temporalio/api/enums/v1/deployment.rb +23 -0
  17. data/lib/temporalio/api/enums/v1/event_type.rb +1 -1
  18. data/lib/temporalio/api/enums/v1/failed_cause.rb +1 -1
  19. data/lib/temporalio/api/enums/v1/nexus.rb +21 -0
  20. data/lib/temporalio/api/enums/v1/reset.rb +1 -1
  21. data/lib/temporalio/api/enums/v1/workflow.rb +2 -1
  22. data/lib/temporalio/api/errordetails/v1/message.rb +3 -1
  23. data/lib/temporalio/api/failure/v1/message.rb +3 -1
  24. data/lib/temporalio/api/history/v1/message.rb +3 -1
  25. data/lib/temporalio/api/nexus/v1/message.rb +2 -1
  26. data/lib/temporalio/api/payload_visitor.rb +75 -7
  27. data/lib/temporalio/api/query/v1/message.rb +2 -1
  28. data/lib/temporalio/api/taskqueue/v1/message.rb +4 -1
  29. data/lib/temporalio/api/workflow/v1/message.rb +9 -1
  30. data/lib/temporalio/api/workflowservice/v1/request_response.rb +40 -11
  31. data/lib/temporalio/api/workflowservice/v1/service.rb +1 -1
  32. data/lib/temporalio/api.rb +1 -0
  33. data/lib/temporalio/client/connection/workflow_service.rb +238 -28
  34. data/lib/temporalio/client/interceptor.rb +39 -0
  35. data/lib/temporalio/client/schedule.rb +25 -1
  36. data/lib/temporalio/client/with_start_workflow_operation.rb +115 -0
  37. data/lib/temporalio/client/workflow_execution.rb +19 -0
  38. data/lib/temporalio/client/workflow_handle.rb +3 -3
  39. data/lib/temporalio/client.rb +125 -2
  40. data/lib/temporalio/contrib/open_telemetry.rb +470 -0
  41. data/lib/temporalio/error.rb +1 -0
  42. data/lib/temporalio/internal/bridge/api/activity_task/activity_task.rb +1 -1
  43. data/lib/temporalio/internal/bridge/api/common/common.rb +2 -1
  44. data/lib/temporalio/internal/bridge/api/workflow_activation/workflow_activation.rb +1 -1
  45. data/lib/temporalio/internal/bridge/api/workflow_commands/workflow_commands.rb +1 -1
  46. data/lib/temporalio/internal/bridge/api/workflow_completion/workflow_completion.rb +2 -1
  47. data/lib/temporalio/internal/bridge/runtime.rb +3 -0
  48. data/lib/temporalio/internal/bridge/testing.rb +3 -0
  49. data/lib/temporalio/internal/client/implementation.rb +232 -10
  50. data/lib/temporalio/internal/proto_utils.rb +34 -2
  51. data/lib/temporalio/internal/worker/activity_worker.rb +20 -8
  52. data/lib/temporalio/internal/worker/multi_runner.rb +2 -2
  53. data/lib/temporalio/internal/worker/workflow_instance/context.rb +57 -3
  54. data/lib/temporalio/internal/worker/workflow_instance/details.rb +4 -2
  55. data/lib/temporalio/internal/worker/workflow_instance/outbound_implementation.rb +11 -26
  56. data/lib/temporalio/internal/worker/workflow_instance/scheduler.rb +22 -2
  57. data/lib/temporalio/internal/worker/workflow_instance.rb +76 -32
  58. data/lib/temporalio/internal/worker/workflow_worker.rb +62 -19
  59. data/lib/temporalio/runtime/metric_buffer.rb +94 -0
  60. data/lib/temporalio/runtime.rb +48 -10
  61. data/lib/temporalio/search_attributes.rb +13 -0
  62. data/lib/temporalio/testing/activity_environment.rb +42 -14
  63. data/lib/temporalio/testing/workflow_environment.rb +26 -3
  64. data/lib/temporalio/version.rb +1 -1
  65. data/lib/temporalio/worker/interceptor.rb +3 -0
  66. data/lib/temporalio/worker/thread_pool.rb +5 -5
  67. data/lib/temporalio/worker/tuner.rb +38 -0
  68. data/lib/temporalio/worker/workflow_executor/thread_pool.rb +13 -8
  69. data/lib/temporalio/worker/workflow_executor.rb +1 -1
  70. data/lib/temporalio/worker/workflow_replayer.rb +350 -0
  71. data/lib/temporalio/worker.rb +58 -52
  72. data/lib/temporalio/workflow/definition.rb +40 -8
  73. data/lib/temporalio/workflow/future.rb +2 -2
  74. data/lib/temporalio/workflow/info.rb +22 -0
  75. data/lib/temporalio/workflow.rb +60 -8
  76. data/lib/temporalio/workflow_history.rb +26 -1
  77. data/temporalio.gemspec +2 -1
  78. metadata +26 -5
@@ -8,10 +8,13 @@ require 'temporalio/client/connection'
8
8
  require 'temporalio/client/interceptor'
9
9
  require 'temporalio/client/schedule'
10
10
  require 'temporalio/client/schedule_handle'
11
+ require 'temporalio/client/with_start_workflow_operation'
11
12
  require 'temporalio/client/workflow_execution'
12
13
  require 'temporalio/client/workflow_execution_count'
13
14
  require 'temporalio/client/workflow_handle'
14
15
  require 'temporalio/client/workflow_query_reject_condition'
16
+ require 'temporalio/client/workflow_update_handle'
17
+ require 'temporalio/client/workflow_update_wait_stage'
15
18
  require 'temporalio/common_enums'
16
19
  require 'temporalio/converters'
17
20
  require 'temporalio/error'
@@ -155,7 +158,7 @@ module Temporalio
155
158
  default_workflow_query_reject_condition:
156
159
  ).freeze
157
160
  # Initialize interceptors
158
- @impl = interceptors.reverse_each.reduce(Internal::Client::Implementation.new(self)) do |acc, int|
161
+ @impl = interceptors.reverse_each.reduce(Internal::Client::Implementation.new(self)) do |acc, int| # steep:ignore
159
162
  int.intercept_client(acc)
160
163
  end
161
164
  end
@@ -191,6 +194,12 @@ module Temporalio
191
194
  # @param args [Array<Object>] Arguments to the workflow.
192
195
  # @param id [String] Unique identifier for the workflow execution.
193
196
  # @param task_queue [String] Task queue to run the workflow on.
197
+ # @param static_summary [String, nil] Fixed single-line summary for this workflow execution that may appear in
198
+ # CLI/UI. This can be in single-line Temporal markdown format. This is currently experimental.
199
+ # @param static_details [String, nil] Fixed details for this workflow execution that may appear in CLI/UI. This can
200
+ # be in Temporal markdown format and can be multiple lines. This is a fixed value on the workflow that cannot be
201
+ # updated. For details that can be updated, use {Workflow.current_details=} within the workflow. This is currently
202
+ # experimental.
194
203
  # @param execution_timeout [Float, nil] Total workflow execution timeout in seconds including retries and continue
195
204
  # as new.
196
205
  # @param run_timeout [Float, nil] Timeout of a single workflow run in seconds.
@@ -217,6 +226,8 @@ module Temporalio
217
226
  *args,
218
227
  id:,
219
228
  task_queue:,
229
+ static_summary: nil,
230
+ static_details: nil,
220
231
  execution_timeout: nil,
221
232
  run_timeout: nil,
222
233
  task_timeout: nil,
@@ -231,10 +242,12 @@ module Temporalio
231
242
  rpc_options: nil
232
243
  )
233
244
  @impl.start_workflow(Interceptor::StartWorkflowInput.new(
234
- workflow:,
245
+ workflow: Workflow::Definition._workflow_type_from_workflow_parameter(workflow),
235
246
  args:,
236
247
  workflow_id: id,
237
248
  task_queue:,
249
+ static_summary:,
250
+ static_details:,
238
251
  execution_timeout:,
239
252
  run_timeout:,
240
253
  task_timeout:,
@@ -257,6 +270,12 @@ module Temporalio
257
270
  # @param args [Array<Object>] Arguments to the workflow.
258
271
  # @param id [String] Unique identifier for the workflow execution.
259
272
  # @param task_queue [String] Task queue to run the workflow on.
273
+ # @param static_summary [String, nil] Fixed single-line summary for this workflow execution that may appear in
274
+ # CLI/UI. This can be in single-line Temporal markdown format. This is currently experimental.
275
+ # @param static_details [String, nil] Fixed details for this workflow execution that may appear in CLI/UI. This can
276
+ # be in Temporal markdown format and can be multiple lines. This is a fixed value on the workflow that cannot be
277
+ # updated. For details that can be updated, use {Workflow.current_details=} within the workflow. This is currently
278
+ # experimental.
260
279
  # @param execution_timeout [Float, nil] Total workflow execution timeout in seconds including retries and continue
261
280
  # as new.
262
281
  # @param run_timeout [Float, nil] Timeout of a single workflow run in seconds.
@@ -284,6 +303,8 @@ module Temporalio
284
303
  *args,
285
304
  id:,
286
305
  task_queue:,
306
+ static_summary: nil,
307
+ static_details: nil,
287
308
  execution_timeout: nil,
288
309
  run_timeout: nil,
289
310
  task_timeout: nil,
@@ -302,6 +323,8 @@ module Temporalio
302
323
  *args,
303
324
  id:,
304
325
  task_queue:,
326
+ static_summary:,
327
+ static_details:,
305
328
  execution_timeout:,
306
329
  run_timeout:,
307
330
  task_timeout:,
@@ -334,6 +357,106 @@ module Temporalio
334
357
  WorkflowHandle.new(client: self, id: workflow_id, run_id:, result_run_id: run_id, first_execution_run_id:)
335
358
  end
336
359
 
360
+ # Start an update, possibly starting the workflow at the same time if it doesn't exist (depending upon ID conflict
361
+ # policy). Note that in some cases this may fail but the workflow will still be started, and the handle can then be
362
+ # retrieved on the start workflow operation.
363
+ #
364
+ # @param update [Workflow::Definition::Update, Symbol, String] Update definition or name.
365
+ # @param args [Array<Object>] Update arguments.
366
+ # @param start_workflow_operation [WithStartWorkflowOperation] Required with-start workflow operation. This must
367
+ # have an `id_conflict_policy` set.
368
+ # @param wait_for_stage [WorkflowUpdateWaitStage] Required stage to wait until returning. ADMITTED is not
369
+ # currently supported. See https://docs.temporal.io/workflows#update for more details.
370
+ # @param id [String] ID of the update.
371
+ # @param rpc_options [RPCOptions, nil] Advanced RPC options.
372
+ #
373
+ # @return [WorkflowUpdateHandle] The update handle.
374
+ # @raise [Error::WorkflowAlreadyStartedError] Workflow already exists and conflict/reuse policy does not allow.
375
+ # @raise [Error::WorkflowUpdateRPCTimeoutOrCanceledError] This update call timed out or was canceled. This doesn't
376
+ # mean the update itself was timed out or canceled, and this doesn't mean the workflow did not start.
377
+ # @raise [Error::RPCError] RPC error from call.
378
+ def start_update_with_start_workflow(
379
+ update,
380
+ *args,
381
+ start_workflow_operation:,
382
+ wait_for_stage:,
383
+ id: SecureRandom.uuid,
384
+ rpc_options: nil
385
+ )
386
+ @impl.start_update_with_start_workflow(
387
+ Interceptor::StartUpdateWithStartWorkflowInput.new(
388
+ update_id: id,
389
+ update: Workflow::Definition::Update._name_from_parameter(update),
390
+ args:,
391
+ wait_for_stage:,
392
+ start_workflow_operation:,
393
+ headers: {},
394
+ rpc_options:
395
+ )
396
+ )
397
+ end
398
+
399
+ # Start an update, possibly starting the workflow at the same time if it doesn't exist (depending upon ID conflict
400
+ # policy), and wait for update result. This is a shortcut for {start_update_with_start_workflow} +
401
+ # {WorkflowUpdateHandle.result}.
402
+ #
403
+ # @param update [Workflow::Definition::Update, Symbol, String] Update definition or name.
404
+ # @param args [Array<Object>] Update arguments.
405
+ # @param start_workflow_operation [WithStartWorkflowOperation] Required with-start workflow operation. This must
406
+ # have an `id_conflict_policy` set.
407
+ # @param id [String] ID of the update.
408
+ # @param rpc_options [RPCOptions, nil] Advanced RPC options.
409
+ #
410
+ # @return [Object] Successful update result.
411
+ # @raise [Error::WorkflowUpdateFailedError] If the update failed.
412
+ # @raise [Error::WorkflowAlreadyStartedError] Workflow already exists and conflict/reuse policy does not allow.
413
+ # @raise [Error::WorkflowUpdateRPCTimeoutOrCanceledError] This update call timed out or was canceled. This doesn't
414
+ # mean the update itself was timed out or canceled, and this doesn't mean the workflow did not start.
415
+ # @raise [Error::RPCError] RPC error from call.
416
+ def execute_update_with_start_workflow(
417
+ update,
418
+ *args,
419
+ start_workflow_operation:,
420
+ id: SecureRandom.uuid,
421
+ rpc_options: nil
422
+ )
423
+ start_update_with_start_workflow(
424
+ update,
425
+ *args,
426
+ start_workflow_operation:,
427
+ wait_for_stage: WorkflowUpdateWaitStage::COMPLETED,
428
+ id:,
429
+ rpc_options:
430
+ ).result
431
+ end
432
+
433
+ # Send a signal, possibly starting the workflow at the same time if it doesn't exist.
434
+ #
435
+ # @param signal [Workflow::Definition::Signal, Symbol, String] Signal definition or name.
436
+ # @param args [Array<Object>] Signal arguments.
437
+ # @param start_workflow_operation [WithStartWorkflowOperation] Required with-start workflow operation. This may not
438
+ # support all `id_conflict_policy` options.
439
+ # @param rpc_options [RPCOptions, nil] Advanced RPC options.
440
+ #
441
+ # @return [WorkflowHandle] A workflow handle to the workflow.
442
+ # @raise [Error::WorkflowAlreadyStartedError] Workflow already exists and conflict/reuse policy does not allow.
443
+ # @raise [Error::RPCError] RPC error from call.
444
+ def signal_with_start_workflow(
445
+ signal,
446
+ *args,
447
+ start_workflow_operation:,
448
+ rpc_options: nil
449
+ )
450
+ @impl.signal_with_start_workflow(
451
+ Interceptor::SignalWithStartWorkflowInput.new(
452
+ signal: Workflow::Definition::Signal._name_from_parameter(signal),
453
+ args:,
454
+ start_workflow_operation:,
455
+ rpc_options:
456
+ )
457
+ )
458
+ end
459
+
337
460
  # List workflows.
338
461
  #
339
462
  # @param query [String, nil] A Temporal visibility list filter.
@@ -0,0 +1,470 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+ require 'opentelemetry' # This import will intentionally fail if the user does not have OTel gem available
5
+ require 'temporalio/client/interceptor'
6
+ require 'temporalio/converters/payload_converter'
7
+ require 'temporalio/worker/interceptor'
8
+
9
+ module Temporalio
10
+ module Contrib
11
+ module OpenTelemetry
12
+ # Tracing interceptor to add OpenTelemetry traces to clients, activities, and workflows.
13
+ class TracingInterceptor
14
+ include Client::Interceptor
15
+ include Worker::Interceptor::Activity
16
+ include Worker::Interceptor::Workflow
17
+
18
+ # @return [OpenTelemetry::Trace::Tracer] Tracer in use.
19
+ attr_reader :tracer
20
+
21
+ # Create interceptor.
22
+ #
23
+ # @param tracer [OpenTelemetry::Trace::Tracer] Tracer to use.
24
+ # @param header_key [String] Temporal header name to serialize spans to/from. Most users should not change this.
25
+ # @param propagator [Object] Propagator to use. Most users should not change this.
26
+ # @param always_create_workflow_spans [Boolean] When false, the default, spans are only created in workflows
27
+ # when an overarching span from the client is present. In cases of starting a workflow elsewhere, e.g. CLI or
28
+ # schedules, a client-created span is not present and workflow spans will not be created. Setting this to true
29
+ # will create spans in workflows no matter what, but there is a risk of them being orphans since they may not
30
+ # have a parent span after replaying.
31
+ def initialize(
32
+ tracer,
33
+ header_key: '_tracer-data',
34
+ propagator: ::OpenTelemetry::Context::Propagation::CompositeTextMapPropagator.compose_propagators(
35
+ [
36
+ ::OpenTelemetry::Trace::Propagation::TraceContext::TextMapPropagator.new,
37
+ ::OpenTelemetry::Baggage::Propagation::TextMapPropagator.new
38
+ ]
39
+ ),
40
+ always_create_workflow_spans: false
41
+ )
42
+ @tracer = tracer
43
+ @header_key = header_key
44
+ @propagator = propagator
45
+ @always_create_workflow_spans = always_create_workflow_spans
46
+ end
47
+
48
+ # @!visibility private
49
+ def intercept_client(next_interceptor)
50
+ ClientOutbound.new(self, next_interceptor)
51
+ end
52
+
53
+ # @!visibility private
54
+ def intercept_activity(next_interceptor)
55
+ ActivityInbound.new(self, next_interceptor)
56
+ end
57
+
58
+ # @!visibility private
59
+ def intercept_workflow(next_interceptor)
60
+ WorkflowInbound.new(self, next_interceptor)
61
+ end
62
+
63
+ # @!visibility private
64
+ def _apply_context_to_headers(headers, context: ::OpenTelemetry::Context.current)
65
+ carrier = {}
66
+ @propagator.inject(carrier, context:)
67
+ headers[@header_key] = carrier unless carrier.empty?
68
+ end
69
+
70
+ # @!visibility private
71
+ def _attach_context(headers)
72
+ context = _context_from_headers(headers)
73
+ ::OpenTelemetry::Context.attach(context) if context
74
+ end
75
+
76
+ # @!visibility private
77
+ def _context_from_headers(headers)
78
+ carrier = headers[@header_key]
79
+ @propagator.extract(carrier) if carrier.is_a?(Hash) && !carrier.empty?
80
+ end
81
+
82
+ # @!visibility private
83
+ def _with_started_span(
84
+ name:,
85
+ kind:,
86
+ attributes: nil,
87
+ outbound_input: nil
88
+ )
89
+ tracer.in_span(name, attributes:, kind:) do
90
+ _apply_context_to_headers(outbound_input.headers) if outbound_input
91
+ yield
92
+ end
93
+ end
94
+
95
+ # @!visibility private
96
+ def _always_create_workflow_spans
97
+ @always_create_workflow_spans
98
+ end
99
+
100
+ # @!visibility private
101
+ class ClientOutbound < Client::Interceptor::Outbound
102
+ def initialize(root, next_interceptor)
103
+ super(next_interceptor)
104
+ @root = root
105
+ end
106
+
107
+ # @!visibility private
108
+ def start_workflow(input)
109
+ @root._with_started_span(
110
+ name: "StartWorkflow:#{input.workflow}",
111
+ kind: :client,
112
+ attributes: { 'temporalWorkflowID' => input.workflow_id },
113
+ outbound_input: input
114
+ ) { super }
115
+ end
116
+
117
+ # @!visibility private
118
+ def start_update_with_start_workflow(input)
119
+ @root._with_started_span(
120
+ name: "UpdateWithStartWorkflow:#{input.update}",
121
+ kind: :client,
122
+ attributes: { 'temporalWorkflowID' => input.start_workflow_operation.options.id,
123
+ 'temporalUpdateID' => input.update_id },
124
+ outbound_input: input
125
+ ) do
126
+ # Also add to start headers
127
+ if input.headers[@header_key]
128
+ input.start_workflow_operation.options.headers[@header_key] = input.headers[@header_key]
129
+ end
130
+ super
131
+ end
132
+ end
133
+
134
+ # @!visibility private
135
+ def signal_with_start_workflow(input)
136
+ @root._with_started_span(
137
+ name: "SignalWithStartWorkflow:#{input.workflow}",
138
+ kind: :client,
139
+ attributes: { 'temporalWorkflowID' => input.start_workflow_operation.options.id },
140
+ outbound_input: input
141
+ ) do
142
+ # Also add to start headers
143
+ if input.headers[@header_key]
144
+ input.start_workflow_operation.options.headers[@header_key] = input.headers[@header_key]
145
+ end
146
+ super
147
+ end
148
+ end
149
+
150
+ # @!visibility private
151
+ def signal_workflow(input)
152
+ @root._with_started_span(
153
+ name: "SignalWorkflow:#{input.signal}",
154
+ kind: :client,
155
+ attributes: { 'temporalWorkflowID' => input.workflow_id },
156
+ outbound_input: input
157
+ ) { super }
158
+ end
159
+
160
+ # @!visibility private
161
+ def query_workflow(input)
162
+ @root._with_started_span(
163
+ name: "QueryWorkflow:#{input.query}",
164
+ kind: :client,
165
+ attributes: { 'temporalWorkflowID' => input.workflow_id },
166
+ outbound_input: input
167
+ ) { super }
168
+ end
169
+
170
+ # @!visibility private
171
+ def start_workflow_update(input)
172
+ @root._with_started_span(
173
+ name: "StartWorkflowUpdate:#{input.update}",
174
+ kind: :client,
175
+ attributes: { 'temporalWorkflowID' => input.workflow_id, 'temporalUpdateID' => input.update_id },
176
+ outbound_input: input
177
+ ) { super }
178
+ end
179
+ end
180
+
181
+ # @!visibility private
182
+ class ActivityInbound < Worker::Interceptor::Activity::Inbound
183
+ def initialize(root, next_interceptor)
184
+ super(next_interceptor)
185
+ @root = root
186
+ end
187
+
188
+ # @!visibility private
189
+ def execute(input)
190
+ @root._attach_context(input.headers)
191
+ info = Activity::Context.current.info
192
+ @root._with_started_span(
193
+ name: "RunActivity:#{info.activity_type}",
194
+ kind: :server,
195
+ attributes: {
196
+ 'temporalWorkflowID' => info.workflow_id,
197
+ 'temporalRunID' => info.workflow_run_id,
198
+ 'temporalActivityID' => info.activity_id
199
+ }
200
+ ) { super }
201
+ end
202
+ end
203
+
204
+ # @!visibility private
205
+ class WorkflowInbound < Worker::Interceptor::Workflow::Inbound
206
+ def initialize(root, next_interceptor)
207
+ super(next_interceptor)
208
+ @root = root
209
+ end
210
+
211
+ # @!visibility private
212
+ def init(outbound)
213
+ # Set root on storage
214
+ Temporalio::Workflow.storage[:__temporal_opentelemetry_tracing_interceptor] = @root
215
+ super(WorkflowOutbound.new(@root, outbound))
216
+ end
217
+
218
+ # @!visibility private
219
+ def execute(input)
220
+ @root._attach_context(Temporalio::Workflow.info.headers)
221
+ Workflow.with_completed_span("RunWorkflow:#{Temporalio::Workflow.info.workflow_type}", kind: :server) do
222
+ super
223
+ ensure
224
+ Workflow.completed_span(
225
+ "CompleteWorkflow:#{Temporalio::Workflow.info.workflow_type}",
226
+ kind: :internal,
227
+ exception: $ERROR_INFO # steep:ignore
228
+ )
229
+ end
230
+ end
231
+
232
+ # @!visibility private
233
+ def handle_signal(input)
234
+ @root._attach_context(Temporalio::Workflow.info.headers)
235
+ Workflow.with_completed_span(
236
+ "HandleSignal:#{input.signal}",
237
+ links: _links_from_headers(input.headers),
238
+ kind: :server
239
+ ) do
240
+ super
241
+ rescue Exception => e # rubocop:disable Lint/RescueException
242
+ Workflow.completed_span("FailHandleSignal:#{input.signal}", kind: :internal, exception: e)
243
+ raise
244
+ end
245
+ end
246
+
247
+ # @!visibility private
248
+ def handle_query(input)
249
+ @root._attach_context(Temporalio::Workflow.info.headers)
250
+ Workflow.with_completed_span(
251
+ "HandleQuery:#{input.query}",
252
+ links: _links_from_headers(input.headers),
253
+ kind: :server,
254
+ even_during_replay: true
255
+ ) do
256
+ super
257
+ rescue Exception => e # rubocop:disable Lint/RescueException
258
+ Workflow.completed_span(
259
+ "FailHandleQuery:#{input.query}",
260
+ kind: :internal,
261
+ exception: e,
262
+ even_during_replay: true
263
+ )
264
+ raise
265
+ end
266
+ end
267
+
268
+ # @!visibility private
269
+ def validate_update(input)
270
+ @root._attach_context(Temporalio::Workflow.info.headers)
271
+ Workflow.with_completed_span(
272
+ "ValidateUpdate:#{input.update}",
273
+ attributes: { 'temporalUpdateID' => input.id },
274
+ links: _links_from_headers(input.headers),
275
+ kind: :server,
276
+ even_during_replay: true
277
+ ) do
278
+ super
279
+ rescue Exception => e # rubocop:disable Lint/RescueException
280
+ Workflow.completed_span(
281
+ "FailValidateUpdate:#{input.update}",
282
+ attributes: { 'temporalUpdateID' => input.id },
283
+ kind: :internal,
284
+ exception: e,
285
+ even_during_replay: true
286
+ )
287
+ raise
288
+ end
289
+ end
290
+
291
+ # @!visibility private
292
+ def handle_update(input)
293
+ @root._attach_context(Temporalio::Workflow.info.headers)
294
+ Workflow.with_completed_span(
295
+ "HandleUpdate:#{input.update}",
296
+ attributes: { 'temporalUpdateID' => input.id },
297
+ links: _links_from_headers(input.headers),
298
+ kind: :server
299
+ ) do
300
+ super
301
+ rescue Exception => e # rubocop:disable Lint/RescueException
302
+ Workflow.completed_span(
303
+ "FailHandleUpdate:#{input.update}",
304
+ attributes: { 'temporalUpdateID' => input.id },
305
+ kind: :internal,
306
+ exception: e
307
+ )
308
+ raise
309
+ end
310
+ end
311
+
312
+ # @!visibility private
313
+ def _links_from_headers(headers)
314
+ context = @root._context_from_headers(headers)
315
+ span = ::OpenTelemetry::Trace.current_span(context) if context
316
+ if span && span != ::OpenTelemetry::Trace::Span::INVALID
317
+ [::OpenTelemetry::Trace::Link.new(span.context)]
318
+ else
319
+ []
320
+ end
321
+ end
322
+ end
323
+
324
+ # @!visibility private
325
+ class WorkflowOutbound < Worker::Interceptor::Workflow::Outbound
326
+ def initialize(root, next_interceptor)
327
+ super(next_interceptor)
328
+ @root = root
329
+ end
330
+
331
+ # @!visibility private
332
+ def execute_activity(input)
333
+ _apply_span_to_headers(input.headers,
334
+ Workflow.completed_span("StartActivity:#{input.activity}", kind: :client))
335
+ super
336
+ end
337
+
338
+ # @!visibility private
339
+ def execute_local_activity(input)
340
+ _apply_span_to_headers(input.headers,
341
+ Workflow.completed_span("StartActivity:#{input.activity}", kind: :client))
342
+ super
343
+ end
344
+
345
+ # @!visibility private
346
+ def initialize_continue_as_new_error(input)
347
+ # Just apply the current context to headers
348
+ @root._apply_context_to_headers(input.error.headers)
349
+ super
350
+ end
351
+
352
+ # @!visibility private
353
+ def signal_child_workflow(input)
354
+ _apply_span_to_headers(input.headers,
355
+ Workflow.completed_span("SignalChildWorkflow:#{input.signal}", kind: :client))
356
+ super
357
+ end
358
+
359
+ # @!visibility private
360
+ def signal_external_workflow(input)
361
+ _apply_span_to_headers(input.headers,
362
+ Workflow.completed_span("SignalExternalWorkflow:#{input.signal}", kind: :client))
363
+ super
364
+ end
365
+
366
+ # @!visibility private
367
+ def start_child_workflow(input)
368
+ _apply_span_to_headers(input.headers,
369
+ Workflow.completed_span("StartChildWorkflow:#{input.workflow}", kind: :client))
370
+ super
371
+ end
372
+
373
+ # @!visibility private
374
+ def _apply_span_to_headers(headers, span)
375
+ @root._apply_context_to_headers(headers, context: ::OpenTelemetry::Trace.context_with_span(span)) if span
376
+ end
377
+ end
378
+
379
+ private_constant :ClientOutbound
380
+ private_constant :ActivityInbound
381
+ private_constant :WorkflowInbound
382
+ private_constant :WorkflowOutbound
383
+ end
384
+
385
+ # Contains workflow methods that can be used for OpenTelemetry.
386
+ module Workflow
387
+ # Create a completed span and execute block with the span set on the context.
388
+ #
389
+ # @param name [String] Span name.
390
+ # @param attributes [Hash] Span attributes. These will have workflow and run ID automatically added.
391
+ # @param links [Array, nil] Span links.
392
+ # @param kind [Symbol, nil] Span kind.
393
+ # @param exception [Exception, nil] Exception to record on the span.
394
+ # @param even_during_replay [Boolean] Set to true to record this span even during replay. Most users should
395
+ # never set this.
396
+ # @yield Block to call. It is UNSAFE to expect any parameters in this block.
397
+ # @return [Object] Result of the block.
398
+ def self.with_completed_span(
399
+ name,
400
+ attributes: {},
401
+ links: nil,
402
+ kind: nil,
403
+ exception: nil,
404
+ even_during_replay: false
405
+ )
406
+ span = completed_span(name, attributes:, links:, kind:, exception:, even_during_replay:)
407
+ if span
408
+ ::OpenTelemetry::Trace.with_span(span) do # rubocop:disable Style/ExplicitBlockArgument
409
+ # Yield with no parameters
410
+ yield
411
+ end
412
+ else
413
+ yield
414
+ end
415
+ end
416
+
417
+ # Create a completed span only if not replaying (or `even_during_replay` is true).
418
+ #
419
+ # @note WARNING: It is UNSAFE to rely on the result of this method as it may be different/absent on replay.
420
+ #
421
+ # @param name [String] Span name.
422
+ # @param attributes [Hash] Span attributes. These will have workflow and run ID automatically added.
423
+ # @param links [Array, nil] Span links.
424
+ # @param kind [Symbol, nil] Span kind.
425
+ # @param exception [Exception, nil] Exception to record on the span.
426
+ # @param even_during_replay [Boolean] Set to true to record this span even during replay. Most users should
427
+ # never set this.
428
+ # @return [OpenTelemetry::Trace::Span, nil] Span if one was created. WARNING: It is UNSAFE to use this value.
429
+ def self.completed_span(
430
+ name,
431
+ attributes: {},
432
+ links: nil,
433
+ kind: nil,
434
+ exception: nil,
435
+ even_during_replay: false
436
+ )
437
+ # Get root interceptor, which also checks if in workflow
438
+ root = Temporalio::Workflow.storage[:__temporal_opentelemetry_tracing_interceptor] #: TracingInterceptor?
439
+ raise 'Tracing interceptor not configured' unless root
440
+
441
+ # Do nothing if replaying and not wanted during replay
442
+ return nil if !even_during_replay && Temporalio::Workflow::Unsafe.replaying?
443
+
444
+ # If there is no span on the context and the user hasn't opted in to always creating, do not create. This
445
+ # prevents orphans if there was no span originally created from the client start-workflow call.
446
+ if ::OpenTelemetry::Trace.current_span == ::OpenTelemetry::Trace::Span::INVALID &&
447
+ !root._always_create_workflow_spans
448
+ return nil
449
+ end
450
+
451
+ # Create attributes, adding user-defined ones
452
+ attributes = { 'temporalWorkflowID' => Temporalio::Workflow.info.workflow_id,
453
+ 'temporalRunID' => Temporalio::Workflow.info.run_id }.merge(attributes)
454
+
455
+ # Create span, which has to be done with illegal call disabling because OTel asks for full exception message
456
+ # which uses error highlighting and such which accesses File#path
457
+ Temporalio::Workflow::Unsafe.illegal_call_tracing_disabled do
458
+ time = Temporalio::Workflow.now
459
+ timestamp = (time.to_i * 1_000_000_000) + time.nsec
460
+ span = root.tracer.start_span(name, attributes:, links:, start_timestamp: timestamp, kind:) # steep:ignore
461
+ # Record exception if present
462
+ span.record_exception(exception) if exception
463
+ # Finish the span (returns self)
464
+ span.finish(end_timestamp: timestamp)
465
+ end
466
+ end
467
+ end
468
+ end
469
+ end
470
+ end
@@ -124,6 +124,7 @@ module Temporalio
124
124
 
125
125
  def create_grpc_status
126
126
  return Api::Common::V1::GrpcStatus.new(code: @code) unless @raw_grpc_status
127
+ return @raw_grpc_status if @raw_grpc_status.is_a?(Api::Common::V1::GrpcStatus)
127
128
 
128
129
  Api::Common::V1::GrpcStatus.decode(@raw_grpc_status)
129
130
  end
@@ -10,7 +10,7 @@ require 'temporalio/api/common/v1/message'
10
10
  require 'temporalio/internal/bridge/api/common/common'
11
11
 
12
12
 
13
- descriptor_data = "\n3temporal/sdk/core/activity_task/activity_task.proto\x12\x15\x63oresdk.activity_task\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a%temporal/sdk/core/common/common.proto\"\x8d\x01\n\x0c\x41\x63tivityTask\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12-\n\x05start\x18\x03 \x01(\x0b\x32\x1c.coresdk.activity_task.StartH\x00\x12/\n\x06\x63\x61ncel\x18\x04 \x01(\x0b\x32\x1d.coresdk.activity_task.CancelH\x00\x42\t\n\x07variant\"\xed\x06\n\x05Start\x12\x1a\n\x12workflow_namespace\x18\x01 \x01(\t\x12\x15\n\rworkflow_type\x18\x02 \x01(\t\x12\x45\n\x12workflow_execution\x18\x03 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x13\n\x0b\x61\x63tivity_id\x18\x04 \x01(\t\x12\x15\n\ractivity_type\x18\x05 \x01(\t\x12\x45\n\rheader_fields\x18\x06 \x03(\x0b\x32..coresdk.activity_task.Start.HeaderFieldsEntry\x12.\n\x05input\x18\x07 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12:\n\x11heartbeat_details\x18\x08 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x32\n\x0escheduled_time\x18\t \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x42\n\x1e\x63urrent_attempt_scheduled_time\x18\n \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x30\n\x0cstarted_time\x18\x0b \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0f\n\x07\x61ttempt\x18\x0c \x01(\r\x12<\n\x19schedule_to_close_timeout\x18\r \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\x0e \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x34\n\x11heartbeat_timeout\x18\x0f \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x0cretry_policy\x18\x10 \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x10\n\x08is_local\x18\x11 \x01(\x08\x1aT\n\x11HeaderFieldsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\"E\n\x06\x43\x61ncel\x12;\n\x06reason\x18\x01 \x01(\x0e\x32+.coresdk.activity_task.ActivityCancelReason*X\n\x14\x41\x63tivityCancelReason\x12\r\n\tNOT_FOUND\x10\x00\x12\r\n\tCANCELLED\x10\x01\x12\r\n\tTIMED_OUT\x10\x02\x12\x13\n\x0fWORKER_SHUTDOWN\x10\x03\x42\x32\xea\x02/Temporalio::Internal::Bridge::Api::ActivityTaskb\x06proto3"
13
+ descriptor_data = "\n3temporal/sdk/core/activity_task/activity_task.proto\x12\x15\x63oresdk.activity_task\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a%temporal/sdk/core/common/common.proto\"\x8d\x01\n\x0c\x41\x63tivityTask\x12\x12\n\ntask_token\x18\x01 \x01(\x0c\x12-\n\x05start\x18\x03 \x01(\x0b\x32\x1c.coresdk.activity_task.StartH\x00\x12/\n\x06\x63\x61ncel\x18\x04 \x01(\x0b\x32\x1d.coresdk.activity_task.CancelH\x00\x42\t\n\x07variant\"\xa1\x07\n\x05Start\x12\x1a\n\x12workflow_namespace\x18\x01 \x01(\t\x12\x15\n\rworkflow_type\x18\x02 \x01(\t\x12\x45\n\x12workflow_execution\x18\x03 \x01(\x0b\x32).temporal.api.common.v1.WorkflowExecution\x12\x13\n\x0b\x61\x63tivity_id\x18\x04 \x01(\t\x12\x15\n\ractivity_type\x18\x05 \x01(\t\x12\x45\n\rheader_fields\x18\x06 \x03(\x0b\x32..coresdk.activity_task.Start.HeaderFieldsEntry\x12.\n\x05input\x18\x07 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12:\n\x11heartbeat_details\x18\x08 \x03(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x32\n\x0escheduled_time\x18\t \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x42\n\x1e\x63urrent_attempt_scheduled_time\x18\n \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x30\n\x0cstarted_time\x18\x0b \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0f\n\x07\x61ttempt\x18\x0c \x01(\r\x12<\n\x19schedule_to_close_timeout\x18\r \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x16start_to_close_timeout\x18\x0e \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x34\n\x11heartbeat_timeout\x18\x0f \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x39\n\x0cretry_policy\x18\x10 \x01(\x0b\x32#.temporal.api.common.v1.RetryPolicy\x12\x32\n\x08priority\x18\x12 \x01(\x0b\x32 .temporal.api.common.v1.Priority\x12\x10\n\x08is_local\x18\x11 \x01(\x08\x1aT\n\x11HeaderFieldsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01\"E\n\x06\x43\x61ncel\x12;\n\x06reason\x18\x01 \x01(\x0e\x32+.coresdk.activity_task.ActivityCancelReason*X\n\x14\x41\x63tivityCancelReason\x12\r\n\tNOT_FOUND\x10\x00\x12\r\n\tCANCELLED\x10\x01\x12\r\n\tTIMED_OUT\x10\x02\x12\x13\n\x0fWORKER_SHUTDOWN\x10\x03\x42\x32\xea\x02/Temporalio::Internal::Bridge::Api::ActivityTaskb\x06proto3"
14
14
 
15
15
  pool = Google::Protobuf::DescriptorPool.generated_pool
16
16
  pool.add_serialized_file(descriptor_data)