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

vavrtom pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/qpid-broker-j.git


The following commit(s) were added to refs/heads/main by this push:
     new 284893860d QPID-8727: [Broker-J] Jetty IllegalStateException leads to 
broker shutdown (#351)
284893860d is described below

commit 284893860db410def4b88beede5c9e04603ea9f3
Author: Daniil Kirilyuk <[email protected]>
AuthorDate: Tue Dec 9 12:20:34 2025 +0100

    QPID-8727: [Broker-J] Jetty IllegalStateException leads to broker shutdown 
(#351)
    
    * QPID-8727: [Broker-J] Jetty IllegalStateException leads to broker shutdown
    
    * JavaDoc formatting in HttpManagement.java
    
    ---------
    
    Co-authored-by: vavrtom <[email protected]>
---
 .../qpid/server/util/DaemonThreadFactory.java      |  28 ++++-
 .../server/management/plugin/HttpManagement.java   | 121 +++++++++++++++++----
 2 files changed, 123 insertions(+), 26 deletions(-)

diff --git 
a/broker-core/src/main/java/org/apache/qpid/server/util/DaemonThreadFactory.java
 
b/broker-core/src/main/java/org/apache/qpid/server/util/DaemonThreadFactory.java
index fe17c39476..2f8babcae5 100644
--- 
a/broker-core/src/main/java/org/apache/qpid/server/util/DaemonThreadFactory.java
+++ 
b/broker-core/src/main/java/org/apache/qpid/server/util/DaemonThreadFactory.java
@@ -25,17 +25,39 @@ import java.util.concurrent.ThreadFactory;
 public final class DaemonThreadFactory implements ThreadFactory
 {
     private final String _threadName;
+    private final Thread.UncaughtExceptionHandler _uncaughtExceptionHandler;
+    private final ThreadGroup _threadGroup;
 
-    public DaemonThreadFactory(String threadName)
+    public DaemonThreadFactory(final String threadName)
     {
         _threadName = threadName;
+        _uncaughtExceptionHandler = null;
+        _threadGroup = null;
+    }
+
+    public DaemonThreadFactory(final String threadName, final 
Thread.UncaughtExceptionHandler uncaughtExceptionHandler)
+    {
+        _threadName = threadName;
+        _uncaughtExceptionHandler = uncaughtExceptionHandler;
+        _threadGroup = null;
+    }
+
+    public DaemonThreadFactory(final String threadName, final 
Thread.UncaughtExceptionHandler uncaughtExceptionHandler, final ThreadGroup 
threadGroup)
+    {
+        _threadName = threadName;
+        _uncaughtExceptionHandler = uncaughtExceptionHandler;
+        _threadGroup = threadGroup;
     }
 
     @Override
-    public Thread newThread(Runnable r)
+    public Thread newThread(final Runnable runnable)
     {
-        Thread thread = new Thread(r, _threadName);
+        final Thread thread = _threadGroup == null ? new Thread(runnable, 
_threadName) : new Thread(_threadGroup, runnable, _threadName);
         thread.setDaemon(true);
+        if (_uncaughtExceptionHandler != null)
+        {
+            thread.setUncaughtExceptionHandler(_uncaughtExceptionHandler);
+        }
         return thread;
     }
 }
\ No newline at end of file
diff --git 
a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagement.java
 
b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagement.java
index bd2715ee00..2781f74746 100644
--- 
a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagement.java
+++ 
b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagement.java
@@ -18,6 +18,7 @@
  * under the License.
  *
  */
+
 package org.apache.qpid.server.management.plugin;
 
 import java.io.IOException;
@@ -36,7 +37,6 @@ import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.ThreadPoolExecutor;
 import java.util.function.BiConsumer;
 
 import javax.net.ssl.SSLContext;
@@ -76,6 +76,8 @@ import org.eclipse.jetty.util.annotation.Name;
 import org.eclipse.jetty.util.ssl.SslContextFactory;
 import org.eclipse.jetty.util.thread.ExecutorThreadPool;
 import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
+import org.eclipse.jetty.util.thread.Scheduler;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -134,9 +136,9 @@ import 
org.apache.qpid.server.util.ServerScopedRuntimeException;
 public class HttpManagement extends AbstractPluginAdapter<HttpManagement> 
implements HttpManagementConfiguration<HttpManagement>, PortManager
 {
     private static final Logger LOGGER = 
LoggerFactory.getLogger(HttpManagement.class);
-    
-    // 10 minutes by default
-    public static final int DEFAULT_TIMEOUT_IN_SECONDS = 60 * 10;
+
+    // 1 minute by default
+    public static final int DEFAULT_TIMEOUT_IN_SECONDS = 60;
     public static final String TIME_OUT = "sessionTimeout";
     public static final String HTTP_BASIC_AUTHENTICATION_ENABLED = 
"httpBasicAuthenticationEnabled";
     public static final String HTTPS_BASIC_AUTHENTICATION_ENABLED = 
"httpsBasicAuthenticationEnabled";
@@ -197,9 +199,13 @@ public class HttpManagement extends 
AbstractPluginAdapter<HttpManagement> implem
     private final Map<HttpPort<?>, SslContextFactory.Server> 
_sslContextFactoryMap = new ConcurrentHashMap<>();
     private final BrokerChangeListener _brokerChangeListener = new 
BrokerChangeListener();
 
+    private final Thread.UncaughtExceptionHandler _uncaughtExceptionHandler = 
new JettyUncaughtExceptionHandler();
+    private final ThreadGroup _threadGroup = new 
JettyThreadGroup("Jetty-ThreadGroup", _uncaughtExceptionHandler);
+
     private volatile boolean _serveUncompressedDojo;
     private volatile Long _saslExchangeExpiry;
-    private volatile ThreadPoolExecutor _jettyServerExecutor;
+    private volatile ScheduledThreadPoolExecutor _jettyServerExecutor;
+    private volatile ScheduledThreadPoolExecutor _jettySchedulerExecutor;
 
     @ManagedObjectFactoryConstructor
     public HttpManagement(final Map<String, Object> attributes, final 
Broker<?> broker)
@@ -272,6 +278,10 @@ public class HttpManagement extends 
AbstractPluginAdapter<HttpManagement> implem
         {
             _jettyServerExecutor.shutdown();
         }
+        if (_jettySchedulerExecutor != null)
+        {
+            _jettySchedulerExecutor.shutdown();
+        }
         
getBroker().getEventLogger().message(ManagementConsoleMessages.STOPPED(OPERATIONAL_LOGGING_NAME));
         return CompletableFuture.completedFuture(null);
     }
@@ -316,8 +326,14 @@ public class HttpManagement extends 
AbstractPluginAdapter<HttpManagement> implem
     {
         LOGGER.debug("Starting up web server on {}", ports);
 
-        _jettyServerExecutor = new ScheduledThreadPoolExecutor(1, new 
DaemonThreadFactory("Jetty-Server-Thread"));
-        final Server server = new Server(new 
ExecutorThreadPool(_jettyServerExecutor));
+        final ThreadFactory serverThreadFactory = new 
DaemonThreadFactory("Jetty-Server-Thread", _uncaughtExceptionHandler, 
_threadGroup);
+        _jettyServerExecutor = new ScheduledThreadPoolExecutor(1, 
serverThreadFactory);
+        _jettyServerExecutor.setRemoveOnCancelPolicy(true);
+        final ThreadFactory schedulerThreadFactory = new 
DaemonThreadFactory("Jetty-Scheduler-Thread", _uncaughtExceptionHandler, 
_threadGroup);
+        _jettySchedulerExecutor = new ScheduledThreadPoolExecutor(1, 
schedulerThreadFactory);
+        _jettySchedulerExecutor.setRemoveOnCancelPolicy(true);
+        final Scheduler scheduler = new 
ScheduledExecutorScheduler(_jettySchedulerExecutor);
+        final Server server = new Server(new 
ExecutorThreadPool(_jettyServerExecutor), scheduler, null);
         int lastPort = -1;
         for (final HttpPort<?> port : ports)
         {
@@ -476,16 +492,16 @@ public class HttpManagement extends 
AbstractPluginAdapter<HttpManagement> implem
         }
 
         getModel().getSupportedCategories()
-                  .stream()
-                  .map(Class::getSimpleName)
-                  .map(String::toLowerCase)
-                  .forEach(name ->
-                  {
-                      root.addServlet(apiDocsServletHolder, "/apidocs/latest/" 
+ name + "/");
-                      root.addServlet(apiDocsServletHolder, "/apidocs/" + 
version + "/" + name + "/");
-                      root.addServlet(apiDocsServletHolder, "/apidocs/latest/" 
+ name);
-                      root.addServlet(apiDocsServletHolder, "/apidocs/" + 
version + "/" + name);
-                  });
+                .stream()
+                .map(Class::getSimpleName)
+                .map(String::toLowerCase)
+                .forEach(name ->
+                {
+                    root.addServlet(apiDocsServletHolder, "/apidocs/latest/" + 
name + "/");
+                    root.addServlet(apiDocsServletHolder, "/apidocs/" + 
version + "/" + name + "/");
+                    root.addServlet(apiDocsServletHolder, "/apidocs/latest/" + 
name);
+                    root.addServlet(apiDocsServletHolder, "/apidocs/" + 
version + "/" + name);
+                });
     }
 
     @Override
@@ -602,7 +618,7 @@ public class HttpManagement extends 
AbstractPluginAdapter<HttpManagement> implem
         }
 
         final ServerConnector connector = new ServerConnector(server,
-                new QBBTrackingThreadPool(port.getThreadPoolMaximum(), 
port.getThreadPoolMinimum()),
+                new QBBTrackingThreadPool(port.getThreadPoolMaximum(), 
port.getThreadPoolMinimum(), _uncaughtExceptionHandler, _threadGroup),
                 null,
                 null,
                 port.getDesiredNumberOfAcceptors(),
@@ -813,8 +829,8 @@ public class HttpManagement extends 
AbstractPluginAdapter<HttpManagement> implem
         for (final Transport transport: transports)
         {
             
getBroker().getEventLogger().message(ManagementConsoleMessages.LISTENING(Protocol.HTTP.name(),
-                                                                               
      transport.name(),
-                                                                               
      localPort));
+                    transport.name(),
+                    localPort));
         }
     }
 
@@ -958,10 +974,20 @@ public class HttpManagement extends 
AbstractPluginAdapter<HttpManagement> implem
     {
         private final ThreadFactory _threadFactory;
 
-        public QBBTrackingThreadPool(@Name("maxThreads") final int maxThreads, 
@Name("minThreads") final int minThreads)
+        public QBBTrackingThreadPool(@Name("maxThreads") final int maxThreads,
+                                     @Name("minThreads") final int minThreads,
+                                     final Thread.UncaughtExceptionHandler 
uncaughtExceptionHandler,
+                                     final ThreadGroup threadGroup )
         {
-            super(maxThreads, minThreads);
-            _threadFactory = 
QpidByteBuffer.createQpidByteBufferTrackingThreadFactory(QBBTrackingThreadPool.super::newThread);
+            super(maxThreads, minThreads, DEFAULT_TIMEOUT_IN_SECONDS * 1000, 
null, threadGroup);
+
+            final ThreadFactory connectorThreadFactory = runnable ->
+            {
+                final Thread thread = super.newThread(runnable);
+                thread.setUncaughtExceptionHandler(uncaughtExceptionHandler);
+                return thread;
+            };
+            _threadFactory = 
QpidByteBuffer.createQpidByteBufferTrackingThreadFactory(connectorThreadFactory);
         }
 
         @Override
@@ -1095,4 +1121,53 @@ public class HttpManagement extends 
AbstractPluginAdapter<HttpManagement> implem
             return _closeFutures.size();
         }
     }
+
+    /** 
+     * Defensive uncaught exception handler, preventing internal Jetty 
exception being propagated to the
+     * broker global UncaughtExceptionHandler
+     */
+    private static class JettyUncaughtExceptionHandler implements 
Thread.UncaughtExceptionHandler
+    {
+        @Override
+        public void uncaughtException(final Thread thread, final Throwable 
throwable)
+        {
+            LOGGER.warn("Uncaught exception in HTTP management thread", 
throwable);
+
+            final Thread.UncaughtExceptionHandler 
defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
+
+            if (defaultUncaughtExceptionHandler != null && 
isCritical(throwable))
+            {
+                defaultUncaughtExceptionHandler.uncaughtException(thread, 
throwable);
+            }
+        }
+
+        private boolean isCritical(final Throwable throwable)
+        {
+            return throwable instanceof Error || throwable instanceof 
ServerScopedRuntimeException;
+        }
+    }
+
+    private static class JettyThreadGroup extends ThreadGroup
+    {
+        private final Thread.UncaughtExceptionHandler 
_uncaughtExceptionHandler;
+
+        JettyThreadGroup(final String name, final 
Thread.UncaughtExceptionHandler uncaughtExceptionHandler)
+        {
+            super(name);
+            _uncaughtExceptionHandler = uncaughtExceptionHandler;
+        }
+
+        @Override
+        public void uncaughtException(final Thread thread, final Throwable 
throwable)
+        {
+            if (_uncaughtExceptionHandler != null)
+            {
+                _uncaughtExceptionHandler.uncaughtException(thread, throwable);
+            }
+            else
+            {
+                LOGGER.warn("Uncaught exception in HTTP management thread, no 
default uncaught exception handler provided", throwable);
+            }
+        }
+    }
 }


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

Reply via email to