Configuring Retry Logic in Spring Batch
1.概述
默认情况下,Spring批处理作业在执行过程中遇到的任何错误都会失败。但是,有时,我们可能需要提高应用程序的弹性来处理间歇性故障。
在本快速教程中,我们将探索如何在Spring Batch框架中配置重试逻辑。
2.示例用例
假设我们有一个批处理作业,它读取输入的CSV文件:
1 2 3 | username, userid, transaction_date, transaction_amount sammy, 1234, 31/10/2015, 10000 john, 9999, 3/12/2015, 12321 |
然后,它通过点击REST端点以获取用户的age和postCode属性来处理每条记录:
1 2 3 4 5 6 7 8 9 10 11 12 | public class RetryItemProcessor implements ItemProcessor<Transaction, Transaction> { @Override public Transaction process(Transaction transaction) throws IOException { log.info("RetryItemProcessor, attempting to process: {}", transaction); HttpResponse response = fetchMoreUserDetails(transaction.getUserId()); //parse user's age and postCode from response and update transaction ... return transaction; } ... } |
最后,它生成一个合并的输出XML:
1 2 3 4 5 6 7 8 9 10 11 | <transactionRecord> <transactionRecord> 10000.0</amount> <transactionDate>2015-10-31 00:00:00</transactionDate> <userId>1234</userId> <username>sammy</username> 10</age> <postCode>430222</postCode> </transactionRecord> ... </transactionRecord> |
3.将重试添加到ItemProcessor
现在,如果由于某些网络缓慢而导致与REST端点的连接超时怎么办?如果是这样,我们的批处理作业将失败。
在这种情况下,我们希望重试失败的项目两次。因此,让我们将批处理作业配置为在失败的情况下最多执行三个重试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @Bean public Step retryStep( ItemProcessor<Transaction, Transaction> processor, ItemWriter<Transaction> writer) throws ParseException { return stepBuilderFactory .get("retryStep") .<Transaction, Transaction>chunk(10) .reader(itemReader(inputCsv)) .processor(processor) .writer(writer) .faultTolerant() .retryLimit(3) .retry(ConnectTimeoutException.class) .retry(DeadlockLoserDataAccessException.class) .build(); } |
在这里,我们调用faultTolerant()以启用重试功能。另外,我们使用retry和retryLimit分别定义了符合重试条件的异常和项的最大重试计数。
4.测试重试
让我们进行一个测试场景,其中REST端点返回了年龄并且postCode下降了一段时间。在此测试方案中,我们仅对前两个API调用获取aConnectTimeoutException,而第三个调用将成功:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | @Test public void whenEndpointFailsTwicePasses3rdTime_thenSuccess() throws Exception { FileSystemResource expectedResult = new FileSystemResource(EXPECTED_OUTPUT); FileSystemResource actualResult = new FileSystemResource(TEST_OUTPUT); when(httpResponse.getEntity()) .thenReturn(new StringEntity("{"age":10,"postCode":"430222" }")); //fails for first two calls and passes third time onwards when(httpClient.execute(any())) .thenThrow(new ConnectTimeoutException("Timeout count 1")) .thenThrow(new ConnectTimeoutException("Timeout count 2")) .thenReturn(httpResponse); JobExecution jobExecution = jobLauncherTestUtils .launchJob(defaultJobParameters()); JobInstance actualJobInstance = jobExecution.getJobInstance(); ExitStatus actualJobExitStatus = jobExecution.getExitStatus(); assertThat(actualJobInstance.getJobName(), is("retryBatchJob")); assertThat(actualJobExitStatus.getExitCode(), is("COMPLETED")); AssertFile.assertFileEquals(expectedResult, actualResult); } |
在这里,我们的工作成功完成。此外,从日志中可以明显看出,id = 1234的第一条记录两次失败,最后一次重试成功:
1 2 3 4 5 6 | 19:06:57.742 [main] INFO o.s.batch.core.job.SimpleStepHandler - Executing step: [retryStep] 19:06:57.758 [main] INFO o.b.batch.service.RetryItemProcessor - Attempting to process user with id=1234 19:06:57.758 [main] INFO o.b.batch.service.RetryItemProcessor - Attempting to process user with id=1234 19:06:57.758 [main] INFO o.b.batch.service.RetryItemProcessor - Attempting to process user with id=1234 19:06:57.758 [main] INFO o.b.batch.service.RetryItemProcessor - Attempting to process user with id=9999 19:06:57.773 [main] INFO o.s.batch.core.step.AbstractStep - Step: [retryStep] executed in 31ms |
同样,让我们??有另一个测试用例,以查看所有重试用尽后会发生什么:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @Test public void whenEndpointAlwaysFail_thenJobFails() throws Exception { when(httpClient.execute(any())) .thenThrow(new ConnectTimeoutException("Endpoint is down")); JobExecution jobExecution = jobLauncherTestUtils .launchJob(defaultJobParameters()); JobInstance actualJobInstance = jobExecution.getJobInstance(); ExitStatus actualJobExitStatus = jobExecution.getExitStatus(); assertThat(actualJobInstance.getJobName(), is("retryBatchJob")); assertThat(actualJobExitStatus.getExitCode(), is("FAILED")); assertThat(actualJobExitStatus.getExitDescription(), containsString("org.apache.http.conn.ConnectTimeoutException")); } |
在这种情况下,在由于ConnectTimeoutException而导致作业最终失败之前,对第一个记录尝试了三次重试。
5.使用XML配置重试
最后,让我们看一下上述配置的XML等效项:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <batch:job id="retryBatchJob"> <batch:step id="retryStep"> <batch:tasklet> <batch:chunk reader="itemReader" writer="itemWriter" processor="retryItemProcessor" commit-interval="10" retry-limit="3"> <batch:retryable-exception-classes> <batch:include class="org.apache.http.conn.ConnectTimeoutException"/> <batch:include class="org.springframework.dao.DeadlockLoserDataAccessException"/> </batch:retryable-exception-classes> </batch:chunk> </batch:tasklet> </batch:step> </batch:job> |
六,结论
在本文中,我们学习了如何在Spring Batch中配置重试逻辑。我们研究了Java和XML配置。
我们还使用了单元测试来查看重试在实践中如何工作。
与往常一样,可以在GitHub上获得本教程的示例代码。