Статьи

Весна Повторите, потому что зима приближается

Хорошо, это на самом деле не о зиме, которая, как мы все знаем , уже наступила . Речь идет о Spring Retry, небольшой библиотеке Spring Framework, которая позволяет нам добавлять функциональность повтора к любой задаче, которую следует повторить.

Здесь есть очень хорошее руководство , объясняющее, как настраиваются простые повторные попытки и восстановление. Он очень хорошо объясняет, как добавить зависимость при повторном запуске , использовать аннотации @Retryable и @Recover и использовать RetryTemplate с простыми политиками. Я хотел бы остановиться на немного более сложном случае, когда мы действительно хотим применить другое поведение повторения в зависимости от типа исключения. Это имеет смысл, потому что мы можем знать, что некоторые исключения восстанавливаемы, а некоторые нет, и поэтому не имеет особого смысла пытаться восстановить их. Для этого есть специальная реализация стратегии повторения, которая называется ExceptionClassifierRetryPolicy , которая используется с Spring RetryTemplate .

Давайте предположим, что мы можем восстановить только исключения IO и пропустить все остальные . Мы создадим три класса для расширения RetryCallback и один класс для расширения RecoveryCallback, чтобы лучше показать, что происходит внутри:

01
02
03
04
05
06
07
08
09
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
private class SuccessCallback implements RetryCallback<Boolean, RuntimeException> {
        @Override
        public Boolean doWithRetry(RetryContext context) throws RuntimeException {
            System.out.println("Success callback: attempt " + context.getRetryCount());
            return true;
        }
    }
 
    private class ExceptionCallback implements RetryCallback<Boolean, Exception> {
        @Override
        public Boolean doWithRetry(RetryContext context) throws Exception {
            System.out.println("Exception callback: attempt " + context.getRetryCount());
            throw new Exception("Test Exception");
        }
    }
 
    private class SpecificExceptionCallback implements RetryCallback<Boolean, IOException> {
        @Override
        public Boolean doWithRetry(RetryContext context) throws IOException {
            System.out.println("IO Exception callback: attempt " + context.getRetryCount());
            throw new IOException("Test IO Exception");
        }
    }
 
    private class LoggingRecoveryCallback implements RecoveryCallback<Boolean> {
        @Override
        public Boolean recover(RetryContext context) throws Exception {
            System.out.println("Attempts exhausted. Total: " + context.getRetryCount());
            System.out.println("Last exception: " + Optional.ofNullable(context.getLastThrowable())
                    .orElse(new Throwable("No exception thrown")).getMessage());
            System.out.println("\n");
            return false;
        }
    }

Затем мы настраиваем наш RetryTemplate . Мы будем использовать SimpeRetryPolicy с фиксированным числом попыток для IOException и NeverRetryPolicy, который просто разрешает начальную попытку для всего остального.

01
02
03
04
05
06
07
08
09
10
11
*
            We want to retry on IOException only.
            Other Exceptions won't be retried.
            IOException will be retried three times, counting the initial attempt.
         */
        final ExceptionClassifierRetryPolicy exRetryPolicy = new ExceptionClassifierRetryPolicy();
        exRetryPolicy.setPolicyMap(new HashMap<Class<? extends Throwable>, RetryPolicy>() {{
            put(IOException.class, new SimpleRetryPolicy(3));
            put(Exception.class, new NeverRetryPolicy());
        }});
        retryTemplate.setRetryPolicy(exRetryPolicy);

Теперь нам нужно использовать эти обратные вызовы, чтобы продемонстрировать, как они работают. Сначала успешное выполнение, которое очень просто:

1
2
3
// we do not catch anything here
        System.out.println("\n*** Executing successfull callback...");
        retryTemplate.execute(new SuccessCallback(), new LoggingRecoveryCallback());

Выход для него выглядит следующим образом:

1
2
*** Executing successfull callback...
Success callback: attempt 0

Тогда исключение :

1
2
3
4
5
6
7
// we catch Exception to allow the program to continue
       System.out.println("\n*** Executing Exception callback...");
       try {
           retryTemplate.execute(new ExceptionCallback(), new LoggingRecoveryCallback());
       } catch (Exception e) {
           System.out.println("Suppressed Exception");
       }
1
2
3
4
*** Executing Exception callback...
Exception callback: attempt 0
Attempts exhausted. Total: 1
Last exception: Test Exception

И наконец наше IOException :

1
2
3
4
5
6
7
// we catch IOException to allow the program to continue
        System.out.println("\n*** Executing IO Exception callback...");
        try {
            retryTemplate.execute(new SpecificExceptionCallback(), new LoggingRecoveryCallback());
        } catch (IOException e) {
            System.out.println("Suppressed IO Exception");
        }
1
2
3
4
5
6
*** Executing IO Exception callback...
IO Exception callback: attempt 0
IO Exception callback: attempt 1
IO Exception callback: attempt 2
Attempts exhausted. Total: 3
Last exception: Test IO Exception

Как мы видим, только IOException инициировал три попытки. Обратите внимание, что попытки пронумерованы от 0, потому что при выполнении обратного вызова попытка не исчерпывается, поэтому последняя попытка имеет # 2, а не # 3. Но на RecoveryCallback все попытки исчерпаны, поэтому контекст содержит 3 попытки.

Мы также видим, что RecoveryCallback не вызывается, когда попытки были успешными. То есть он вызывается только тогда, когда выполнение завершается с исключением.

RetryTemplate является синхронным, поэтому все выполнение происходит в нашем основном потоке. Вот почему я добавил блоки try / catch вокруг вызовов, чтобы программа без проблем выполнила все три примера. В противном случае политика повторных попыток перезапустит исключение после его последней неудачной попытки и остановит выполнение.

Существует также очень интересная CompositeRetryPolicy, которая позволяет добавлять несколько политик и делегатов для их вызова по порядку, один за другим. Это также может позволить создать довольно гибкую стратегию повторов, но это уже другая тема.

Я думаю, что spring-retry — это очень полезная библиотека, которая позволяет сделать обычные повторяющиеся задачи более предсказуемыми, тестируемыми и более простыми в реализации.