使用 Spring Kafka 进行非阻塞重试的集成测试

​Kafka的非阻塞重试是通过为主题配置重试主题来实现的。如果需要,还可以配置额外的死信主题。如果所有重试都耗尽,事件将被转发到DLT。在公共领域中有很多资源可用于了解技术细节。对于代码中的重试机制编写集成测试确实是一项具有挑战性的工作。以下是一些测试方法,可以用来验证重试机制的正确性:

  1. 验证事件已经按照所需的次数进行了重试:
  • 在测试中,模拟一个会触发重试的事件,并设置重试次数为所需的次数。

  • 使用断言来验证事件是否被重试了指定的次数。

  1. 验证只有在特定的异常发生时才进行重试,而不是其他异常:
  • 在测试中,模拟不同的异常情况,包括需要重试的异常和不需要重试的异常。

  • 使用断言来验证只有特定的异常触发了重试,而其他异常没有触发重试。

  1. 验证如果前一次重试已经解决了异常,不会进行另一次重试:
  • 在测试中,模拟一个会触发重试的事件,并在每次重试之间解决异常。

  • 使用断言来验证只有在异常没有被解决的情况下才进行重试。

  1. 验证在前面的 (n-1) 次重试失败后,第 n 次重试成功:
  • 在测试中,模拟一个会触发重试的事件,并设置重试次数为 n。

  • 使用断言来验证在前面的 (n-1) 次重试失败后,第 n 次重试成功。

  1. 验证如果所有的重试尝试都失败,事件是否已经发送到了死信队列:
  • 在测试中,模拟一个会触发重试的事件,并设置重试次数为一个较小的值。

  • 使用断言来验证当所有的重试尝试都失败后,事件是否已经发送到了死信队列。

设置可重试的消费者

less 复制代码
@Slf4j
@Component
@RequiredArgsConstructor
public class CustomEventConsumer {
​
    private final CustomEventHandler handler;
​
    @RetryableTopic(attempts = "${retry.attempts}",
            backoff = @Backoff(
                    delayExpression = "${retry.delay}",
                    multiplierExpression = "${retry.delay.multiplier}"
            ),
            topicSuffixingStrategy = TopicSuffixingStrategy.SUFFIX_WITH_INDEX_VALUE,
            dltStrategy = FAIL_ON_ERROR,
            autoStartDltHandler = "true",
            autoCreateTopics = "false",
            include = {CustomRetryableException.class})
    @KafkaListener(topics = "${topic}", id = "${default-consumer-group:default}")
    public void consume(CustomEvent event, @Header(KafkaHeaders.RECEIVED_TOPIC) String topic) {
        try {
            log.info("Received event on topic {}", topic);
            handler.handleEvent(event);
        } catch (Exception e) {
            log.error("Error occurred while processing event", e);
            throw e;
        }
    }
​
    @DltHandler
    public void listenOnDlt(@Payload CustomEvent event) {
        log.error("Received event on dlt.");
        handler.handleEventFromDlt(event);
    }
​
}

如果您注意上面的代码片段,参数@RetryableTopic中包含includes。这告诉消费者只在方法抛出CustomRetryableException时进行重试。您可以添加任意数量的异常类型。还有一个exclude参数,但一次只能使用其中一个。在将事件发布到死信队列之前,事件处理最多应重试指定的次数。

设置测试基础设施

为了编写集成测试,您需要确保拥有一个正常运行的Kafka代理(最好是嵌入式的)和一个完全运行的发布者。让我们设置我们的基础设施:

less 复制代码
@EnableKafka
@SpringBootTest
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
@EmbeddedKafka(partitions = 1,
        brokerProperties = {"listeners=" + "${kafka.broker.listeners}", 
                            "port=" + "${kafka.broker.port}"},
        controlledShutdown = true,
        topics = {"test", "test-retry-0", "test-retry-1", "test-dlt"}
)
@ActiveProfiles("test")
class DocumentEventConsumerIntegrationTest {
​
  @Autowired
  private KafkaTemplate<String, CustomEvent> testKafkaTemplate;
​
​
    // tests
​
}

配置从application-test.yml文件中导入。当使用嵌入式Kafka代理时,重要的是要提及要创建的主题。它们不会自动创建。在这种情况下,我们将创建四个主题,分别是:

arduino 复制代码
"test", "test-retry-0", "test-retry-1", "test-dlt"

我们将最大重试次数设置为三次。每个主题对应于每次重试尝试。因此,如果三次重试都耗尽,事件应该被转发到DLT(死信队列)。

测试用例

如果在第一次尝试中成功消费,就不应该进行重试。可以通过方法只被调用一次来测试这一点。还可以添加对日志语句的进一步测试。

scss 复制代码
 @Test
    void test_should_not_retry_if_consumption_is_successful() throws ExecutionException, InterruptedException {
        CustomEvent event = new CustomEvent("Hello");
        // GIVEN
        doNothing().when(customEventHandler).handleEvent(any(CustomEvent.class));
​
        // WHEN
        testKafkaTemplate.send("test", event).get();
​
        // THEN
        verify(customEventHandler, timeout(2000).times(1)).handleEvent(any(CustomEvent.class));
        verify(customEventHandler, timeout(2000).times(0)).handleEventFromDlt(any(CustomEvent.class));
    }

如果引发了不可重试的异常,就不应该进行重试。在这种情况下,方法CustomEventHandler#handleEvent应该只被调用一次。

python 复制代码
 @Test    void test_should_not_retry_if_non_retryable_exception_raised() throws ExecutionException, InterruptedException {        CustomEvent event = new CustomEvent("Hello");        // GIVEN        doThrow(CustomNonRetryableException.class).when(customEventHandler).handleEvent(any(CustomEvent.class));​        // WHEN        testKafkaTemplate.send("test", event).get();​        // THEN        verify(customEventHandler, timeout(2000).times(1)).handleEvent(any(CustomEvent.class));        verify(customEventHandler, timeout(2000).times(0)).handleEventFromDlt(any(CustomEvent.class));    }

如果抛出了RetryableException,则应该按照配置的最大重试次数进行重试,当重试次数耗尽时,事件应该被发布到死信主题。在这种情况下,方法CustomEventHandler#handleEvent应该被调用三次(maxRetries次),而方法CustomEventHandler#handleEventFromDlt应该只被调用一次。

scss 复制代码
 @Test
    void test_should_not_retry_if_non_retryable_exception_raised() throws ExecutionException, InterruptedException {
        CustomEvent event = new CustomEvent("Hello");
        // GIVEN
        doThrow(CustomNonRetryableException.class).when(customEventHandler).handleEvent(any(CustomEvent.class));
​
        // WHEN
        testKafkaTemplate.send("test", event).get();
​
        // THEN
        verify(customEventHandler, timeout(2000).times(1)).handleEvent(any(CustomEvent.class));
        verify(customEventHandler, timeout(2000).times(0)).handleEventFromDlt(any(CustomEvent.class));
    }

在验证阶段添加了相当长的超时时间,以便在测试完成之前考虑指数退避延迟。这是很重要的,如果没有正确设置,可能会导致断言失败。应该重试直到RetryableException被解决,并且如果引发了不可重试的异常或者最终成功消费,就不应该继续重试。测试已经设置为首先抛出RetryableException,然后再抛出NonRetryableException,以便进行一次重试。

scss 复制代码
@Test
    void test_should_retry_until_retryable_exception_is_resolved_by_non_retryable_exception() throws ExecutionException,
            InterruptedException {
        CustomEvent event = new CustomEvent("Hello");
        // GIVEN
        doThrow(CustomRetryableException.class).doThrow(CustomNonRetryableException.class).when(customEventHandler).handleEvent(any(CustomEvent.class));
​
        // WHEN
        testKafkaTemplate.send("test", event).get();
​
        // THEN
        verify(customEventHandler, timeout(10000).times(2)).handleEvent(any(CustomEvent.class));
        verify(customEventHandler, timeout(2000).times(0)).handleEventFromDlt(any(CustomEvent.class));
    }ndleEventFromDlt(any(CustomEvent.class));    }
scss 复制代码
 @Test
    void test_should_retry_until_retryable_exception_is_resolved_by_successful_consumption() throws ExecutionException,
            InterruptedException {
        CustomEvent event = new CustomEvent("Hello");
        // GIVEN
        doThrow(CustomRetryableException.class).doNothing().when(customEventHandler).handleEvent(any(CustomEvent.class));
​
        // WHEN
        testKafkaTemplate.send("test", event).get();
​
        // THEN
        verify(customEventHandler, timeout(10000).times(2)).handleEvent(any(CustomEvent.class));
        verify(customEventHandler, timeout(2000).times(0)).handleEventFromDlt(any(CustomEvent.class));
        }

结论

因此,您可以看到集成测试是一种混合和匹配的策略,超时时间,延迟和验证,以确保您的Kafka事件驱动架构的重试机制是可靠的。

作者: Mukut Bhattacharjee

更多技术干货请关注公众号"云原生数据库"

squids.cn ,目前可体验全网zui低价RDS,免费的迁移工具DBMotion、SQL开发工具等

相关推荐
云和数据.ChenGuang12 分钟前
Django 应用安装脚本 – 如何将应用添加到 INSTALLED_APPS 设置中 原创
数据库·django·sqlite
woshilys40 分钟前
sql server 查询对象的修改时间
运维·数据库·sqlserver
Hacker_LaoYi41 分钟前
SQL注入的那些面试题总结
数据库·sql
建投数据2 小时前
建投数据与腾讯云数据库TDSQL完成产品兼容性互认证
数据库·腾讯云
Hacker_LaoYi3 小时前
【渗透技术总结】SQL手工注入总结
数据库·sql
岁月变迁呀3 小时前
Redis梳理
数据库·redis·缓存
独行soc3 小时前
#渗透测试#漏洞挖掘#红蓝攻防#护网#sql注入介绍06-基于子查询的SQL注入(Subquery-Based SQL Injection)
数据库·sql·安全·web安全·漏洞挖掘·hw
你的微笑,乱了夏天3 小时前
linux centos 7 安装 mongodb7
数据库·mongodb
工业甲酰苯胺3 小时前
分布式系统架构:服务容错
数据库·架构
独行soc4 小时前
#渗透测试#漏洞挖掘#红蓝攻防#护网#sql注入介绍08-基于时间延迟的SQL注入(Time-Based SQL Injection)
数据库·sql·安全·渗透测试·漏洞挖掘