From 50a86c710a7e2c6b03eccea74886fd973c692ede Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Wed, 25 Jun 2025 09:56:41 +0200 Subject: [PATCH 1/2] 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. --- log4j-osgi-test/pom.xml | 2 + .../log4j/osgi/tests/DisruptorTest.java | 100 +++++++++++++++--- 2 files changed, 87 insertions(+), 15 deletions(-) diff --git a/log4j-osgi-test/pom.xml b/log4j-osgi-test/pom.xml index b63666715e0..9517f3385c9 100644 --- a/log4j-osgi-test/pom.xml +++ b/log4j-osgi-test/pom.xml @@ -257,6 +257,7 @@ org.apache.logging.log4j.osgi.tests.DisruptorTest + org.apache.logging.log4j.osgi.tests.DisruptorTest$TestExceptionHandler org.apache.logging.log4j.core.async.BasicAsyncLoggerContextSelector @@ -291,6 +292,7 @@ org.apache.logging.log4j.osgi.tests.DisruptorTest + org.apache.logging.log4j.osgi.tests.DisruptorTest$TestExceptionHandler org.apache.logging.log4j.core.async.BasicAsyncLoggerContextSelector 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 45ce5fef61c..813331210c5 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 @@ @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 Option[] config() { } @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 @@ private static LoggerContext getLoggerContext() { assertEquals("AsyncDefault", ctx.getName()); return ctx; } + + public static class TestExceptionHandler implements ExceptionHandler { + + private static final AtomicReference 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); + } + } } From eff296eb591eae0f16c460f9db92d846559c94b6 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Wed, 25 Jun 2025 10:00:41 +0200 Subject: [PATCH 2/2] 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 +++++++++++++-- src/changelog/.2.x.x/3706_disruptor-tccl.xml | 12 ++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 src/changelog/.2.x.x/3706_disruptor-tccl.xml 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 bdd21bd4ef2..f115e2ae378 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/src/changelog/.2.x.x/3706_disruptor-tccl.xml b/src/changelog/.2.x.x/3706_disruptor-tccl.xml new file mode 100644 index 00000000000..a6a446fb14f --- /dev/null +++ b/src/changelog/.2.x.x/3706_disruptor-tccl.xml @@ -0,0 +1,12 @@ + + + + + Fix detection of the Disruptor major version in some environments. + +