在Spring Batch中配置重试逻辑

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上获得本教程的示例代码。