Croway commented on code in PR #26028:
URL: https://github.com/apache/camel/pull/26028#discussion_r3912089383


##########
components/camel-master/src/main/java/org/apache/camel/component/master/MasterConsumer.java:
##########
@@ -225,30 +290,50 @@ private void onLeadershipLost() {
         }
     }
 
+    private boolean isStartPending() {
+        return leaderTaskFuture != null && !leaderTaskFuture.isDone();
+    }
+
+    private void cancelLeaderTask(boolean mayInterruptIfRunning) {
+        if (leaderTaskFuture != null) {
+            leaderTaskFuture.cancel(mayInterruptIfRunning);
+            leaderTaskFuture = null;
+        }
+    }
+
     // **************************************
     // Listener
     // **************************************
 
     private final class LeadershipListener implements 
CamelClusterEventListener.Leadership {
         @Override
         public void leadershipChanged(CamelClusterView view, 
CamelClusterMember leader) {
-            if (!isRunAllowed()) {
-                return;
-            }
-
-            if (view.getLocalMember().isLeader()) {
-                try {
-                    onLeadershipTaken();
-                } catch (Exception e) {
-                    getExceptionHandler().handleException("Error starting 
consumer while taking leadership", e);
+            lock.lock();

Review Comment:
   **Lock-order inversion, can deadlock on stop.**
   
   `leadershipChanged` now takes `BaseService.lock` unconditionally. The view 
dispatches this listener while holding its `StampedLock` **read** lock 
(`AbstractCamelClusterView.doWithListener` -> `LockHelper.doWithReadLock`). On 
the other side, `MasterConsumer.stop()` holds `BaseService.lock` while 
`doStop()` calls `view.removeEventListener(...)`, which needs the `StampedLock` 
**write** lock.
   
   - Thread S: `stop()` holds `lock` -> `doStop()` -> `removeEventListener` -> 
waits for the read lock to be released.
   - Thread E (FileLock `tryLock` tick, Curator `LeaderSelector`, Kubernetes 
notifier): holds the read lock -> `leadershipChanged` -> `lock.lock()` -> waits 
for S.
   
   Both threads block forever, shutdown hangs, and every other master route on 
that view stops receiving events. Pre-PR the unlocked `isRunAllowed()` 
pre-check returned before contending for the lock once `STOPPING` was set (and 
lost-events never locked without a consumer), so the window was a microsecond 
race. Now every event contends, and combined with the start running under the 
lock (see below) the event thread can already be parked on `lock` for the whole 
delegated start when `stop()` arrives.
   
   Suggestion: keep an unlocked `isRunAllowed()` fast-path before taking the 
lock, and/or use `tryLock` with a bail-out here, and make sure nothing that 
runs under `BaseService.lock` needs the view's write lock (e.g. remove the 
listener before/outside the locked section of `doStop`).



##########
components/camel-master/src/main/java/org/apache/camel/component/master/MasterConsumer.java:
##########
@@ -151,60 +162,105 @@ private BackgroundTask createTask() {
                 .build();
     }
 
-    private void onLeadershipTaken() throws Exception {
+    private void onLeadershipTaken() {
         lock.lock();
         try {
             if (!isRunAllowed()) {
                 return;
             }
 
-            if (delegatedConsumer != null) {
+            leadershipTaken = true;
+
+            if (delegatedConsumer != null || isStartPending()) {
                 return;
             }
 
-            final BackgroundTask leaderTask = createTask();
-            leaderTask.schedule(getEndpoint().getCamelContext(), () -> {
-                if (!isRunAllowed()) {
-                    return false;
-                }
-                LOG.info("Leadership taken. Attempt #{} to start consumer: 
{}", leaderTask.iteration(), delegatedEndpoint);
-
-                Exception cause = null;
-                try {
-                    if (delegatedConsumer == null) {
-                        delegatedConsumer = 
delegatedEndpoint.createConsumer(processor);
-                        if (delegatedConsumer instanceof StartupListener) {
-                            
getEndpoint().getCamelContext().addStartupListener((StartupListener) 
delegatedConsumer);
-                        }
-                        if (delegatedConsumer instanceof ResumeAware 
resumeAwareConsumer && resumeStrategy != null) {
-                            LOG.debug("Setting up the resume adapter for the 
resume strategy in consumer");
-                            ResumeAdapter resumeAdapter
-                                    = 
AdapterHelper.eval(clusterService.getCamelContext(), resumeAwareConsumer,
-                                            resumeStrategy);
-                            resumeStrategy.setAdapter(resumeAdapter);
-
-                            LOG.debug("Setting up the resume strategy for 
consumer");
-                            
resumeAwareConsumer.setResumeStrategy(resumeStrategy);
-                        }
-                    }
-                    ServiceHelper.startService(delegatedEndpoint, 
delegatedConsumer);
+            // a task from a previous leadership term may still be scheduled, 
drop it
+            cancelLeaderTask(false);
+
+            final BackgroundTask task = createTask();
+            // the consumer is created once and re-used by the start attempts 
of this task
+            final AtomicReference<Consumer> attempt = new AtomicReference<>();
+            leaderTaskFuture = task.schedule(getEndpoint().getCamelContext(), 
() -> startDelegatedConsumer(task, attempt));
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    private boolean startDelegatedConsumer(BackgroundTask task, 
AtomicReference<Consumer> attempt) {
+        try {
+            // interruptibly, so cancelling the task while this consumer is 
being stopped does not
+            // keep the leader pool thread waiting for a lock the stopping 
thread holds
+            lock.lockInterruptibly();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            return true; // no more attempts
+        }
+        try {
+            if (!isRunAllowed()) {
+                return false;
+            }
+
+            if (!leadershipTaken) {
+                // leadership was lost while this start was pending. Starting 
now would run the consumer on a
+                // node that is not the leader, and no further leadership 
event is coming to stop it again
+                LOG.debug("Leadership lost while the start was pending. Not 
starting consumer: {}", delegatedEndpoint);
+                return true; // no more attempts
+            }
 
-                } catch (Exception e) {
-                    cause = e;
+            if (delegatedConsumer != null) {
+                return true; // no more attempts
+            }
+
+            LOG.info("Leadership taken. Attempt #{} to start consumer: {}", 
task.iteration(), delegatedEndpoint);
+
+            Exception cause = null;
+            try {
+                Consumer consumer = attempt.get();
+                if (consumer == null) {
+                    consumer = delegatedEndpoint.createConsumer(processor);
+                    attempt.set(consumer);
+                    if (consumer instanceof StartupListener startupListener) {
+                        
getEndpoint().getCamelContext().addStartupListener(startupListener);
+                    }
+                    if (consumer instanceof ResumeAware resumeAwareConsumer && 
resumeStrategy != null) {
+                        LOG.debug("Setting up the resume adapter for the 
resume strategy in consumer");
+                        ResumeAdapter resumeAdapter
+                                = 
AdapterHelper.eval(clusterService.getCamelContext(), resumeAwareConsumer,
+                                        resumeStrategy);
+                        resumeStrategy.setAdapter(resumeAdapter);
+
+                        LOG.debug("Setting up the resume strategy for 
consumer");
+                        resumeAwareConsumer.setResumeStrategy(resumeStrategy);
+                    }
                 }
+                ServiceHelper.startService(delegatedEndpoint, consumer);

Review Comment:
   **The delegated start now runs while holding `BaseService.lock`.**
   
   Before this PR the start lambda ran on the leader pool without the lock; 
only `schedule()` was under it. Now `createConsumer()` and 
`ServiceHelper.startService(delegatedEndpoint, consumer)` run with the lock 
held. A delegated JMS/Kafka consumer that spends 30 s+ in connect retries 
inside `doStart()` means:
   
   - `MasterConsumer.stop()` (context shutdown) blocks on `lock` until the 
start returns, so the `cancelLeaderTask(true)` in `doStop` can never interrupt 
an in-flight start (`lockInterruptibly` only helps a task that has not acquired 
the lock yet).
   - The view's dispatch thread parks on `lock` under the `StampedLock` read 
lock, so `addEventListener`/`removeEventListener` of every other 
`MasterConsumer` on the namespace block.
   - For `FileLockClusterView` the same `tryLock()` thread that fires the event 
also writes the leader heartbeat (`writeClusterLeaderInfo`), so other members 
can judge this leader stale and take over while this node is still starting.
   
   Suggestion: claim the start under the lock (check `leadershipTaken`, 
`delegatedConsumer == null`, set a "starting" marker), run create/start **off** 
the lock, then re-acquire to publish `delegatedConsumer`, and if leadership was 
lost in the meantime stop/shutdown the freshly started consumer instead of 
publishing it.



##########
components/camel-master/src/main/java/org/apache/camel/component/master/MasterConsumer.java:
##########
@@ -151,60 +162,105 @@ private BackgroundTask createTask() {
                 .build();
     }
 
-    private void onLeadershipTaken() throws Exception {
+    private void onLeadershipTaken() {
         lock.lock();
         try {
             if (!isRunAllowed()) {
                 return;
             }
 
-            if (delegatedConsumer != null) {
+            leadershipTaken = true;
+
+            if (delegatedConsumer != null || isStartPending()) {
                 return;
             }
 
-            final BackgroundTask leaderTask = createTask();
-            leaderTask.schedule(getEndpoint().getCamelContext(), () -> {
-                if (!isRunAllowed()) {
-                    return false;
-                }
-                LOG.info("Leadership taken. Attempt #{} to start consumer: 
{}", leaderTask.iteration(), delegatedEndpoint);
-
-                Exception cause = null;
-                try {
-                    if (delegatedConsumer == null) {
-                        delegatedConsumer = 
delegatedEndpoint.createConsumer(processor);
-                        if (delegatedConsumer instanceof StartupListener) {
-                            
getEndpoint().getCamelContext().addStartupListener((StartupListener) 
delegatedConsumer);
-                        }
-                        if (delegatedConsumer instanceof ResumeAware 
resumeAwareConsumer && resumeStrategy != null) {
-                            LOG.debug("Setting up the resume adapter for the 
resume strategy in consumer");
-                            ResumeAdapter resumeAdapter
-                                    = 
AdapterHelper.eval(clusterService.getCamelContext(), resumeAwareConsumer,
-                                            resumeStrategy);
-                            resumeStrategy.setAdapter(resumeAdapter);
-
-                            LOG.debug("Setting up the resume strategy for 
consumer");
-                            
resumeAwareConsumer.setResumeStrategy(resumeStrategy);
-                        }
-                    }
-                    ServiceHelper.startService(delegatedEndpoint, 
delegatedConsumer);
+            // a task from a previous leadership term may still be scheduled, 
drop it
+            cancelLeaderTask(false);
+
+            final BackgroundTask task = createTask();
+            // the consumer is created once and re-used by the start attempts 
of this task
+            final AtomicReference<Consumer> attempt = new AtomicReference<>();
+            leaderTaskFuture = task.schedule(getEndpoint().getCamelContext(), 
() -> startDelegatedConsumer(task, attempt));
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    private boolean startDelegatedConsumer(BackgroundTask task, 
AtomicReference<Consumer> attempt) {
+        try {
+            // interruptibly, so cancelling the task while this consumer is 
being stopped does not
+            // keep the leader pool thread waiting for a lock the stopping 
thread holds
+            lock.lockInterruptibly();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            return true; // no more attempts
+        }
+        try {
+            if (!isRunAllowed()) {
+                return false;
+            }
+
+            if (!leadershipTaken) {
+                // leadership was lost while this start was pending. Starting 
now would run the consumer on a
+                // node that is not the leader, and no further leadership 
event is coming to stop it again
+                LOG.debug("Leadership lost while the start was pending. Not 
starting consumer: {}", delegatedEndpoint);
+                return true; // no more attempts
+            }
 
-                } catch (Exception e) {
-                    cause = e;
+            if (delegatedConsumer != null) {
+                return true; // no more attempts
+            }
+
+            LOG.info("Leadership taken. Attempt #{} to start consumer: {}", 
task.iteration(), delegatedEndpoint);
+
+            Exception cause = null;
+            try {
+                Consumer consumer = attempt.get();
+                if (consumer == null) {
+                    consumer = delegatedEndpoint.createConsumer(processor);
+                    attempt.set(consumer);
+                    if (consumer instanceof StartupListener startupListener) {
+                        
getEndpoint().getCamelContext().addStartupListener(startupListener);
+                    }
+                    if (consumer instanceof ResumeAware resumeAwareConsumer && 
resumeStrategy != null) {
+                        LOG.debug("Setting up the resume adapter for the 
resume strategy in consumer");
+                        ResumeAdapter resumeAdapter
+                                = 
AdapterHelper.eval(clusterService.getCamelContext(), resumeAwareConsumer,
+                                        resumeStrategy);
+                        resumeStrategy.setAdapter(resumeAdapter);
+
+                        LOG.debug("Setting up the resume strategy for 
consumer");
+                        resumeAwareConsumer.setResumeStrategy(resumeStrategy);
+                    }
                 }
+                ServiceHelper.startService(delegatedEndpoint, consumer);
+                // publish the consumer only once it is started, so a failed 
attempt cannot leave an unstarted
+                // consumer behind that makes every later leadership term 
believe there is nothing left to do
+                delegatedConsumer = consumer;
+
+            } catch (Exception e) {
+                cause = e;
+            }
 
-                if (cause != null) {
-                    String message = "Leadership taken. Attempt #" + 
leaderTask.iteration()
-                                     + " failed to start consumer due to: " + 
cause.getMessage();
-                    getExceptionHandler().handleException(message, cause);
-                    // make the task runner aware of the exception (will retry)
-                    throw new TaskRunFailureException(message, cause);
+            if (cause != null) {
+                String message = "Leadership taken. Attempt #" + 
task.iteration()
+                                 + " failed to start consumer due to: " + 
cause.getMessage();
+                getExceptionHandler().handleException(message, cause);
+                if (task.iteration() >= 
masterEndpoint.getComponent().getBackOffMaxAttempts()) {

Review Comment:
   **This log never fires with default settings, `backOffMaxAttempts` is 
effectively ignored.**
   
   `createTask()` uses `Budgets.iterationTimeBudget()` without 
`withMaxDuration`/`withUnlimitedDuration`, so the builder's 
`DEFAULT_MAX_DURATION = 5000` ms applies, measured from task construction. With 
the defaults (`backOffDelay=5000`, `backOffMaxAttempts=10`, initial delay 1 s): 
attempt #1 runs at t~1 s and fails; the tick at t~6 s finds the budget 
exhausted and the task ends. `task.iteration()` is 1, never >= 10, so this 
ERROR is never emitted and the node silently sits as an idle leader (only 
`BackgroundTask`'s generic WARN appears). The budget config itself is 
pre-existing, but the PR's give-up contract and this message rely on it, and 
`MasterConsumerLeadershipTest` masks it with `backOffDelay=200` / 
`maxAttempts=2`, which fits inside 5 s.
   
   Also, `withMaxIterations()` ignores values <= 0, so `backOffMaxAttempts=0` 
keeps unlimited iterations but `iteration() >= 0` is always true here: "Giving 
up after 1 attempts" is logged on every failed attempt while retries continue.
   
   Suggestion: add `.withUnlimitedDuration()` (or a duration derived from 
`backOffDelay * backOffMaxAttempts`) in `createTask()`, and gate this log on 
the task actually being exhausted (its status/budget) rather than re-deriving 
the arithmetic here. A test with the default component settings would catch 
this.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to