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 1ba98b8108d6 CAMEL-23996: Fix SingleNodeKafkaResumeStrategy 
correctness bugs
1ba98b8108d6 is described below

commit 1ba98b8108d6c132921c4a7c33d922c5eff94f77
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Jul 15 09:03:47 2026 +0200

    CAMEL-23996: Fix SingleNodeKafkaResumeStrategy correctness bugs
    
    Fix three bugs in SingleNodeKafkaResumeStrategy:
    
    - Double subscribe replaces rebalance listener: refresh() called
      subscribe(consumer) which registers a ConsumerRebalanceListener, then
      immediately called consumer.subscribe() again without the listener,
      replacing the subscription. Removed the redundant second subscribe.
    - getAdapter() never waits for initialization: the condition adapter==null
      was never true because setAdapter() is always called before loadCache().
      Changed to check initLatch != null so callers block until cache loading
      completes.
    - stop() throws IllegalMonitorStateException: writeLock.tryLock() can
      return false, but the finally block always called unlock(). Added
      isHeldByCurrentThread() guard before unlocking.
    
    Closes #24692
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../kafka/SingleNodeKafkaResumeStrategy.java       |   8 +-
 .../kafka/SingleNodeKafkaResumeStrategyTest.java   | 104 +++++++++++++++++++++
 2 files changed, 108 insertions(+), 4 deletions(-)

diff --git 
a/components/camel-kafka/src/main/java/org/apache/camel/processor/resume/kafka/SingleNodeKafkaResumeStrategy.java
 
b/components/camel-kafka/src/main/java/org/apache/camel/processor/resume/kafka/SingleNodeKafkaResumeStrategy.java
index 32850784eaab..7bd0ad00875b 100644
--- 
a/components/camel-kafka/src/main/java/org/apache/camel/processor/resume/kafka/SingleNodeKafkaResumeStrategy.java
+++ 
b/components/camel-kafka/src/main/java/org/apache/camel/processor/resume/kafka/SingleNodeKafkaResumeStrategy.java
@@ -209,8 +209,6 @@ public class SingleNodeKafkaResumeStrategy implements 
KafkaResumeStrategy, Camel
             subscribe(consumer);
 
             LOG.debug("Loading records from topic {}", 
resumeStrategyConfiguration.getTopic());
-            
consumer.subscribe(Collections.singletonList(resumeStrategyConfiguration.getTopic()));
-
             poll(consumer, latch);
         } catch (WakeupException e) {
             LOG.info("Kafka consumer was interrupted during a blocking call");
@@ -358,7 +356,7 @@ public class SingleNodeKafkaResumeStrategy implements 
KafkaResumeStrategy, Camel
 
     @Override
     public ResumeAdapter getAdapter() {
-        if (adapter == null) {
+        if (initLatch != null) {
             waitForInitialization();
         }
 
@@ -405,7 +403,9 @@ public class SingleNodeKafkaResumeStrategy implements 
KafkaResumeStrategy, Camel
         } catch (Exception e) {
             LOG.warn("Error closing the Kafka producer: {} (this error will be 
ignored)", e.getMessage(), e);
         } finally {
-            writeLock.unlock();
+            if (writeLock.isHeldByCurrentThread()) {
+                writeLock.unlock();
+            }
         }
 
         try {
diff --git 
a/components/camel-kafka/src/test/java/org/apache/camel/processor/resume/kafka/SingleNodeKafkaResumeStrategyTest.java
 
b/components/camel-kafka/src/test/java/org/apache/camel/processor/resume/kafka/SingleNodeKafkaResumeStrategyTest.java
new file mode 100644
index 000000000000..521152ace7c9
--- /dev/null
+++ 
b/components/camel-kafka/src/test/java/org/apache/camel/processor/resume/kafka/SingleNodeKafkaResumeStrategyTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.processor.resume.kafka;
+
+import java.lang.reflect.Field;
+import java.util.Properties;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.apache.camel.resume.ResumeAdapter;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Unit tests for {@link SingleNodeKafkaResumeStrategy}.
+ */
+public class SingleNodeKafkaResumeStrategyTest {
+
+    @Test
+    void testStopDoesNotThrowWhenLockNotAcquired() throws Exception {
+        SingleNodeKafkaResumeStrategy strategy = new 
SingleNodeKafkaResumeStrategy();
+
+        ReentrantLock lock = getField(strategy, "writeLock");
+        // Acquire the lock from another thread so tryLock fails in stop()
+        Thread holder = new Thread(lock::lock);
+        holder.start();
+        holder.join();
+
+        // stop() should not throw IllegalMonitorStateException
+        assertDoesNotThrow(strategy::stop);
+
+        // Release the lock from the holder thread
+        Thread releaser = new Thread(lock::unlock);
+        releaser.start();
+        releaser.join();
+    }
+
+    @Test
+    void testGetAdapterReturnsAdapterWithoutLoadCache() {
+        SingleNodeKafkaResumeStrategy strategy = new 
SingleNodeKafkaResumeStrategy();
+
+        ResumeAdapter adapter = new TestAdapter();
+        strategy.setAdapter(adapter);
+
+        assertSame(adapter, strategy.getAdapter());
+    }
+
+    @Test
+    void testGetAdapterWaitsForInitializationWhenLatchIsSet() throws Exception 
{
+        Properties props = new Properties();
+        props.put("bootstrap.servers", "localhost:9092");
+        KafkaResumeStrategyConfiguration config
+                = new KafkaResumeStrategyConfigurationBuilder(props, props)
+                        .withTopic("test-topic")
+                        .build();
+
+        SingleNodeKafkaResumeStrategy strategy = new 
SingleNodeKafkaResumeStrategy(config);
+
+        ResumeAdapter adapter = new TestAdapter();
+        strategy.setAdapter(adapter);
+
+        CountDownLatch latch = new CountDownLatch(1);
+        setField(strategy, "initLatch", latch);
+
+        // Count down so waitForInitialization() completes immediately
+        latch.countDown();
+
+        assertSame(adapter, strategy.getAdapter());
+    }
+
+    static class TestAdapter implements ResumeAdapter {
+        @Override
+        public void resume() {
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <T> T getField(Object target, String fieldName) throws 
Exception {
+        Field field = target.getClass().getDeclaredField(fieldName);
+        field.setAccessible(true);
+        return (T) field.get(target);
+    }
+
+    private static void setField(Object target, String fieldName, Object 
value) throws Exception {
+        Field field = target.getClass().getDeclaredField(fieldName);
+        field.setAccessible(true);
+        field.set(target, value);
+    }
+}

Reply via email to