This is an automated email from the ASF dual-hosted git repository.

rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit a598185c99b1652df1bc578ab8fca7afec79c0e9
Author: opencode <[email protected]>
AuthorDate: Fri Oct 9 11:44:08 2026 +0200

    Keep monitor, heartbeat and JMX alive across partial channel stops
    
    GroupChannel.stop(svc) unconditionally cancelled the monitor and
    heartbeat futures, unregistered the channel from JMX and, when the
    channel owned its utility executor, shut it down, even when only a
    subset of the services was stopped. A channel with services still
    running was left without heartbeats and monitoring, and a following
    start scheduled its periodic monitor task on an executor that had
    been shut down. A shutdown of a scheduled executor does not cancel
    periodic tasks, so a monitor task scheduled by an earlier start could
    keep the non daemon pool threads of an owned executor alive and
    prevent the JVM from exiting. GroupChannel.start(svc) also scheduled
    a new monitor task on every call, leaking duplicates on repeated
    partial or full starts.
    
    Perform that teardown only once all services have been stopped,
    based on the started service flags of the coordinator, and schedule
    the monitor task only when it is not already running. Add tests for
    the monitor leak on a repeated start and for the partial stop.
---
 .../catalina/tribes/group/ChannelCoordinator.java  |  9 +++++
 .../apache/catalina/tribes/group/GroupChannel.java | 43 +++++++++++++---------
 .../tribes/group/TestGroupChannelStartStop.java    | 38 +++++++++++++++++++
 3 files changed, 73 insertions(+), 17 deletions(-)

diff --git a/java/org/apache/catalina/tribes/group/ChannelCoordinator.java 
b/java/org/apache/catalina/tribes/group/ChannelCoordinator.java
index f107a5d9e5..0f383eddfd 100644
--- a/java/org/apache/catalina/tribes/group/ChannelCoordinator.java
+++ b/java/org/apache/catalina/tribes/group/ChannelCoordinator.java
@@ -125,6 +125,15 @@ public class ChannelCoordinator extends 
ChannelInterceptorBase implements Messag
         this.internalStop(svc);
     }
 
+    /**
+     * Returns the bit mask of the services that are currently started.
+     *
+     * @return the started service flags
+     */
+    synchronized int getStartLevel() {
+        return startLevel;
+    }
+
 
     /**
      * Starts up the channel. This can be called multiple times for individual 
services to start The svc parameter can
diff --git a/java/org/apache/catalina/tribes/group/GroupChannel.java 
b/java/org/apache/catalina/tribes/group/GroupChannel.java
index b99f0b5794..b790ccf7c4 100644
--- a/java/org/apache/catalina/tribes/group/GroupChannel.java
+++ b/java/org/apache/catalina/tribes/group/GroupChannel.java
@@ -457,7 +457,9 @@ public class GroupChannel extends ChannelInterceptorBase 
implements ManagedChann
             ownExecutor = true;
         }
         super.start(svc);
-        monitorFuture = 
utilityExecutor.scheduleWithFixedDelay(this::startHeartbeat, 0, 60, 
TimeUnit.SECONDS);
+        if (monitorFuture == null || monitorFuture.isDone()) {
+            monitorFuture = 
utilityExecutor.scheduleWithFixedDelay(this::startHeartbeat, 0, 60, 
TimeUnit.SECONDS);
+        }
     }
 
     /**
@@ -480,23 +482,30 @@ public class GroupChannel extends ChannelInterceptorBase 
implements ManagedChann
 
     @Override
     public synchronized void stop(int svc) throws ChannelException {
-        if (monitorFuture != null) {
-            monitorFuture.cancel(true);
-            monitorFuture = null;
-        }
-        if (heartbeatFuture != null) {
-            heartbeatFuture.cancel(true);
-            heartbeatFuture = null;
-        }
         super.stop(svc);
-        if (ownExecutor) {
-            utilityExecutor.shutdown();
-            utilityExecutor = null;
-            ownExecutor = false;
-        }
-        if (oname != null) {
-            JmxRegistry.getRegistry(this).unregisterJmx(oname);
-            oname = null;
+        if (coordinator.getStartLevel() == 0) {
+            // The monitor and the heartbeat only make sense while services
+            // are running. Cancel them, and release an executor that this
+            // channel owns, only once all services have been stopped. A
+            // partial stop must not tear down what the started services
+            // still rely on.
+            if (monitorFuture != null) {
+                monitorFuture.cancel(true);
+                monitorFuture = null;
+            }
+            if (heartbeatFuture != null) {
+                heartbeatFuture.cancel(true);
+                heartbeatFuture = null;
+            }
+            if (ownExecutor && utilityExecutor != null) {
+                utilityExecutor.shutdown();
+                utilityExecutor = null;
+                ownExecutor = false;
+            }
+            if (oname != null) {
+                JmxRegistry.getRegistry(this).unregisterJmx(oname);
+                oname = null;
+            }
         }
     }
 
diff --git 
a/test/org/apache/catalina/tribes/group/TestGroupChannelStartStop.java 
b/test/org/apache/catalina/tribes/group/TestGroupChannelStartStop.java
index 48bc9631f2..4460f5dc1b 100644
--- a/test/org/apache/catalina/tribes/group/TestGroupChannelStartStop.java
+++ b/test/org/apache/catalina/tribes/group/TestGroupChannelStartStop.java
@@ -16,6 +16,8 @@
  */
 package org.apache.catalina.tribes.group;
 
+import java.util.concurrent.ScheduledFuture;
+
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
@@ -126,6 +128,42 @@ public class TestGroupChannelStartStop {
         channel.stop(Channel.DEFAULT);
     }
 
+    @Test
+    public void testNoMonitorLeakOnRepeatedStart() throws Exception {
+        channel.start(Channel.DEFAULT);
+        ScheduledFuture<?> firstMonitor = channel.monitorFuture;
+        Assert.assertNotNull(firstMonitor);
+        // a second start must not schedule a second monitor task
+        channel.start(Channel.DEFAULT);
+        Assert.assertSame(firstMonitor, channel.monitorFuture);
+        channel.stop(Channel.DEFAULT);
+    }
+
+    @Test
+    public void testPartialStopKeepsHeartbeatAlive() throws Exception {
+        channel.start(Channel.DEFAULT);
+        // the monitor task runs immediately and schedules the heartbeat
+        long deadline = System.currentTimeMillis() + 10000;
+        while (channel.heartbeatFuture == null && System.currentTimeMillis() < 
deadline) {
+            Thread.sleep(50);
+        }
+        ScheduledFuture<?> monitor = channel.monitorFuture;
+        ScheduledFuture<?> heartbeat = channel.heartbeatFuture;
+        Assert.assertNotNull(heartbeat);
+        // a partial stop must not cancel the monitor or the heartbeat
+        channel.stop(Channel.SND_RX_SEQ);
+        Assert.assertSame(monitor, channel.monitorFuture);
+        Assert.assertFalse(monitor.isCancelled());
+        Assert.assertFalse(heartbeat.isCancelled());
+        Assert.assertNotNull(channel.utilityExecutor);
+        // a full stop cancels them
+        channel.stop(Channel.DEFAULT);
+        Assert.assertNull(channel.monitorFuture);
+        Assert.assertNull(channel.heartbeatFuture);
+        Assert.assertTrue(monitor.isCancelled());
+        Assert.assertTrue(heartbeat.isCancelled());
+    }
+
     @Test
     public void testOverlappingPartialStart() throws Exception {
         // start one service, then start everything: the already


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to