17

I've been trying to do some POC work for Spring Kafka. Specifically, I wanted to experiment with what are the best practices in terms of dealing with errors while consuming messages within Kafka.

I am wondering if anyone is able to help with:

  1. Sharing best practices surrounding what Kafka consumers should do when there is a failure
  2. Help me understand how AckMode Record works, and how to prevent commits to the Kafka offset queue when an exception is thrown in the listener method.

The code example for 2 is given below:

Given that AckMode is set to RECORD, which according to the documentation:

commit the offset when the listener returns after processing the record.

I would have thought the the offset would not be incremented if the listener method threw an exception. However, this was not the case when I tested it using the code/config/command combination below. The offset still gets updated, and the next message continues to be processed.

My config:

    private Map<String, Object> producerConfigs() {
    Map<String, Object> props = new HashMap<>();
    props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.0.1:9092");
    props.put(ProducerConfig.RETRIES_CONFIG, 0);
    props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
    props.put(ProducerConfig.LINGER_MS_CONFIG, 1);
    props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);
    props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class);
    props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
    return props;
}

   @Bean
ConcurrentKafkaListenerContainerFactory<Integer, String> kafkaListenerContainerFactory() {
    ConcurrentKafkaListenerContainerFactory<Integer, String> factory =
            new ConcurrentKafkaListenerContainerFactory<>();
    factory.setConsumerFactory(new DefaultKafkaConsumerFactory<>(consumerConfigs()));
    factory.getContainerProperties().setAckMode(AbstractMessageListenerContainer.AckMode.RECORD);
    return factory;
}

My code:

@Component
public class KafkaMessageListener{
    @KafkaListener(topicPartitions = {@TopicPartition( topic = "my-replicated-topic", partitionOffsets = @PartitionOffset(partition = "0", initialOffset = "0", relativeToCurrent = "true"))})
    public void onReplicatedTopicMessage(ConsumerRecord<Integer, String> data) throws InterruptedException {
            throw new RuntimeException("Oops!");
    }

Command to verify offset:

bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group test-group

I'm using kafka_2.12-0.10.2.0 and org.springframework.kafka:spring-kafka:1.1.3.RELEASE

yfl
  • 347
  • 2
  • 3
  • 10

1 Answers1

18

The container (via ContainerProperties) has a property, ackOnError which is true by default...

/**
 * Set whether or not the container should commit offsets (ack messages) where the
 * listener throws exceptions. This works in conjunction with {@link #ackMode} and is
 * effective only when the kafka property {@code enable.auto.commit} is {@code false};
 * it is not applicable to manual ack modes. When this property is set to {@code true}
 * (the default), all messages handled will have their offset committed. When set to
 * {@code false}, offsets will be committed only for successfully handled messages.
 * Manual acks will be always be applied. Bear in mind that, if the next message is
 * successfully handled, its offset will be committed, effectively committing the
 * offset of the failed message anyway, so this option has limited applicability.
 * Perhaps useful for a component that starts throwing exceptions consistently;
 * allowing it to resume when restarted from the last successfully processed message.
 * @param ackOnError whether the container should acknowledge messages that throw
 * exceptions.
 */
public void setAckOnError(boolean ackOnError) {
    this.ackOnError = ackOnError;
}

Bear in mind, though, that if the next message is successful, its offset will be committed anyway, which effectively commits the failed offset too.

EDIT

Starting with version 2.3, ackOnError is now false by default.

Gary Russell
  • 166,535
  • 14
  • 146
  • 179
  • Thanks for the tip @Gary. Do you know if there are best practices around how to deal with errors in Kafka consumers? It seems like out of the box an error reading a message will simply be logged and then swallowed. – yfl Mar 29 '17 at 20:03
  • Also, I noticed that the comments for the code seems to be contradictory: "is effective only when auto ack is false; it is not applicable to manual acks.". I'm guessing the first part should be true, rather than false? – yfl Mar 29 '17 at 20:04
  • 1
    We should fix that; they are different things; in that context, auto ack is referring to a kafka property `enable.auto.commit` (where the kafka library consumer client does its own acks internally). `AckMode` is used to tell the container when/if to do commits when `enable.auto.commit` is `false`. If that is set for manual acks then the container never acks. – Gary Russell Mar 29 '17 at 20:13
  • 3
    Best practice for a failed delivery is probably to save off the bad message someplace (perhaps in another - dead letter - topic). If strict message ordering is required, then not committing the offset (`ackOnError=false`) and stopping the container is probably necessary. – Gary Russell Mar 29 '17 at 20:13
  • 1
    Hi @Gary, I noticed that when there is an error with deserialization of the message, Spring Kafka then gets stuck in a loop, constantly trying to read the same undeserializable message again and again and never moving to the next offset. This sounds somewhat inconsistent - i.e. if you can deserialize the message, but it errors, Spring Kafka will move on to the next offset. However, if you cannot deserialize the message, Spring Kafka essentially gets stuck. Is that the behaviour as you understand it? And would you have any advice on how to handle deserialization errors? Many thanks in advance! – yfl Mar 30 '17 at 22:57
  • 2
    Unfortunately, kafka deserialization occurs before Spring Kafka gets to see the data; so there's nothing we can do about it. You need a smarter deserializer that catches the exception and, perhaps, returns some value that conveys the deserialization error to the application layer. – Gary Russell Mar 31 '17 at 02:59
  • The error handling deserializer @GaryRussell talks above is now implemented, see class ErrorHandlingDeserializer : https://docs.spring.io/spring-kafka/docs/2.2.0.RELEASE/reference/html/_reference.html#error-handling-deserializer – Istvan Devai Jul 05 '21 at 12:06