Add share-groups module: Kafka 4.3.1 share groups with Spring for Apache Kafka 4.1.1
Six consumers on two partitions under a consumer group, a share group with the default share.acquire.mode and a share group with record_limit; the three acknowledgement types and a record that is released until Kafka stops redelivering it; a new share group starting at latest; the ordering a share group gives up; and Spring Kafka's share containers in implicit and manual acknowledgement modes. Tests run against a real broker (scripts/broker-up.sh) and write output/. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Lm655hdgDo5xsSLNomgdsy
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
package com.ankurm.sharegroups;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||
|
||||
import org.springframework.kafka.annotation.KafkaListener;
|
||||
import org.springframework.kafka.support.ShareAcknowledgment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** Two listeners on share containers. They only record what they saw; the tests read the lists. */
|
||||
@Component
|
||||
public class PaymentListeners {
|
||||
|
||||
public final List<String> implicitSeen = new CopyOnWriteArrayList<>();
|
||||
|
||||
public final List<String> manualSeen = new CopyOnWriteArrayList<>();
|
||||
|
||||
/** Implicit mode: returning normally acknowledges (ACCEPT); throwing makes the container reject or release. */
|
||||
@KafkaListener(id = "implicit", topics = "${share.topic.implicit}", groupId = "${share.group.implicit}",
|
||||
containerFactory = "implicitShareFactory")
|
||||
void implicit(ConsumerRecord<String, String> record) throws InterruptedException {
|
||||
Thread.sleep(20);
|
||||
implicitSeen.add(Thread.currentThread().getName() + " <- " + record.value());
|
||||
}
|
||||
|
||||
/** Manual mode: the listener decides per record. "bad" is rejected, "again" is released once, the rest accepted. */
|
||||
@KafkaListener(id = "manual", topics = "${share.topic.manual}", groupId = "${share.group.manual}",
|
||||
containerFactory = "explicitShareFactory")
|
||||
void manual(ConsumerRecord<String, String> record, ShareAcknowledgment ack) {
|
||||
int attempt = record.deliveryCount().orElse((short) 1);
|
||||
switch (record.value()) {
|
||||
case "bad" -> {
|
||||
manualSeen.add("bad delivery #" + attempt + " -> reject");
|
||||
ack.reject();
|
||||
}
|
||||
case "again" -> {
|
||||
if (attempt == 1) {
|
||||
manualSeen.add("again delivery #1 -> release");
|
||||
ack.release();
|
||||
}
|
||||
else {
|
||||
manualSeen.add("again delivery #" + attempt + " -> acknowledge");
|
||||
ack.acknowledge();
|
||||
}
|
||||
}
|
||||
default -> {
|
||||
manualSeen.add(record.value() + " delivery #" + attempt + " -> acknowledge");
|
||||
ack.acknowledge();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.ankurm.sharegroups;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.kafka.clients.consumer.ConsumerConfig;
|
||||
import org.apache.kafka.common.serialization.StringDeserializer;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.kafka.annotation.EnableKafka;
|
||||
import org.springframework.kafka.config.ShareKafkaListenerContainerFactory;
|
||||
import org.springframework.kafka.core.DefaultShareConsumerFactory;
|
||||
import org.springframework.kafka.core.ShareConsumerFactory;
|
||||
import org.springframework.kafka.listener.ContainerProperties.ShareAckMode;
|
||||
|
||||
/**
|
||||
* Spring Boot 4.1.1 does not auto-configure share consumers, so the factory and the listener container
|
||||
* factories are plain beans. Two container factories: implicit (the container acknowledges for you) and
|
||||
* explicit (the listener must call ShareAcknowledgment).
|
||||
*/
|
||||
@Configuration
|
||||
@EnableKafka
|
||||
public class ShareConfig {
|
||||
|
||||
@Bean
|
||||
ShareConsumerFactory<String, String> shareConsumerFactory(@Value("${broker:localhost:9092}") String bootstrap) {
|
||||
Map<String, Object> props = new HashMap<>();
|
||||
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrap);
|
||||
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
|
||||
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
|
||||
props.put("share.acquire.mode", "record_limit");
|
||||
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, "3");
|
||||
return new DefaultShareConsumerFactory<>(props);
|
||||
}
|
||||
|
||||
@Bean
|
||||
ShareKafkaListenerContainerFactory<String, String> implicitShareFactory(ShareConsumerFactory<String, String> factory) {
|
||||
ShareKafkaListenerContainerFactory<String, String> f = new ShareKafkaListenerContainerFactory<>(factory);
|
||||
f.setConcurrency(4);
|
||||
f.getContainerProperties().setShareAckMode(ShareAckMode.IMPLICIT);
|
||||
return f;
|
||||
}
|
||||
|
||||
@Bean
|
||||
ShareKafkaListenerContainerFactory<String, String> explicitShareFactory(ShareConsumerFactory<String, String> factory) {
|
||||
ShareKafkaListenerContainerFactory<String, String> f = new ShareKafkaListenerContainerFactory<>(factory);
|
||||
f.setConcurrency(2);
|
||||
f.getContainerProperties().setShareAckMode(ShareAckMode.MANUAL);
|
||||
return f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.ankurm.sharegroups;
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/** Marker application class so the Spring tests have something to bootstrap. Nothing here starts a broker. */
|
||||
@SpringBootApplication
|
||||
public class ShareGroupsApplication {
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user