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

markt-asf pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/main by this push:
     new 161734d72c Better reporting of bound ports for cluster receiver
161734d72c is described below

commit 161734d72cfbd855ac6c39e7a44be490be822549
Author: Mark Thomas <[email protected]>
AuthorDate: Tue Oct 20 19:51:34 2026 +0100

    Better reporting of bound ports for cluster receiver
    
    Co-authored by: Claude / Sonnet 5.5
    Co-authored by: Codex / GPT-6.1-Sol-High
---
 .../apache/catalina/tribes/ChannelReceiver.java    |  24 +++++
 .../catalina/tribes/group/ChannelCoordinator.java  |   8 +-
 .../catalina/tribes/transport/ReceiverBase.java    |  27 ++++-
 .../catalina/tribes/transport/nio/NioReceiver.java |  40 +++++---
 .../tribes/transport/nio/NioReceiverMBean.java     |  24 +++++
 .../tribes/transport/nio/TestNioReceiver.java      | 114 +++++++++++++++++++++
 webapps/docs/changelog.xml                         |   9 ++
 webapps/docs/config/cluster-receiver.xml           |  16 ++-
 8 files changed, 236 insertions(+), 26 deletions(-)

diff --git a/java/org/apache/catalina/tribes/ChannelReceiver.java 
b/java/org/apache/catalina/tribes/ChannelReceiver.java
index 24105d6e67..6ec9703bc5 100644
--- a/java/org/apache/catalina/tribes/ChannelReceiver.java
+++ b/java/org/apache/catalina/tribes/ChannelReceiver.java
@@ -56,6 +56,14 @@ public interface ChannelReceiver extends Heartbeat {
      */
     int getPort();
 
+    /**
+     * Returns the port the receiver is actually bound to. This may differ 
from the configured port if the receiver
+     * automatically bound to another port because the configured port was not 
available.
+     *
+     * @return the bound port, -1 if the non-secure port is not bound
+     */
+    int getBoundPort();
+
     /**
      * Returns the secure listening port
      *
@@ -63,6 +71,14 @@ public interface ChannelReceiver extends Heartbeat {
      */
     int getSecurePort();
 
+    /**
+     * Returns the secure port the receiver is actually bound to. This may 
differ from the configured secure port if
+     * the receiver automatically bound to another port because the configured 
port was not available.
+     *
+     * @return the bound secure port, -1 if the secure port is not bound
+     */
+    int getBoundSecurePort();
+
     /**
      * Returns the UDP port
      *
@@ -70,6 +86,14 @@ public interface ChannelReceiver extends Heartbeat {
      */
     int getUdpPort();
 
+    /**
+     * Returns the UDP port the receiver is actually bound to. This may differ 
from the configured UDP port if the
+     * receiver automatically bound to another port because the configured 
port was not available.
+     *
+     * @return the bound UDP port, -1 if the UDP port is not bound
+     */
+    int getBoundUdpPort();
+
     /**
      * Sets the message listener to receive notification of incoming messages.
      *
diff --git a/java/org/apache/catalina/tribes/group/ChannelCoordinator.java 
b/java/org/apache/catalina/tribes/group/ChannelCoordinator.java
index 0f383eddfd..e05c6a807c 100644
--- a/java/org/apache/catalina/tribes/group/ChannelCoordinator.java
+++ b/java/org/apache/catalina/tribes/group/ChannelCoordinator.java
@@ -180,13 +180,13 @@ public class ChannelCoordinator extends 
ChannelInterceptorBase implements Messag
                 if (localMember instanceof StaticMember staticMember) {
                     // static member
                     staticMember.setHost(getClusterReceiver().getHost());
-                    staticMember.setPort(getClusterReceiver().getPort());
-                    
staticMember.setSecurePort(getClusterReceiver().getSecurePort());
+                    staticMember.setPort(getClusterReceiver().getBoundPort());
+                    
staticMember.setSecurePort(getClusterReceiver().getBoundSecurePort());
                 } else {
                     // multicast member
                     
membershipService.setLocalMemberProperties(getClusterReceiver().getHost(),
-                            getClusterReceiver().getPort(), 
getClusterReceiver().getSecurePort(),
-                            getClusterReceiver().getUdpPort());
+                            getClusterReceiver().getBoundPort(), 
getClusterReceiver().getBoundSecurePort(),
+                            getClusterReceiver().getBoundUdpPort());
                 }
                 valid = true;
             }
diff --git a/java/org/apache/catalina/tribes/transport/ReceiverBase.java 
b/java/org/apache/catalina/tribes/transport/ReceiverBase.java
index 6154c88547..7a31d3d19c 100644
--- a/java/org/apache/catalina/tribes/transport/ReceiverBase.java
+++ b/java/org/apache/catalina/tribes/transport/ReceiverBase.java
@@ -65,6 +65,9 @@ public abstract class ReceiverBase implements 
ChannelReceiver, ListenCallback, R
     private String host = "auto";
     private InetAddress bind;
     private int port = 4000;
+    private volatile int boundPort = -1;
+    private volatile int boundSecurePort = -1;
+    private volatile int boundUdpPort = -1;
     private int udpPort = -1;
     private int securePort = -1;
     private int rxBufSize = Constants.DEFAULT_CLUSTER_MSG_BUFFER_SIZE;
@@ -147,6 +150,9 @@ public abstract class ReceiverBase implements 
ChannelReceiver, ListenCallback, R
             oname = null;
         }
         channel = null;
+        boundPort = -1;
+        boundSecurePort = -1;
+        boundUdpPort = -1;
     }
 
     @Override
@@ -159,6 +165,11 @@ public abstract class ReceiverBase implements 
ChannelReceiver, ListenCallback, R
         return port;
     }
 
+    @Override
+    public int getBoundPort() {
+        return boundPort;
+    }
+
     /**
      * Returns the size of the receive buffer for TCP connections.
      *
@@ -280,7 +291,7 @@ public abstract class ReceiverBase implements 
ChannelReceiver, ListenCallback, R
                     socket.bind(addr);
                     String type;
                     if (secure) {
-                        setSecurePort(port);
+                        boundSecurePort = socket.getLocalPort();
                         if (getChannel() instanceof GroupChannel groupChannel) 
{
                             type = 
groupChannel.getSslContext().getImplementationName();
                         } else {
@@ -288,7 +299,7 @@ public abstract class ReceiverBase implements 
ChannelReceiver, ListenCallback, R
                         }
                     } else {
                         type = "cleartext";
-                        setPort(port);
+                        boundPort = socket.getLocalPort();
                     }
                     log.info(sm.getString("receiverBase.socket.bind", addr, 
type));
                     retries = 0;
@@ -325,7 +336,7 @@ public abstract class ReceiverBase implements 
ChannelReceiver, ListenCallback, R
             try {
                 addr = new InetSocketAddress(address, port);
                 socket.bind(addr);
-                setUdpPort(port);
+                boundUdpPort = socket.getLocalPort();
                 log.info(sm.getString("receiverBase.udp.bind", addr));
                 return 0;
             } catch (IOException ioe) {
@@ -575,6 +586,11 @@ public abstract class ReceiverBase implements 
ChannelReceiver, ListenCallback, R
         return securePort;
     }
 
+    @Override
+    public int getBoundSecurePort() {
+        return boundSecurePort;
+    }
+
     /**
      * Returns the minimum number of tasks in the pool.
      *
@@ -842,6 +858,11 @@ public abstract class ReceiverBase implements 
ChannelReceiver, ListenCallback, R
         return udpPort;
     }
 
+    @Override
+    public int getBoundUdpPort() {
+        return boundUdpPort;
+    }
+
     /**
      * Sets the UDP port number.
      *
diff --git a/java/org/apache/catalina/tribes/transport/nio/NioReceiver.java 
b/java/org/apache/catalina/tribes/transport/nio/NioReceiver.java
index 7d30658970..66a641c824 100644
--- a/java/org/apache/catalina/tribes/transport/nio/NioReceiver.java
+++ b/java/org/apache/catalina/tribes/transport/nio/NioReceiver.java
@@ -94,7 +94,7 @@ public class NioReceiver extends ReceiverBase implements 
Runnable, NioReceiverMB
             if (groupChannel.getSecure()) {
                 throw new 
IOException(sm.getString("nioReceiver.tlsUnavailable"));
             }
-            setSecurePort(-1);
+            // TLS is not available so nothing is bound to the secure port
             return;
         }
         secureServerChannel = ServerSocketChannel.open();
@@ -193,6 +193,23 @@ public class NioReceiver extends ReceiverBase implements 
Runnable, NioReceiverMB
     @Override
     public void stop() {
         this.stopListening();
+        // Startup may fail before the channels are registered with the 
selector or the listener thread is started.
+        if (serverChannel != null) {
+            try {
+                serverChannel.close();
+            } catch (IOException ioe) {
+                log.debug(sm.getString("nioReceiver.closeError"), ioe);
+            }
+            serverChannel = null;
+        }
+        if (datagramChannel != null) {
+            try {
+                datagramChannel.close();
+            } catch (IOException ioe) {
+                log.debug(sm.getString("nioReceiver.closeError"), ioe);
+            }
+            datagramChannel = null;
+        }
         if (secureServerChannel != null) {
             try {
                 secureServerChannel.close();
@@ -225,18 +242,9 @@ public class NioReceiver extends ReceiverBase implements 
Runnable, NioReceiverMB
 
     @Override
     public void start() throws IOException {
-        super.start();
         try {
+            super.start();
             setPool(new RxTaskPool(getMaxThreads(), getMinThreads(), this));
-        } catch (Exception e) {
-            log.fatal(sm.getString("nioReceiver.threadpool.fail"), e);
-            if (e instanceof IOException) {
-                throw (IOException) e;
-            } else {
-                throw new IOException(e.getMessage());
-            }
-        }
-        try {
             resolveBind();
             boolean tlsOnly = getChannel() instanceof GroupChannel 
groupChannel && groupChannel.getSecure();
             if (!tlsOnly) {
@@ -244,8 +252,7 @@ public class NioReceiver extends ReceiverBase implements 
Runnable, NioReceiverMB
             }
             startSecureListener();
             if (tlsOnly) {
-                // Disable non-secure port
-                setPort(-1);
+                // Non-secure port is disabled so nothing is bound to it
                 setListen(true);
                 return;
             }
@@ -257,11 +264,16 @@ public class NioReceiver extends ReceiverBase implements 
Runnable, NioReceiverMB
             t.setDaemon(true);
             t.start();
         } catch (Exception e) {
+            try {
+                stop();
+            } catch (Exception cleanupException) {
+                e.addSuppressed(cleanupException);
+            }
             log.fatal(sm.getString("nioReceiver.start.fail"), e);
             if (e instanceof IOException) {
                 throw (IOException) e;
             } else {
-                throw new IOException(e.getMessage());
+                throw new IOException(e.getMessage(), e);
             }
         }
     }
diff --git 
a/java/org/apache/catalina/tribes/transport/nio/NioReceiverMBean.java 
b/java/org/apache/catalina/tribes/transport/nio/NioReceiverMBean.java
index 3117c34a1c..643e44701b 100644
--- a/java/org/apache/catalina/tribes/transport/nio/NioReceiverMBean.java
+++ b/java/org/apache/catalina/tribes/transport/nio/NioReceiverMBean.java
@@ -44,6 +44,14 @@ public interface NioReceiverMBean {
      */
     int getPort();
 
+    /**
+     * Returns the TCP port the receiver is actually bound to, which may 
differ from the configured port if
+     * auto-bind was used.
+     *
+     * @return the bound TCP port, -1 if not bound
+     */
+    int getBoundPort();
+
     /**
      * Returns the auto-bind mode.
      *
@@ -58,6 +66,14 @@ public interface NioReceiverMBean {
      */
     int getSecurePort();
 
+    /**
+     * Returns the secure port the receiver is actually bound to, which may 
differ from the configured secure port if
+     * auto-bind was used.
+     *
+     * @return the bound secure port, -1 if not bound
+     */
+    int getBoundSecurePort();
+
     /**
      * Returns the UDP port.
      *
@@ -65,6 +81,14 @@ public interface NioReceiverMBean {
      */
     int getUdpPort();
 
+    /**
+     * Returns the UDP port the receiver is actually bound to, which may 
differ from the configured UDP port if
+     * auto-bind was used.
+     *
+     * @return the bound UDP port, -1 if not bound
+     */
+    int getBoundUdpPort();
+
     /**
      * Returns the selector timeout.
      *
diff --git a/test/org/apache/catalina/tribes/transport/nio/TestNioReceiver.java 
b/test/org/apache/catalina/tribes/transport/nio/TestNioReceiver.java
index c85baec767..b931ba0c44 100644
--- a/test/org/apache/catalina/tribes/transport/nio/TestNioReceiver.java
+++ b/test/org/apache/catalina/tribes/transport/nio/TestNioReceiver.java
@@ -17,18 +17,27 @@
 package org.apache.catalina.tribes.transport.nio;
 
 import java.io.IOException;
+import java.lang.management.ManagementFactory;
 import java.lang.reflect.Field;
 import java.net.DatagramSocket;
 import java.net.InetAddress;
 import java.net.ServerSocket;
 import java.nio.channels.DatagramChannel;
 import java.nio.channels.ServerSocketChannel;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
 import org.junit.Assert;
 import org.junit.Test;
 
 import org.apache.catalina.tribes.group.GroupChannel;
+import org.apache.catalina.tribes.jmx.JmxRegistry;
+import org.apache.catalina.tribes.transport.RxTaskPool;
 
 public class TestNioReceiver {
 
@@ -180,6 +189,111 @@ public class TestNioReceiver {
         assertReleased(receiver);
     }
 
+    @Test
+    public void testFailedStartReleasesResourcesAndCanRestart() throws 
Exception {
+        GroupChannel channel = new GroupChannel();
+        channel.setName("TestNioReceiver-" + System.nanoTime());
+        NioReceiver receiver = new NioReceiver();
+        receiver.setChannel(channel);
+        receiver.setAddress("127.0.0.1");
+        receiver.setPort(0);
+        receiver.setAutoBind(1);
+        receiver.setSoReuseAddress(false);
+        ExecutorService executor = Executors.newSingleThreadExecutor();
+        receiver.setExecutor(executor);
+        MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
+        ObjectName objectName = new 
ObjectName("ClusterChannel:type=Channel,channel=" + channel.getName() +
+                ",component=Receiver");
+        try {
+            int udpPort;
+            try (DatagramSocket blocker = new DatagramSocket(0, 
InetAddress.getByName("127.0.0.1"))) {
+                udpPort = blocker.getLocalPort();
+                receiver.setUdpPort(udpPort);
+                Assert.assertThrows(IOException.class, receiver::start);
+                assertReleased(receiver);
+                Assert.assertEquals(-1, receiver.getBoundPort());
+                Assert.assertEquals(-1, receiver.getBoundSecurePort());
+                Assert.assertEquals(-1, receiver.getBoundUdpPort());
+                Assert.assertFalse(receiver.isListening());
+                Assert.assertNull(receiver.getExecutor());
+                Assert.assertTrue(executor.isShutdown());
+                Assert.assertNull(receiver.getChannel());
+                Assert.assertFalse(mbeanServer.isRegistered(objectName));
+            }
+
+            // As on a normal restart, the coordinator must reattach the 
channel before starting the receiver.
+            receiver.setChannel(channel);
+            receiver.start();
+            long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10);
+            while (!receiver.isListening() && System.nanoTime() < deadline) {
+                Thread.sleep(50);
+            }
+            Assert.assertTrue("the receiver did not start after the UDP port 
was released", receiver.isListening());
+            Assert.assertTrue(receiver.getBoundPort() > 0);
+            Assert.assertEquals(udpPort, receiver.getBoundUdpPort());
+            Assert.assertEquals(0, receiver.getPort());
+            Assert.assertEquals(udpPort, receiver.getUdpPort());
+            Assert.assertFalse(receiver.getExecutor().isShutdown());
+            Assert.assertTrue(mbeanServer.isRegistered(objectName));
+        } finally {
+            receiver.stop();
+            JmxRegistry.removeRegistry(channel, true);
+        }
+        assertReleased(receiver);
+        Assert.assertFalse(mbeanServer.isRegistered(objectName));
+    }
+
+    @Test
+    public void testStopBeforeListenClosesBoundDatagramChannel() throws 
Exception {
+        NioReceiver receiver = new NioReceiver();
+        receiver.setChannel(new GroupChannel());
+        receiver.setAddress("127.0.0.1");
+        receiver.setPort(0);
+        receiver.setUdpPort(45678);
+        try {
+            receiver.bind();
+            Field field = 
NioReceiver.class.getDeclaredField("datagramChannel");
+            field.setAccessible(true);
+            DatagramChannel datagramChannel = (DatagramChannel) 
field.get(receiver);
+            Assert.assertTrue(datagramChannel.isOpen());
+            receiver.stop();
+            Assert.assertFalse("stop must close UDP even before listen 
registers it", datagramChannel.isOpen());
+            assertReleased(receiver);
+        } finally {
+            receiver.stop();
+        }
+    }
+
+    @Test
+    public void testPoolFailurePreservesCauseAndSuppressedCleanupFailure() {
+        RuntimeException startupFailure = new IllegalStateException("pool 
initialization failed");
+        RuntimeException cleanupFailure = new IllegalStateException("cleanup 
failed");
+        NioReceiver receiver = new NioReceiver() {
+            @Override
+            public void setPool(RxTaskPool pool) {
+                throw startupFailure;
+            }
+
+            @Override
+            public void stop() {
+                super.stop();
+                throw cleanupFailure;
+            }
+        };
+        receiver.setChannel(new GroupChannel());
+        ExecutorService executor = Executors.newSingleThreadExecutor();
+        receiver.setExecutor(executor);
+        try {
+            IOException ioe = Assert.assertThrows(IOException.class, 
receiver::start);
+            Assert.assertSame(startupFailure, ioe.getCause());
+            Assert.assertArrayEquals(new Throwable[] { cleanupFailure }, 
startupFailure.getSuppressed());
+            Assert.assertTrue(executor.isShutdown());
+            Assert.assertNull(receiver.getExecutor());
+        } finally {
+            executor.shutdownNow();
+        }
+    }
+
     private void assertReleased(NioReceiver receiver) throws Exception {
         Field serverChannelField = 
NioReceiver.class.getDeclaredField("serverChannel");
         serverChannelField.setAccessible(true);
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 61f2a42e52..b7c8deec05 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -745,6 +745,15 @@
   <subsection name="Cluster">
     <changelog>
       <!-- Entries for backport and removal before 12.0.0-M1 below this line 
-->
+      <fix>
+        When the cluster receiver automatically binds to a port other than the
+        configured port, retain the configured <code>port</code>,
+        <code>securePort</code> and <code>udpPort</code> values and expose the
+        bound ports via the new read-only <code>boundPort</code>,
+        <code>boundSecurePort</code> and <code>boundUdpPort</code> properties
+        so that the configured values are not lost when the configuration is
+        stored. (markt)
+      </fix>
     </changelog>
   </subsection>
   <subsection name="WebSocket">
diff --git a/webapps/docs/config/cluster-receiver.xml 
b/webapps/docs/config/cluster-receiver.xml
index 8dd89bc201..c6983cc480 100644
--- a/webapps/docs/config/cluster-receiver.xml
+++ b/webapps/docs/config/cluster-receiver.xml
@@ -73,12 +73,14 @@
       from the sockets.
     </attribute>
     <attribute name="port" required="false">
-      The listen port for incoming data. The default value is 
<code>4000</code> unless the channel is
-      secure in which case this attribute will be forced to <code>-1</code> to 
disable the non-secure
-      port. If enabled, to avoid port conflicts the receiver will 
automatically bind to a free port
-      within the range of <code>port &lt;= bindPort &lt; port+autoBind</code>
+      The listen port for incoming data. The default value is 
<code>4000</code> unless the channel
+      is secure in which case the non-secure port is not opened. If enabled, 
to avoid port conflicts
+      the receiver will automatically bind to a free port within the range of
+      <code>port &lt;= bindPort &lt; port+autoBind</code>.
       So for example, if port is 4000, and autoBind is set to 10, then the 
receiver will open up
-      a server socket on the first available port in the range 4000-4009.
+      a server socket on the first available port in the range 4000-4009. The 
configured value is not
+      changed by auto-binding; the port actually bound is available via the 
read-only
+      <code>boundPort</code> property.
     </attribute>
     <attribute name="autoBind" required="false">
       Default value is <code>100</code>.
@@ -88,10 +90,14 @@
     <attribute name="securePort" required="false">
       The secure listen port. This port is TLS enabled. If this attribute is 
omitted no TLS port is opened up.
       The default value is unset, meaning there is no TLS socket available.
+      As with <code>port</code>, auto-binding does not change the configured 
value; the port actually bound is
+      available via the read-only <code>boundSecurePort</code> property.
     </attribute>
     <attribute name="udpPort" required="false">
       The UDP listen port. If this attribute is omitted no UDP port is opened 
up.
       The default value is unset, meaning there is no UDP listener available.
+      As with <code>port</code>, auto-binding does not change the configured 
value; the port actually bound is
+      available via the read-only <code>boundUdpPort</code> property.
     </attribute>
     <attribute name="selectorTimeout" required="false">
       The value in milliseconds for the polling timeout in the 
<code>NioReceiver</code>. On older versions of the JDK


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

Reply via email to