99use Temporal \Client \WorkflowStubInterface ;
1010use Temporal \Exception \Failure \CanceledFailure ;
1111use Temporal \Exception \Failure \ChildWorkflowFailure ;
12+ use Temporal \Promise ;
1213use Temporal \Tests \Acceptance \App \Attribute \Stub ;
1314use Temporal \Tests \Acceptance \App \TestCase ;
1415use Temporal \Workflow ;
16+ use Temporal \Workflow \CancellationScopeInterface ;
1517use Temporal \Workflow \WorkflowInterface ;
1618use Temporal \Workflow \WorkflowMethod ;
1719
1820class CancelAbandonTest extends TestCase
1921{
22+ /**
23+ * If an abandoned Child Workflow is started in the main Workflow scope,
24+ * the Child Workflow should not be affected by the cancellation of the parent workflow.
25+ * But need to consider that we can miss the Cancellation signal if awaiting only on the Child Workflow.
26+ * In the {@see MainScopeWorkflow} we use Timer + Child Workflow to ensure we catch the Cancellation signal.
27+ */
2028 #[Test]
21- public static function check (
22- #[Stub('Harness_ChildWorkflow_CancelAbandon ' )]
29+ public static function childWorkflowInMainScope (
30+ #[Stub('Harness_ChildWorkflow_CancelAbandon_MainScope ' , args: [ ' test 42 ' ] )]
2331 WorkflowStubInterface $ stub ,
2432 WorkflowClientInterface $ client ,
2533 ): void {
26- self ::markTestSkipped ('To be resolved with https://github.com/temporalio/sdk-php/issues/634 ' );
34+ self ::runTestScenario ($ stub , $ client , 'test 42 ' );
35+ }
36+
37+ /**
38+ * If an abandoned Child Workflow is started in an async Scope {@see Workflow::async()} that is later cancelled,
39+ * the Child Workflow should not be affected by the cancellation of the parent workflow.
40+ * Int his case the Scope will throw the CanceledFailure.
41+ * @see InnerScopeCancelWorkflow
42+ */
43+ #[Test]
44+ public static function childWorkflowInInnerScopeCancel (
45+ #[Stub('Harness_ChildWorkflow_CancelAbandon_InnerScopeCancel ' , args: ['baz ' ])]
46+ WorkflowStubInterface $ stub ,
47+ WorkflowClientInterface $ client ,
48+ ): void {
49+ self ::runTestScenario ($ stub , $ client , 'baz ' );
50+ }
51+
52+ /**
53+ * If an abandoned Child Workflow is started in an async scope {@see Workflow::async()} that
54+ * is later cancelled manually by a Signal to the parent workflow {@see InnerScopeCancelWorkflow::close()},
55+ * the Child Workflow should not be affected by the cancellation of the parent scope.
56+ */
57+ #[Test]
58+ public static function childWorkflowInClosingInnerScope (
59+ #[Stub('Harness_ChildWorkflow_CancelAbandon_InnerScopeCancel ' , args: ['foo bar ' ])]
60+ WorkflowStubInterface $ stub ,
61+ WorkflowClientInterface $ client ,
62+ ): void {
63+ # Get Child Workflow Stub
64+ $ child = self ::getChildWorkflowStub ($ client , $ stub );
65+
66+ # Cancel the async scope
67+ /** @see InnerScopeCancelWorkflow::close() */
68+ $ stub ->signal ('close ' );
69+ # Expect the CanceledFailure in the parent workflow
70+ self ::assertSame ('cancelled ' , $ stub ->getResult (timeout: 5 ));
71+
72+ # Signal the child workflow to exit
73+ $ child ->signal ('exit ' );
74+ # No canceled failure in the child workflow
75+ self ::assertSame ('foo bar ' , $ child ->getResult ());
76+ }
77+
78+ /**
79+ * Send cancel to the parent workflow and expect the child workflow to be abandoned
80+ * and not cancelled.
81+ */
82+ private static function runTestScenario (
83+ WorkflowStubInterface $ stub ,
84+ WorkflowClientInterface $ client ,
85+ string $ result ,
86+ ): void {
87+ # Get Child Workflow Stub
88+ $ child = self ::getChildWorkflowStub ($ client , $ stub );
89+
90+ # Cancel the parent workflow
91+ $ stub ->cancel ();
92+ # Expect the CanceledFailure in the parent workflow
93+ self ::assertSame ('cancelled ' , $ stub ->getResult (timeout: 5 ));
94+
95+ # Signal the child workflow to exit
96+ $ child ->signal ('exit ' );
97+ # No canceled failure in the child workflow
98+ self ::assertSame ($ result , $ child ->getResult ());
99+ }
27100
101+ /**
102+ * Get Child Workflow Stub
103+ */
104+ private static function getChildWorkflowStub (
105+ WorkflowClientInterface $ client ,
106+ WorkflowStubInterface $ stub ,
107+ ): WorkflowStubInterface {
28108 # Find the child workflow execution ID
29109 $ deadline = \microtime (true ) + 10 ;
30110 child_id:
@@ -40,52 +120,99 @@ public static function check(
40120 goto child_id;
41121 }
42122
43- self ::assertNotNull ($ execution , 'Child workflow execution not found in history ' );
123+ self ::assertNotNull ($ execution , 'Child Workflow execution not found in the history. ' );
44124
45125 # Get Child Workflow Stub
46- $ child = $ client ->newUntypedRunningWorkflowStub (
126+ return $ client ->newUntypedRunningWorkflowStub (
47127 $ execution ->getWorkflowId (),
48128 $ execution ->getRunId (),
49129 'Harness_ChildWorkflow_CancelAbandon_Child ' ,
50130 );
51-
52- # Cancel the parent workflow
53- $ stub ->cancel ();
54- # Expect the CanceledFailure in the parent workflow
55- self ::assertSame ('cancelled ' , $ stub ->getResult ());
56-
57- # Signal the child workflow to exit
58- $ child ->signal ('exit ' );
59- # No canceled failure in the child workflow
60- self ::assertSame ('test 42 ' , $ child ->getResult ());
61131 }
62132}
63133
64134#[WorkflowInterface]
65- class MainWorkflow
135+ class MainScopeWorkflow
66136{
67- #[WorkflowMethod('Harness_ChildWorkflow_CancelAbandon ' )]
68- public function run ()
137+ #[WorkflowMethod('Harness_ChildWorkflow_CancelAbandon_MainScope ' )]
138+ public function run (string $ input )
69139 {
70- $ child = Workflow::newUntypedChildWorkflowStub (
140+ /** @see ChildWorkflow */
141+ $ stub = Workflow::newUntypedChildWorkflowStub (
71142 'Harness_ChildWorkflow_CancelAbandon_Child ' ,
72143 Workflow \ChildWorkflowOptions::new ()
144+ ->withWorkflowRunTimeout ('20 seconds ' )
73145 ->withParentClosePolicy (Workflow \ParentClosePolicy::Abandon),
74146 );
75147
76- yield $ child ->start ('test 42 ' );
148+ yield $ stub ->start ($ input );
149+
150+ try {
151+ yield Promise::race ([$ stub ->getResult (), Workflow::timer (5 )]);
152+ return 'timer ' ;
153+ } catch (CanceledFailure ) {
154+ return 'cancelled ' ;
155+ } catch (ChildWorkflowFailure $ failure ) {
156+ # Check CanceledFailure
157+ return $ failure ->getPrevious ()::class === CanceledFailure::class
158+ ? 'cancelled '
159+ : throw $ failure ;
160+ } finally {
161+ yield Workflow::asyncDetached (function () {
162+ # We shouldn't complete the Workflow immediately:
163+ # all the commands from the tick must be sent for testing purposes.
164+ yield Workflow::timer (1 );
165+ });
166+ }
167+ }
168+ }
169+
170+ #[WorkflowInterface]
171+ class InnerScopeCancelWorkflow
172+ {
173+ private CancellationScopeInterface $ scope ;
174+
175+ #[WorkflowMethod('Harness_ChildWorkflow_CancelAbandon_InnerScopeCancel ' )]
176+ public function run (string $ input )
177+ {
178+ $ this ->scope = Workflow::async (static function () use ($ input ) {
179+ /** @see ChildWorkflow */
180+ $ stub = Workflow::newUntypedChildWorkflowStub (
181+ 'Harness_ChildWorkflow_CancelAbandon_Child ' ,
182+ Workflow \ChildWorkflowOptions::new ()
183+ ->withWorkflowRunTimeout ('20 seconds ' )
184+ ->withParentClosePolicy (Workflow \ParentClosePolicy::Abandon),
185+ );
186+ yield $ stub ->start ($ input );
187+
188+ return yield $ stub ->getResult ('string ' );
189+ });
190+
77191
78192 try {
79- return yield $ child ->getResult ();
193+ yield Promise::race ([Workflow::timer (5 ) ,$ this ->scope ]);
194+ return 'timer ' ;
80195 } catch (CanceledFailure ) {
81196 return 'cancelled ' ;
82197 } catch (ChildWorkflowFailure $ failure ) {
83198 # Check CanceledFailure
84199 return $ failure ->getPrevious ()::class === CanceledFailure::class
85200 ? 'cancelled '
86201 : throw $ failure ;
202+ } finally {
203+ yield Workflow::asyncDetached (function () {
204+ # We shouldn't complete the Workflow immediately:
205+ # all the commands from the tick must be sent for testing purposes.
206+ yield Workflow::timer (1 );
207+ });
87208 }
88209 }
210+
211+ #[Workflow \SignalMethod('close ' )]
212+ public function close (): void
213+ {
214+ $ this ->scope ->cancel ();
215+ }
89216}
90217
91218#[WorkflowInterface]
0 commit comments