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 <= bindPort < 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 <= bindPort < 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]