This is an automated email from the ASF dual-hosted git repository. pkarwasz pushed a commit to branch 2.25.x in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
commit 600cfe2b83a0e0faa2a47767374fdb26986a061d Author: Piotr P. Karwasz <[email protected]> AuthorDate: Sat Jun 28 09:14:25 2025 +0200 fix: correctly detect Disruptor major version (#3778) ### fix(test): fail `DisruptorTest` on async thread exceptions Ensure that DisruptorTest explicitly fails when an exception occurs on an asynchronous thread. This improves error detection and prevents silent test passes in the presence of async failures. ### fix: correctly detect Disruptor major version Ensure the Disruptor version is detected using the classloader that loaded `DisruptorUtil`, rather than the thread-context classloader. The previous implementation relied on `LoaderUtil.isClassAvailable`, which may fail in environments where the Disruptor classes aren't visible to the thread-context classloader. --- .../logging/log4j/core/async/DisruptorUtil.java | 15 +++- log4j-osgi-test/pom.xml | 2 + .../logging/log4j/osgi/tests/DisruptorTest.java | 100 +++++++++++++++++---- src/changelog/.2.x.x/3706_disruptor-tccl.xml | 12 +++ 4 files changed, 112 insertions(+), 17 deletions(-) diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/DisruptorUtil.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/DisruptorUtil.java index bdd21bd4ef..f115e2ae37 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/DisruptorUtil.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/DisruptorUtil.java @@ -50,8 +50,19 @@ final class DisruptorUtil { static final boolean ASYNC_CONFIG_SYNCHRONIZE_ENQUEUE_WHEN_QUEUE_FULL = PropertiesUtil.getProperties() .getBooleanProperty("AsyncLoggerConfig.SynchronizeEnqueueWhenQueueFull", true); - static final int DISRUPTOR_MAJOR_VERSION = - LoaderUtil.isClassAvailable("com.lmax.disruptor.SequenceReportingEventHandler") ? 3 : 4; + static final int DISRUPTOR_MAJOR_VERSION = detectDisruptorMajorVersion(); + + // TODO: replace with LoaderUtil.isClassAvailable() when TCCL is removed + // See: https://github.com/apache/logging-log4j2/issues/3706 + private static int detectDisruptorMajorVersion() { + try { + Class.forName( + "com.lmax.disruptor.SequenceReportingEventHandler", true, DisruptorUtil.class.getClassLoader()); + return 3; + } catch (final ClassNotFoundException e) { + return 4; + } + } private DisruptorUtil() {} diff --git a/log4j-osgi-test/pom.xml b/log4j-osgi-test/pom.xml index b63666715e..9517f3385c 100644 --- a/log4j-osgi-test/pom.xml +++ b/log4j-osgi-test/pom.xml @@ -257,6 +257,7 @@ <include>org.apache.logging.log4j.osgi.tests.DisruptorTest</include> </includes> <systemPropertyVariables> + <log4j2.asyncLoggerExceptionHandler>org.apache.logging.log4j.osgi.tests.DisruptorTest$TestExceptionHandler</log4j2.asyncLoggerExceptionHandler> <log4j2.contextSelector>org.apache.logging.log4j.core.async.BasicAsyncLoggerContextSelector</log4j2.contextSelector> </systemPropertyVariables> </configuration> @@ -291,6 +292,7 @@ <include>org.apache.logging.log4j.osgi.tests.DisruptorTest</include> </includes> <systemPropertyVariables> + <log4j2.asyncLoggerExceptionHandler>org.apache.logging.log4j.osgi.tests.DisruptorTest$TestExceptionHandler</log4j2.asyncLoggerExceptionHandler> <log4j2.contextSelector>org.apache.logging.log4j.core.async.BasicAsyncLoggerContextSelector</log4j2.contextSelector> </systemPropertyVariables> </configuration> diff --git a/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/DisruptorTest.java b/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/DisruptorTest.java index 45ce5fef61..813331210c 100644 --- a/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/DisruptorTest.java +++ b/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/DisruptorTest.java @@ -17,19 +17,26 @@ package org.apache.logging.log4j.osgi.tests; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.ops4j.pax.exam.CoreOptions.junitBundles; import static org.ops4j.pax.exam.CoreOptions.linkBundle; import static org.ops4j.pax.exam.CoreOptions.options; +import com.lmax.disruptor.ExceptionHandler; +import java.io.IOException; +import java.net.URL; +import java.util.concurrent.atomic.AtomicReference; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.async.AsyncLoggerContext; +import org.apache.logging.log4j.core.async.RingBufferLogEvent; import org.junit.Test; import org.junit.runner.RunWith; +import org.junitpioneer.jupiter.Issue; import org.ops4j.pax.exam.Option; import org.ops4j.pax.exam.junit.PaxExam; import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; @@ -39,6 +46,8 @@ import org.ops4j.pax.exam.spi.reactors.PerClass; @ExamReactorStrategy(PerClass.class) public class DisruptorTest { + private static final int MESSAGE_COUNT = 128; + @org.ops4j.pax.exam.Configuration public Option[] config() { return options( @@ -59,21 +68,58 @@ public class DisruptorTest { } @Test - public void testDisruptorLog() { - // Logger context - LoggerContext context = getLoggerContext(); - assertTrue("LoggerContext is an instance of AsyncLoggerContext", context instanceof AsyncLoggerContext); - final CustomConfiguration custom = (CustomConfiguration) context.getConfiguration(); - // Logging - final Logger logger = LogManager.getLogger(getClass()); - logger.info("Hello OSGI from Log4j2!"); - - context.stop(); - assertEquals(1, custom.getEvents().size()); - final LogEvent event = custom.getEvents().get(0); - assertEquals("Hello OSGI from Log4j2!", event.getMessage().getFormattedMessage()); - assertEquals(Level.INFO, event.getLevel()); - custom.clearEvents(); + @Issue("https://github.com/apache/logging-log4j2/issues/3706") + public void testDisruptorLog() throws IOException { + ClassLoader threadContextClassLoader = Thread.currentThread().getContextClassLoader(); + ClassLoader classLoader = createClassLoader(); + try { + // Set the context classloader to an empty classloader, so attempts to use the TCCL will not find any + // classes. + Thread.currentThread().setContextClassLoader(classLoader); + // Logger context + LoggerContext context = getLoggerContext(); + assertTrue("LoggerContext is an instance of AsyncLoggerContext", context instanceof AsyncLoggerContext); + final CustomConfiguration custom = (CustomConfiguration) context.getConfiguration(); + // Logging + final Logger logger = LogManager.getLogger(getClass()); + for (int i = 0; i < MESSAGE_COUNT; i++) { + logger.info("Hello OSGI from Log4j2! {}", i); + } + + context.stop(); + assertEquals(MESSAGE_COUNT, custom.getEvents().size()); + for (int i = 0; i < MESSAGE_COUNT; i++) { + final LogEvent event = custom.getEvents().get(i); + assertEquals( + "Message nr " + i, + "Hello OSGI from Log4j2! " + i, + event.getMessage().getFormattedMessage()); + assertEquals(Level.INFO, event.getLevel()); + } + custom.clearEvents(); + assertNull("Asynchronous exception", TestExceptionHandler.exception.get()); + } finally { + Thread.currentThread().setContextClassLoader(threadContextClassLoader); + } + } + + private static ClassLoader createClassLoader() { + // We want a classloader capable only of loading TestExceptionHandler. + // This is needed to detect exceptions thrown by the asynchronous thread. + return new ClassLoader() { + @Override + public Class<?> loadClass(String name) throws ClassNotFoundException { + if (name.equals(TestExceptionHandler.class.getName())) { + return TestExceptionHandler.class; + } + throw new ClassNotFoundException(name); + } + + @Override + public URL getResource(String name) { + return null; // No resources available. + } + }; } private static LoggerContext getLoggerContext() { @@ -81,4 +127,28 @@ public class DisruptorTest { assertEquals("AsyncDefault", ctx.getName()); return ctx; } + + public static class TestExceptionHandler implements ExceptionHandler<RingBufferLogEvent> { + + private static final AtomicReference<Throwable> exception = new AtomicReference<>(); + + @Override + public void handleEventException(Throwable ex, long sequence, RingBufferLogEvent event) { + setException(ex); + } + + @Override + public void handleOnStartException(Throwable ex) { + setException(ex); + } + + @Override + public void handleOnShutdownException(Throwable ex) { + setException(ex); + } + + private static void setException(Throwable ex) { + exception.compareAndSet(null, ex); + } + } } diff --git a/src/changelog/.2.x.x/3706_disruptor-tccl.xml b/src/changelog/.2.x.x/3706_disruptor-tccl.xml new file mode 100644 index 0000000000..a6a446fb14 --- /dev/null +++ b/src/changelog/.2.x.x/3706_disruptor-tccl.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<entry xmlns="https://logging.apache.org/xml/ns" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation=" + https://logging.apache.org/xml/ns + https://logging.apache.org/xml/ns/log4j-changelog-0.xsd" + type="fixed"> + <issue id="3706" link="https://github.com/apache/logging-log4j2/issues/3706"/> + <description format="asciidoc"> + Fix detection of the Disruptor major version in some environments. + </description> +</entry>
