From dc9f81f979dcca0dcc8bd41969d233844985b4b8 Mon Sep 17 00:00:00 2001 From: Ramanathan Date: Sun, 10 May 2026 18:45:49 +0530 Subject: [PATCH 1/3] Improve handling of LinkageErrors during LMAX Disruptor initialization and add tests for error logging --- .../log4j/core/async/DisruptorUtilTest.java | 111 ++++++++++++++++++ .../log4j/core/async/DisruptorUtil.java | 27 ++++- .../async/RingBufferLogEventHandler4.java | 4 +- 3 files changed, 136 insertions(+), 6 deletions(-) create mode 100644 log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/DisruptorUtilTest.java diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/DisruptorUtilTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/DisruptorUtilTest.java new file mode 100644 index 00000000000..0ac45e75c71 --- /dev/null +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/DisruptorUtilTest.java @@ -0,0 +1,111 @@ +/* + * 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.logging.log4j.core.async; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.lmax.disruptor.ExceptionHandler; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.status.StatusData; +import org.apache.logging.log4j.test.ListStatusListener; +import org.apache.logging.log4j.test.junit.SetTestProperty; +import org.apache.logging.log4j.test.junit.UsingStatusListener; +import org.junit.jupiter.api.Test; + +@UsingStatusListener +class DisruptorUtilTest { + + private static final String LINKAGE_ERROR_MESSAGE = + "Log4j2 detected a linkage error while initializing LMAX Disruptor"; + + @Test + @SetTestProperty( + key = DisruptorUtil.LOGGER_EXCEPTION_HANDLER_PROPERTY, + value = "org.apache.logging.log4j.core.async.DisruptorUtilTest$BrokenAsyncLoggerExceptionHandler") + void getAsyncLoggerExceptionHandler_logsLinkageErrors(final ListStatusListener statusListener) { + final ExceptionHandler exceptionHandler = DisruptorUtil.getAsyncLoggerExceptionHandler(); + + assertThat(exceptionHandler).isInstanceOf(AsyncLoggerDefaultExceptionHandler.class); + + final List statusData = + statusListener.findStatusData(Level.ERROR).collect(Collectors.toList()); + assertThat(statusData).anySatisfy(data -> { + assertThat(data.getMessage().getFormattedMessage()).contains(LINKAGE_ERROR_MESSAGE); + assertThat(data.getThrowable()).isInstanceOf(NoClassDefFoundError.class); + assertThat(data.getThrowable().getMessage()).contains("broken AsyncLogger handler"); + }); + } + + @Test + @SetTestProperty( + key = DisruptorUtil.LOGGER_CONFIG_EXCEPTION_HANDLER_PROPERTY, + value = "org.apache.logging.log4j.core.async.DisruptorUtilTest$BrokenAsyncLoggerConfigExceptionHandler") + void getAsyncLoggerConfigExceptionHandler_logsLinkageErrors(final ListStatusListener statusListener) { + final ExceptionHandler exceptionHandler = + DisruptorUtil.getAsyncLoggerConfigExceptionHandler(); + + assertThat(exceptionHandler).isInstanceOf(AsyncLoggerConfigDefaultExceptionHandler.class); + + final List statusData = + statusListener.findStatusData(Level.ERROR).collect(Collectors.toList()); + assertThat(statusData).anySatisfy(data -> { + assertThat(data.getMessage().getFormattedMessage()).contains(LINKAGE_ERROR_MESSAGE); + assertThat(data.getThrowable()).isInstanceOf(NoClassDefFoundError.class); + assertThat(data.getThrowable().getMessage()).contains("broken AsyncLoggerConfig handler"); + }); + } + + public static class BrokenAsyncLoggerExceptionHandler implements ExceptionHandler { + + static { + if (System.nanoTime() >= 0) { + throw new NoClassDefFoundError("broken AsyncLogger handler"); + } + } + + @Override + public void handleEventException(final Throwable ex, final long sequence, final RingBufferLogEvent event) {} + + @Override + public void handleOnStartException(final Throwable ex) {} + + @Override + public void handleOnShutdownException(final Throwable ex) {} + } + + public static class BrokenAsyncLoggerConfigExceptionHandler + implements ExceptionHandler { + + static { + if (System.nanoTime() >= 0) { + throw new NoClassDefFoundError("broken AsyncLoggerConfig handler"); + } + } + + @Override + public void handleEventException( + final Throwable ex, final long sequence, final AsyncLoggerConfigDisruptor.Log4jEventWrapper event) {} + + @Override + public void handleOnStartException(final Throwable ex) {} + + @Override + public void handleOnShutdownException(final Throwable ex) {} + } +} 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 f115e2ae378..676b7474576 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 @@ -32,6 +32,9 @@ */ final class DisruptorUtil { private static final Logger LOGGER = StatusLogger.getLogger(); + private static final String LINKAGE_ERROR_MESSAGE = + "Log4j2 detected a linkage error while initializing LMAX Disruptor. " + + "Please check your classpath and ensure Disruptor version is compatible."; private static final int RINGBUFFER_MIN_SIZE = 128; private static final int RINGBUFFER_DEFAULT_SIZE = 256 * 1024; private static final int RINGBUFFER_NO_GC_DEFAULT_SIZE = 4 * 1024; @@ -52,14 +55,13 @@ final class DisruptorUtil { 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() { + // Avoid using `LoaderUtil`, which might choose an incorrect class loader (e.g. TCCL in OSGi) – see #2768. try { Class.forName( - "com.lmax.disruptor.SequenceReportingEventHandler", true, DisruptorUtil.class.getClassLoader()); + "com.lmax.disruptor.SequenceReportingEventHandler", false, DisruptorUtil.class.getClassLoader()); return 3; - } catch (final ClassNotFoundException e) { + } catch (final ClassNotFoundException | LinkageError ignored) { return 4; } } @@ -77,7 +79,12 @@ static WaitStrategy createWaitStrategy( LOGGER.debug( "Using configured AsyncWaitStrategyFactory {}", asyncWaitStrategyFactory.getClass().getName()); - return asyncWaitStrategyFactory.createWaitStrategy(); + try { + return asyncWaitStrategyFactory.createWaitStrategy(); + } catch (final LinkageError e) { + logDisruptorLinkageError("while creating the async wait strategy", e); + return new DefaultAsyncWaitStrategyFactory(propertyName).createWaitStrategy(); + } } static int calculateRingBufferSize(final String propertyName) { @@ -105,6 +112,9 @@ static ExceptionHandler getAsyncLoggerExceptionHandler() { } catch (final ReflectiveOperationException e) { LOGGER.debug("Invalid AsyncLogger.ExceptionHandler value: {}", e.getMessage(), e); return new AsyncLoggerDefaultExceptionHandler(); + } catch (final LinkageError e) { + logDisruptorLinkageError("while creating AsyncLogger exception handler", e); + return new AsyncLoggerDefaultExceptionHandler(); } } @@ -117,9 +127,16 @@ static ExceptionHandler getAsyncLo } catch (final ReflectiveOperationException e) { LOGGER.debug("Invalid AsyncLogger.ExceptionHandler value: {}", e.getMessage(), e); return new AsyncLoggerConfigDefaultExceptionHandler(); + } catch (final LinkageError e) { + logDisruptorLinkageError("while creating AsyncLoggerConfig exception handler", e); + return new AsyncLoggerConfigDefaultExceptionHandler(); } } + private static void logDisruptorLinkageError(final String phase, final LinkageError error) { + LOGGER.error("{} ({})", LINKAGE_ERROR_MESSAGE, phase, error); + } + /** * Returns the thread ID of the background appender thread. This allows us to detect Logger.log() calls initiated * from the appender thread, which may cause deadlock when the RingBuffer is full. (LOG4J2-471) diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEventHandler4.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEventHandler4.java index 6cb1e686126..321afb719db 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEventHandler4.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEventHandler4.java @@ -64,7 +64,9 @@ public void onEvent(final RingBufferLogEvent event, final long sequence, final b private void notifyCallback(final long sequence) { if (++counter > NOTIFY_PROGRESS_THRESHOLD) { - sequenceCallback.set(sequence); + if (sequenceCallback != null) { + sequenceCallback.set(sequence); + } counter = 0; } } From 08932384ce53e75a79ce89637cad8230e102b674 Mon Sep 17 00:00:00 2001 From: Ramanathan Date: Wed, 20 May 2026 23:24:22 +0530 Subject: [PATCH 2/3] Refactor LMAX Disruptor initialization to improve error handling and logging --- .../log4j/core/async/DisruptorUtilTest.java | 85 +++---------------- .../log4j/core/async/DisruptorUtil.java | 28 ++---- .../async/RingBufferLogEventHandler4.java | 4 +- 3 files changed, 18 insertions(+), 99 deletions(-) diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/DisruptorUtilTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/DisruptorUtilTest.java index 0ac45e75c71..06b65855596 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/DisruptorUtilTest.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/DisruptorUtilTest.java @@ -18,94 +18,29 @@ import static org.assertj.core.api.Assertions.assertThat; -import com.lmax.disruptor.ExceptionHandler; +import java.lang.reflect.Method; import java.util.List; import java.util.stream.Collectors; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.status.StatusData; import org.apache.logging.log4j.test.ListStatusListener; -import org.apache.logging.log4j.test.junit.SetTestProperty; import org.apache.logging.log4j.test.junit.UsingStatusListener; import org.junit.jupiter.api.Test; @UsingStatusListener class DisruptorUtilTest { - private static final String LINKAGE_ERROR_MESSAGE = - "Log4j2 detected a linkage error while initializing LMAX Disruptor"; - @Test - @SetTestProperty( - key = DisruptorUtil.LOGGER_EXCEPTION_HANDLER_PROPERTY, - value = "org.apache.logging.log4j.core.async.DisruptorUtilTest$BrokenAsyncLoggerExceptionHandler") - void getAsyncLoggerExceptionHandler_logsLinkageErrors(final ListStatusListener statusListener) { - final ExceptionHandler exceptionHandler = DisruptorUtil.getAsyncLoggerExceptionHandler(); - - assertThat(exceptionHandler).isInstanceOf(AsyncLoggerDefaultExceptionHandler.class); - - final List statusData = - statusListener.findStatusData(Level.ERROR).collect(Collectors.toList()); - assertThat(statusData).anySatisfy(data -> { - assertThat(data.getMessage().getFormattedMessage()).contains(LINKAGE_ERROR_MESSAGE); - assertThat(data.getThrowable()).isInstanceOf(NoClassDefFoundError.class); - assertThat(data.getThrowable().getMessage()).contains("broken AsyncLogger handler"); - }); - } - - @Test - @SetTestProperty( - key = DisruptorUtil.LOGGER_CONFIG_EXCEPTION_HANDLER_PROPERTY, - value = "org.apache.logging.log4j.core.async.DisruptorUtilTest$BrokenAsyncLoggerConfigExceptionHandler") - void getAsyncLoggerConfigExceptionHandler_logsLinkageErrors(final ListStatusListener statusListener) { - final ExceptionHandler exceptionHandler = - DisruptorUtil.getAsyncLoggerConfigExceptionHandler(); - - assertThat(exceptionHandler).isInstanceOf(AsyncLoggerConfigDefaultExceptionHandler.class); - - final List statusData = - statusListener.findStatusData(Level.ERROR).collect(Collectors.toList()); - assertThat(statusData).anySatisfy(data -> { - assertThat(data.getMessage().getFormattedMessage()).contains(LINKAGE_ERROR_MESSAGE); - assertThat(data.getThrowable()).isInstanceOf(NoClassDefFoundError.class); - assertThat(data.getThrowable().getMessage()).contains("broken AsyncLoggerConfig handler"); - }); - } - - public static class BrokenAsyncLoggerExceptionHandler implements ExceptionHandler { - - static { - if (System.nanoTime() >= 0) { - throw new NoClassDefFoundError("broken AsyncLogger handler"); - } - } - - @Override - public void handleEventException(final Throwable ex, final long sequence, final RingBufferLogEvent event) {} - - @Override - public void handleOnStartException(final Throwable ex) {} - - @Override - public void handleOnShutdownException(final Throwable ex) {} - } - - public static class BrokenAsyncLoggerConfigExceptionHandler - implements ExceptionHandler { - - static { - if (System.nanoTime() >= 0) { - throw new NoClassDefFoundError("broken AsyncLoggerConfig handler"); - } - } - - @Override - public void handleEventException( - final Throwable ex, final long sequence, final AsyncLoggerConfigDisruptor.Log4jEventWrapper event) {} + void detectDisruptorMajorVersion_logsVersionDetection(final ListStatusListener statusListener) throws Exception { + final Method method = DisruptorUtil.class.getDeclaredMethod("detectDisruptorMajorVersion"); + method.setAccessible(true); + final int detectedVersion = (int) method.invoke(null); - @Override - public void handleOnStartException(final Throwable ex) {} + assertThat(detectedVersion).isIn(3, 4); - @Override - public void handleOnShutdownException(final Throwable ex) {} + final List debugData = + statusListener.findStatusData(Level.DEBUG).collect(Collectors.toList()); + assertThat(debugData).anySatisfy(data -> assertThat(data.getMessage().getFormattedMessage()) + .contains("LMAX Disruptor version " + detectedVersion + " detected.")); } } 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 676b7474576..59a5644a4e0 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 @@ -32,9 +32,6 @@ */ final class DisruptorUtil { private static final Logger LOGGER = StatusLogger.getLogger(); - private static final String LINKAGE_ERROR_MESSAGE = - "Log4j2 detected a linkage error while initializing LMAX Disruptor. " - + "Please check your classpath and ensure Disruptor version is compatible."; private static final int RINGBUFFER_MIN_SIZE = 128; private static final int RINGBUFFER_DEFAULT_SIZE = 256 * 1024; private static final int RINGBUFFER_NO_GC_DEFAULT_SIZE = 4 * 1024; @@ -55,13 +52,17 @@ final class DisruptorUtil { 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() { - // Avoid using `LoaderUtil`, which might choose an incorrect class loader (e.g. TCCL in OSGi) – see #2768. + LOGGER.trace("Detecting LMAX Disruptor version from classpath."); try { Class.forName( "com.lmax.disruptor.SequenceReportingEventHandler", false, DisruptorUtil.class.getClassLoader()); + LOGGER.debug("LMAX Disruptor version {} detected.", 3); return 3; - } catch (final ClassNotFoundException | LinkageError ignored) { + } catch (final ClassNotFoundException e) { + LOGGER.debug("LMAX Disruptor version {} detected.", 4); return 4; } } @@ -79,12 +80,7 @@ static WaitStrategy createWaitStrategy( LOGGER.debug( "Using configured AsyncWaitStrategyFactory {}", asyncWaitStrategyFactory.getClass().getName()); - try { - return asyncWaitStrategyFactory.createWaitStrategy(); - } catch (final LinkageError e) { - logDisruptorLinkageError("while creating the async wait strategy", e); - return new DefaultAsyncWaitStrategyFactory(propertyName).createWaitStrategy(); - } + return asyncWaitStrategyFactory.createWaitStrategy(); } static int calculateRingBufferSize(final String propertyName) { @@ -112,9 +108,6 @@ static ExceptionHandler getAsyncLoggerExceptionHandler() { } catch (final ReflectiveOperationException e) { LOGGER.debug("Invalid AsyncLogger.ExceptionHandler value: {}", e.getMessage(), e); return new AsyncLoggerDefaultExceptionHandler(); - } catch (final LinkageError e) { - logDisruptorLinkageError("while creating AsyncLogger exception handler", e); - return new AsyncLoggerDefaultExceptionHandler(); } } @@ -127,16 +120,9 @@ static ExceptionHandler getAsyncLo } catch (final ReflectiveOperationException e) { LOGGER.debug("Invalid AsyncLogger.ExceptionHandler value: {}", e.getMessage(), e); return new AsyncLoggerConfigDefaultExceptionHandler(); - } catch (final LinkageError e) { - logDisruptorLinkageError("while creating AsyncLoggerConfig exception handler", e); - return new AsyncLoggerConfigDefaultExceptionHandler(); } } - private static void logDisruptorLinkageError(final String phase, final LinkageError error) { - LOGGER.error("{} ({})", LINKAGE_ERROR_MESSAGE, phase, error); - } - /** * Returns the thread ID of the background appender thread. This allows us to detect Logger.log() calls initiated * from the appender thread, which may cause deadlock when the RingBuffer is full. (LOG4J2-471) diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEventHandler4.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEventHandler4.java index 321afb719db..6cb1e686126 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEventHandler4.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEventHandler4.java @@ -64,9 +64,7 @@ public void onEvent(final RingBufferLogEvent event, final long sequence, final b private void notifyCallback(final long sequence) { if (++counter > NOTIFY_PROGRESS_THRESHOLD) { - if (sequenceCallback != null) { - sequenceCallback.set(sequence); - } + sequenceCallback.set(sequence); counter = 0; } } From 3548108c1211106683e8a8399976fc5f63f23168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Volkan=20Yaz=C4=B1c=C4=B1?= Date: Thu, 21 May 2026 15:09:29 +0200 Subject: [PATCH 3/3] Simplify logging --- .../logging/log4j/core/async/DisruptorUtilTest.java | 2 +- .../logging/log4j/core/async/DisruptorUtil.java | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/DisruptorUtilTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/DisruptorUtilTest.java index 06b65855596..634021189cc 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/DisruptorUtilTest.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/DisruptorUtilTest.java @@ -41,6 +41,6 @@ void detectDisruptorMajorVersion_logsVersionDetection(final ListStatusListener s final List debugData = statusListener.findStatusData(Level.DEBUG).collect(Collectors.toList()); assertThat(debugData).anySatisfy(data -> assertThat(data.getMessage().getFormattedMessage()) - .contains("LMAX Disruptor version " + detectedVersion + " detected.")); + .contains("LMAX Disruptor version detected: " + detectedVersion)); } } 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 59a5644a4e0..2b39141bbd8 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 @@ -55,16 +55,17 @@ final class DisruptorUtil { // TODO: replace with LoaderUtil.isClassAvailable() when TCCL is removed // See: https://github.com/apache/logging-log4j2/issues/3706 private static int detectDisruptorMajorVersion() { - LOGGER.trace("Detecting LMAX Disruptor version from classpath."); + final int version; try { Class.forName( "com.lmax.disruptor.SequenceReportingEventHandler", false, DisruptorUtil.class.getClassLoader()); - LOGGER.debug("LMAX Disruptor version {} detected.", 3); + version = 3; return 3; - } catch (final ClassNotFoundException e) { - LOGGER.debug("LMAX Disruptor version {} detected.", 4); - return 4; + } catch (final ClassNotFoundException ignored) { + version = 4; } + LOGGER.debug("LMAX Disruptor version detected: {}", version); + return version; } private DisruptorUtil() {}