Add ability to start reading from a custom offset in KafkaItemReader

Issue #737
This commit is contained in:
Mahmoud Ben Hassine
2020-07-07 10:14:21 +02:00
parent 5fa821f25f
commit 15a393b8ef
2 changed files with 109 additions and 5 deletions

View File

@@ -140,12 +140,30 @@ public class KafkaItemReader<K, V> extends AbstractItemStreamItemReader<V> {
return this.saveState;
}
/**
* Setter for partition offsets. This mapping tells the reader the offset to start
* reading from in each partition. This is optional, defaults to starting from
* offset 0 in each partition. Passing an empty map makes the reader start
* from the offset stored in Kafka for the consumer group ID.
*
* <p><strong>In case of a restart, offsets stored in the execution context
* will take precedence.</strong></p>
*
* @param partitionOffsets mapping of starting offset in each partition
*/
public void setPartitionOffsets(Map<TopicPartition, Long> partitionOffsets) {
Assert.notNull(partitionOffsets, "partitionOffsets must not be null");
this.partitionOffsets = partitionOffsets;
}
@Override
public void open(ExecutionContext executionContext) {
this.kafkaConsumer = new KafkaConsumer<>(this.consumerProperties);
this.partitionOffsets = new HashMap<>();
for (TopicPartition topicPartition : this.topicPartitions) {
this.partitionOffsets.put(topicPartition, 0L);
if (this.partitionOffsets == null) {
this.partitionOffsets = new HashMap<>();
for (TopicPartition topicPartition : this.topicPartitions) {
this.partitionOffsets.put(topicPartition, 0L);
}
}
if (this.saveState && executionContext.containsKey(TOPIC_PARTITION_OFFSETS)) {
Map<TopicPartition, Long> offsets = (Map<TopicPartition, Long>) executionContext.get(TOPIC_PARTITION_OFFSETS);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2019 the original author or authors.
* Copyright 2019-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,6 +26,7 @@ import java.util.concurrent.ExecutionException;
import org.apache.kafka.clients.admin.NewTopic;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.OffsetAndMetadata;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.junit.Before;
@@ -67,7 +68,9 @@ public class KafkaItemReaderTests {
new NewTopic("topic1", 1, (short) 1),
new NewTopic("topic2", 2, (short) 1),
new NewTopic("topic3", 1, (short) 1),
new NewTopic("topic4", 2, (short) 1)
new NewTopic("topic4", 2, (short) 1),
new NewTopic("topic5", 1, (short) 1),
new NewTopic("topic6", 1, (short) 1)
);
}
@@ -212,6 +215,89 @@ public class KafkaItemReaderTests {
this.reader.close();
}
@Test
public void testReadFromSinglePartitionFromCustomOffset() {
this.template.setDefaultTopic("topic5");
this.template.sendDefault("val0"); // <-- offset 0
this.template.sendDefault("val1"); // <-- offset 1
this.template.sendDefault("val2"); // <-- offset 2
this.template.sendDefault("val3"); // <-- offset 3
this.reader = new KafkaItemReader<>(this.consumerProperties, "topic5", 0);
// specify which offset to start from
Map<TopicPartition, Long> partitionOffsets = new HashMap<>();
partitionOffsets.put(new TopicPartition("topic5", 0), 2L);
this.reader.setPartitionOffsets(partitionOffsets);
this.reader.setPollTimeout(Duration.ofSeconds(1));
this.reader.open(new ExecutionContext());
String item = this.reader.read();
assertThat(item, is("val2"));
item = this.reader.read();
assertThat(item, is("val3"));
item = this.reader.read();
assertNull(item);
this.reader.close();
}
@Test
public void testReadFromSinglePartitionFromTheOffsetStoredInKafka() throws Exception {
// first run: read a topic from the beginning
this.template.setDefaultTopic("topic6");
this.template.sendDefault("val0"); // <-- offset 0
this.template.sendDefault("val1"); // <-- offset 1
this.reader = new KafkaItemReader<>(this.consumerProperties, "topic6", 0);
this.reader.setPollTimeout(Duration.ofSeconds(1));
this.reader.open(new ExecutionContext());
String item = this.reader.read();
assertThat(item, is("val0"));
item = this.reader.read();
assertThat(item, is("val1"));
item = this.reader.read();
assertNull(item);
this.reader.close();
// The offset stored in Kafka should be equal to 2 at this point
OffsetAndMetadata currentOffset = KafkaTestUtils.getCurrentOffset(
embeddedKafka.getEmbeddedKafka().getBrokersAsString(),
"1", "topic6",
0);
assertEquals(2, currentOffset.offset());
// second run (with same consumer group ID): new messages arrived since the last run.
this.template.sendDefault("val2"); // <-- offset 2
this.template.sendDefault("val3"); // <-- offset 3
this.reader = new KafkaItemReader<>(this.consumerProperties, "topic6", 0);
// Passing an empty map means the reader should start from the offset stored in Kafka (offset 2 in this case)
this.reader.setPartitionOffsets(new HashMap<>());
this.reader.setPollTimeout(Duration.ofSeconds(1));
this.reader.open(new ExecutionContext());
item = this.reader.read();
assertThat(item, is("val2"));
item = this.reader.read();
assertThat(item, is("val3"));
item = this.reader.read();
assertNull(item);
this.reader.close();
}
@Test
public void testReadFromMultiplePartitions() {
this.template.setDefaultTopic("topic2");