В этой статье рассмотрим, как сделать шаг 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 из коробки, но можно еще написать пользовательские стратегии.