Can a child workflow/activity be triggered from a Workflow CancellationScope in the exception block?

I have a workflow with CancellationScope. Can a child workflow/activity be triggered from CancellationScope in the exception block?

Are there any restrictions?

Below is an example where I call an activity method in the exception block?

            try {
                cancelScope.run();
            } catch (Exception e) {
                cancelScope.cancel();
                helloActivity.cancelStuff();
            }

Your sample should run without any problem as helloActivity.cancelStuff() is executed outside of the scope that was canceled.

A trickier use case is when you need to run cleanup logic from within a canceled scope. Then you need to run a child workflow in a detached scope:

             CancellationScope cancelScope = Workflow.newCancellationScope(
                      (scope) -> {
                                  try {
                                    Workflow.sleep(Duration.ofHours(1));
                                  } finally {
                                    Workflow.newDetachedCancellationScope(
                                       () -> { 
                                          helloActivity.cancelStuff();
                                       });
                                  }
                                 });

Thanks

Yes, in fact i am trying to run clean up logic via a child workflow.

So triggering a child workflow as below should work with detached scope?

                 try {
                            cancelScope.run();
                        } catch (Exception e) {
                            cancelScope.cancel();

                            CancellationScope detached = Workflow.newDetachedCancellationScope(
                                    () -> {
                                        Promise<Void> promise = procedure(
                                                Workflow.newChildWorkflowStub(
                                                        ChildWorkflow.class, options)::doStuff, arg1);
                                        promise.get();
                                    });
                                  
                            detached.run();
                        }

You don’t need the detached scope in this case as the exception handler is not part of the scope that was canceled.

Sorry I am confused as to when to use newDetachedCancellationScope.

Your comments

A trickier use case is when you need to run cleanup logic from within a canceled scope. Then you need to run a child workflow in a detached scope:

Would this be a right approach?

CancellationScope cancelScope = Workflow.newCancellationScope(() -> {
                // do stuff
            });
            try {
                cancelScope.run();
            } catch (Exception e) {
                cancelScope.cancel();
                CancellationScope detached = Workflow.newDetachedCancellationScope(
                        () -> {
                            //clean up logic
                            Promise<Void> promise = procedure(
                                    Workflow.newChildWorkflowStub(
                                            ChildWF.class, options)::doCleanUpStuff, arg1);
                            promise.get();
                        });
                detached.run();
            }

You need to use detached cancellation scope for the code that runs within cancelScope which is where your comment says “do stuff”. The cancelScope.cancel only cancels that code. The catch statement is outside of that scope, so there is no need to use the detached scope.

Thank you for clarifying

I do get this error when i call a Workflow in the exception block and the parent workflow is cancelled

execute called from a canceled scope

try {
          cancelScope.run();
      } catch (Exception e) {

          cancelScope.cancel();


          // clean up logic

          Promise<Void> promise =
              procedure(
                  Workflow.newChildWorkflowStub(CleanupWorkflow.class, options)::cleanUp,
                  signals);
          promise.get();

      }

It works fine if I wrap the child workflow call in newDetachedCancellationScope

 try {
      cancelScope.run();
    } catch (Exception e) {
      cancelScope.cancel();
      CancellationScope detached =
          Workflow.newDetachedCancellationScope(
              () -> {
                // clean up logic
                Promise<Void> promise =
                    procedure(
                        Workflow.newChildWorkflowStub(CleanupWorkflow.class, options)::cleanUp,
                        signals);
                promise.get();
              });
      detached.run();
    }

Here is the complete sample - samples-java/HelloSignalWorkflowImpl.java at SignalWF_Test · ansujohn/samples-java · GitHub

CancellationScopes are hierarchical. If you cancel a parent scope all child scopes are canceled. If a child scope is canceled directly the parent scope is not affected.

In the original example you canceled the child scope explicitly through cancelScope.cancel() command. This didn’t affect the parent scope that contained the try-catch statement. So the detached cancellation scope wasn’t needed.

In the last example, you canceled the whole workflow which canceled the root workflow scope that wraps the main workflow method. As in this case the scope that contains try-catch is canceled the detached cancellation scope is needed.

Thanku again.

I am trying to unit test the same scenario, cancel scenario with a mock for the CleanupWorkflow.

I get the error

java.lang.IllegalStateException: Operation allowed only while eventLoop is running

Sample code is here :

I have used mocking workflow example from here - samples-java/HelloChildTest.java at 986f4965ca9799af36784f80e28a7a04c0e6f9eb · temporalio/samples-java · GitHub

Also get this error Argument passed to verify() should be a mock but is null!

Cancellation is asynchronous. On line 121 you request cancellation and don’t wait for it to be processed before checking.

Thanks
Adding testEnv.sleep worked, though as you pointed out in another thread, it doesn’t skip and hence increases the test run time.

1 Like

You can wait for the workflow to complete instead of sleeping after canceling it.

Okay will try that.

Test using a mocked child workflow has issues.

Gives the error

Argument passed to verify() should be a mock but is null!

I’m not sure why this happens. May be the way factory is implemented affects the result?