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 b1fbaa979314 CAMEL-26798: camel-hazelcast - HazelcastRoutePolicy 
retries a failed consumer start and does not start the consumer of a stopping 
route (#29585)
b1fbaa979314 is described below

commit b1fbaa979314a750d8e6555bb01e544bf6c45a8e
Author: allthingssecurity <[email protected]>
AuthorDate: Wed Nov 11 12:27:06 2026 +0530

    CAMEL-26798: camel-hazelcast - HazelcastRoutePolicy retries a failed 
consumer start and does not start the consumer of a stopping route (#29585)
    
    * CAMEL-26798: camel-hazelcast - HazelcastRoutePolicy retries a failed 
consumer start and does not start the consumer of a stopping route
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../hazelcast/policy/HazelcastRoutePolicy.java     | 287 +++++++++----
 .../policy/HazelcastRoutePolicyLeadershipTest.java | 452 +++++++++++++++++++++
 2 files changed, 666 insertions(+), 73 deletions(-)

diff --git 
a/components/camel-hazelcast/src/main/java/org/apache/camel/component/hazelcast/policy/HazelcastRoutePolicy.java
 
b/components/camel-hazelcast/src/main/java/org/apache/camel/component/hazelcast/policy/HazelcastRoutePolicy.java
index 9bf14d299004..4cc630527656 100644
--- 
a/components/camel-hazelcast/src/main/java/org/apache/camel/component/hazelcast/policy/HazelcastRoutePolicy.java
+++ 
b/components/camel-hazelcast/src/main/java/org/apache/camel/component/hazelcast/policy/HazelcastRoutePolicy.java
@@ -17,14 +17,18 @@
 package org.apache.camel.component.hazelcast.policy;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 import com.hazelcast.core.HazelcastInstance;
 import com.hazelcast.map.IMap;
@@ -35,6 +39,7 @@ import org.apache.camel.api.management.ManagedAttribute;
 import org.apache.camel.api.management.ManagedResource;
 import org.apache.camel.component.hazelcast.HazelcastUtil;
 import org.apache.camel.support.RoutePolicySupport;
+import org.apache.camel.support.service.ServiceHelper;
 import org.apache.camel.util.StringHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -42,14 +47,22 @@ import org.slf4j.LoggerFactory;
 @ManagedResource(description = "Route policy using Hazelcast as clustered 
lock")
 public class HazelcastRoutePolicy extends RoutePolicySupport implements 
CamelContextAware {
     private static final Logger LOGGER = 
LoggerFactory.getLogger(HazelcastRoutePolicy.class);
+    // delay before the leader tries again to start a consumer whose start 
failed
+    private static final long RETRY_DELAY_MILLIS = 5000;
 
     private final boolean managedInstance;
     private final AtomicBoolean leader;
     private final Set<Route> suspendedRoutes;
     private final Set<Route> routes;
     private final Set<Route> suspendedWhileStopped;
-    // the leadership threads told to end that may still wait for or release 
the lock (guarded by lock)
-    private final List<Future<Void>> releasedFutures;
+    // failed consumer starts in a row per route, for the log
+    private final Map<Route, Integer> failedStarts = new HashMap<>();
+    // guards the leader flag, the route sets and the leadership thread, so a 
leadership taken while a route starts,
+    // stops or resumes is not missed. Not the lifecycle lock of the service: 
stop() holds that one while it waits for
+    // the leadership thread
+    private final Lock leadershipLock = new ReentrantLock();
+    // the leadership threads told to end that may still wait for or release 
the lock (guarded by leadershipLock)
+    private final List<Future<Void>> releasedFutures = new ArrayList<>();
 
     // a route of the policy, for JMX
     private volatile Route route;
@@ -63,8 +76,12 @@ public class HazelcastRoutePolicy extends RoutePolicySupport 
implements CamelCon
     private TimeUnit tryLockTimeoutUnit;
     private IMap<String, String> locks;
     private volatile Future<Void> future;
-    // counted down to make the leadership thread release the lock and end 
(guarded by lock)
-    private CountDownLatch leadershipRelease;
+    // counted down to make the leadership thread release the lock and end 
(set under leadershipLock, also read
+    // without it)
+    private volatile CountDownLatch leadershipRelease;
+    // the latch of the leadership task that runs (and may hold the cluster 
lock)
+    private volatile CountDownLatch runningRelease;
+    private volatile long retryDelay = RETRY_DELAY_MILLIS;
     private boolean shouldStopConsumer;
 
     public HazelcastRoutePolicy() {
@@ -81,7 +98,6 @@ public class HazelcastRoutePolicy extends RoutePolicySupport 
implements CamelCon
         this.suspendedRoutes = new HashSet<>();
         this.routes = new HashSet<>();
         this.suspendedWhileStopped = new HashSet<>();
-        this.releasedFutures = new ArrayList<>();
         this.leader = new AtomicBoolean();
         this.lockMapName = null;
         this.lockKey = null;
@@ -107,20 +123,36 @@ public class HazelcastRoutePolicy extends 
RoutePolicySupport implements CamelCon
     public void onInit(Route route) {
         super.onInit(route);
 
+        // the lifecycle lock, so the start of the policy is not in progress
         lock.lock();
+        leadershipLock.lock();
         try {
-            addRoute(route);
+            if (routes.add(route)) {
+                if (this.route == null) {
+                    this.route = route;
+                }
+                // a route added to a policy that gave up the leadership when 
its routes were removed
+                if (leadershipRelease == null && isStarted()) {
+                    startLeadership();
+                }
+            }
         } finally {
+            leadershipLock.unlock();
             lock.unlock();
         }
     }
 
     @Override
     public void onRemove(Route route) {
-        lock.lock();
+        if (camelContext != null && camelContext.isStopping()) {
+            // all the routes go: end the leadership first, so a consumer 
start in flight does not delay the stop
+            interruptLeadership();
+        }
+        leadershipLock.lock();
         try {
             suspendedRoutes.remove(route);
             suspendedWhileStopped.remove(route);
+            failedStarts.remove(route);
             if (routes.remove(route)) {
                 if (this.route == route) {
                     this.route = routes.isEmpty() ? null : 
routes.iterator().next();
@@ -132,43 +164,54 @@ public class HazelcastRoutePolicy extends 
RoutePolicySupport implements CamelCon
                 }
             }
         } finally {
-            lock.unlock();
+            leadershipLock.unlock();
         }
     }
 
     @Override
     public void onStart(Route route) {
-        if (!leader.get() && shouldStopConsumer) {
-            stopConsumer(route);
+        // the flag is read under the lock that setLeader holds while it sets 
it and starts the stopped consumers
+        leadershipLock.lock();
+        try {
+            if (!leader.get() && shouldStopConsumer) {
+                stopConsumer(route);
+            } else if (leader.get() && 
ServiceHelper.isStopped(route.getConsumer())) {
+                // a start by the leadership thread that overlapped the start 
of the route stopped this consumer again
+                // (the route status was not started yet): the leadership 
thread starts it
+                suspendedRoutes.add(route);
+            }
+        } finally {
+            leadershipLock.unlock();
         }
     }
 
     @Override
     public void onStop(Route route) {
-        lock.lock();
+        leadershipLock.lock();
         try {
             suspendedRoutes.remove(route);
             suspendedWhileStopped.remove(route);
+            failedStarts.remove(route);
         } finally {
-            lock.unlock();
+            leadershipLock.unlock();
         }
     }
 
     @Override
     public void onSuspend(Route route) {
-        lock.lock();
+        leadershipLock.lock();
         try {
             if (suspendedRoutes.remove(route)) {
                 suspendedWhileStopped.add(route);
             }
         } finally {
-            lock.unlock();
+            leadershipLock.unlock();
         }
     }
 
     @Override
     public void onResume(Route route) {
-        lock.lock();
+        leadershipLock.lock();
         try {
             boolean stoppedByPolicy = suspendedWhileStopped.remove(route);
             if (!leader.get() && shouldStopConsumer) {
@@ -178,13 +221,16 @@ public class HazelcastRoutePolicy extends 
RoutePolicySupport implements CamelCon
                 // the leadership was taken while the route was suspended: 
resuming a consumer that this policy had
                 // stopped does not start it again (a SEDA consumer is marked 
started without consuming), so restart it
                 LOGGER.debug("Restarting consumer for {} ({})", route.getId(), 
route.getConsumer());
+                // the leadership thread tries again if the start fails
+                suspendedRoutes.add(route);
                 stopConsumer(route.getConsumer());
                 startConsumer(route.getConsumer());
+                suspendedRoutes.remove(route);
             }
         } catch (Exception e) {
             handleException(e);
         } finally {
-            lock.unlock();
+            leadershipLock.unlock();
         }
     }
 
@@ -198,9 +244,13 @@ public class HazelcastRoutePolicy extends 
RoutePolicySupport implements CamelCon
         executorService = 
getCamelContext().getExecutorServiceManager().newSingleThreadExecutor(this, 
"HazelcastRoutePolicy");
 
         locks = instance.getMap(lockMapName);
-        // under the lock: BaseService.start() holds it (the same lock as the 
route callbacks of the policy)
-        if (!routes.isEmpty()) {
-            startLeadership();
+        leadershipLock.lock();
+        try {
+            if (!routes.isEmpty()) {
+                startLeadership();
+            }
+        } finally {
+            leadershipLock.unlock();
         }
 
         super.doStart();
@@ -208,9 +258,15 @@ public class HazelcastRoutePolicy extends 
RoutePolicySupport implements CamelCon
 
     @Override
     protected void doStop() throws Exception {
-        // also interrupt the threads, so a wait for the lock does not delay 
the stop on a Hazelcast client (on a member
-        // tryLock ends after its timeout). Under the lock, which 
BaseService.stop() holds
-        stopLeadership(true);
+        // also interrupt the threads first, so a consumer start in flight, or 
a wait for the lock on a Hazelcast client
+        // (on a member tryLock ends after its timeout), does not delay the 
stop
+        interruptLeadership();
+        leadershipLock.lock();
+        try {
+            stopLeadership(true);
+        } finally {
+            leadershipLock.unlock();
+        }
 
         if (managedInstance) {
             instance.shutdown();
@@ -224,30 +280,29 @@ public class HazelcastRoutePolicy extends 
RoutePolicySupport implements CamelCon
     //
     // 
*************************************************************************
 
-    // under the lock
-    private void addRoute(Route route) {
-        if (routes.add(route)) {
-            if (this.route == null) {
-                this.route = route;
-            }
-            // a route added to a policy that gave up the leadership when its 
routes were removed
-            if (leadershipRelease == null && isStarted()) {
-                startLeadership();
-            }
-        }
-    }
-
-    // under the lock
+    // under the leadership lock
     private void startLeadership() {
         CountDownLatch release = new CountDownLatch(1);
         leadershipRelease = release;
         future = executorService.submit(() -> acquireLeadership(release));
     }
 
-    // under the lock: the leadership thread releases the lock and ends, and 
this node is no longer the leader. The
-    // thread is told to end with its own latch, as isRunAllowed() stays true 
while the policy runs, and is not
-    // interrupted unless the policy stops: an interrupted thread cannot 
release the lock on a client instance, and a
-    // wait for the lock ends by itself after the tryLock timeout
+    // without the leadership lock: tells the leadership thread to end and 
interrupts it (when stopping)
+    private void interruptLeadership() {
+        CountDownLatch release = leadershipRelease;
+        if (release != null) {
+            release.countDown();
+        }
+        Future<Void> task = future;
+        if (task != null) {
+            task.cancel(true);
+        }
+    }
+
+    // under the leadership lock: the leadership thread releases the lock and 
ends, and this node is no longer the
+    // leader. The thread is told to end with its own latch, as isRunAllowed() 
stays true while the policy runs, and is
+    // not interrupted unless the policy stops: an interrupted thread cannot 
release the lock on a client instance, and
+    // a wait for the lock ends by itself after the tryLock timeout
     private void stopLeadership(boolean interrupt) {
         if (leadershipRelease != null) {
             leadershipRelease.countDown();
@@ -268,23 +323,49 @@ public class HazelcastRoutePolicy extends 
RoutePolicySupport implements CamelCon
     }
 
     protected void setLeader(boolean isLeader) {
-        if (isLeader && leader.compareAndSet(false, isLeader)) {
-            LOGGER.info("Leadership taken (map={}, key={}, val={})",
-                    lockMapName,
-                    lockKey,
-                    lockValue);
-
-            startAllStoppedConsumers();
-        } else if (!isLeader && leader.getAndSet(false)) {
-            LOGGER.info("Leadership lost (map={}, key={}, val={})",
-                    lockMapName,
-                    lockKey,
-                    lockValue);
+        if (isLeader) {
+            List<Route> toStart;
+            leadershipLock.lock();
+            try {
+                toStart = markLeader();
+            } finally {
+                leadershipLock.unlock();
+            }
+            startStoppedConsumers(toStart);
+        } else {
+            leadershipLock.lock();
+            try {
+                if (leader.compareAndSet(true, false)) {
+                    LOGGER.info("Leadership lost (map={}, key={}, val={})",
+                            lockMapName,
+                            lockKey,
+                            lockValue);
+                }
+            } finally {
+                leadershipLock.unlock();
+            }
+        }
+    }
+
+    // under the leadership lock: sets the leader flag, and returns the routes 
whose consumer this policy stopped. Not
+    // when the routes were removed meanwhile (stopLeadership), so the 
leadership is not taken after that
+    private List<Route> markLeader() {
+        // the latch of the running task, not only the current one: a task 
that ended its leadership (its routes were
+        // removed) must not take it for a route added meanwhile, as it 
releases the cluster lock
+        CountDownLatch release = leadershipRelease;
+        if (release == null || release.getCount() == 0 || release != 
runningRelease
+                || !leader.compareAndSet(false, true)) {
+            return List.of();
         }
+        LOGGER.info("Leadership taken (map={}, key={}, val={})",
+                lockMapName,
+                lockKey,
+                lockValue);
+        return new ArrayList<>(suspendedRoutes);
     }
 
+    // under the leadership lock
     private void stopConsumer(Route route) {
-        lock.lock();
         try {
             if (!suspendedRoutes.contains(route)) {
                 LOGGER.debug("Stopping consumer for {} ({})", route.getId(), 
route.getConsumer());
@@ -293,25 +374,87 @@ public class HazelcastRoutePolicy extends 
RoutePolicySupport implements CamelCon
             }
         } catch (Exception e) {
             handleException(e);
-        } finally {
-            lock.unlock();
         }
     }
 
-    private void startAllStoppedConsumers() {
-        lock.lock();
-        try {
-            for (Route route : suspendedRoutes) {
+    // Starts the consumers of the routes this policy stopped, without the 
leadership lock: a consumer start can take
+    // long (a broker connect), and the route callbacks and the stop must not 
wait for it. Each start is checked again
+    // under the lock: a route is forgotten only once its consumer started (so 
the leader tries again a start that
+    // failed), and a consumer started for a route that is stopping or 
suspending meanwhile is stopped again.
+    private void startStoppedConsumers(List<Route> toStart) {
+        for (Route route : toStart) {
+            if (!isStartWanted(route)) {
+                continue;
+            }
+            Exception cause = null;
+            try {
                 LOGGER.debug("Starting consumer for {} ({})", route.getId(), 
route.getConsumer());
                 startConsumer(route.getConsumer());
+            } catch (Exception e) {
+                cause = e;
             }
+            afterStart(route, cause);
+        }
+    }
+
+    private boolean isStartWanted(Route route) {
+        leadershipLock.lock();
+        try {
+            return isRunAllowed() && leader.get() && 
suspendedRoutes.contains(route) && isResumeOrStartConsumerAllowed(route);
+        } finally {
+            leadershipLock.unlock();
+        }
+    }
 
-            suspendedRoutes.clear();
+    private void afterStart(Route route, Exception cause) {
+        leadershipLock.lock();
+        try {
+            boolean wanted = isRunAllowed() && leader.get() && 
isResumeOrStartConsumerAllowed(route);
+            if (cause != null) {
+                if (!wanted) {
+                    LOGGER.debug("Failed to start consumer for {} while it is 
no longer wanted", route.getId(), cause);
+                    return;
+                }
+                int attempt = failedStarts.merge(route, 1, Integer::sum);
+                getExceptionHandler().handleException("Failed to start 
consumer for route " + route.getId()
+                                                      + " (attempt #" + 
attempt + "). Trying again in " + retryDelay
+                                                      + " millis.",
+                        cause);
+            } else if (!wanted) {
+                // the route is stopping or suspending (or this node is 
stopping): its consumer must not run
+                LOGGER.debug("Stopping consumer for {} again, as the route is 
{}", route.getId(),
+                        
route.getCamelContext().getRouteController().getRouteStatus(route.getId()));
+                stopConsumer(route.getConsumer());
+            } else if (suspendedRoutes.remove(route)) {
+                Integer failed = failedStarts.remove(route);
+                if (failed != null) {
+                    LOGGER.info("Consumer for route {} started after {} failed 
attempts", route.getId(), failed);
+                }
+            }
         } catch (Exception e) {
             handleException(e);
         } finally {
-            lock.unlock();
+            leadershipLock.unlock();
+        }
+    }
+
+    // the leader starts the consumers whose start failed
+    private void retryStoppedConsumers() {
+        List<Route> toStart;
+        leadershipLock.lock();
+        try {
+            toStart = leader.get() ? new ArrayList<>(suspendedRoutes) : 
List.of();
+        } finally {
+            leadershipLock.unlock();
         }
+        startStoppedConsumers(toStart);
+    }
+
+    /**
+     * The delay before the leader tries again to start a consumer whose start 
failed (5 seconds). For tests.
+     */
+    void setRetryDelay(long retryDelay) {
+        this.retryDelay = retryDelay;
     }
 
     // 
*************************************************************************
@@ -405,6 +548,7 @@ public class HazelcastRoutePolicy extends 
RoutePolicySupport implements CamelCon
     // 
*************************************************************************
 
     private Void acquireLeadership(CountDownLatch release) {
+        runningRelease = release;
         boolean locked = false;
         while (isRunAllowed() && release.getCount() > 0) {
             try {
@@ -412,8 +556,11 @@ public class HazelcastRoutePolicy extends 
RoutePolicySupport implements CamelCon
                 if (locked && release.getCount() > 0) {
                     locks.put(lockKey, lockValue);
                     if (takeLeadership(release)) {
-                        // hold the lock until the policy stops or its routes 
are removed
-                        release.await();
+                        // hold the lock until the policy stops or its routes 
are removed, and meanwhile try again to
+                        // start the consumers whose start failed
+                        while (!release.await(retryDelay, 
TimeUnit.MILLISECONDS)) {
+                            retryStoppedConsumers();
+                        }
                     }
                 } else if (!locked) {
                     LOGGER.debug("Failed to acquire lock (map={}, key={}, 
val={}) after {} {}",
@@ -441,17 +588,11 @@ public class HazelcastRoutePolicy extends 
RoutePolicySupport implements CamelCon
     }
 
     private boolean takeLeadership(CountDownLatch release) {
-        // under the lock, so the leadership is not taken after the routes 
were removed (stopLeadership)
-        lock.lock();
-        try {
-            if (release.getCount() == 0) {
-                return false;
-            }
-            setLeader(true);
-            return true;
-        } finally {
-            lock.unlock();
+        if (release.getCount() == 0) {
+            return false;
         }
+        setLeader(true);
+        return true;
     }
 
     private void releaseLock() {
diff --git 
a/components/camel-hazelcast/src/test/java/org/apache/camel/component/hazelcast/policy/HazelcastRoutePolicyLeadershipTest.java
 
b/components/camel-hazelcast/src/test/java/org/apache/camel/component/hazelcast/policy/HazelcastRoutePolicyLeadershipTest.java
new file mode 100644
index 000000000000..bb5307475adc
--- /dev/null
+++ 
b/components/camel-hazelcast/src/test/java/org/apache/camel/component/hazelcast/policy/HazelcastRoutePolicyLeadershipTest.java
@@ -0,0 +1,452 @@
+/*
+ * 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.hazelcast.policy;
+
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import com.hazelcast.config.Config;
+import com.hazelcast.core.Hazelcast;
+import com.hazelcast.core.HazelcastInstance;
+import com.hazelcast.map.IMap;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Consumer;
+import org.apache.camel.Endpoint;
+import org.apache.camel.Processor;
+import org.apache.camel.Producer;
+import org.apache.camel.Route;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.support.DefaultComponent;
+import org.apache.camel.support.DefaultConsumer;
+import org.apache.camel.support.DefaultEndpoint;
+import org.apache.camel.support.RoutePolicySupport;
+import org.apache.camel.support.service.ServiceHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The leadership taken by the policy's thread against the route start, the 
route stop, a consumer start that fails and
+ * the policy stop, with an embedded member. The other node is the test 
thread, which holds the cluster lock.
+ */
+class HazelcastRoutePolicyLeadershipTest {
+
+    private static final String LOCK_MAP = "route-policy-locks";
+    private static final String LOCK_KEY = "route-lock";
+
+    private HazelcastInstance member;
+    private CamelContext context;
+
+    @BeforeEach
+    void startMember() {
+        Config config = new Config();
+        config.setClusterName("route-policy-" + UUID.randomUUID());
+        config.getMetricsConfig().setEnabled(false);
+        
config.getNetworkConfig().getJoin().getMulticastConfig().setEnabled(false);
+        
config.getNetworkConfig().getJoin().getAutoDetectionConfig().setEnabled(false);
+        config.getNetworkConfig().getJoin().getTcpIpConfig().setEnabled(false);
+        member = Hazelcast.newHazelcastInstance(config);
+    }
+
+    @AfterEach
+    void stop() {
+        if (context != null) {
+            // the other node goes away first, so the stop does not wait for 
the tryLock of the policy's thread
+            if (member != null) {
+                member.getMap(LOCK_MAP).forceUnlock(LOCK_KEY);
+            }
+            context.stop();
+        }
+        if (member != null) {
+            member.shutdown();
+        }
+    }
+
+    @Test
+    void leadershipTakenWhileTheRouteStartsIsNotLost() throws Exception {
+        // another node holds the cluster lock, so the policy's own thread 
waits in tryLock during the test
+        IMap<String, String> locks = member.getMap(LOCK_MAP);
+        locks.lock(LOCK_KEY);
+
+        TestPolicy policy = newPolicy(TimeUnit.MINUTES.toMillis(1));
+        startContext(policy);
+
+        // holds the lock that onStart on main waited for after it read the 
flag, and takes the leadership meanwhile.
+        // The fixed onStart does not take this lock, so this checks the 
ordered case; the race test covers the
+        // atomicity
+        ReentrantLock held = (ReentrantLock) policy.lifecycleLock();
+        held.lock();
+        CompletableFuture<Void> routeStart;
+        try {
+            routeStart = CompletableFuture.runAsync(() -> {
+                try {
+                    context.getRouteController().startRoute("route");
+                } catch (Exception e) {
+                    throw new IllegalStateException(e);
+                }
+            });
+            await().atMost(20, TimeUnit.SECONDS).until(() -> 
held.hasQueuedThreads() || routeStart.isDone());
+
+            // meanwhile this node takes the leadership: acquireLeadership 
calls setLeader(true) once it holds the
+            // cluster lock, which starts the consumers the policy has stopped 
(none yet)
+            policy.setLeader(true);
+        } finally {
+            held.unlock();
+        }
+        routeStart.get(20, TimeUnit.SECONDS);
+
+        assertTrue(policy.isLeader());
+        
assertTrue(ServiceHelper.isStarted(context.getRoute("route").getConsumer()),
+                "The node that holds the leadership must consume");
+    }
+
+    @Test
+    void leadershipTakenWhileRoutesStartNeverLeavesTheLeaderIdle() throws 
Exception {
+        IMap<String, String> locks = member.getMap(LOCK_MAP);
+        locks.lock(LOCK_KEY);
+
+        TestPolicy policy = newPolicy(TimeUnit.MINUTES.toMillis(1));
+        startContext(policy);
+        // a started route: the policy does not start the consumer of a route 
that is stopped
+        context.getRouteController().startRoute("route");
+        Route route = context.getRoute("route");
+
+        // the route start (onStart) and the leadership thread 
(setLeader(true)) race, no hooks
+        for (int i = 0; i < 1000; i++) {
+            ServiceHelper.startService(route.getConsumer());
+            policy.onStop(route);
+            policy.setLeader(false);
+
+            CyclicBarrier barrier = new CyclicBarrier(2);
+            long spinNanos = ThreadLocalRandom.current().nextLong(2000);
+            CompletableFuture<Void> routeStart = CompletableFuture.runAsync(() 
-> {
+                awaitBarrier(barrier);
+                policy.onStart(route);
+            });
+            CompletableFuture<Void> leadership = CompletableFuture.runAsync(() 
-> {
+                awaitBarrier(barrier);
+                long end = System.nanoTime() + spinNanos;
+                while (System.nanoTime() < end) {
+                    Thread.onSpinWait();
+                }
+                policy.setLeader(true);
+            });
+            CompletableFuture.allOf(routeStart, leadership).get(20, 
TimeUnit.SECONDS);
+
+            assertTrue(ServiceHelper.isStarted(route.getConsumer()),
+                    "The node that holds the leadership must consume (round " 
+ i + ")");
+        }
+    }
+
+    @Test
+    void stopWhileAConsumerStartsAtTakeoverDoesNotWait() throws Exception {
+        // another node holds the cluster lock, so the route starts on a 
follower and the policy stops its consumer
+        IMap<String, String> locks = member.getMap(LOCK_MAP);
+        locks.lock(LOCK_KEY);
+
+        // the consumer start at takeover blocks (a broker connect) until it 
is interrupted
+        CountDownLatch starting = new CountDownLatch(1);
+        CountDownLatch neverReleased = new CountDownLatch(1);
+        TestPolicy policy = newPolicy(TimeUnit.SECONDS.toMillis(1));
+        startContext(policy);
+        context.getRouteController().startRoute("route");
+        
assertFalse(ServiceHelper.isStarted(context.getRoute("route").getConsumer()));
+        policy.beforeStart = () -> {
+            starting.countDown();
+            try {
+                neverReleased.await();
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new IllegalStateException("Interrupted while starting", 
e);
+            }
+        };
+
+        // the other node goes away: this node takes the leadership and starts 
the consumer
+        locks.unlock(LOCK_KEY);
+        assertTrue(starting.await(20, TimeUnit.SECONDS), "The consumer start 
did not begin");
+
+        // the policy stops (the CamelContext shuts down) meanwhile: it must 
not wait for the consumer start
+        CompletableFuture<Void> stop = 
CompletableFuture.runAsync(policy::stop);
+        try {
+            await().atMost(5, TimeUnit.SECONDS).until(stop::isDone);
+        } finally {
+            neverReleased.countDown();
+        }
+        stop.get(20, TimeUnit.SECONDS);
+    }
+
+    @Test
+    void consumerWhoseStartFailsAtTakeoverIsStartedAgain() throws Exception {
+        // another node holds the cluster lock, so the routes start on a 
follower and the policy stops their consumers
+        IMap<String, String> locks = member.getMap(LOCK_MAP);
+        locks.lock(LOCK_KEY);
+
+        AtomicBoolean firstFails = new AtomicBoolean();
+        AtomicInteger firstAttempts = new AtomicInteger();
+        TestPolicy policy = newPolicy(TimeUnit.SECONDS.toMillis(1));
+        policy.setRetryDelay(100);
+        context = new DefaultCamelContext();
+        context.addComponent("failing", new FailingStartComponent(firstFails, 
firstAttempts));
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                
from("failing:first").routeId("first").routePolicy(policy).to("log:first");
+                
from("failing:second").routeId("second").routePolicy(policy).to("log:second");
+            }
+        });
+        context.start();
+        
assertFalse(ServiceHelper.isStarted(context.getRoute("first").getConsumer()));
+        
assertFalse(ServiceHelper.isStarted(context.getRoute("second").getConsumer()));
+
+        // the other node goes away, and the consumer of the first route 
cannot start (a broker not reachable yet)
+        firstFails.set(true);
+        firstAttempts.set(0);
+        locks.unlock(LOCK_KEY);
+
+        // the other consumer starts, and the failed one is tried again
+        await().atMost(20, TimeUnit.SECONDS)
+                .untilAsserted(() -> {
+                    assertTrue(policy.isLeader(), "This node is not the 
leader");
+                    
assertTrue(ServiceHelper.isStarted(context.getRoute("second").getConsumer()),
+                            "The consumer of the second route did not start");
+                    assertTrue(firstAttempts.get() >= 2, "The start of the 
first consumer was not tried again");
+                });
+        
assertFalse(ServiceHelper.isStarted(context.getRoute("first").getConsumer()));
+
+        // and starts once it can
+        firstFails.set(false);
+        await().atMost(20, TimeUnit.SECONDS)
+                .untilAsserted(() -> 
assertTrue(ServiceHelper.isStarted(context.getRoute("first").getConsumer()),
+                        "The leader does not consume from the first route"));
+    }
+
+    @Test
+    void consumerOfARouteBeingStoppedIsNotStartedAtTakeover() throws Exception 
{
+        IMap<String, String> locks = member.getMap(LOCK_MAP);
+        locks.lock(LOCK_KEY);
+
+        TestPolicy policy = newPolicy(TimeUnit.SECONDS.toMillis(1));
+        AtomicBoolean taken = new AtomicBoolean();
+        // listed before the Hazelcast policy, so its onStop runs while the 
route stops, before the one of the policy
+        RoutePolicySupport takeoverWhileStopping = new RoutePolicySupport() {
+            @Override
+            public void onStop(Route route) {
+                if (taken.compareAndSet(false, true)) {
+                    // the other node goes away now: this node takes the 
leadership and starts the consumers it stopped
+                    locks.unlock(LOCK_KEY);
+                    await().atMost(20, 
TimeUnit.SECONDS).until(policy::isLeader);
+                }
+            }
+        };
+        context = new DefaultCamelContext();
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                
from("direct:start").routeId("route").routePolicy(takeoverWhileStopping, 
policy).to("log:route");
+            }
+        });
+        context.start();
+        Route route = context.getRoute("route");
+        assertFalse(ServiceHelper.isStarted(route.getConsumer()));
+
+        context.getRouteController().stopRoute("route");
+
+        assertTrue(policy.isLeader());
+        await().during(500, TimeUnit.MILLISECONDS).atMost(10, TimeUnit.SECONDS)
+                .untilAsserted(() -> 
assertFalse(ServiceHelper.isStarted(route.getConsumer()),
+                        "The consumer of the stopped route runs"));
+
+        // and it is started with the route
+        context.getRouteController().startRoute("route");
+        
assertTrue(ServiceHelper.isStarted(context.getRoute("route").getConsumer()));
+    }
+
+    @Test
+    void consumerOfARouteBeingSuspendedIsNotStartedAtTakeover() throws 
Exception {
+        IMap<String, String> locks = member.getMap(LOCK_MAP);
+        locks.lock(LOCK_KEY);
+
+        TestPolicy policy = newPolicy(TimeUnit.SECONDS.toMillis(1));
+        AtomicBoolean taken = new AtomicBoolean();
+        // listed before the Hazelcast policy, so its onSuspend runs while the 
route suspends, before the one of the
+        // policy
+        RoutePolicySupport takeoverWhileSuspending = new RoutePolicySupport() {
+            @Override
+            public void onSuspend(Route route) {
+                if (taken.compareAndSet(false, true)) {
+                    locks.unlock(LOCK_KEY);
+                    await().atMost(20, 
TimeUnit.SECONDS).until(policy::isLeader);
+                }
+            }
+        };
+        context = new DefaultCamelContext();
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                
from("seda:start").routeId("route").routePolicy(takeoverWhileSuspending, 
policy).to("log:route");
+            }
+        });
+        context.start();
+        Route route = context.getRoute("route");
+        assertFalse(ServiceHelper.isStarted(route.getConsumer()));
+
+        context.getRouteController().suspendRoute("route");
+
+        assertTrue(policy.isLeader());
+        await().during(500, TimeUnit.MILLISECONDS).atMost(10, TimeUnit.SECONDS)
+                .untilAsserted(() -> 
assertFalse(ServiceHelper.isStarted(route.getConsumer()),
+                        "The consumer of the suspended route runs"));
+
+        // and it is started when the route is resumed
+        context.getRouteController().resumeRoute("route");
+        assertTrue(ServiceHelper.isStarted(route.getConsumer()));
+    }
+
+    @Test
+    void consumerStartedWhileItsRouteStopsIsStoppedAgain() throws Exception {
+        IMap<String, String> locks = member.getMap(LOCK_MAP);
+        locks.lock(LOCK_KEY);
+
+        TestPolicy policy = newPolicy(TimeUnit.SECONDS.toMillis(1));
+        startContext(policy);
+        context.getRouteController().startRoute("route");
+        Route route = context.getRoute("route");
+        assertFalse(ServiceHelper.isStarted(route.getConsumer()));
+        // the route is stopped after the policy checked it and before it 
starts the consumer
+        policy.beforeStart = () -> {
+            try {
+                context.getRouteController().stopRoute("route");
+            } catch (Exception e) {
+                throw new IllegalStateException(e);
+            }
+        };
+
+        locks.unlock(LOCK_KEY);
+
+        await().atMost(20, TimeUnit.SECONDS).until(policy::isLeader);
+        await().during(500, TimeUnit.MILLISECONDS).atMost(10, TimeUnit.SECONDS)
+                .untilAsserted(() -> {
+                    
assertTrue(context.getRouteController().getRouteStatus("route").isStopped(), 
"The route is not stopped");
+                    assertFalse(ServiceHelper.isStarted(route.getConsumer()), 
"The consumer of the stopped route runs");
+                });
+    }
+
+    private TestPolicy newPolicy(long tryLockTimeoutMillis) {
+        TestPolicy policy = new TestPolicy(member);
+        policy.setLockMapName(LOCK_MAP);
+        policy.setLockKey(LOCK_KEY);
+        policy.setLockValue("this-node");
+        policy.setTryLockTimeout(tryLockTimeoutMillis, TimeUnit.MILLISECONDS);
+        return policy;
+    }
+
+    private void startContext(TestPolicy policy) throws Exception {
+        if (context == null) {
+            context = new DefaultCamelContext();
+        }
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                
from("direct:start").routeId("route").autoStartup(false).routePolicy(policy).to("log:route");
+            }
+        });
+        context.start();
+    }
+
+    private static void awaitBarrier(CyclicBarrier barrier) {
+        try {
+            barrier.await(20, TimeUnit.SECONDS);
+        } catch (Exception e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    private static final class TestPolicy extends HazelcastRoutePolicy {
+        // runs before a consumer is started by the policy (when this node 
takes the leadership)
+        volatile Runnable beforeStart;
+
+        TestPolicy(HazelcastInstance instance) {
+            super(instance);
+        }
+
+        Lock lifecycleLock() {
+            return lock;
+        }
+
+        @Override
+        public void startConsumer(Consumer consumer) throws Exception {
+            Runnable hook = beforeStart;
+            if (hook != null) {
+                beforeStart = null;
+                hook.run();
+            }
+            super.startConsumer(consumer);
+        }
+    }
+
+    // the consumer of the endpoint "first" fails to start while firstFails is 
set
+    private static final class FailingStartComponent extends DefaultComponent {
+        private final AtomicBoolean firstFails;
+        private final AtomicInteger firstAttempts;
+
+        FailingStartComponent(AtomicBoolean firstFails, AtomicInteger 
firstAttempts) {
+            this.firstFails = firstFails;
+            this.firstAttempts = firstAttempts;
+        }
+
+        @Override
+        protected Endpoint createEndpoint(String uri, String remaining, 
Map<String, Object> parameters) {
+            boolean first = "first".equals(remaining);
+            return new DefaultEndpoint(uri, this) {
+                @Override
+                public Producer createProducer() {
+                    throw new UnsupportedOperationException("No producer");
+                }
+
+                @Override
+                public Consumer createConsumer(Processor processor) {
+                    return new DefaultConsumer(this, processor) {
+                        @Override
+                        protected void doStart() throws Exception {
+                            if (first) {
+                                firstAttempts.incrementAndGet();
+                                if (firstFails.get()) {
+                                    throw new IllegalStateException("Cannot 
start the consumer yet");
+                                }
+                            }
+                            super.doStart();
+                        }
+                    };
+                }
+            };
+        }
+    }
+}

Reply via email to