How to limit amount of action retries in business process

OOTB hybris allows to automatically retry business process action after some time. To do this is enough to set delay in RetryLaterException and throw it. But there is no OOTB implementation of limiting amount of such retries in business processes.

Seems like to implement such behavior is enough to:

  1. Extend BusinessProcess with custom integer attribute, which will hold current retry iteration:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10

<itemtype code="BusinessProcess" autocreate="false" generate="false">
    <attributes>
        <attribute qualifier="retry" type="java.lang.Integer">
            <defaultvalue>new Integer(0)</defaultvalue>
            <persistence type="property"/>
            <modifiers optional="false" read="true" write="true"/>
        </attribute>
    </attributes>
</itemtype>
  1. In business process action populate and save this attribute before throwing retry exception:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
   public class ExampleAction extends AbstractSimpleDecisionAction<OrderProcessModel> {

    @Override
    public Transition executeAction(final OrderProcessModel process) {
        if (process.getRetry() <= MAX_RETRIES) {
            process.setRetry(process.getRetry() + 1);
            modelService.save(process);

            RetryLaterException ex = new RetryLaterException("Retry ");
            ex.setDelay(1000);
            throw ex;
        } else {
            throw new IllegalStateException("Fail process.");
        }
    }
}

Unfortunately it would not work and after debugging you could see that retry attribute is always 0 and is not persisted to db. This is OOTB behaviour of process execution in DefaultBusinessProcessService. All actions are executed within transactions. If action execution throws error, transaction would be reverted and no changes would be persisted to DB. So retry attribute for BusinessProcessModel will never change its value.

According to DefaultTaskExecutionStrategy#run coulbe used isRollBack value of RetryLaterException to skip exception throwing, so Transaction#finishExecute will not rollback changes, what will allow to save amount of retries on BusinessProcessModel.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class ExampleAction extends AbstractSimpleDecisionAction<OrderProcessModel> {

    @Resource
    private ModelService modelService;

    @Override
    public Transition executeAction(final OrderProcessModel process) {
        if (process.getRetry() <= MAX_RETRIES) {
            process.setRetry(process.getRetry() + 1);
            modelService.save(process);

            RetryLaterException ex = new RetryLaterException("Retry ");
            ex.setDelay(1000);
            ex.setRollBack(false); // Will skip transaction rollback on exception
            throw ex;
        } else {
            throw new IllegalStateException("Fail process.");
        }
    }
}

Keep in mind that you retry method for RetryLaterException can be set up. By default, is used RetryLaterException.Method.EXPONENTIAL, which will use 2 to the power of retries with some random deviation. It means that code above would be executed in around 1 sec, 2 sec, 4 sec, 8 sec etc.

To manually control time of retries could be used RetryLaterException.Method.LINEAR:

1
2
3
4
5
6
7
8
9
// Will retry in 10 min, in 20 min, in 30 min etc ..
int delayInMinutes = businessProcessModel.getRetry() * 10;
RetryLaterException ex = new RetryLaterException();
ex.

setDelay(delayInMinutes *60*1000);
ex.

setMethod(RetryLaterException.Method.LINEAR);

UPD 2020-09-03

One more way to bypass not saving BusinessProcessModel on exception throw. For that we should close current transaction, save changes in BusinessProcessModel separate transaction, and create one more transaction, which would be roll backed by DefaultBusinessProcessService.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class ExampleAction extends AbstractSimpleDecisionAction<OrderProcessModel> {

    @Resource
    private ModelService modelService;

    @Override
    public Transition executeAction(final OrderProcessModel process) {

        modelService.refresh(process);
        if (process.getRetry() <= MAX_RETRIES) {
            // On exception throw in action hybris transaction manager will rollback everything
            // But we need to store amount of retries, that`s why transaction is rollbacked here
            // and new transaction is started and commited

            // Rollback action transaction
            Transaction currentTransaction = Transaction.current();
            if (currentTransaction.isRunning()) {
                currentTransaction.rollback();
            }

            // Create and commit transaction to persist amount of retries
            currentTransaction.begin();
            process.setRetry(process.getRetry() + 1);
            modelService.save(process);
            currentTransaction.commit();

            // Create new transaction, which would be rollbacked by task service
            currentTransaction.begin();

            RetryLaterException ex = new RetryLaterException("Retry ");
            ex.setDelay(1000);
            throw ex;
        } else {
            throw new IllegalStateException("Fail process.");
        }
    }
}
comments powered by Disqus