0

I have 2 Spring Boot projects, 1st one is a Producer that sends message to a Topic with one partition.

2nd one is a Consumer application, that reads from the Topic and one partition. For the consumer, I use KafkaMessageDrivenChannelAdapter, KafkaMessageListenerContainer and specify the Consumer Group Id in the ConsumerFactory.

Note, I am using spring-integration-kafka 2.0.0.RELEASE with spring-kafka 1.0.2.RELEASE, which uses kakfa 0.9. I am running 3 docker instances or kafka 0.10.0 with one instance of zookeeper in docker containers.

When I run one instance of my consumer, it works beautifully, reading message, processing it.

However, when I run a second instance of the application (I just change the port), any message produced by the producer application gets received by both of the instances, resulting in processing each message twice.

Based on the documentation, I felt this scenario should work, as the reason for second instance in this example is for resiliency, in case one app instance goes down, the other one takes over, but not both should get the message for the same topic/partition within a Consumer Group. Note, I am using Service Activators (Facade) to process a message.

Is there something that I am missing.

Please help.

Here is my consumer app configuration based on example from spring-integration-kafka:

{
    @ServiceActivator(inputChannel = "received", outputChannel = "nullChannel", adviceChain = {"requestHandlerRetryAdvice"})
        @Bean
        public MessageConsumerServiceFacade messageConsumerServiceFacade() {
            return new DefaultMessageConsumerServiceFacade();
        }

        @ServiceActivator(inputChannel = "errorChannel", outputChannel = "nullChannel")
        @Bean
        public MessageConsumerServiceFacade messageConsumerErrorServiceFacade() {
            return new DefaultMessageConsumerErrorServiceFacade();
        }
        @ServiceActivator(inputChannel = "received", outputChannel = "nullChannel", adviceChain = {"requestHandlerRetryAdvice"})
        @Bean
        public MessageConsumerServiceFacade messageConsumerServiceFacade() {
            return new DefaultMessageConsumerServiceFacade();
        }

        @ServiceActivator(inputChannel = "errorChannel", outputChannel = "nullChannel")
        @Bean
        public MessageConsumerServiceFacade messageConsumerErrorServiceFacade() {
            return new DefaultMessageConsumerErrorServiceFacade();
        }

            @Bean
            public IntegrationFlow consumer() throws Exception {

                LOGGER.info("starting consumer..");


                return IntegrationFlows
                        .from(adapter(container()))
                        .get();
            }

         @Bean  
         public KafkaMessageListenerContainer<String, byte[]> container() throws Exception {
    // This variant of the constructors DOES NOT WORK with Consumer Group, with this setting, all consumers receives the message - BAD for a cluster of consumer apps - duplicate message
    //ContainerProperties containerProperties = new ContainerProperties( new TopicPartitionInitialOffset(this.topic, 0));

    // Use THIS variant of the constructors to use Consumer Group successfully
    // with auto re-balance of partitions to distribute loads among consumers, perfect for a cluster of consumer app
    ContainerProperties containerProperties = new ContainerProperties(this.topic);
    containerProperties.setAckOnError(false);
    containerProperties.setAckMode(AbstractMessageListenerContainer.AckMode.MANUAL_IMMEDIATE);

    KafkaMessageListenerContainer kafkaMessageListenerContainer = new KafkaMessageListenerContainer<>(consumerFactory(), containerProperties);


    return kafkaMessageListenerContainer;

}



            @Bean 
            public ConsumerFactory<String, byte[]> consumerFactory() {
                Map<String, Object> props = new HashMap<>();
                props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);

                props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, this.brokerAddress);

                props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
                props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 2);
                props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 15000);


                props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
                props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class);
                props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");  // earliest, latest, none


                return new DefaultKafkaConsumerFactory<>(props);
            }

            @Bean
            public KafkaMessageDrivenChannelAdapter<String, byte[]> adapter(KafkaMessageListenerContainer<String, byte[]> container) {
                KafkaMessageDrivenChannelAdapter<String, byte[]> kafkaMessageDrivenChannelAdapter =
                        new KafkaMessageDrivenChannelAdapter<>(container);

                kafkaMessageDrivenChannelAdapter.setOutputChannel(received());
                kafkaMessageDrivenChannelAdapter.setErrorChannel(errorChannel());

                return kafkaMessageDrivenChannelAdapter;
            }


            @Bean 
            public MessageChannel received() {

                return new PublishSubscribeChannel();
            }

            @Bean 
            public MessageChannel errorChannel() {
                return new PublishSubscribeChannel();

            }
        }
halfer
  • 19,824
  • 17
  • 99
  • 186
Chris G
  • 1
  • 1
  • Try setting auto-commit to true. Both may be receiving the messages because neither has actually acknowledged that it's processed it. – Gandalf Aug 02 '16 at 18:00
  • Hi Gandalf, I actually do manual commit in my service class method(which i defined as a service activator). I also noticed that offset looks correct after the commit (using some gui tool) { // process the message // Manual Commit by acknowledgement Acknowledgment acknowledgment = (Acknowledgment) incomingMessage.getHeaders().get(KafkaHeaders.ACKNOWLEDGMENT); acknowledgment.acknowledge(); LOGGER.debug("offset committed... " + incomingMessage.getHeaders().get(KafkaHeaders.OFFSET)); } – Chris G Aug 02 '16 at 18:27
  • How are you dealing with rebalances? – Gandalf Aug 02 '16 at 20:24
  • @Gandalf, for the consumer application, most of the relevant configuration is as I posted. I am not sure if I understand rebalances from consumer perspective. **Note: In my producer app, I created a topic with replication.factor: 3, min.insync.replicas: 2. With this, my producer can tolerate 2 of the replicas failure. – Chris G Aug 02 '16 at 20:56
  • Try setting your AUTO_OFFSET_RESET_CONFIG to "latest" – Gandalf Aug 02 '16 at 21:04
  • Just tried setting AUTO_OFFSET_RESET_CONFIG to "latest", no affect, ie, each instance is still procssing the message. – Chris G Aug 02 '16 at 21:51
  • 1
    @Gandalf, thank you for all of your help and allowed me to go through debugger and narrow down my issue. I looked at the documentation and it turns out that the way I added the Topic and Partition in the ContainerProperties would end up going through the path of KafkaConsumer.assign vs **KafkaConsumer.subscribe**. Consumer group only works with the **KafkaConsumer.subscribe** path. I changed the way I create the ContainerProperties. Now, only one of the instances receive the message. – Chris G Aug 03 '16 at 21:50
  • This document talks about assign vs subscribe. For Consumer Group, we need to use subscribe which is controlled by how ConsumerProperties is constructed. https://kafka.apache.org/090/javadoc/index.html?org/apache/kafka/clients/consumer/KafkaConsumer.html – Chris G Aug 03 '16 at 21:55

0 Answers0