Skip to content

Commit f60a554

Browse files
garyrussellartembilan
authored andcommitted
Partition list, range with initial offsets
- support partition lists and lists of ranges with `@TopicPartition.partitionOffset()`.
1 parent e666153 commit f60a554

File tree

4 files changed

+61
-25
lines changed

4 files changed

+61
-25
lines changed

spring-kafka/src/main/java/org/springframework/kafka/annotation/KafkaListenerAnnotationBeanPostProcessor.java

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
import org.springframework.kafka.listener.KafkaListenerErrorHandler;
7777
import org.springframework.kafka.support.KafkaNull;
7878
import org.springframework.kafka.support.TopicPartitionOffset;
79+
import org.springframework.lang.Nullable;
7980
import org.springframework.messaging.Message;
8081
import org.springframework.messaging.converter.GenericMessageConverter;
8182
import org.springframework.messaging.converter.MessageConverter;
@@ -564,7 +565,7 @@ private List<TopicPartitionOffset> resolveTopicPartitionsList(TopicPartition top
564565
() -> "At least one 'partition' or 'partitionOffset' required in @TopicPartition for topic '" + topic + "'");
565566
List<TopicPartitionOffset> result = new ArrayList<>();
566567
for (String partition : partitions) {
567-
resolvePartitionAsInteger((String) topic, resolveExpression(partition), result);
568+
resolvePartitionAsInteger((String) topic, resolveExpression(partition), result, null, false, false);
568569
}
569570
if (partitionOffsets.length == 1 && partitionOffsets[0].partition().equals("*")) {
570571
result.forEach(tpo -> {
@@ -576,19 +577,8 @@ private List<TopicPartitionOffset> resolveTopicPartitionsList(TopicPartition top
576577
for (PartitionOffset partitionOffset : partitionOffsets) {
577578
Assert.isTrue(!partitionOffset.partition().equals("*"), () ->
578579
"Partition wildcard '*' is only allowed in a single @PartitionOffset in " + result);
579-
TopicPartitionOffset topicPartitionOffset =
580-
new TopicPartitionOffset((String) topic,
581-
resolvePartition(topic, partitionOffset),
582-
resolveInitialOffset(topic, partitionOffset),
583-
isRelative(topic, partitionOffset));
584-
if (!result.contains(topicPartitionOffset)) {
585-
result.add(topicPartitionOffset);
586-
}
587-
else {
588-
throw new IllegalArgumentException(
589-
String.format("@TopicPartition can't have the same partition configuration twice: [%s]",
590-
topicPartitionOffset));
591-
}
580+
resolvePartitionAsInteger((String) topic, resolveExpression(partitionOffset.partition()), result,
581+
resolveInitialOffset(topic, partitionOffset), isRelative(topic, partitionOffset), true);
592582
}
593583
}
594584
Assert.isTrue(result.size() > 0, () -> "At least one partition required for " + topic);
@@ -673,18 +663,27 @@ else if (resolvedValue instanceof Iterable) {
673663

674664
@SuppressWarnings("unchecked")
675665
private void resolvePartitionAsInteger(String topic, Object resolvedValue,
676-
List<TopicPartitionOffset> result) {
666+
List<TopicPartitionOffset> result, @Nullable Long offset, boolean isRelative, boolean checkDups) {
667+
677668
if (resolvedValue instanceof String[]) {
678669
for (Object object : (String[]) resolvedValue) {
679-
resolvePartitionAsInteger(topic, object, result);
670+
resolvePartitionAsInteger(topic, object, result, offset, isRelative, checkDups);
680671
}
681672
}
682673
else if (resolvedValue instanceof String) {
683674
Assert.state(StringUtils.hasText((String) resolvedValue),
684675
() -> "partition in @TopicPartition for topic '" + topic + "' cannot be empty");
685-
result.addAll(parsePartitions((String) resolvedValue)
686-
.map(part -> new TopicPartitionOffset(topic, part))
687-
.collect(Collectors.toList()));
676+
List<TopicPartitionOffset> collected = parsePartitions((String) resolvedValue)
677+
.map(part -> new TopicPartitionOffset(topic, part, offset, isRelative))
678+
.collect(Collectors.toList());
679+
if (checkDups) {
680+
collected.forEach(tpo -> {
681+
Assert.state(!result.contains(tpo), () ->
682+
String.format("@TopicPartition can't have the same partition configuration twice: [%s]",
683+
tpo));
684+
});
685+
}
686+
result.addAll(collected);
688687
}
689688
else if (resolvedValue instanceof Integer[]) {
690689
for (Integer partition : (Integer[]) resolvedValue) {
@@ -696,7 +695,7 @@ else if (resolvedValue instanceof Integer) {
696695
}
697696
else if (resolvedValue instanceof Iterable) {
698697
for (Object object : (Iterable<Object>) resolvedValue) {
699-
resolvePartitionAsInteger(topic, object, result);
698+
resolvePartitionAsInteger(topic, object, result, offset, isRelative, checkDups);
700699
}
701700
}
702701
else {

spring-kafka/src/main/java/org/springframework/kafka/annotation/PartitionOffset.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@
3434
* The partition within the topic to listen on. Property place holders and SpEL
3535
* expressions are supported, which must resolve to Integer (or String that can be
3636
* parsed as Integer). '*' indicates that the initial offset will be applied to all
37-
* partitions in the encompassing {@link TopicPartition}
37+
* partitions in the encompassing {@link TopicPartition} The string can contain a
38+
* comma-delimited list of partitions, or ranges of partitions (e.g.
39+
* {@code 0-5, 7, 10-15}, in which case, the offset will be applied to all of those
40+
* partitions.
3841
* @return partition within the topic.
3942
*/
4043
String partition();

spring-kafka/src/test/java/org/springframework/kafka/listener/ManualAssignmentInitialSeekTests.java

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,9 @@
2929
import java.util.Arrays;
3030
import java.util.Collection;
3131
import java.util.Collections;
32-
import java.util.List;
3332
import java.util.Map;
3433
import java.util.concurrent.CountDownLatch;
3534
import java.util.concurrent.TimeUnit;
36-
import java.util.stream.Collectors;
3735
import java.util.stream.Stream;
3836

3937
import org.apache.kafka.clients.consumer.Consumer;
@@ -102,9 +100,21 @@ void parsePartitions() {
102100
TopicPartitionOffset[] topicPartitions = registry.getListenerContainer("pp")
103101
.getContainerProperties()
104102
.getTopicPartitions();
105-
List<Integer> collected = Arrays.stream(topicPartitions).map(tp -> tp.getPartition())
106-
.collect(Collectors.toList());
103+
Stream<Integer> collected = Arrays.stream(topicPartitions)
104+
.map(tp -> tp.getPartition());
107105
assertThat(collected).containsExactly(0, 1, 2, 3, 4, 5, 7, 10, 11, 12, 13, 14, 15);
106+
107+
assertThat(Arrays.stream(this.registry.getListenerContainer("ppo")
108+
.getContainerProperties()
109+
.getTopicPartitions())).containsExactly(
110+
new TopicPartitionOffset("foo", 0),
111+
new TopicPartitionOffset("foo", 1),
112+
new TopicPartitionOffset("foo", 2),
113+
new TopicPartitionOffset("foo", 3));
114+
assertThat(Arrays.stream(this.registry.getListenerContainer("ppo")
115+
.getContainerProperties()
116+
.getTopicPartitions())
117+
.map(tpo -> tpo.getOffset())).containsExactly(0L, 0L, 1L, 1L);
108118
}
109119

110120
@SuppressWarnings({ "rawtypes", "unchecked" })
@@ -147,6 +157,13 @@ public void foo(String in) {
147157
public void bar(String in) {
148158
}
149159

160+
@KafkaListener(id = "ppo", autoStartup = "false",
161+
topicPartitions = @org.springframework.kafka.annotation.TopicPartition(topic = "foo",
162+
partitionOffsets = { @PartitionOffset(partition = "0-1", initialOffset = "0"),
163+
@PartitionOffset(partition = "#{'2-3'}", initialOffset = "1") }))
164+
public void baz(String in) {
165+
}
166+
150167
@SuppressWarnings({ "rawtypes" })
151168
@Bean
152169
public ConsumerFactory consumerFactory() {

src/reference/asciidoc/kafka.adoc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1320,6 +1320,23 @@ public void process(String in) {
13201320

13211321
The range is inclusive; the example above will assign partitions `0, 1, 2, 3, 4, 5, 7, 10, 11, 12, 13, 14, 15`.
13221322

1323+
The same technique can be used when specifying initial offsets:
1324+
1325+
====
1326+
[source, java]
1327+
----
1328+
@KafkaListener(id = "thing3", topicPartitions =
1329+
{ @TopicPartition(topic = "topic1",
1330+
partitionOffsets = @PartitionOffset(partition = "0-5", initialOffset = "0"))
1331+
})
1332+
public void listen(ConsumerRecord<?, ?> record) {
1333+
...
1334+
}
1335+
----
1336+
====
1337+
1338+
The initial offset will be applied to all 6 partitions.
1339+
13231340
====== Manual Acknowledgment
13241341

13251342
When using manual `AckMode`, you can also provide the listener with the `Acknowledgment`.

0 commit comments

Comments
 (0)