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 6d4134a6b6 QPID-8726: [Broker-J] Provide out-of-box uncaught exception 
handlers for the Main class (#350)
6d4134a6b6 is described below

commit 6d4134a6b64de0aa3eb7be744ce7740f6fb82585
Author: Daniil Kirilyuk <[email protected]>
AuthorDate: Tue Dec 9 11:27:40 2025 +0100

    QPID-8726: [Broker-J] Provide out-of-box uncaught exception handlers for 
the Main class (#350)
    
    * QPID-8726: [Broker-J] Provide out-of-box uncaught exception handlers for 
the Main class
    
    * Fixed formatting in pom.xml
    
    * Added empty line to the end of GracefulShutdownExceptionHandler.java file
    
    ---------
    
    Co-authored-by: vavrtom <[email protected]>
---
 broker/pom.xml                                     |  26 ++++
 .../src/main/java/org/apache/qpid/server/Main.java |  39 +-----
 .../server/utils/AbstractExceptionHandler.java     |  59 ++++++++
 .../org/apache/qpid/server/utils/ExitHandler.java  |  38 +++++
 .../server/utils/FailFastExceptionHandler.java     |  44 ++++++
 .../utils/GracefulShutdownExceptionHandler.java    |  44 ++++++
 .../utils/LogAndContinueExceptionHandler.java      |  41 ++++++
 .../test/java/org/apache/qpid/server/MainTest.java |   2 +-
 .../server/utils/FailFastExceptionHandlerTest.java | 152 ++++++++++++++++++++
 .../GracefulShutdownExceptionHandlerTest.java      | 152 ++++++++++++++++++++
 .../utils/LogAndContinueExceptionHandlerTest.java  | 154 +++++++++++++++++++++
 .../Java-Broker-Appendix-System-Properties.xml     |  30 ++++
 12 files changed, 747 insertions(+), 34 deletions(-)

diff --git a/broker/pom.xml b/broker/pom.xml
index 00dc77b7d3..8b9f4b5084 100644
--- a/broker/pom.xml
+++ b/broker/pom.xml
@@ -196,4 +196,30 @@
     </profile>
   </profiles>
 
+    <build>
+      <plugins>
+
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-dependency-plugin</artifactId>
+          <executions>
+            <execution>
+              <goals>
+                <goal>properties</goal>
+              </goals>
+            </execution>
+          </executions>
+        </plugin>
+
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-surefire-plugin</artifactId>
+          <configuration>
+            <argLine>@{argLine} 
-javaagent:${org.mockito:mockito-core:jar}</argLine>
+          </configuration>
+        </plugin>
+
+      </plugins>
+    </build>
+
 </project>
diff --git a/broker/src/main/java/org/apache/qpid/server/Main.java 
b/broker/src/main/java/org/apache/qpid/server/Main.java
index b2a12ec6ee..4231da3575 100644
--- a/broker/src/main/java/org/apache/qpid/server/Main.java
+++ b/broker/src/main/java/org/apache/qpid/server/Main.java
@@ -38,8 +38,8 @@ import org.apache.commons.cli.Option;
 import org.apache.commons.cli.Options;
 import org.apache.commons.cli.ParseException;
 import org.apache.commons.cli.help.HelpFormatter;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.apache.qpid.server.utils.ExitHandler;
+import org.apache.qpid.server.utils.FailFastExceptionHandler;
 
 import org.apache.qpid.server.configuration.CommonProperties;
 import org.apache.qpid.server.configuration.IllegalConfigurationException;
@@ -428,37 +428,10 @@ public class Main
         
         if (handler == null)
         {
-            handler = (thread, exception) ->
-            {
-                final boolean continueOnError = 
Boolean.getBoolean("qpid.broker.exceptionHandler.continue");
-                try
-                {
-                    
System.err.println("########################################################################");
-                    System.err.println("#");
-                    System.err.print("# Unhandled Exception ");
-                    System.err.print(exception.toString());
-                    System.err.print(" in Thread ");
-                    System.err.println(thread.getName());
-                    System.err.println("#");
-                    System.err.println(continueOnError ? "# Forced to continue 
by JVM setting 'qpid.broker.exceptionHandler.continue'" : "# Exiting");
-                    System.err.println("#");
-                    
System.err.println("########################################################################");
-                    exception.printStackTrace(System.err);
-
-                    final Logger logger = 
LoggerFactory.getLogger("org.apache.qpid.server.Main");
-                    logger.error("Uncaught exception, " + (continueOnError ? 
"continuing." : "shutting down."), exception);
-                }
-                finally
-                {
-                    if (!continueOnError)
-                    {
-                        Runtime.getRuntime().halt(1);
-                    }
-                }
-            };
+            handler = new FailFastExceptionHandler();
+        }
 
-            Thread.setDefaultUncaughtExceptionHandler(handler);
-        } 
+        Thread.setDefaultUncaughtExceptionHandler(handler);
     }
 
     protected void startBroker(final Map<String,Object> attributes) throws 
Exception
@@ -481,7 +454,7 @@ public class Main
 
     protected void shutdown(final int status)
     {
-        System.exit(status);
+        ExitHandler.exit(status);
     }
 
     private void printHelp()
diff --git 
a/broker/src/main/java/org/apache/qpid/server/utils/AbstractExceptionHandler.java
 
b/broker/src/main/java/org/apache/qpid/server/utils/AbstractExceptionHandler.java
new file mode 100644
index 0000000000..bd77ef6da3
--- /dev/null
+++ 
b/broker/src/main/java/org/apache/qpid/server/utils/AbstractExceptionHandler.java
@@ -0,0 +1,59 @@
+/*
+ *
+ * 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.qpid.server.utils;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AbstractExceptionHandler
+{
+    protected static final String CONTINUE_ON_ERROR_PROPERTY_NAME = 
"qpid.broker.exceptionHandler.continue";
+
+    protected String getMessage(final boolean continueOnError)
+    {
+        return continueOnError
+                ? "# Forced to continue by JVM setting 
'qpid.broker.exceptionHandler.continue'"
+                : "# Exiting";
+    }
+
+    protected void printDiagnostic(final Thread thread, final Throwable 
throwable, final boolean continueOnError)
+    {
+        
System.err.println("########################################################################");
+        System.err.println("#");
+        System.err.print("# Unhandled Exception ");
+        System.err.print(throwable.toString());
+        System.err.print(" in Thread ");
+        System.err.println(thread.getName());
+        System.err.println("#");
+        System.err.println(getMessage(continueOnError));
+        System.err.println("#");
+        
System.err.println("########################################################################");
+        throwable.printStackTrace(System.err);
+    }
+
+    protected void logError(final Throwable throwable, final boolean 
continueOnError)
+    {
+        final Logger logger = 
LoggerFactory.getLogger("org.apache.qpid.server.Main");
+        final String message = "Uncaught exception, " + (continueOnError ? 
"continuing." : "shutting down.");
+        logger.error(message, throwable);
+    }
+}
diff --git a/broker/src/main/java/org/apache/qpid/server/utils/ExitHandler.java 
b/broker/src/main/java/org/apache/qpid/server/utils/ExitHandler.java
new file mode 100644
index 0000000000..dde25d60b3
--- /dev/null
+++ b/broker/src/main/java/org/apache/qpid/server/utils/ExitHandler.java
@@ -0,0 +1,38 @@
+/*
+ *
+ * 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.qpid.server.utils;
+
+/** Wrapper for JVM shutdown functionality */
+public class ExitHandler
+{
+    /** Initiates JVM shutdown */
+    public static void exit(final int status)
+    {
+        System.exit(status);
+    }
+
+    /** Terminates JVM */
+    public static void halt(final int status)
+    {
+        Runtime.getRuntime().halt(status);
+    }
+}
diff --git 
a/broker/src/main/java/org/apache/qpid/server/utils/FailFastExceptionHandler.java
 
b/broker/src/main/java/org/apache/qpid/server/utils/FailFastExceptionHandler.java
new file mode 100644
index 0000000000..30ca0665b4
--- /dev/null
+++ 
b/broker/src/main/java/org/apache/qpid/server/utils/FailFastExceptionHandler.java
@@ -0,0 +1,44 @@
+/*
+ *
+ * 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.qpid.server.utils;
+
+/** UncaughtExceptionHandler implementation logs an exception and shutdowns 
the broker */
+public class FailFastExceptionHandler extends AbstractExceptionHandler 
implements Thread.UncaughtExceptionHandler
+{
+    @Override
+    public void uncaughtException(final Thread thread, final Throwable 
throwable)
+    {
+        final boolean continueOnError = 
Boolean.getBoolean(CONTINUE_ON_ERROR_PROPERTY_NAME);
+        try
+        {
+            printDiagnostic(thread, throwable, continueOnError);
+            logError(throwable, continueOnError);
+        }
+        finally
+        {
+            if (!continueOnError)
+            {
+                ExitHandler.halt(1);
+            }
+        }
+    }
+}
diff --git 
a/broker/src/main/java/org/apache/qpid/server/utils/GracefulShutdownExceptionHandler.java
 
b/broker/src/main/java/org/apache/qpid/server/utils/GracefulShutdownExceptionHandler.java
new file mode 100644
index 0000000000..8546c59e76
--- /dev/null
+++ 
b/broker/src/main/java/org/apache/qpid/server/utils/GracefulShutdownExceptionHandler.java
@@ -0,0 +1,44 @@
+/*
+ *
+ * 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.qpid.server.utils;
+
+/** UncaughtExceptionHandler implementation logs an exception and gracefully 
stops the broker */
+public class GracefulShutdownExceptionHandler extends AbstractExceptionHandler 
implements Thread.UncaughtExceptionHandler
+{
+    @Override
+    public void uncaughtException(final Thread thread, final Throwable 
throwable)
+    {
+        final boolean continueOnError = 
Boolean.getBoolean(CONTINUE_ON_ERROR_PROPERTY_NAME);
+        try
+        {
+            printDiagnostic(thread, throwable, continueOnError);
+            logError(throwable, continueOnError);
+        }
+        finally
+        {
+            if (!continueOnError)
+            {
+                ExitHandler.exit(1);
+            }
+        }
+    }
+}
diff --git 
a/broker/src/main/java/org/apache/qpid/server/utils/LogAndContinueExceptionHandler.java
 
b/broker/src/main/java/org/apache/qpid/server/utils/LogAndContinueExceptionHandler.java
new file mode 100644
index 0000000000..2ab55a1af6
--- /dev/null
+++ 
b/broker/src/main/java/org/apache/qpid/server/utils/LogAndContinueExceptionHandler.java
@@ -0,0 +1,41 @@
+/*
+ *
+ * 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.qpid.server.utils;
+
+/** UncaughtExceptionHandler implementation logs an exception, but doesn't 
shut down the broker */
+public class LogAndContinueExceptionHandler extends AbstractExceptionHandler 
implements Thread.UncaughtExceptionHandler
+{
+    @Override
+    public void uncaughtException(final Thread thread, final Throwable 
throwable)
+    {
+        printDiagnostic(thread, throwable, true);
+        logError(throwable, true);
+    }
+
+
+    @Override
+    protected String getMessage(boolean continueOnError)
+    {
+        return "# Forced to continue by JVM setting 
'qpid.broker.exceptionHandler=%s'"
+                
.formatted(LogAndContinueExceptionHandler.class.getCanonicalName());
+    }
+}
diff --git a/broker/src/test/java/org/apache/qpid/server/MainTest.java 
b/broker/src/test/java/org/apache/qpid/server/MainTest.java
index bf1ada577d..c7414787b3 100644
--- a/broker/src/test/java/org/apache/qpid/server/MainTest.java
+++ b/broker/src/test/java/org/apache/qpid/server/MainTest.java
@@ -18,6 +18,7 @@
  * under the License.
  *
  */
+
 package org.apache.qpid.server;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -35,7 +36,6 @@ import org.apache.qpid.server.model.Broker;
 import org.apache.qpid.server.model.SystemConfig;
 import org.apache.qpid.test.utils.UnitTestBase;
 
-
 /**
  * Test to verify the command line parsing within the Main class, by
  * providing it a series of command line arguments and verifying the
diff --git 
a/broker/src/test/java/org/apache/qpid/server/utils/FailFastExceptionHandlerTest.java
 
b/broker/src/test/java/org/apache/qpid/server/utils/FailFastExceptionHandlerTest.java
new file mode 100644
index 0000000000..a6fd6b78f2
--- /dev/null
+++ 
b/broker/src/test/java/org/apache/qpid/server/utils/FailFastExceptionHandlerTest.java
@@ -0,0 +1,152 @@
+/*
+ *
+ * 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.qpid.server.utils;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+public class FailFastExceptionHandlerTest
+{
+    private static final String EXITING = "# Exiting";
+    private static final String FORCED_TO_CONTINUE = "# Forced to continue by 
JVM setting 'qpid.broker.exceptionHandler.continue'";
+
+    private final FailFastExceptionHandler handler = new 
FailFastExceptionHandler();
+    private final PrintStream standardErr = System.err;
+    private final ByteArrayOutputStream errorStreamCaptor = new 
ByteArrayOutputStream();
+
+    @BeforeEach
+    public void setUp()
+    {
+        System.setErr(new PrintStream(errorStreamCaptor));
+    }
+
+    /** Clear the system property between the tests */
+    @AfterEach
+    void tearDown()
+    {
+        
System.clearProperty(FailFastExceptionHandler.CONTINUE_ON_ERROR_PROPERTY_NAME);
+        System.setErr(standardErr);
+    }
+
+    /** ExceptionHandler should call ExitHandler.halt(1) when continueOnError 
is false */
+    @Test
+    void exceptionHandlerCallsExitHandlerExitOnFlagFalse()
+    {
+        try (final MockedStatic<ExitHandler> mockedExitHandler = 
mockStatic(ExitHandler.class))
+        {
+            
System.setProperty(FailFastExceptionHandler.CONTINUE_ON_ERROR_PROPERTY_NAME, 
"false");
+            final Thread testThread = mock(Thread.class);
+            final Throwable testThrowable = new RuntimeException("Test 
Exception");
+
+            handler.uncaughtException(testThread, testThrowable);
+
+            // verify that ExitHandler.exit(1) was called
+            mockedExitHandler.verify(() -> ExitHandler.halt(1));
+            mockedExitHandler.verify(() -> ExitHandler.exit(anyInt()), 
never());
+
+            assertTrue(errorStreamCaptor.toString().contains(EXITING));
+            
assertFalse(errorStreamCaptor.toString().contains(FORCED_TO_CONTINUE));
+        }
+    }
+
+    /** ExceptionHandler should call ExitHandler.halt(1) when continueOnError 
property is not set */
+    @Test
+    void exceptionHandlerCallsExitHandlerExitOnFlagNotSet()
+    {
+        try (final MockedStatic<ExitHandler> mockedExitHandler = 
mockStatic(ExitHandler.class))
+        {
+            final Thread testThread = mock(Thread.class);
+            final Throwable testThrowable = new RuntimeException("Test 
Exception");
+
+            handler.uncaughtException(testThread, testThrowable);
+
+            // verify that ExitHandler.exit(1) was called
+            mockedExitHandler.verify(() -> ExitHandler.halt(1));
+            mockedExitHandler.verify(() -> ExitHandler.exit(anyInt()), 
never());
+
+            assertTrue(errorStreamCaptor.toString().contains(EXITING));
+            
assertFalse(errorStreamCaptor.toString().contains(FORCED_TO_CONTINUE));
+        }
+    }
+
+    /** ExceptionHandler shouldn't call ExitHandler.halt() when 
continueOnError is true */
+    @Test
+    void exceptionHandlerCallsExitHandlerExitOnFlagTrue()
+    {
+        try (final MockedStatic<ExitHandler> mockedExitHandler = 
mockStatic(ExitHandler.class))
+        {
+            
System.setProperty(FailFastExceptionHandler.CONTINUE_ON_ERROR_PROPERTY_NAME, 
"true");
+            final Thread testThread = mock(Thread.class);
+            final Throwable testThrowable = new RuntimeException("Test 
Exception");
+
+            handler.uncaughtException(testThread, testThrowable);
+
+            // verify that ExitHandler.exit() or ExitHandler.halt() was never 
called
+            mockedExitHandler.verify(() -> ExitHandler.halt(anyInt()), 
never());
+            mockedExitHandler.verify(() -> ExitHandler.exit(anyInt()), 
never());
+
+            assertFalse(errorStreamCaptor.toString().contains(EXITING));
+            
assertTrue(errorStreamCaptor.toString().contains(FORCED_TO_CONTINUE));
+        }
+    }
+
+    /** ExceptionHandler should call diagnostic and logging methods */
+    @Test
+    void exceptionHandlerCallsDiagnosticAndLoggingMethods()
+    {
+        final FailFastExceptionHandler spyHandler = spy(new 
FailFastExceptionHandler());
+        final Thread testThread = mock(Thread.class);
+        final Throwable testThrowable = new RuntimeException("Test Exception");
+        final boolean continueOnError = 
Boolean.getBoolean(FailFastExceptionHandler.CONTINUE_ON_ERROR_PROPERTY_NAME);
+
+        try (final MockedStatic<ExitHandler> mockedExitHandler = 
mockStatic(ExitHandler.class))
+        {
+            spyHandler.uncaughtException(testThread, testThrowable);
+
+            // verify that printDiagnostic and logError were called with the 
correct parameters.
+            verify(spyHandler, times(1)).printDiagnostic(testThread, 
testThrowable, continueOnError);
+            verify(spyHandler, times(1)).logError(testThrowable, 
continueOnError);
+
+            // verify that ExitHandler.exit(1) was called
+            mockedExitHandler.verify(() -> ExitHandler.halt(1));
+            mockedExitHandler.verify(() -> ExitHandler.exit(anyInt()), 
never());
+
+            assertTrue(errorStreamCaptor.toString().contains(EXITING));
+            
assertFalse(errorStreamCaptor.toString().contains(FORCED_TO_CONTINUE));
+        }
+    }
+}
diff --git 
a/broker/src/test/java/org/apache/qpid/server/utils/GracefulShutdownExceptionHandlerTest.java
 
b/broker/src/test/java/org/apache/qpid/server/utils/GracefulShutdownExceptionHandlerTest.java
new file mode 100644
index 0000000000..0d4052cac2
--- /dev/null
+++ 
b/broker/src/test/java/org/apache/qpid/server/utils/GracefulShutdownExceptionHandlerTest.java
@@ -0,0 +1,152 @@
+/*
+ *
+ * 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.qpid.server.utils;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+public class GracefulShutdownExceptionHandlerTest
+{
+    private static final String EXITING = "# Exiting";
+    private static final String FORCED_TO_CONTINUE = "# Forced to continue by 
JVM setting 'qpid.broker.exceptionHandler.continue'";
+
+    private final GracefulShutdownExceptionHandler handler = new 
GracefulShutdownExceptionHandler();
+    private final PrintStream standardErr = System.err;
+    private final ByteArrayOutputStream errorStreamCaptor = new 
ByteArrayOutputStream();
+
+    @BeforeEach
+    public void setUp()
+    {
+        System.setErr(new PrintStream(errorStreamCaptor));
+    }
+
+    /** Clear the system property between the tests */
+    @AfterEach
+    void tearDown()
+    {
+        
System.clearProperty(GracefulShutdownExceptionHandler.CONTINUE_ON_ERROR_PROPERTY_NAME);
+        System.setErr(standardErr);
+    }
+
+    /** ExceptionHandler should call ExitHandler.exit(1) when continueOnError 
is false */
+    @Test
+    void exceptionHandlerCallsExitHandlerExitOnFlagFalse()
+    {
+        try (final MockedStatic<ExitHandler> mockedExitHandler = 
mockStatic(ExitHandler.class))
+        {
+            
System.setProperty(GracefulShutdownExceptionHandler.CONTINUE_ON_ERROR_PROPERTY_NAME,
 "false");
+            final Thread testThread = mock(Thread.class);
+            final Throwable testThrowable = new RuntimeException("Test 
Exception");
+
+            handler.uncaughtException(testThread, testThrowable);
+
+            // verify that ExitHandler.exit(1) was called
+            mockedExitHandler.verify(() -> ExitHandler.exit(1));
+            mockedExitHandler.verify(() -> ExitHandler.halt(anyInt()), 
never());
+
+            assertTrue(errorStreamCaptor.toString().contains(EXITING));
+            
assertFalse(errorStreamCaptor.toString().contains(FORCED_TO_CONTINUE));
+        }
+    }
+
+    /** ExceptionHandler should call ExitHandler.exit(1) when continueOnError 
property is not set */
+    @Test
+    void exceptionHandlerCallsExitHandlerExitOnFlagNotSet()
+    {
+        try (final MockedStatic<ExitHandler> mockedExitHandler = 
mockStatic(ExitHandler.class))
+        {
+            final Thread testThread = mock(Thread.class);
+            final Throwable testThrowable = new RuntimeException("Test 
Exception");
+
+            handler.uncaughtException(testThread, testThrowable);
+
+            // verify that ExitHandler.exit(1) was called
+            mockedExitHandler.verify(() -> ExitHandler.exit(1));
+            mockedExitHandler.verify(() -> ExitHandler.halt(anyInt()), 
never());
+
+            assertTrue(errorStreamCaptor.toString().contains(EXITING));
+            
assertFalse(errorStreamCaptor.toString().contains(FORCED_TO_CONTINUE));
+        }
+    }
+
+    /** ExceptionHandler shouldn't call ExitHandler.exit() when 
continueOnError is true */
+    @Test
+    void exceptionHandlerCallsExitHandlerExitOnFlagTrue()
+    {
+        try (final MockedStatic<ExitHandler> mockedExitHandler = 
mockStatic(ExitHandler.class))
+        {
+            
System.setProperty(GracefulShutdownExceptionHandler.CONTINUE_ON_ERROR_PROPERTY_NAME,
 "true");
+            final Thread testThread = mock(Thread.class);
+            final Throwable testThrowable = new RuntimeException("Test 
Exception");
+
+            handler.uncaughtException(testThread, testThrowable);
+
+            // verify that ExitHandler.exit() was never called
+            mockedExitHandler.verify(() -> ExitHandler.exit(anyInt()), 
never());
+            mockedExitHandler.verify(() -> ExitHandler.halt(anyInt()), 
never());
+
+            assertFalse(errorStreamCaptor.toString().contains(EXITING));
+            
assertTrue(errorStreamCaptor.toString().contains(FORCED_TO_CONTINUE));
+        }
+    }
+
+    /** ExceptionHandler should call diagnostic and logging methods */
+    @Test
+    void exceptionHandlerCallsDiagnosticAndLoggingMethods()
+    {
+        final GracefulShutdownExceptionHandler spyHandler1 = spy(new 
GracefulShutdownExceptionHandler());
+        final Thread testThread = mock(Thread.class);
+        final Throwable testThrowable = new RuntimeException("Test Exception");
+        boolean continueOnError = 
Boolean.getBoolean(GracefulShutdownExceptionHandler.CONTINUE_ON_ERROR_PROPERTY_NAME);
+
+        try (final MockedStatic<ExitHandler> mockedExitHandler = 
mockStatic(ExitHandler.class))
+        {
+            spyHandler1.uncaughtException(testThread, testThrowable);
+
+            // verify that printDiagnostic and logError were called with the 
correct parameters
+            verify(spyHandler1, times(1)).printDiagnostic(testThread, 
testThrowable, continueOnError);
+            verify(spyHandler1, times(1)).logError(testThrowable, 
continueOnError);
+
+            // verify that the exit path was taken, as continueOnError is 
false by default
+            mockedExitHandler.verify(() -> ExitHandler.exit(1));
+            mockedExitHandler.verify(() -> ExitHandler.halt(anyInt()), 
never());
+
+            assertTrue(errorStreamCaptor.toString().contains(EXITING));
+            
assertFalse(errorStreamCaptor.toString().contains(FORCED_TO_CONTINUE));
+        }
+    }
+}
diff --git 
a/broker/src/test/java/org/apache/qpid/server/utils/LogAndContinueExceptionHandlerTest.java
 
b/broker/src/test/java/org/apache/qpid/server/utils/LogAndContinueExceptionHandlerTest.java
new file mode 100644
index 0000000000..5c5c0cfb2a
--- /dev/null
+++ 
b/broker/src/test/java/org/apache/qpid/server/utils/LogAndContinueExceptionHandlerTest.java
@@ -0,0 +1,154 @@
+/*
+ *
+ * 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.qpid.server.utils;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class LogAndContinueExceptionHandlerTest
+{
+    private static final String EXITING = "# Exiting";
+    private static final String FORCED_TO_CONTINUE = "# Forced to continue by 
JVM setting 'qpid.broker.exceptionHandler=%s'"
+            
.formatted(LogAndContinueExceptionHandler.class.getCanonicalName());
+
+    private final LogAndContinueExceptionHandler handler = new 
LogAndContinueExceptionHandler();
+    private final PrintStream standardErr = System.err;
+    private final ByteArrayOutputStream errorStreamCaptor = new 
ByteArrayOutputStream();
+
+    @BeforeEach
+    public void setUp()
+    {
+        System.setErr(new PrintStream(errorStreamCaptor));
+    }
+
+    /** Clear the system property between the tests */
+    @AfterEach
+    void tearDown()
+    {
+        
System.clearProperty(LogAndContinueExceptionHandler.CONTINUE_ON_ERROR_PROPERTY_NAME);
+        System.setErr(standardErr);
+    }
+
+    /** ExceptionHandler should never call ExitHandler.halt(1) when 
continueOnError is false */
+    @Test
+    void exceptionHandlerCallsExitHandlerExitOnFlagFalse()
+    {
+        try (final MockedStatic<ExitHandler> mockedExitHandler = 
mockStatic(ExitHandler.class))
+        {
+            
System.setProperty(LogAndContinueExceptionHandler.CONTINUE_ON_ERROR_PROPERTY_NAME,
 "false");
+            final Thread testThread = mock(Thread.class);
+            final Throwable testThrowable = new RuntimeException("Test 
Exception");
+
+            handler.uncaughtException(testThread, testThrowable);
+
+            // verify that ExitHandler.exit(1) or ExitHandler.exit(1) was 
never called
+            mockedExitHandler.verify(() -> ExitHandler.exit(1), never());
+            mockedExitHandler.verify(() -> ExitHandler.halt(1), never());
+
+            assertFalse(errorStreamCaptor.toString().contains(EXITING));
+            
assertTrue(errorStreamCaptor.toString().contains(FORCED_TO_CONTINUE));
+        }
+    }
+
+    /** ExceptionHandler should never call ExitHandler.halt(1) when 
continueOnError property is not set */
+    @Test
+    void exceptionHandlerCallsExitHandlerExitOnFlagNotSet()
+    {
+        try (final MockedStatic<ExitHandler> mockedExitHandler = 
mockStatic(ExitHandler.class))
+        {
+            final Thread testThread = mock(Thread.class);
+            final Throwable testThrowable = new RuntimeException("Test 
Exception");
+
+            handler.uncaughtException(testThread, testThrowable);
+
+            // verify that ExitHandler.exit(1) or ExitHandler.exit(1) was 
never called
+            mockedExitHandler.verify(() -> ExitHandler.exit(1), never());
+            mockedExitHandler.verify(() -> ExitHandler.halt(1), never());
+
+            assertFalse(errorStreamCaptor.toString().contains(EXITING));
+            
assertTrue(errorStreamCaptor.toString().contains(FORCED_TO_CONTINUE));
+        }
+    }
+
+    /** ExceptionHandler should never call ExitHandler.halt() when 
continueOnError is true */
+    @Test
+    void exceptionHandlerCallsExitHandlerExitOnFlagTrue()
+    {
+        try (final MockedStatic<ExitHandler> mockedExitHandler = 
mockStatic(ExitHandler.class))
+        {
+            
System.setProperty(LogAndContinueExceptionHandler.CONTINUE_ON_ERROR_PROPERTY_NAME,
 "true");
+            final Thread testThread = mock(Thread.class);
+            final Throwable testThrowable = new RuntimeException("Test 
Exception");
+
+            handler.uncaughtException(testThread, testThrowable);
+
+            // verify that ExitHandler.exit() or ExitHandler.halt() was never 
called
+            mockedExitHandler.verify(() -> ExitHandler.exit(anyInt()), 
never());
+            mockedExitHandler.verify(() -> ExitHandler.halt(anyInt()), 
never());
+
+            assertFalse(errorStreamCaptor.toString().contains(EXITING));
+            
assertTrue(errorStreamCaptor.toString().contains(FORCED_TO_CONTINUE));
+        }
+    }
+
+    /** ExceptionHandler should call diagnostic and logging methods */
+    @Test
+    void exceptionHandlerCallsDiagnosticAndLoggingMethods()
+    {
+        final LogAndContinueExceptionHandler spyHandler = spy(new 
LogAndContinueExceptionHandler());
+        final Thread testThread = mock(Thread.class);
+        when(testThread.getName()).thenReturn("testThread");
+        final Throwable testThrowable = new RuntimeException("Test Exception");
+
+        try (final MockedStatic<ExitHandler> mockedExitHandler = 
mockStatic(ExitHandler.class))
+        {
+            spyHandler.uncaughtException(testThread, testThrowable);
+
+            // verify that printDiagnostic and logError were called with the 
correct parameters.
+            verify(spyHandler, times(1)).printDiagnostic(testThread, 
testThrowable, true);
+            verify(spyHandler, times(1)).logError(testThrowable, true);
+
+            // verify that ExitHandler.exit(1) or ExitHandler.halt(1) was 
never called
+            mockedExitHandler.verify(() -> ExitHandler.exit(1), never());
+            mockedExitHandler.verify(() -> ExitHandler.halt(1), never());
+
+            assertFalse(errorStreamCaptor.toString().contains(EXITING));
+            
assertTrue(errorStreamCaptor.toString().contains(FORCED_TO_CONTINUE));
+        }
+    }
+}
diff --git 
a/doc/java-broker/src/docbkx/Java-Broker-Appendix-System-Properties.xml 
b/doc/java-broker/src/docbkx/Java-Broker-Appendix-System-Properties.xml
index 3b47e68084..9702725a60 100644
--- a/doc/java-broker/src/docbkx/Java-Broker-Appendix-System-Properties.xml
+++ b/doc/java-broker/src/docbkx/Java-Broker-Appendix-System-Properties.xml
@@ -62,6 +62,36 @@
             <para>Feature names should be comma separated.</para>
           </entry>
         </row>
+        <row 
xml:id="Java-Broker-Appendix-System-Properties-Broker-Exception-Handler">
+          <entry>qpid.broker.exceptionHandler</entry>
+          <entry>org.apache.qpid.server.utils.FailFastExceptionHandler</entry>
+          <entry>
+            <para>Allows to set UncaughtExceptionHandler for the main broker 
thread. Out of box are provided
+              following options:
+            </para>
+            <itemizedlist>
+              <listitem>
+                
<para>org.apache.qpid.server.utils.FailFastExceptionHandler</para>
+              </listitem>
+              <listitem>
+                
<para>org.apache.qpid.server.utils.GracefulShutdownExceptionHandler</para>
+              </listitem>
+              <listitem>
+                
<para>org.apache.qpid.server.utils.LogAndContinueExceptionHandler</para>
+              </listitem>
+            </itemizedlist>
+          </entry>
+        </row>
+        <row 
xml:id="Java-Broker-Appendix-System-Properties-Broker-Exception-Handler-Continue">
+          <entry>qpid.broker.exceptionHandler.continue</entry>
+          <entry>false</entry>
+          <entry>
+            <para>When set to "true", FailFastExceptionHandler and 
GracefulShutdownExceptionHandler will only log
+              the uncaught exception without shutting the broker down. When 
set to "false" (default value),
+              FailFastExceptionHandler and GracefulShutdownExceptionHandler 
will log the exception and shut the broker down.
+            </para>
+          </entry>
+        </row>
       </tbody>
     </tgroup>
   </table>


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


Reply via email to