Workflow timeout and saga compensation

HI,
I have a question related to workflow timeout and saga campnsation.

Let’s say I have a workflow which defines a saga object and registers several compensation activities (depending on code path).
The workflow defines a workflow execution timeout.

Is there any way to have the compensation activities invoked even if the workflow timeout expires?
Thanks

1 Like

Hello @AndreaColombo

Workflow timeout is performed by the server and can not be handled in workflow code

You can use workflow timer to handle business-level timeouts.

      Saga saga = new Saga...;

      Workflow.newTimer(Duration.ofSeconds(3))
          .thenApply(
              result -> {
                saga.compensate();
                return result;
              });
1 Like

it makes sense.
thanks for the prompt answer

I have one more question, anyway.
Is there any other way to trigger the compensation on workflow timeout without defining the timer within the workflow code? In a way that is “transparent” to the workflow implementation?
Thanks
A

Hello @AndreaColombo

You can maybe try putting the logic in listeners instead of in workflow code, not sure if it will work I have not tested it.

But in the end, is still part of the workflow implementation.

You can abstract this behavior in its own utility class or even interceptor.

Question to this solution - lets say I have a saga like this:

final Saga saga = new Saga(sagaOptions);
Workflow.newTimer(Duration.ofSeconds(3))
          .thenApply(
              result -> {
                saga.compensate();
                return result;
              });
try {
            activity1.doStuff();
            saga.addCompensation(activity1::compensate);
            activity2.doStuff();
}
catch(ActivityFailure cause) {
            saga.compensate();
            throw cause;
}

What happens, if the timeout happens after starting activity1.doStuff() and before reaching saga.addCompensation? Is workflow waiting for activity1 to finish execution before starting compensation? Even if so, following statement addCompensation won’t be executed and therefore compensation of activity1 won’t happen?
It seems weird to add compensation first and then calling actual operation, especially if the compensation depends on the operation outcome.

You don’t want to call compensate on a timer in a separate thread. This would lead to a behavior in the main thread that is hard to understand. I would recommend calling cancellation using CancellationScope on the timeout.
Something like:

    final Saga saga = new Saga(sagaOptions);
    
    CancellationScope cancellationScope = Workflow.newCancellationScope(()-> {
      try {
        activity1.doStuff();
        saga.addCompensation(activity1::compensate);
        activity2.doStuff();
      } catch (ActivityFailure cause) {
        saga.compensate();
        throw cause;
      }
    });
    Workflow.newTimer(Duration.ofSeconds(3))
            .thenApply(
                    result -> {
                      cancellationScope.cancel();
                      return null;
                    });

    cancellationScope.run();

  }

1 Like

Thank you for the quick response. So if I understand correctly with the CancellationScope, when the timeout is triggered, cancellation for the workflow is called and saga is then compensated within same thread as its execution.
Does the thread upon cancellation wait for current activity execution to finish and also does it add the following compensation? Or is saga compensation executed immediately and therefore the last activity may not be compensated?

Thank you for the quick response. So if I understand correctly with the CancellationScope, when the timeout is triggered, cancellation for the workflow is called and saga is then compensated within same thread as its execution.

The cancellation of the code in the cancellation scope is triggered, not the whole workflow. Each workflow runs in a root cancellation scope which is canceled when the whole workflow is canceled.

Does the thread upon cancellation wait for current activity execution to finish and also does it add the following compensation?

You can configure the behavior of an activity in case of compensation through ActivityOptions.cancellationType. You can decide to wait or send cancellation request and immediately return an exception. As activity fails with an exception then compensation is not going to be added. To handle this use case add compensation before calling an activity:

        saga.addCompensation(activity1::compensate);
        activity1.doStuff();

Note that in this case the compensate should account for cases that activity never started.

1 Like

ah, I was worried compensation needs to be done like this, which makes it much more difficult to implement. But now it seems clear, thank you very much.

Hi all

I tried to use this pattern - cancellationScope + Timer - to run compensations if a timeout fires for workflow and meet an unexpected behaviour:
my compensation activity immediately fails because of CanceledFailure ::

Is it true cancel should trigger compensation and allow it to complete?


UPDATE:

Just found another thread in which you say we need to use DetachedCancellationScope to run compensations on cancelFailures.
This fixed my issue.

Thank you

1 Like