This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 9.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 8e2373333a3c428502ba30520b935b6dc2d7b129 Author: opencode <[email protected]> AuthorDate: Sun Oct 11 10:41:52 2026 +0200 Protect the multicast shutdown send so that stop completes When the last service level was stopped, the SHUTDOWN broadcast was sent outside the protection that the surrounding leaveGroup and close calls enjoy. The send routinely fails in exactly the circumstances that accompany a shutdown, a down network device or an already broken socket, and its IOException then skipped leaving the multicast group and closing the socket, and kept the facade from releasing its implementation, leaking the socket and the group membership for the rest of the JVM's life. A recovery would retry the close on the next attempt, but a plain cluster shutdown has no retry. The send is now wrapped like its neighbours, logging at debug while the shutdown message simply does not reach the peers, as it already did not, and they expire the member on the drop time as usual. Add a test that breaks the socket before stopping and checks that the implementation is released, which fails on the previous code. --- .../tribes/membership/McastServiceImpl.java | 9 ++- .../tribes/membership/TestMcastService.java | 72 ++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/java/org/apache/catalina/tribes/membership/McastServiceImpl.java b/java/org/apache/catalina/tribes/membership/McastServiceImpl.java index 5fdfb9de87..78f93066a7 100644 --- a/java/org/apache/catalina/tribes/membership/McastServiceImpl.java +++ b/java/org/apache/catalina/tribes/membership/McastServiceImpl.java @@ -341,7 +341,14 @@ public class McastServiceImpl extends MembershipProviderBase { if (startLevel == 0) { // send a stop message member.setCommand(Member.SHUTDOWN_PAYLOAD); - send(false); + try { + send(false); + } catch (Exception e) { + // Shutting down. Only log at debug. + if (log.isDebugEnabled()) { + log.debug(sm.getString("mcastServiceImpl.error.stop"), e); + } + } // leave mcast group try { socket.leaveGroup(new InetSocketAddress(address, 0), null); diff --git a/test/org/apache/catalina/tribes/membership/TestMcastService.java b/test/org/apache/catalina/tribes/membership/TestMcastService.java new file mode 100644 index 0000000000..ad5b981db5 --- /dev/null +++ b/test/org/apache/catalina/tribes/membership/TestMcastService.java @@ -0,0 +1,72 @@ +/* + * 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.catalina.tribes.membership; + +import java.io.ByteArrayInputStream; +import java.util.Properties; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.group.GroupChannel; + +public class TestMcastService { + + /* + * The shutdown message that stop() broadcasts routinely fails when the network device is down + * or the socket is already broken. That must not skip leaving the multicast group and closing + * the socket, nor prevent the service facade from releasing its implementation. + */ + @Test(timeout = 60000) + public void testStopWithFailingShutdownSendCompletes() throws Exception { + int base = 40000 + (int) Math.floorMod(System.nanoTime(), 10000); + StringBuilder properties = new StringBuilder(); + properties.append("mcastAddress=228.0.0.4\n"); + properties.append("mcastPort=").append(base).append("\n"); + properties.append("tcpListenHost=127.0.0.1\n"); + properties.append("tcpListenPort=").append(base + 1).append("\n"); + properties.append("tcpSecurePort=0\n"); + properties.append("udpListenPort=0\n"); + properties.append("mcastFrequency=100\n"); + properties.append("memberDropTime=3000\n"); + properties.append("recoveryEnabled=false\n"); + properties.append("recoveryCounter=10\n"); + properties.append("recoverySleepTime=3000\n"); + + GroupChannel channel = new GroupChannel(); + channel.setName("TestMcastService-" + System.nanoTime()); + + McastService service = new McastService(); + service.setChannel(channel); + Properties props = new Properties(); + props.load(new ByteArrayInputStream(properties.toString().getBytes("UTF-8"))); + service.setProperties(props); + service.start(Channel.MBR_TX_SEQ); + + McastServiceImpl impl = service.impl; + Assert.assertNotNull(impl); + + // Break the socket so that the shutdown message cannot be sent + impl.socket.close(); + + // Stop must complete and release the implementation despite the failing shutdown send + service.stop(Channel.MBR_TX_SEQ); + + Assert.assertNull("the membership implementation must be released on stop", service.impl); + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
