This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new aa1cec25fbc7 CAMEL-23994: Fix batching auto-commit committing 
unprocessed offsets
aa1cec25fbc7 is described below

commit aa1cec25fbc72add327a46efca1825274e12e802
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Jul 14 18:17:20 2026 +0200

    CAMEL-23994: Fix batching auto-commit committing unprocessed offsets
    
    In batching mode, CommitSynchronization.onComplete() called the no-arg
    commitManager.commit() which commits the consumer's current position.
    Because Kafka pre-fetches, this position is ahead of what was actually
    processed, silently skipping records on restart.
    
    Fix: compute the max offset per TopicPartition from the batch exchanges
    and explicitly recordOffset + commit for each partition, so only the
    offsets of actually-processed records are committed.
    
    Closes #24688
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../batching/KafkaRecordBatchingProcessor.java     | 37 +++++++++++++++++++---
 1 file changed, 33 insertions(+), 4 deletions(-)

diff --git 
a/components/camel-kafka/src/main/java/org/apache/camel/component/kafka/consumer/support/batching/KafkaRecordBatchingProcessor.java
 
b/components/camel-kafka/src/main/java/org/apache/camel/component/kafka/consumer/support/batching/KafkaRecordBatchingProcessor.java
index 34e12c2b9bfb..ad81b501732b 100644
--- 
a/components/camel-kafka/src/main/java/org/apache/camel/component/kafka/consumer/support/batching/KafkaRecordBatchingProcessor.java
+++ 
b/components/camel-kafka/src/main/java/org/apache/camel/component/kafka/consumer/support/batching/KafkaRecordBatchingProcessor.java
@@ -16,7 +16,9 @@
  */
 package org.apache.camel.component.kafka.consumer.support.batching;
 
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Queue;
 import java.util.concurrent.ArrayBlockingQueue;
 
@@ -53,16 +55,26 @@ final class KafkaRecordBatchingProcessor extends 
KafkaRecordProcessor {
     private final class CommitSynchronization implements Synchronization {
         private final ExceptionHandler exceptionHandler;
         private final int size;
+        private final Map<TopicPartition, Long> batchOffsets;
 
-        public CommitSynchronization(ExceptionHandler exceptionHandler, int 
size) {
+        public CommitSynchronization(ExceptionHandler exceptionHandler, int 
size,
+                                     Map<TopicPartition, Long> batchOffsets) {
             this.exceptionHandler = exceptionHandler;
             this.size = size;
+            this.batchOffsets = batchOffsets;
+        }
+
+        private void commitBatchOffsets() {
+            for (Map.Entry<TopicPartition, Long> entry : 
batchOffsets.entrySet()) {
+                commitManager.recordOffset(entry.getKey(), entry.getValue());
+                commitManager.commit(entry.getKey());
+            }
         }
 
         @Override
         public void onComplete(Exchange exchange) {
             LOG.debug("Calling commit on {} exchanges using {}", size, 
commitManager.getClass().getSimpleName());
-            commitManager.commit();
+            commitBatchOffsets();
         }
 
         @Override
@@ -80,7 +92,7 @@ final class KafkaRecordBatchingProcessor extends 
KafkaRecordProcessor {
                     // This allows the consumer to move forward and 
potentially retry the problematic batch
                     LOG.debug("Calling commit on {} exchanges using {} due to 
breakOnFirstError", size,
                             commitManager.getClass().getSimpleName());
-                    commitManager.commit();
+                    commitBatchOffsets();
                 } else {
                     // Standard error handling - just log and continue 
(original behavior)
                     exceptionHandler.handleException(
@@ -182,6 +194,21 @@ final class KafkaRecordBatchingProcessor extends 
KafkaRecordProcessor {
         return timeout || interval;
     }
 
+    private Map<TopicPartition, Long> computeBatchOffsets(List<Exchange> 
exchanges) {
+        Map<TopicPartition, Long> offsets = new HashMap<>();
+        for (Exchange ex : exchanges) {
+            Message msg = ex.getMessage();
+            String topic = (String) msg.getHeader(KafkaConstants.TOPIC);
+            Integer partition = (Integer) 
msg.getHeader(KafkaConstants.PARTITION);
+            Long offset = (Long) msg.getHeader(KafkaConstants.OFFSET);
+            if (topic != null && partition != null && offset != null) {
+                TopicPartition tp = new TopicPartition(topic, partition);
+                offsets.merge(tp, offset, Math::max);
+            }
+        }
+        return offsets;
+    }
+
     private ProcessingResult processBatch(KafkaConsumer camelKafkaConsumer) {
         intervalWatch.restart();
 
@@ -217,7 +244,9 @@ final class KafkaRecordBatchingProcessor extends 
KafkaRecordProcessor {
     private ProcessingResult autoCommitResultProcessing(
             KafkaConsumer camelKafkaConsumer, Exchange exchange, 
List<Exchange> exchanges) {
         ExceptionHandler exceptionHandler = 
camelKafkaConsumer.getExceptionHandler();
-        CommitSynchronization commitSynchronization = new 
CommitSynchronization(exceptionHandler, exchanges.size());
+        Map<TopicPartition, Long> batchOffsets = 
computeBatchOffsets(exchanges);
+        CommitSynchronization commitSynchronization
+                = new CommitSynchronization(exceptionHandler, 
exchanges.size(), batchOffsets);
         exchange.getExchangeExtension().addOnCompletion(commitSynchronization);
         try {
             processor.process(exchange);

Reply via email to