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]
