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]