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 9be82b6130c4 CAMEL-26537: camel-core - TimerListenerManager: adding or 
removing a listener while the timer runs must not stop the timer (#29121)
9be82b6130c4 is described below

commit 9be82b6130c4c8491654b0f4e8559119315316cb
Author: allthingssecurity <[email protected]>
AuthorDate: Wed Nov 4 13:43:57 2026 +0530

    CAMEL-26537: camel-core - TimerListenerManager: adding or removing a 
listener while the timer runs must not stop the timer (#29121)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../camel/impl/engine/PeriodTaskSchedulerTest.java |  65 ++++++++++++
 .../camel/support/TimerListenerManagerTest.java    | 109 +++++++++++++++++++++
 .../apache/camel/support/TimerListenerManager.java |   5 +-
 3 files changed, 177 insertions(+), 2 deletions(-)

diff --git 
a/core/camel-core/src/test/java/org/apache/camel/impl/engine/PeriodTaskSchedulerTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/impl/engine/PeriodTaskSchedulerTest.java
index f1cff294801f..4a167462f293 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/impl/engine/PeriodTaskSchedulerTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/impl/engine/PeriodTaskSchedulerTest.java
@@ -16,7 +16,9 @@
  */
 package org.apache.camel.impl.engine;
 
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.camel.ContextTestSupport;
@@ -128,4 +130,67 @@ public class PeriodTaskSchedulerTest extends 
ContextTestSupport {
         Awaitility.waitAtMost(5, TimeUnit.SECONDS).until(() -> counter.get() 
== 1 && taskCounter.get() == 10);
     }
 
+    @Test
+    public void testSchedulePeriodTaskWhileTimerRuns() throws Exception {
+        final AtomicBoolean first = new AtomicBoolean(true);
+        final CountDownLatch entered = new CountDownLatch(1);
+        final CountDownLatch released = new CountDownLatch(1);
+        final AtomicInteger secondCounter = new AtomicInteger();
+        final AtomicInteger addedCounter = new AtomicInteger();
+
+        PeriodTaskScheduler scheduler = 
PluginHelper.getPeriodTaskScheduler(context);
+        if (scheduler instanceof TimerListenerManager timerListenerManager) {
+            // speedup unit test
+            timerListenerManager.setInterval(10);
+        }
+        // the first task blocks the timer thread in its first run (like a 
task calling a remote service)
+        scheduler.schedulePeriodTask(() -> {
+            if (first.compareAndSet(true, false)) {
+                entered.countDown();
+                try {
+                    released.await(10, TimeUnit.SECONDS);
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                }
+            }
+        }, 1);
+        scheduler.schedulePeriodTask(secondCounter::incrementAndGet, 1);
+        context.start();
+        try {
+            Assertions.assertTrue(entered.await(5, TimeUnit.SECONDS), "The 
first task should run");
+            // a task scheduled while the timer runs the first task (like a 
consumer started at runtime)
+            scheduler.schedulePeriodTask(addedCounter::incrementAndGet, 1);
+            released.countDown();
+
+            // the timer must keep running the tasks
+            Awaitility.waitAtMost(5, TimeUnit.SECONDS).until(() -> 
secondCounter.get() > 1 && addedCounter.get() > 1);
+        } finally {
+            released.countDown();
+        }
+    }
+
+    @Test
+    public void testSchedulePeriodTaskFromRunningTask() {
+        final AtomicBoolean first = new AtomicBoolean(true);
+        final AtomicInteger secondCounter = new AtomicInteger();
+        final AtomicInteger addedCounter = new AtomicInteger();
+
+        PeriodTaskScheduler scheduler = 
PluginHelper.getPeriodTaskScheduler(context);
+        if (scheduler instanceof TimerListenerManager timerListenerManager) {
+            // speedup unit test
+            timerListenerManager.setInterval(10);
+        }
+        // the first task schedules a task from its run, on the timer thread 
(like a vault refresh task that
+        // reloads the routes, whose consumers schedule their own tasks when 
they start)
+        scheduler.schedulePeriodTask(() -> {
+            if (first.compareAndSet(true, false)) {
+                scheduler.schedulePeriodTask(addedCounter::incrementAndGet, 1);
+            }
+        }, 1);
+        scheduler.schedulePeriodTask(secondCounter::incrementAndGet, 1);
+        context.start();
+
+        // the timer must keep running the tasks
+        Awaitility.waitAtMost(5, TimeUnit.SECONDS).until(() -> 
secondCounter.get() > 1 && addedCounter.get() > 1);
+    }
 }
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/support/TimerListenerManagerTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/support/TimerListenerManagerTest.java
index 5340eb10ee02..81345bd83b30 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/support/TimerListenerManagerTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/support/TimerListenerManagerTest.java
@@ -20,6 +20,7 @@ import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.apache.camel.ContextTestSupport;
 import org.apache.camel.TimerListener;
@@ -54,6 +55,114 @@ public class TimerListenerManagerTest extends 
ContextTestSupport {
         executor.shutdown();
     }
 
+    @Test
+    public void testAddListenerWhileTimerRuns() throws Exception {
+        TimerListenerManager manager = new TimerListenerManager();
+        manager.setCamelContext(context);
+        manager.setInterval(10);
+        BlockingListener blocking = new BlockingListener();
+        CountingListener next = new CountingListener(2);
+        manager.addTimerListener(blocking);
+        manager.addTimerListener(next);
+        manager.start();
+        try {
+            // the timer thread is in onTimer of the first listener, the 
second listener is still to be called
+            assertTrue(blocking.awaitEntered(), "The timer should call the 
first listener");
+            CountingListener added = new CountingListener(1);
+            manager.addTimerListener(added);
+            blocking.release();
+
+            // the timer must keep running and call every listener on the next 
runs
+            assertTrue(next.await(), "The timer should keep calling the second 
listener");
+            assertTrue(added.await(), "The timer should call the listener 
added while it was running");
+        } finally {
+            blocking.release();
+            manager.shutdown();
+        }
+    }
+
+    @Test
+    public void testRemoveListenerWhileTimerRuns() throws Exception {
+        TimerListenerManager manager = new TimerListenerManager();
+        manager.setCamelContext(context);
+        manager.setInterval(10);
+        BlockingListener blocking = new BlockingListener();
+        // the run in progress may still call the removed listener once
+        TimerListener removed = new CountingListener(1);
+        CountingListener last = new CountingListener(2);
+        manager.addTimerListener(blocking);
+        manager.addTimerListener(removed);
+        manager.addTimerListener(last);
+        manager.start();
+        try {
+            assertTrue(blocking.awaitEntered(), "The timer should call the 
first listener");
+            manager.removeTimerListener(removed);
+            blocking.release();
+
+            // the timer must keep running and call the remaining listeners on 
the next runs
+            assertTrue(last.await(), "The timer should keep calling the 
remaining listeners");
+            assertTrue(blocking.awaitCalledAgain(), "The timer should keep 
calling the first listener");
+        } finally {
+            blocking.release();
+            manager.shutdown();
+        }
+    }
+
+    /**
+     * Blocks the timer thread in its first call until released.
+     */
+    private static class BlockingListener implements TimerListener {
+
+        private final AtomicBoolean first = new AtomicBoolean(true);
+        private final CountDownLatch entered = new CountDownLatch(1);
+        private final CountDownLatch released = new CountDownLatch(1);
+        private final CountDownLatch calledAgain = new CountDownLatch(1);
+
+        @Override
+        public void onTimer() {
+            if (first.compareAndSet(true, false)) {
+                entered.countDown();
+                try {
+                    released.await(10, TimeUnit.SECONDS);
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                }
+            } else {
+                calledAgain.countDown();
+            }
+        }
+
+        boolean awaitEntered() throws InterruptedException {
+            return entered.await(5, TimeUnit.SECONDS);
+        }
+
+        boolean awaitCalledAgain() throws InterruptedException {
+            return calledAgain.await(5, TimeUnit.SECONDS);
+        }
+
+        void release() {
+            released.countDown();
+        }
+    }
+
+    private static class CountingListener implements TimerListener {
+
+        private final CountDownLatch latch;
+
+        CountingListener(int calls) {
+            this.latch = new CountDownLatch(calls);
+        }
+
+        @Override
+        public void onTimer() {
+            latch.countDown();
+        }
+
+        boolean await() throws InterruptedException {
+            return latch.await(5, TimeUnit.SECONDS);
+        }
+    }
+
     private static class MyTask implements TimerListener {
 
         private final CountDownLatch latch = new CountDownLatch(1);
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/TimerListenerManager.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/TimerListenerManager.java
index 58a25324f44e..13c817194823 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/TimerListenerManager.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/TimerListenerManager.java
@@ -17,8 +17,8 @@
 package org.apache.camel.support;
 
 import java.util.Collections;
-import java.util.LinkedHashSet;
 import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
@@ -44,7 +44,8 @@ import org.slf4j.LoggerFactory;
 public class TimerListenerManager extends ServiceSupport implements Runnable, 
CamelContextAware, StaticService {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(TimerListenerManager.class);
-    private final Set<TimerListener> listeners = new LinkedHashSet<>();
+    // listeners are added and removed by other threads while the timer thread 
iterates them
+    private final Set<TimerListener> listeners = new CopyOnWriteArraySet<>();
     private CamelContext camelContext;
     private ScheduledExecutorService executorService;
     private volatile ScheduledFuture<?> task;

Reply via email to