GH-87: Add ackOnError and fix ack commit issues

Fixes #87 (https://github.com/spring-projects/spring-kafka/issues/87)

- allow for acking on both success and error (make the latter conditional upon `ackOnError`)
- fixes an issue where `processCommits()` wasn't handled on invoker stop, allowing for missed commits
This commit is contained in:
Marius Bogoevici
2016-06-01 18:12:17 -04:00
committed by Artem Bilan
parent b1dc76e3a9
commit be8b438fdd
3 changed files with 81 additions and 1 deletions

View File

@@ -470,6 +470,7 @@ public class KafkaMessageListenerContainer<K, V> extends AbstractMessageListener
}
// handle the last manual acks, after the listeners have closed
handleManualAcks();
processCommits();
if (this.offsets.size() > 0) {
// we always commit after stopping the invoker
commitIfNecessary();
@@ -602,6 +603,9 @@ public class KafkaMessageListenerContainer<K, V> extends AbstractMessageListener
}
}
catch (Exception e) {
if (this.containerProperties.isAckOnError()) {
this.acks.add(record);
}
if (this.containerProperties.getErrorHandler() != null) {
this.containerProperties.getErrorHandler().handle(e, record);
}

View File

@@ -181,6 +181,8 @@ public class ContainerProperties {
*/
private boolean syncCommits = true;
private boolean ackOnError = true;
private Long idleEventInterval;
public ContainerProperties(String... topics) {
@@ -385,6 +387,21 @@ public class ContainerProperties {
this.idleEventInterval = idleEventInterval;
}
/**
* Set whether the container should ack messages that throw exceptions or not. This
* works in conjunction with {@link #ackMode} and is effective only when auto ack is
* false; it is not applicable to manual acks. When this property is set to
* {@code true}, all messages handled will be acked. When set to {@code false}, acks
* will be produced only for successful messages. This allows a component that starts
* throwing exceptions consistently to resume from the last successfully processed
* message. Manual acks will be always be applied.
* @param ackOnError whether the container should acknowledge messages that throw
* exceptions.
*/
public void setAckOnError(boolean ackOnError) {
this.ackOnError = ackOnError;
}
public String[] getTopics() {
return this.topics;
}
@@ -473,4 +490,7 @@ public class ContainerProperties {
return this.idleEventInterval;
}
public boolean isAckOnError() {
return this.ackOnError;
}
}

View File

@@ -22,6 +22,7 @@ import static org.mockito.Matchers.anyLong;
import static org.mockito.Mockito.mock;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.List;
@@ -65,6 +66,7 @@ import org.springframework.kafka.test.utils.KafkaTestUtils;
* @author Gary Russell
* @author Artem Bilan
* @author Jerome Mirc
* @author Marius Bogoevici
*
*/
public class ConcurrentMessageListenerContainerTests {
@@ -87,9 +89,11 @@ public class ConcurrentMessageListenerContainerTests {
private static String topic8 = "testTopic8";
private static String topic9 = "testTopic9";
@ClassRule
public static KafkaEmbedded embeddedKafka = new KafkaEmbedded(1, true, topic1, topic2, topic3, topic4, topic5,
topic6, topic7, topic8);
topic6, topic7, topic8, topic9);
@Test
public void testAutoCommit() throws Exception {
@@ -608,4 +612,56 @@ public class ConcurrentMessageListenerContainerTests {
}
@Test
public void testAckOnErrorRecord() throws Exception {
logger.info("Start ack on error");
Map<String, Object> props = KafkaTestUtils.consumerProps("test9", "false", embeddedKafka);
DefaultKafkaConsumerFactory<Integer, String> cf = new DefaultKafkaConsumerFactory<Integer, String>(props);
final CountDownLatch latch = new CountDownLatch(4);
ContainerProperties containerProps = new ContainerProperties(topic9);
containerProps.setMessageListener(new MessageListener<Integer, String>() {
@Override
public void onMessage(ConsumerRecord<Integer, String> message) {
logger.info("auto ack on error: " + message);
latch.countDown();
if (message.value().startsWith("b")) {
throw new RuntimeException();
}
}
});
containerProps.setSyncCommits(true);
containerProps.setAckMode(AckMode.RECORD);
containerProps.setAckOnError(false);
ConcurrentMessageListenerContainer<Integer, String> container = new ConcurrentMessageListenerContainer<>(cf,
containerProps);
container.setConcurrency(2);
container.setBeanName("testAckOnError");
container.start();
ContainerTestUtils.waitForAssignment(container, embeddedKafka.getPartitionsPerTopic());
Map<String, Object> senderProps = KafkaTestUtils.producerProps(embeddedKafka);
ProducerFactory<Integer, String> pf = new DefaultKafkaProducerFactory<Integer, String>(senderProps);
KafkaTemplate<Integer, String> template = new KafkaTemplate<>(pf);
template.setDefaultTopic(topic9);
template.sendDefault(0, 0, "foo");
template.sendDefault(1, 0, "bar");
template.sendDefault(0, 0, "baz");
template.sendDefault(1, 0, "qux");
template.flush();
assertThat(latch.await(60, TimeUnit.SECONDS)).isTrue();
container.stop();
Consumer<Integer, String> consumer = cf.createConsumer();
consumer.assign(Arrays.asList(new TopicPartition(topic9, 0), new TopicPartition(topic9, 1)));
// this consumer is positioned at 1, the next offset after the successfully
// processed 'foo'
// it has not been updated because 'bar' failed
assertThat(consumer.position(new TopicPartition(topic9, 0))).isEqualTo(1);
// this consumer is positioned at 1, the next offset after the successfully
// processed 'qux'
// it has been updated even 'baz' failed
assertThat(consumer.position(new TopicPartition(topic9, 1))).isEqualTo(2);
logger.info("Stop ack on error");
}
}