Отказоустойчивость в Spring Batch: retry() и skip()

В этой статье рассмотрим, как сделать шаг Step отказоустойчивым. Отказоустойчивость можно настроить только для шага типа Chunks (не для Tasklet).

Продолжим рассматривать предыдущий пример с запросами к GitHub API и загрузкой результата в базу.

RetryPolicy — настройка повторных попыток обработки элемента

В этом примере мы делаем запрос по сети, а значит, возможны перебои. Если какой-то запрос выполнить с первого раза не удалось (выбросилось исключение), не хотелось бы сразу прерывать задание и считать его невыполненным (FAILED). Хотелось бы попробовать еще пару раз.

Здесь то и пригодится RetryPolicy, а именно SimpleRetryPolicy у нас в примере. SimpleRetryPolicy позволяет задать количество попыток обработки каждого элемента:

.retryLimit(3)

А также само исключение, при котором попытку следует повторить:

.retry(ResourceAccessException.class)

Вообще не всегда попытку нужно повторять. Бывают исключения, при которых задание следует прервать — их можно задать в методе:

.noRetry(SomeException.class)

При этом неважно, где выбросилось исключение: в ItemReader, ItemWriter или ItemProcessor —  RetryPolicy действует в любом случае.

Мы задали такую стратегию: повторять попытку обработки элемента (в нашем случае это запрос элемента по сети, парсинг и запись в базу) три раза при исключении ResourceAccessException — оно возникает при отключении сети.

При других исключениях (кроме ResourceAccessException ) повторные попытки делаться не будут, задание прервется и получит статус FAILED.

Настроить описанную политику можно, добавив три строчки в описание шага:

@Bean
public Step step() {
    return steps.get("step")
            .<Integer, ReposPage>chunk(2)
            .reader(getItemReader())
            .processor(repoPageProcessor)
            .writer(repoPageWriter)
            .faultTolerant()
            .retry(ResourceAccessException.class)
            .retryLimit(3)
            .build();
}

Перерывы между попытками будут делаться за счет того, что в RepoPageProcessor у нас стоит Thread.sleep между запросами, причем до запроса (а значит, до возможного исключения).

Еще один пример

Чтобы было нагляднее, рассмотрим еще пример с числами. Здесь мы ничего не загружаем по сети. В ItemReader берем числа из массива, а в ItemWriter записываем их в базу в таблицу numbers:

@EnableBatchProcessing
@Configuration
@SpringBootApplication
public class BatchConfig {
    private static final Logger LOGGER= LoggerFactory.getLogger(BatchConfig.class);
    @Autowired
    private JobBuilderFactory jobs;
    @Autowired
    private StepBuilderFactory steps;

    @Autowired
    private NumberRepository numberRepository;

    @Bean
    public Job job() {
        return jobs.get("job")
                .start(step())
                .build();
    }


    @Bean
    public Step step() {

        return steps.get("step")
                .<Integer, Integer>chunk(5)
                .reader(itemReader())
                .faultTolerant()
                .retryLimit(3)
                .retry(Exception.class)
                .skipLimit(3)
                .skip(Exception.class)
                .writer(itemWriter())
                .build();
    }
    @Bean
    public ItemReader<Integer> itemReader() {
        return new ListItemReader<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
    }


    @Bean
     public ItemWriter<Integer> itemWriter() {

        return items -> {
            System.out.println(items);
            for (Integer item : items) {
                System.out.println("writer item = " + item);
                numberRepository.save(new Number(item));

                if (item.equals(7)) {
                    throw new Exception("7 выбрасывает исключение");
                }

            }
        };
    }
}

Исключение выбросим намеренно, если число равно 7.

Сколько Spring Batch ни будет пытаться, он не обработает 7 ни при первом повторе, ни при 3. Потому что исключение выбрасывается всегда.  Задание после трех попыток помечается статусом FAILED.

Но первая порция (chunk) из пяти элементов сохраняется в базе, поскольку транзакции оборачивают каждую порцию, а порция из 1-5 прошла успешно и подтвердилась.

В консоли видно, что попытка считать вторую порцию происходит трижды:

[1, 2, 3, 4, 5]
writer item = 1
writer item = 2
writer item = 3
writer item = 4
writer item = 5
Hibernate: insert into number (number, id) values (?, ?)
Hibernate: insert into number (number, id) values (?, ?)
Hibernate: insert into number (number, id) values (?, ?)
Hibernate: insert into number (number, id) values (?, ?)
Hibernate: insert into number (number, id) values (?, ?)
[6, 7, 8, 9, 10]
writer item = 6
writer item = 7
[6, 7, 8, 9, 10]
writer item = 6
writer item = 7
[6, 7, 8, 9, 10]
writer item = 6
writer item = 7

но когда лимит .retryLimit(3) исчерпывается, Spring Batch сдается и помечает задание FAILED. Это можно посмотреть в служебной таблице batch_job_execution:

SkipPolicy — настройка пропуска (обработки) элементов

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

Допустим, нам необязательно обработать все числа, а достаточно большую часть. Зададим, что максимум одно число можно пропустить:

@Bean
public Step step() {

    return steps.get("step")
            .<Integer, Integer>chunk(5)
            .reader(itemReader())
            .faultTolerant()
            .retryLimit(3)
            .retry(Exception.class)
            .skipLimit(1)
            .skip(Exception.class)
            .writer(itemWriter())
            .build();
}

Тогда задание закончится успешно со статусом COMPLETED. При этом в базе окажутся все числа, кроме 7-ки, которая пропускается.

Причем второй chunk с числом 7 выполняется не целиком в одной транзакции, как это обычно бывает. Вместо этого каждый элемент обрабатывается в отдельной транзакции. Именно поэтому и удается записать остальные (не выбрасывающие исключение) элементы порции.

Итоги

Исходный код примера доступен на GitHub: пример с API и пример с числами. Мы рассмотрели RetryPolicy и SkipPolicy из коробки, но можно еще написать пользовательские стратегии.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *