anjo
March 17, 2021, 4:51pm
1
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();
}
maxim
March 17, 2021, 5:17pm
2
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();
});
}
});
anjo
March 17, 2021, 5:26pm
3
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();
}
maxim
March 17, 2021, 6:09pm
4
You don’t need the detached scope in this case as the exception handler is not part of the scope that was canceled.
anjo
March 17, 2021, 6:22pm
5
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();
}
maxim
March 17, 2021, 7:13pm
6
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.
anjo
March 18, 2021, 6:35pm
8
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();
}
anjo
March 18, 2021, 6:52pm
9
maxim
March 18, 2021, 9:38pm
10
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.
anjo
March 19, 2021, 10:08am
11
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 :
workflow.signalChange("INIT");
workflow.signalChange("STATE1");
workflow.signalChange("STATE2");
workflow.signalChange("STATE3");
testEnv.sleep(Duration.ofSeconds(2));
verify(greetingActivity, times(4)).composeGreeting(any());
}
@Test
public void testCancellation() {
GreetingActivity greetingActivity = mock(GreetingActivity.class);
when(greetingActivity.composeGreeting(anyString())).thenReturn("Hello Test");
worker.registerActivitiesImplementations(greetingActivity);
// As new mock is created on each workflow task the only last one is useful to verify calls.
AtomicReference<CleanupWorkflow> lastChildMock = new AtomicReference<>();
// Factory is called to create a new workflow object on each workflow task.
worker.addWorkflowImplementationFactory(
CleanupWorkflow.class,
I have used mocking workflow example from here - https://github.com/temporalio/samples-java/blob/986f4965ca9799af36784f80e28a7a04c0e6f9eb/src/test/java/io/temporal/samples/hello/HelloChildTest.java
Also get this error Argument passed to verify() should be a mock but is null!
// Start workflow
WorkflowClient.start(workflow::start);
workflow.signalChange("INIT");
workflow.signalChange("STATE1");
testEnv.sleep(Duration.ofSeconds(2));
client.newUntypedWorkflowStub(wfId.toString(), empty(), empty()).cancel();
testEnv.sleep(Duration.ofSeconds(2));
verify(greetingActivity, times(2)).composeGreeting(any());
CleanupWorkflow cleanupWorkflow = lastChildMock.get();
verify(cleanupWorkflow, times(1)).cleanUp(anyList());
}
}
maxim
March 19, 2021, 3:51pm
12
Cancellation is asynchronous. On line 121 you request cancellation and don’t wait for it to be processed before checking.
anjo
March 19, 2021, 4:13pm
13
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
maxim
March 19, 2021, 4:50pm
14
You can wait for the workflow to complete instead of sleeping after canceling it.
anjo
March 19, 2021, 5:02pm
15
Okay will try that.
Test using a mocked child workflow has issues.
// Start workflow
WorkflowClient.start(workflow::start);
workflow.signalChange("INIT");
workflow.signalChange("STATE1");
testEnv.sleep(Duration.ofSeconds(2));
client.newUntypedWorkflowStub(wfId.toString(), empty(), empty()).cancel();
testEnv.sleep(Duration.ofSeconds(2));
verify(greetingActivity, times(2)).composeGreeting(any());
CleanupWorkflow cleanupWorkflow = lastChildMock.get();
verify(cleanupWorkflow, times(1)).cleanUp(anyList());
}
}
Gives the error
Argument passed to verify() should be a mock but is null!
maxim
March 19, 2021, 5:07pm
16
I’m not sure why this happens. May be the way factory is implemented affects the result?