Teste de integração de novas tentativas sem bloqueio com Spring Kafka

        Como escrever testes de integração para uma implementação Spring Kafka de um consumidor com novas tentativas e publicação de mensagens mortas habilitadas.

Novas tentativas sem bloqueio do Kafka

As novas tentativas sem bloqueio no Kafka são realizadas configurando um tópico de nova tentativa para o tópico principal. Tópicos adicionais de mensagens mortas também podem ser configurados, se desejado. Se todas as tentativas forem esgotadas, o evento é encaminhado para o DLT. Existem muitos recursos de domínio público para aprender sobre os detalhes técnicos. 

O que testar?

Esta pode ser uma tarefa desafiadora ao escrever testes de integração para o mecanismo de nova tentativa no código. 

  • Como posso testar se o evento foi repetido o número necessário de vezes? 
  • Como posso testar se as novas tentativas são executadas apenas quando ocorrem determinadas exceções, e não para outras exceções?
  • Como posso testar se não foi feita outra nova tentativa se a exceção foi resolvida na última tentativa?
  • Após (n-1) tentativas fracassadas de nova tentativa, como posso testar se a enésima tentativa na nova tentativa foi bem-sucedida?
  • Como posso testar se um evento foi enviado para a fila de mensagens não entregues quando todas as novas tentativas foram esgotadas?

Vejamos algum código. Você pode encontrar muitos artigos bons mostrando como configurar novas tentativas sem bloqueio com Spring Kafka. Uma dessas implementações é fornecida abaixo.   Isso é feito usando anotações @RetryableTopicde soma do Spring-Kafka .@DltHandler

Configurar um consumidor que pode ser repetido

@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);
    }

}

Se você notou no trecho de código acima, includeo parâmetro contém CustomRetryableException.class. Isso informa ao usuário para tentar novamente apenas se o método lançar uma CustomRetryableException CustomEventHandler#handleEvent. Você pode adicionar quantos quiser. Também existe um parâmetro de exclusão, mas qualquer um deles pode ser usado por vez.

${retry.attempts}O número máximo de vezes que o processamento de eventos deve ser repetido antes de postar no DLT.

Configure a infraestrutura de teste

Para escrever testes de integração, você precisa garantir que possui um corretor Kafka funcional (preferencialmente incorporado) e um editor totalmente funcional. Vamos configurar nossa infraestrutura:

@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

}

** A configuração é importada do arquivo application-test.yml.

Ao utilizar o broker kafka incorporado é importante mencionar os tópicos a serem criados. Eles não são criados automaticamente. Neste exemplo, criamos quatro tópicos, a saber 

"test", "test-retry-0", "test-retry-1", "test-dlt"

Definimos o número máximo de novas tentativas como 3. Cada tópico corresponde a cada nova tentativa. Portanto, se todas as 3 tentativas forem esgotadas, o evento deverá ser encaminhado para o DLT.

caso de teste

Se a primeira tentativa de consumo for bem-sucedida, não deverá ser tentada novamente.

CustomEventHandler#handleEventIsso pode ser testado pelo fato de que o método é chamado apenas uma vez. Outros testes para instruções de Log também podem ser adicionados.

    @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));
    }

Se uma exceção não repetível for lançada, ela não deverá ser tentada novamente.

Neste caso, o CustomEventHandler#handleEventmétodo deve ser chamado apenas uma vez:

    @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));
    }

Se a for lançado, tente novamente o número máximo de vezes configurado RetryableExceptione publique no tópico de mensagens não entregues quando as tentativas se esgotarem.

Neste caso o CustomEventHandler#handleEventmétodo deve ser chamado três vezes (maxRetries) e CustomEventHandler#handleEventFromDlto método deve ser chamado uma vez.

    @Test
    void test_should_retry_maximum_times_and_publish_to_dlt_if_retryable_exception_raised() throws ExecutionException, InterruptedException {
        CustomEvent event = new CustomEvent("Hello");
        // GIVEN
        doThrow(CustomRetryableException.class).when(customEventHandler).handleEvent(any(CustomEvent.class));

        // WHEN
        testKafkaTemplate.send("test", event).get();

        // THEN
        verify(customEventHandler, timeout(10000).times(maxRetries)).handleEvent(any(CustomEvent.class));
        verify(customEventHandler, timeout(2000).times(1)).handleEventFromDlt(any(CustomEvent.class));
    }

** Adicionado um tempo limite considerável à fase de validação para que atrasos exponenciais de espera possam ser contabilizados antes da conclusão do teste. Isso é importante e, se definido incorretamente, pode causar falha nas asserções.

Deve ser tentado novamente até RetryableExceptionser resolvido e não deve continuar a ser tentado novamente se uma exceção não repetível for levantada ou se o consumidor eventualmente tiver sucesso.

O teste foi configurado para RetryableExceptionlançar a e depois lançar a  NonRetryable exceptionpara que seja repetido uma vez.

    @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));
    }

    @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));
    }

para concluir

Portanto, você pode ver que os testes de integração são uma combinação de políticas, tempos limite, atrasos e validações para garantir que o mecanismo de nova tentativa da arquitetura orientada a eventos do Kafka seja infalível.

Acho que você gosta

Origin blog.csdn.net/qq_28245905/article/details/132354937
Recomendado
Clasificación