4

Question from Twitter:

Just trying to find out a simple example with spring-kafka 2.1.7 that works with a KafkaListener and AckMode.MANUAL_IMMEDIATE , to retry last failed message.

https://twitter.com/tolbier/status/1028936942447149056

PAA
  • 1
  • 46
  • 174
  • 282
Gary Russell
  • 166,535
  • 14
  • 146
  • 179

1 Answers1

14

it's generally better to ask such questions on Stack Overflow (tagged with .

There are two ways:

  • Add a RetryTemplate to the listener container factory - the retries will be performed in memory and you can set backoff properties.
  • Add a SeekToCurrentErrorHandler which will re-seek the unprocessed records.

Here is an example:

@SpringBootApplication
public class Twitter1Application {

    public static void main(String[] args) {
        SpringApplication.run(Twitter1Application.class, args);
    }

    boolean fail = true;

    @KafkaListener(id = "foo", topics = "twitter1")
    public void listen(String in, Acknowledgment ack) {
        System.out.println(in);
        if (fail) {
            fail = false;
            throw new RuntimeException("failed");
        }
        ack.acknowledge();
    }

    @Bean
    public ConcurrentKafkaListenerContainerFactory<?, ?> kafkaListenerContainerFactory(
            ConcurrentKafkaListenerContainerFactoryConfigurer configurer,
            ConsumerFactory<Object, Object> kafkaConsumerFactory) {
        ConcurrentKafkaListenerContainerFactory<Object, Object> factory = new ConcurrentKafkaListenerContainerFactory<>();
        configurer.configure(factory, kafkaConsumerFactory);
        factory.getContainerProperties().setErrorHandler(new SeekToCurrentErrorHandler());
        // or factory.setRetryTemplate(aRetryTemplate);
        // and factory.setRecoveryCallback(aRecoveryCallback);
        return factory;
    }

    @Bean
    public ApplicationRunner runner(KafkaTemplate<String, String> template) {
        return args -> {
            Thread.sleep(2000);
            template.send("twitter1", "foo");
            template.send("twitter1", "bar");
        };
    }

    @Bean
    public NewTopic topic() {
        return new NewTopic("twitter1", 1, (short) 1);
    }

}

and

spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.consumer.enable-auto-commit=false

spring.kafka.listener.ack-mode=manual-immediate

logging.level.org.springframework.kafka=debug

and

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>twitter1</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>twitter1</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>

(Boot 2.0.4 pulls in 2.1.8, which is the current version).

and

foo
2018-08-13 17:36:14.901 ERROR 3945 --- [      foo-0-C-1] essageListenerContainer$ListenerConsumer : Error handler threw an exception

org.springframework.kafka.KafkaException: Seek to current after exception; nested exception is  ...    

2018-08-13 17:36:15.396 DEBUG 3945 --- [      foo-0-C-1] essageListenerContainer$ListenerConsumer : Received: 2 records
foo
2018-08-13 17:36:15.398 DEBUG 3945 --- [      foo-0-C-1] essageListenerContainer$ListenerConsumer : Committing: {twitter1-0=OffsetAndMetadata{offset=5, metadata=''}}
bar
2018-08-13 17:36:15.403 DEBUG 3945 --- [      foo-0-C-1] essageListenerContainer$ListenerConsumer : Committing: {twitter1-0=OffsetAndMetadata{offset=6, metadata=''}}

In the upcoming 2.2 release, the error handler can be configured with a recoverer and standard recoverer is provided to publish the failed record to a dead-letter topic.

Commit here. Docs Here.

akokskis
  • 1,486
  • 15
  • 32
Gary Russell
  • 166,535
  • 14
  • 146
  • 179
  • Hi - sorry for reviving an old thread, but I'm having trouble working out how to get SeekToCurrentErrorHandler to use that recoverer (Kafka 2.3). Since we are using auto commit=false, how can the recoverer commit the failed record so it is not read again? It is a simple BiConsumer and has no access to any context or acknowledge object it seems. – user1283068 Dec 13 '19 at 13:36
  • SeekToCurrentErrorHandler eh = new SeekToCurrentErrorHandler(recoverer, backoff); eh.setCommitRecovered(true); doesn't work. Even after calling the recoverer the offset is not committed. (Sorry for code in the comments) – user1283068 Dec 13 '19 at 13:55
  • You should ask a new question and show more context; commenting on old answers with new questions is discouraged. commitRecovered is used when using MANUAL_IMMEDIATE ack mode. Starting with SK version 2.3.2, STCEH returns true for `isAckAfterHandle()`. – Gary Russell Dec 13 '19 at 14:51
  • I completely understand but this question is exacfly the same question as I wanted to ask that it seemed redundant to ask the same thing again (and have that flagged as a duplicate). I really appreciate you taking time to respond. I'm using errorHandler.setCommitRecovered(true) and the manual immediate ack mode on the containerfactory. But recovered records are not committed no matter what. The recoverer lambda is invoked but it has no way to acknowledge the recovery, the error handler must assume that this is the desired action. But it just doesn't appear to be doing that. – user1283068 Dec 16 '19 at 10:53
  • Ouch. I think I've run into this: https://github.com/spring-projects/spring-kafka/issues/1309 – user1283068 Dec 16 '19 at 11:04
  • Yes that was it. Not having a sync commit timeout resulted in an NPE during the commit. I set that to 60 seconds and now everything works as expected. – user1283068 Dec 16 '19 at 11:10
  • Well almost everything. No matter which examples I follow from 2.3 or 2.2, I can't get recovery to work properly. I always run into a rebalance. Lawd why does this have to be so hard. – user1283068 Dec 16 '19 at 11:54
  • 1
    See [this answer](https://stackoverflow.com/questions/60172304/how-to-retry-with-spring-kafka-version-2-2/60173862#60173862) for an up-to-date working example. – Gary Russell Feb 11 '20 at 17:11
  • @Gary Russell, still today on Nov-25,2021. Do we need to go ahead with this java bean based configuration for retry template and error handling. I mean is it possible to configure the number of retries from application.properties using kakaproperties feature ? – Arun Sai Mustyala Nov 25 '21 at 06:36
  • Don't ask questions on 3+ year old answers; the use of a retry template is deprecated because its functionality is completely available in the `SeekToCurrentErrorHandler` (now called the `DefaultErrorHandler` since 2.8.0). You cannot configure the properties; you need to define the error handler bean with the settings you need (Spring Boot will auto-wire the error handler bean into the container factory). You no longer need to modify the container factory. – Gary Russell Nov 25 '21 at 13:54