@@ -96,10 +96,12 @@ def update(scenario)
96
96
) do
97
97
Temporalio ::Workflow . execute_local_activity ( TestActivity , :fail_first_attempt , start_to_close_timeout : 30 )
98
98
end
99
- when :child_workflow
100
- # Start a child, send child signal and external signal, finish
99
+ when :child_workflow_child_signal
100
+ handle = Temporalio ::Workflow . start_child_workflow ( TestWorkflow , :wait_on_signal )
101
+ handle . signal ( TestWorkflow . signal , :mark_finished )
102
+ [ handle . id , handle . first_execution_run_id , handle . result ]
103
+ when :child_workflow_external_signal
101
104
handle = Temporalio ::Workflow . start_child_workflow ( TestWorkflow , :wait_on_signal )
102
- handle . signal ( TestWorkflow . signal , :complete )
103
105
Temporalio ::Workflow . external_workflow_handle ( handle . id ) . signal ( TestWorkflow . signal , :mark_finished )
104
106
[ handle . id , handle . first_execution_run_id , handle . result ]
105
107
else
@@ -138,11 +140,18 @@ def init_tracer_and_exporter
138
140
[ tracer , exporter ]
139
141
end
140
142
141
- def trace ( tracer_and_exporter : init_tracer_and_exporter , &)
143
+ def trace (
144
+ tracer_and_exporter : init_tracer_and_exporter ,
145
+ always_create_workflow_spans : false ,
146
+ check_root : true ,
147
+ &
148
+ )
142
149
tracer , exporter = tracer_and_exporter
143
150
144
151
# Make client with interceptors
145
- interceptor = Temporalio ::Contrib ::OpenTelemetry ::TracingInterceptor . new ( tracer )
152
+ interceptor = Temporalio ::Contrib ::OpenTelemetry ::TracingInterceptor . new (
153
+ tracer , always_create_workflow_spans :
154
+ )
146
155
new_options = env . client . options . with ( interceptors : [ interceptor ] )
147
156
client = Temporalio ::Client . new ( **new_options . to_h ) # steep:ignore
148
157
@@ -153,13 +162,22 @@ def trace(tracer_and_exporter: init_tracer_and_exporter, &)
153
162
154
163
# Convert spans, confirm there is only the outer, and return children
155
164
spans = ExpectedSpan . from_span_data ( exporter . finished_spans )
156
- assert_equal 1 , spans . size
157
- assert_equal 'root' , spans . first &.name
165
+ if check_root
166
+ assert_equal 1 , spans . size
167
+ assert_equal 'root' , spans . first &.name
168
+ end
158
169
spans . first
159
170
end
160
171
161
- def trace_workflow ( scenario , tracer_and_exporter : init_tracer_and_exporter , &)
162
- trace ( tracer_and_exporter :) do |client |
172
+ def trace_workflow (
173
+ scenario ,
174
+ tracer_and_exporter : init_tracer_and_exporter ,
175
+ start_with_untraced_client : false ,
176
+ always_create_workflow_spans : false ,
177
+ check_root : true ,
178
+ &
179
+ )
180
+ trace ( tracer_and_exporter :, always_create_workflow_spans :, check_root :) do |client |
163
181
# Must capture and attach outer context
164
182
outer_context = OpenTelemetry ::Context . current
165
183
attach_token = nil
@@ -169,7 +187,8 @@ def trace_workflow(scenario, tracer_and_exporter: init_tracer_and_exporter, &)
169
187
client :,
170
188
activities : [ TestActivity . new ( tracer_and_exporter . first ) ] ,
171
189
# Have to reattach outer context inside worker run to check outer span
172
- on_worker_run : proc { attach_token = OpenTelemetry ::Context . attach ( outer_context ) }
190
+ on_worker_run : proc { attach_token = OpenTelemetry ::Context . attach ( outer_context ) } ,
191
+ start_workflow_client : start_with_untraced_client ? env . client : client
173
192
) do |handle |
174
193
yield handle
175
194
ensure
@@ -398,42 +417,61 @@ def test_client_fail
398
417
end
399
418
400
419
def test_child_and_external
401
- exp_root = ExpectedSpan . new ( name : 'root' )
402
- act_root = trace_workflow ( :wait_on_signal ) do |handle |
403
- exp_cl_attrs = { 'temporalWorkflowID' => handle . id }
404
- exp_run_attrs = exp_cl_attrs . merge ( { 'temporalRunID' => handle . result_run_id } )
405
- exp_start_wf = exp_root . add_child ( name : 'StartWorkflow:TestWorkflow' , attributes : exp_cl_attrs )
406
- exp_start_wf . add_child ( name : 'RunWorkflow:TestWorkflow' , attributes : exp_run_attrs )
407
-
408
- # Wait for task completion so update isn't accidentally first before run
409
- assert_eventually { assert handle . fetch_history_events . any? ( &:workflow_task_completed_event_attributes ) }
410
-
411
- # Update calls child and sends signals to it in two ways
412
- child_id , child_run_id , child_result = handle . execute_update ( TestWorkflow . update ,
413
- :child_workflow , id : 'my-update-id' )
414
- exp_update = exp_root . add_child ( name : 'StartWorkflowUpdate:update' ,
415
- attributes : exp_cl_attrs . merge ( { 'temporalUpdateID' => 'my-update-id' } ) )
416
- # Expected span for update
417
- exp_hnd_update = exp_start_wf . add_child (
418
- name : 'HandleUpdate:update' ,
419
- attributes : exp_run_attrs . merge ( { 'temporalUpdateID' => 'my-update-id' } ) ,
420
- links : [ exp_update ]
421
- )
422
- # Expected for children
423
- exp_child_run_attrs = { 'temporalWorkflowID' => child_id , 'temporalRunID' => child_run_id }
424
- exp_child_start = exp_hnd_update . add_child ( name : 'StartChildWorkflow:TestWorkflow' , attributes : exp_run_attrs )
425
- exp_child_start
426
- . add_child ( name : 'RunWorkflow:TestWorkflow' , attributes : exp_child_run_attrs )
427
- . add_child ( name : 'CompleteWorkflow:TestWorkflow' , attributes : exp_child_run_attrs )
428
- # Two signals we send to the child
429
- exp_sig_child = exp_hnd_update . add_child ( name : 'SignalChildWorkflow:signal' , attributes : exp_run_attrs )
430
- exp_sig_ext = exp_hnd_update . add_child ( name : 'SignalExternalWorkflow:signal' , attributes : exp_run_attrs )
431
- exp_child_start . add_child ( name : 'HandleSignal:signal' , attributes : exp_child_run_attrs , links : [ exp_sig_child ] )
432
- exp_child_start . add_child ( name : 'HandleSignal:signal' , attributes : exp_child_run_attrs , links : [ exp_sig_ext ] )
433
-
434
- assert_equal 'workflow-done' , child_result
420
+ # We have to test child signal and external signal separately because sending both back-to-back can result in
421
+ # rare cases where one is delivered before the other (yes, even if you wait on the first to get an initiated
422
+ # event)
423
+ %i[ child_workflow_child_signal child_workflow_external_signal ] . each do |scenario |
424
+ exp_root = ExpectedSpan . new ( name : 'root' )
425
+ act_root = trace_workflow ( :wait_on_signal ) do |handle |
426
+ exp_cl_attrs = { 'temporalWorkflowID' => handle . id }
427
+ exp_run_attrs = exp_cl_attrs . merge ( { 'temporalRunID' => handle . result_run_id } )
428
+ exp_start_wf = exp_root . add_child ( name : 'StartWorkflow:TestWorkflow' , attributes : exp_cl_attrs )
429
+ exp_start_wf . add_child ( name : 'RunWorkflow:TestWorkflow' , attributes : exp_run_attrs )
430
+
431
+ # Wait for task completion so update isn't accidentally first before run
432
+ assert_eventually { assert handle . fetch_history_events . any? ( &:workflow_task_completed_event_attributes ) }
433
+
434
+ # Update calls child and sends signals to it in two ways
435
+ child_id , child_run_id , child_result = handle . execute_update ( TestWorkflow . update ,
436
+ scenario , id : 'my-update-id' )
437
+ exp_update = exp_root . add_child ( name : 'StartWorkflowUpdate:update' ,
438
+ attributes : exp_cl_attrs . merge ( { 'temporalUpdateID' => 'my-update-id' } ) )
439
+ # Expected span for update
440
+ exp_hnd_update = exp_start_wf . add_child (
441
+ name : 'HandleUpdate:update' ,
442
+ attributes : exp_run_attrs . merge ( { 'temporalUpdateID' => 'my-update-id' } ) ,
443
+ links : [ exp_update ]
444
+ )
445
+ # Expected for children
446
+ exp_child_run_attrs = { 'temporalWorkflowID' => child_id , 'temporalRunID' => child_run_id }
447
+ exp_child_start = exp_hnd_update . add_child ( name : 'StartChildWorkflow:TestWorkflow' , attributes : exp_run_attrs )
448
+ exp_child_start
449
+ . add_child ( name : 'RunWorkflow:TestWorkflow' , attributes : exp_child_run_attrs )
450
+ . add_child ( name : 'CompleteWorkflow:TestWorkflow' , attributes : exp_child_run_attrs )
451
+
452
+ # There are cases where signal comes _before_ start and cases where signal _comes_ after and server gives us
453
+ # no way of knowing that a child _actually_ began running, so we check whether task completed comes before
454
+ # signal
455
+ assert_equal 'workflow-done' , child_result
456
+ child_events = env . client . workflow_handle ( child_id . to_s ) . fetch_history_events . to_a
457
+ signal_comes_first = child_events . index ( &:workflow_execution_signaled_event_attributes ) . to_i <
458
+ child_events . index ( &:workflow_task_completed_event_attributes ) . to_i
459
+ # Signal we send to the child
460
+ exp_sig = if scenario == :child_workflow_child_signal
461
+ exp_hnd_update . add_child ( name : 'SignalChildWorkflow:signal' , attributes : exp_run_attrs )
462
+ else
463
+ exp_hnd_update . add_child ( name : 'SignalExternalWorkflow:signal' , attributes : exp_run_attrs )
464
+ end
465
+ exp_child_start . add_child (
466
+ name : 'HandleSignal:signal' ,
467
+ attributes : exp_child_run_attrs ,
468
+ links : [ exp_sig ] ,
469
+ insert_at : signal_comes_first ? 0 : 1
470
+ )
471
+ end
472
+ assert_equal exp_root . to_s_indented , act_root . to_s_indented ,
473
+ "Expected:\n #{ exp_root . to_s_indented } \n Actual:#{ act_root . to_s_indented } "
435
474
end
436
- assert_equal exp_root . to_s_indented , act_root . to_s_indented
437
475
end
438
476
439
477
def test_continue_as_new
@@ -458,6 +496,29 @@ def test_continue_as_new
458
496
assert_equal exp_root . to_s_indented , act_root . to_s_indented
459
497
end
460
498
499
+ def test_always_create_workflow_spans
500
+ # Untraced client has no spans by default
501
+ act = trace_workflow ( :complete , start_with_untraced_client : true , check_root : false ) do |handle |
502
+ assert_equal 'workflow-done' , handle . result
503
+ end
504
+ assert_empty act . children
505
+
506
+ # Untraced client has no spans by default
507
+ exp_root = ExpectedSpan . new ( name : 'root' )
508
+ act = trace_workflow (
509
+ :complete ,
510
+ start_with_untraced_client : true ,
511
+ always_create_workflow_spans : true ,
512
+ check_root : false
513
+ ) do |handle |
514
+ exp_attrs = { 'temporalWorkflowID' => handle . id , 'temporalRunID' => handle . result_run_id }
515
+ exp_run_wf = exp_root . add_child ( name : 'RunWorkflow:TestWorkflow' , attributes : exp_attrs )
516
+ exp_run_wf . add_child ( name : 'CompleteWorkflow:TestWorkflow' , attributes : exp_attrs )
517
+ assert_equal 'workflow-done' , handle . result
518
+ end
519
+ assert_equal exp_root . children . first &.to_s_indented , act . to_s_indented
520
+ end
521
+
461
522
ExpectedSpan = Data . define ( :name , :children , :attributes , :links , :exception_message ) # rubocop:disable Layout/ClassStructure
462
523
463
524
class ExpectedSpan
@@ -493,13 +554,16 @@ def self.from_span_data(all_spans)
493
554
end
494
555
495
556
def initialize ( name :, children : [ ] , attributes : { } , links : [ ] , exception_message : nil )
496
- children = children . to_set
497
557
super
498
558
end
499
559
500
- def add_child ( name :, attributes : { } , links : [ ] , exception_message : nil )
560
+ def add_child ( name :, attributes : { } , links : [ ] , exception_message : nil , insert_at : nil )
501
561
span = ExpectedSpan . new ( name :, attributes :, links :, exception_message :)
502
- children << span
562
+ if insert_at . nil?
563
+ children << span
564
+ else
565
+ children . insert ( insert_at , span )
566
+ end
503
567
span
504
568
end
505
569
0 commit comments