@@ -5792,6 +5792,90 @@ await ExecuteWorkerAsync<NullWithCodecWorkflow>(
5792
5792
client ) ;
5793
5793
}
5794
5794
5795
+ [ Workflow ]
5796
+ public class DetachedCancellationWorkflow
5797
+ {
5798
+ public class Activities
5799
+ {
5800
+ public TaskCompletionSource WaitingForCancel { get ; } = new ( ) ;
5801
+
5802
+ public bool CleanupCalled { get ; set ; }
5803
+
5804
+ [ Activity ]
5805
+ public async Task WaitForCancel ( )
5806
+ {
5807
+ WaitingForCancel . SetResult ( ) ;
5808
+ await Task . Delay (
5809
+ Timeout . Infinite ,
5810
+ ActivityExecutionContext . Current . CancellationToken ) ;
5811
+ }
5812
+
5813
+ [ Activity ]
5814
+ public void Cleanup ( ) => CleanupCalled = true ;
5815
+ }
5816
+
5817
+ [ WorkflowRun ]
5818
+ public async Task RunAsync ( )
5819
+ {
5820
+ // Wait forever for cancellation, then cleanup
5821
+ try
5822
+ {
5823
+ await Workflow . ExecuteActivityAsync (
5824
+ ( Activities acts ) => acts . WaitForCancel ( ) ,
5825
+ new ( ) { StartToCloseTimeout = TimeSpan . FromMinutes ( 10 ) } ) ;
5826
+ }
5827
+ catch ( Exception e ) when ( TemporalException . IsCanceledException ( e ) )
5828
+ {
5829
+ // Run cleanup with another token
5830
+ using var detachedCancelSource = new CancellationTokenSource ( ) ;
5831
+ await Workflow . ExecuteActivityAsync (
5832
+ ( Activities acts ) => acts . Cleanup ( ) ,
5833
+ new ( )
5834
+ {
5835
+ StartToCloseTimeout = TimeSpan . FromMinutes ( 10 ) ,
5836
+ CancellationToken = detachedCancelSource . Token ,
5837
+ } ) ;
5838
+ // Rethrow
5839
+ throw ;
5840
+ }
5841
+ }
5842
+ }
5843
+
5844
+ [ Fact ]
5845
+ public async Task ExecuteWorkflowAsync_DetachedCancellation_WorksProperly ( )
5846
+ {
5847
+ var activities = new DetachedCancellationWorkflow . Activities ( ) ;
5848
+ await ExecuteWorkerAsync < DetachedCancellationWorkflow > (
5849
+ async worker =>
5850
+ {
5851
+ // Start workflow
5852
+ var handle = await Client . StartWorkflowAsync (
5853
+ ( DetachedCancellationWorkflow wf ) => wf . RunAsync ( ) ,
5854
+ new ( id : $ "workflow-{ Guid . NewGuid ( ) } ", taskQueue : worker . Options . TaskQueue ! ) ) ;
5855
+
5856
+ // Wait until waiting for cancel
5857
+ await activities . WaitingForCancel . Task ;
5858
+
5859
+ // Send workflow cancel
5860
+ await handle . CancelAsync ( ) ;
5861
+
5862
+ // Confirm canceled
5863
+ var exc = await Assert . ThrowsAsync < WorkflowFailedException > (
5864
+ ( ) => handle . GetResultAsync ( ) ) ;
5865
+ Assert . IsType < CanceledFailureException > ( exc . InnerException ) ;
5866
+
5867
+ // Confirm cleanup called
5868
+ Assert . True ( activities . CleanupCalled ) ;
5869
+
5870
+ // Run through replayer to confirm deterministic on replay
5871
+ var history = await handle . FetchHistoryAsync ( ) ;
5872
+ var replayer = new WorkflowReplayer (
5873
+ new WorkflowReplayerOptions ( ) . AddWorkflow < DetachedCancellationWorkflow > ( ) ) ;
5874
+ await replayer . ReplayWorkflowAsync ( history ) ;
5875
+ } ,
5876
+ new TemporalWorkerOptions ( ) . AddAllActivities ( activities ) ) ;
5877
+ }
5878
+
5795
5879
internal static Task AssertTaskFailureContainsEventuallyAsync (
5796
5880
WorkflowHandle handle , string messageContains )
5797
5881
{
0 commit comments