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 abdb48b29960 CAMEL-23997: Fix manual commit mode auto-committing 
offsets
abdb48b29960 is described below

commit abdb48b299602579c292ce6558c3247c4a2f1511
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Jul 15 12:54:37 2026 +0200

    CAMEL-23997: Fix manual commit mode auto-committing offsets
    
    When allowManualCommit=true was used with DefaultKafkaManualCommitFactory
    or DefaultKafkaManualAsyncCommitFactory, the streaming processor
    auto-committed offsets for every processed record — even when the route
    did not call KafkaManualCommit.commit(). This defeated the purpose of
    manual commit, causing silent message loss (at-most-once instead of
    at-least-once).
    
    The fix guards the framework's auto-commit call in
    KafkaRecordStreamingProcessorFacade.processPolledRecords() with
    !isAllowManualCommit(). The recordOffset() call (which only caches
    offsets internally) remains unconditional because breakOnFirstError
    and offset repository updates depend on the cached offsets.
    
    Also fixes state repository not being updated when allowManualCommit=true
    with an offsetRepository.
    
    Closes #24696
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../kafka/consumer/AbstractCommitManager.java      |   5 +
 .../consumer/DefaultKafkaManualAsyncCommit.java    |   1 +
 .../KafkaRecordStreamingProcessorFacade.java       |   3 +-
 .../kafka/consumer/CommitManagersTest.java         | 101 +++++++++++++++++++++
 .../KafkaRecordStreamingProcessorTest.java         |  80 ++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc    |  12 +++
 6 files changed, 201 insertions(+), 1 deletion(-)

diff --git 
a/components/camel-kafka/src/main/java/org/apache/camel/component/kafka/consumer/AbstractCommitManager.java
 
b/components/camel-kafka/src/main/java/org/apache/camel/component/kafka/consumer/AbstractCommitManager.java
index 1e7638a80fa8..e3f48c5f749b 100644
--- 
a/components/camel-kafka/src/main/java/org/apache/camel/component/kafka/consumer/AbstractCommitManager.java
+++ 
b/components/camel-kafka/src/main/java/org/apache/camel/component/kafka/consumer/AbstractCommitManager.java
@@ -92,6 +92,11 @@ public abstract class AbstractCommitManager implements 
CommitManager {
         consumer.commitSync(
                 Collections.singletonMap(partition, new 
OffsetAndMetadata(partitionLastOffset + 1)),
                 Duration.ofMillis(timeout));
+
+        StateRepository<String, String> offsetRepository = 
configuration.getOffsetRepository();
+        if (offsetRepository != null) {
+            saveStateToOffsetRepository(partition, partitionLastOffset, 
offsetRepository);
+        }
     }
 
     protected void saveStateToOffsetRepository(
diff --git 
a/components/camel-kafka/src/main/java/org/apache/camel/component/kafka/consumer/DefaultKafkaManualAsyncCommit.java
 
b/components/camel-kafka/src/main/java/org/apache/camel/component/kafka/consumer/DefaultKafkaManualAsyncCommit.java
index 7eea8cdc5f46..20a86258ad6d 100644
--- 
a/components/camel-kafka/src/main/java/org/apache/camel/component/kafka/consumer/DefaultKafkaManualAsyncCommit.java
+++ 
b/components/camel-kafka/src/main/java/org/apache/camel/component/kafka/consumer/DefaultKafkaManualAsyncCommit.java
@@ -29,5 +29,6 @@ public class DefaultKafkaManualAsyncCommit extends 
DefaultKafkaManualCommit impl
     @Override
     public void commit() {
         commitManager.recordOffset(getPartition(), getRecordOffset());
+        commitManager.commit(getPartition());
     }
 }
diff --git 
a/components/camel-kafka/src/main/java/org/apache/camel/component/kafka/consumer/support/streaming/KafkaRecordStreamingProcessorFacade.java
 
b/components/camel-kafka/src/main/java/org/apache/camel/component/kafka/consumer/support/streaming/KafkaRecordStreamingProcessorFacade.java
index 0e6aa6ace087..e7fcde868ccd 100644
--- 
a/components/camel-kafka/src/main/java/org/apache/camel/component/kafka/consumer/support/streaming/KafkaRecordStreamingProcessorFacade.java
+++ 
b/components/camel-kafka/src/main/java/org/apache/camel/component/kafka/consumer/support/streaming/KafkaRecordStreamingProcessorFacade.java
@@ -116,7 +116,8 @@ public class KafkaRecordStreamingProcessorFacade extends 
AbstractKafkaRecordProc
                 }
             }
 
-            if (!result.isBreakOnErrorHit()) {
+            if (!result.isBreakOnErrorHit()
+                    && 
!camelKafkaConsumer.getEndpoint().getConfiguration().isAllowManualCommit()) {
                 LOG.debug("Committing offset on successful execution");
                 // all records processed from partition so commit them
                 commitManager.commit(partition);
diff --git 
a/components/camel-kafka/src/test/java/org/apache/camel/component/kafka/consumer/CommitManagersTest.java
 
b/components/camel-kafka/src/test/java/org/apache/camel/component/kafka/consumer/CommitManagersTest.java
new file mode 100644
index 000000000000..0e151d8cf111
--- /dev/null
+++ 
b/components/camel-kafka/src/test/java/org/apache/camel/component/kafka/consumer/CommitManagersTest.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.kafka.consumer;
+
+import org.apache.camel.component.kafka.KafkaConfiguration;
+import org.apache.camel.component.kafka.KafkaConsumer;
+import org.apache.camel.component.kafka.KafkaEndpoint;
+import org.apache.kafka.clients.consumer.Consumer;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class CommitManagersTest {
+
+    private final Consumer<?, ?> consumer = mock(Consumer.class);
+    private final KafkaConsumer kafkaConsumer = mock(KafkaConsumer.class);
+    private final KafkaEndpoint endpoint = mock(KafkaEndpoint.class);
+    private final KafkaConfiguration configuration = 
mock(KafkaConfiguration.class);
+
+    @BeforeEach
+    void setup() {
+        when(kafkaConsumer.getEndpoint()).thenReturn(endpoint);
+        when(endpoint.getConfiguration()).thenReturn(configuration);
+    }
+
+    @Test
+    void manualCommitWithNoFactoryUsesNoop() {
+        when(configuration.isAllowManualCommit()).thenReturn(true);
+        when(endpoint.getKafkaManualCommitFactory()).thenReturn(null);
+
+        CommitManager cm = CommitManagers.createCommitManager(consumer, 
kafkaConsumer, "t1", "topic");
+
+        assertInstanceOf(NoopCommitManager.class, cm);
+    }
+
+    @Test
+    void manualCommitWithSyncFactoryUsesSync() {
+        when(configuration.isAllowManualCommit()).thenReturn(true);
+        when(endpoint.getKafkaManualCommitFactory()).thenReturn(new 
DefaultKafkaManualCommitFactory());
+
+        CommitManager cm = CommitManagers.createCommitManager(consumer, 
kafkaConsumer, "t1", "topic");
+
+        assertInstanceOf(SyncCommitManager.class, cm);
+    }
+
+    @Test
+    void manualCommitWithAsyncFactoryUsesAsync() {
+        when(configuration.isAllowManualCommit()).thenReturn(true);
+        when(endpoint.getKafkaManualCommitFactory()).thenReturn(new 
DefaultKafkaManualAsyncCommitFactory());
+
+        CommitManager cm = CommitManagers.createCommitManager(consumer, 
kafkaConsumer, "t1", "topic");
+
+        assertInstanceOf(AsyncCommitManager.class, cm);
+    }
+
+    @Test
+    void autoCommitWithOffsetRepositoryUsesCommitToOffset() {
+        when(configuration.isAllowManualCommit()).thenReturn(false);
+        when(configuration.getOffsetRepository()).thenReturn(mock());
+
+        CommitManager cm = CommitManagers.createCommitManager(consumer, 
kafkaConsumer, "t1", "topic");
+
+        assertInstanceOf(CommitToOffsetManager.class, cm);
+    }
+
+    @Test
+    void autoCommitWithBatchingUsesAsync() {
+        when(configuration.isAllowManualCommit()).thenReturn(false);
+        when(configuration.isBatching()).thenReturn(true);
+
+        CommitManager cm = CommitManagers.createCommitManager(consumer, 
kafkaConsumer, "t1", "topic");
+
+        assertInstanceOf(AsyncCommitManager.class, cm);
+    }
+
+    @Test
+    void defaultConfigUsesNoop() {
+        when(configuration.isAllowManualCommit()).thenReturn(false);
+
+        CommitManager cm = CommitManagers.createCommitManager(consumer, 
kafkaConsumer, "t1", "topic");
+
+        assertInstanceOf(NoopCommitManager.class, cm);
+    }
+}
diff --git 
a/components/camel-kafka/src/test/java/org/apache/camel/component/kafka/consumer/support/streaming/KafkaRecordStreamingProcessorTest.java
 
b/components/camel-kafka/src/test/java/org/apache/camel/component/kafka/consumer/support/streaming/KafkaRecordStreamingProcessorTest.java
new file mode 100644
index 000000000000..10c075536164
--- /dev/null
+++ 
b/components/camel-kafka/src/test/java/org/apache/camel/component/kafka/consumer/support/streaming/KafkaRecordStreamingProcessorTest.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.kafka.consumer.support.streaming;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.Message;
+import org.apache.camel.Processor;
+import org.apache.camel.component.kafka.KafkaConfiguration;
+import org.apache.camel.component.kafka.KafkaConsumer;
+import org.apache.camel.component.kafka.consumer.CommitManager;
+import org.apache.camel.component.kafka.consumer.KafkaManualCommit;
+import org.apache.camel.component.kafka.serde.KafkaHeaderDeserializer;
+import org.apache.camel.spi.HeaderFilterStrategy;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.apache.kafka.common.TopicPartition;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class KafkaRecordStreamingProcessorTest {
+
+    private final KafkaConfiguration configuration = 
mock(KafkaConfiguration.class);
+    private final Processor processor = mock(Processor.class);
+    private final CommitManager commitManager = mock(CommitManager.class);
+    private final KafkaConsumer kafkaConsumer = mock(KafkaConsumer.class);
+
+    private final TopicPartition topicPartition = new 
TopicPartition("test-topic", 0);
+    private final ConsumerRecord<Object, Object> record = new 
ConsumerRecord<>("test-topic", 0, 42L, "key", "value");
+
+    @BeforeEach
+    void setup() {
+        Exchange exchange = mock(Exchange.class);
+        Message message = mock(Message.class);
+        when(kafkaConsumer.createExchange(false)).thenReturn(exchange);
+        when(exchange.getMessage()).thenReturn(message);
+        when(exchange.getIn()).thenReturn(message);
+
+        
when(configuration.getHeaderFilterStrategy()).thenReturn(mock(HeaderFilterStrategy.class));
+        
when(configuration.getHeaderDeserializer()).thenReturn(mock(KafkaHeaderDeserializer.class));
+    }
+
+    @Test
+    void manualCommitStillRecordsOffset() {
+        when(configuration.isAllowManualCommit()).thenReturn(true);
+        when(commitManager.getManualCommit(any(), any(), 
any())).thenReturn(mock(KafkaManualCommit.class));
+
+        KafkaRecordStreamingProcessor proc = new 
KafkaRecordStreamingProcessor(configuration, processor, commitManager);
+        proc.processExchange(kafkaConsumer, topicPartition, false, false, 
record);
+
+        verify(commitManager).recordOffset(topicPartition, 42L);
+    }
+
+    @Test
+    void autoCommitRecordsOffset() {
+        when(configuration.isAllowManualCommit()).thenReturn(false);
+
+        KafkaRecordStreamingProcessor proc = new 
KafkaRecordStreamingProcessor(configuration, processor, commitManager);
+        proc.processExchange(kafkaConsumer, topicPartition, false, false, 
record);
+
+        verify(commitManager).recordOffset(topicPartition, 42L);
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
index 859c222605bc..80325fece7c1 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
@@ -295,6 +295,18 @@ The `queueBufferingMaxMessages` producer option has been 
deprecated. This option
 since the old Scala Kafka producer was removed in Camel 2.17 (CAMEL-9467). Use 
`bufferMemorySize`
 or `maxBlockMs` instead.
 
+=== camel-kafka - Manual commit no longer auto-commits offsets
+
+When using `allowManualCommit=true` together with a 
`DefaultKafkaManualCommitFactory` or
+`DefaultKafkaManualAsyncCommitFactory`, the framework previously 
auto-committed offsets for every
+processed record — even when the route did not call 
`KafkaManualCommit.commit()`. This defeated the
+purpose of manual commit mode and could cause message loss.
+
+The framework no longer auto-commits offsets after processing each partition 
when manual commit
+is enabled. Offsets are only committed when the route explicitly calls 
`KafkaManualCommit.commit()`
+on the exchange header. The configured factory still controls whether that 
explicit commit executes
+synchronously or asynchronously.
+
 === camel-jbang catalog tables fill the terminal width
 
 The `camel catalog` commands (`camel catalog component`, `camel catalog 
dataformat`,

Reply via email to