Async loggers silently discard events when the ring buffer is full and LMAX Disruptor 4 is used
Maintainer thường phản hồi trong vòng 1 ngày
@ramanathan1504 đang làm issue này rồi.
Từ ngày 26/9/2026.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 70/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- java
- Lĩnh vực
- backend, observability, performance
Hướng nghiên cứu
Start with AsyncLoggerDisruptor.java and its getEventRoute() and remainingDisruptorCapacity() entry points, then compare the corresponding logic in AsyncLoggerConfigDisruptor. Run the supplied Log4jDisruptor4Loss.java reproducer with Disruptor 4.0.0 and inspect behavior under a full ring buffer. Done means the queue-full policy is reached during saturation and the reproducer retains all expected events for both async logger paths.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Title: Async loggers silently discard events when the ring buffer is full and LMAX Disruptor 4 is used
Description
When AsyncLoggerContextSelector is combined with LMAX Disruptor 4.0.0, log events are dropped without any trace once the ring buffer runs full. In our server, a burst of warnings at start-up (about 180k events in under 2 s from 16 threads) made single lines of unrelated threads and appenders disappear. The standalone reproducer below loses 56–82% of its events. Nothing is written to the StatusLogger (tested with status="WARN") or to stderr.
Cause
Disruptor 4.0.0 changed MultiProducerSequencer.next() to claim a sequence first and wait for free capacity afterwards:
long current = cursor.getAndAdd(n); // claims before capacity is available
long nextSequence = current + n;
long wrapPoint = nextSequence - bufferSize;
...
while (wrapPoint > (gatingSequence = Util.getMinimumSequence(gatingSequences, current))) {
LockSupport.parkNanos(1L);
}
While one thread waits in that loop, the cursor is ahead of the consumer by more than the buffer size. remainingCapacity() (bufferSize - (produced - consumed)) therefore returns a negative value, typically -1. Disruptor 3.x next() only advances the cursor with a CAS after capacity is available, so this never happens there.
AsyncLoggerDisruptor.getEventRoute() treats any negative capacity as "log4j has been shut down":
EventRoute getEventRoute(final Level logLevel) {
final int remainingCapacity = remainingDisruptorCapacity();
if (remainingCapacity < 0) {
return EventRoute.DISCARD;
}
return asyncQueueFullPolicy.getRoute(backgroundThreadId, logLevel);
}
remainingDisruptorCapacity() returns -1 only when disruptor == null, but it also passes through negative values from RingBuffer.remainingCapacity(). The sequence when the ring buffer is full:
- Thread A's
tryPublishfails,getEventRoute()returns ENQUEUE, and A entersenqueueLogMessageWhenQueueFull→publishEvent→next(). There it claims a slot beyond the buffer and parks. - Every other thread whose
tryPublishfails now seesremainingCapacity() == -1. It getsDISCARD, andhandleRingBufferFullclears the translator without a message.
Under sustained saturation effectively only the thread holding queueFullEnqueueLock gets its events in; all others lose theirs. The configured AsyncQueueFullPolicy is never consulted, because the DISCARD decision is made before it. log4j2.asyncLoggerSynchronizeEnqueueWhenQueueFull=false makes it worse, since more threads wait inside next() at the same time.
AsyncLoggerConfigDisruptor.getEventRoute() has the same check, so mixed async loggers (<AsyncLogger>) should be affected too; we have only tested AsyncLoggerContextSelector. The check is unchanged on the current 2.x branch.
A much rarer variant exists with Disruptor 3.x as well. remainingCapacity() reads consumed and produced separately. If the consumer advances between the two reads and producers immediately claim the freed slots, the result is briefly negative. We observed -51, one progress step of RingBufferLogEventHandler, when polling it while the buffer was full. We never saw an event lost this way, but the same check would discard it.
Suggested fix
Check for shutdown explicitly instead of inferring it from the capacity. In AsyncLoggerDisruptor (and likewise in AsyncLoggerConfigDisruptor):
EventRoute getEventRoute(final Level logLevel) {
if (hasLog4jBeenShutDown(disruptor)) {
return EventRoute.DISCARD;
}
return asyncQueueFullPolicy.getRoute(backgroundThreadId, logLevel);
}
With this change the reproducer below loses no events with Disruptor 4.0.0 (3 of 3 runs, 800,000 of 800,000 lines).
Configuration
Version: log4j 2.25.3. 2.24.3 and 2.25.2 behave the same with Disruptor 4.0.0. Disruptor 3.4.1 does not show the loss with any of them.
Operating system: Windows 11, 8 logical CPUs
JDK: Eclipse Temurin 25.0.3+9
Logs
Nothing is logged, and that is the core of the problem. With status="WARN", the StatusLogger stays silent and nothing appears on stderr.
Reproduction
Single-file program, run with Java 17+ in source-file mode. It uses the default ring buffer size, which is 4096 in garbage-free mode:
java -cp log4j-api-2.25.3.jar:log4j-core-2.25.3.jar:disruptor-4.0.0.jar Log4jDisruptor4Loss.java
Log4jDisruptor4Loss.java
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Log4jDisruptor4Loss {
static final int THREADS = 16;
static final int LINES = 50_000;
public static void main(String[] args) throws Exception {
Path dir = Files.createTempDirectory("log4j-loss");
Path log = dir.resolve("test.log");
Path config = dir.resolve("log4j2.xml");
Files.writeString(config, """
<Configuration status="WARN">
<Appenders>
<File name="file" fileName="%s">
<PatternLayout pattern="%%d %%-5p [%%t] %%c - %%m%%n"/>
</File>
</Appenders>
<Loggers>
<Root level="INFO"><AppenderRef ref="file"/></Root>
</Loggers>
</Configuration>
""".formatted(log.toAbsolutePath().toString().replace('\\', '/')));
System.setProperty("log4j2.contextSelector", "org.apache.logging.log4j.core.async.AsyncLoggerContextSelector");
System.setProperty("log4j2.configurationFile", config.toUri().toString());
Logger logger = LogManager.getLogger("test");
CountDownLatch start = new CountDownLatch(1);
List<Thread> threads = new ArrayList<>();
for (int t = 0; t < THREADS; t++) {
int thread = t;
Thread th = new Thread(() -> {
try {
start.await();
} catch (InterruptedException e) {
return;
}
for (int i = 0; i < LINES; i++) {
logger.info("LINE {} {} padding padding padding padding padding padding padding padding", thread, i);
}
}, "producer-" + t);
th.start();
threads.add(th);
}
long startNanos = System.nanoTime();
start.countDown();
for (Thread th : threads) {
th.join();
}
long producedMs = (System.nanoTime() - startNanos) / 1_000_000;
// stops the disruptor after draining it and closes the file
LogManager.shutdown();
BitSet[] seen = new BitSet[THREADS];
for (int t = 0; t < THREADS; t++) {
seen[t] = new BitSet(LINES);
}
for (String line : Files.readAllLines(log, StandardCharsets.UTF_8)) {
int at = line.indexOf(" - LINE ");
if (at < 0) {
continue;
}
String[] fields = line.substring(at + 8).split(" ", 3);
seen[Integer.parseInt(fields[0])].set(Integer.parseInt(fields[1]));
}
long expected = (long) THREADS * LINES;
long found = 0;
for (BitSet bits : seen) {
found += bits.cardinality();
}
String disruptor = com.lmax.disruptor.RingBuffer.class.getProtectionDomain().getCodeSource().getLocation().getPath();
System.out.printf("Disruptor %s: %d threads logged %d lines in %d ms, %d in the file, %d missing (%.1f%%)%n",
disruptor.substring(disruptor.lastIndexOf('/') + 1), THREADS, expected, producedMs, found,
expected - found, 100.0 * (expected - found) / expected);
}
}
Results, 3 runs each:
| Setup | Missing lines |
|---|---|
| log4j 2.25.3 + Disruptor 4.0.0 | 81.7%, 56.4%, 68.1% |
| log4j 2.25.3 + Disruptor 3.4.1 | 0, 0, 0 |
| log4j 2.25.3 + Disruptor 4.0.0 + suggested fix | 0, 0, 0 |
In a larger test with 16 flooding threads plus low-rate probe threads, repeated rounds and several appenders, we saw the same picture:
- Disruptor 4.0.0 lost 50–97% with log4j 2.24.3, 2.25.2 and 2.25.3.
- Disruptor 3.4.1 lost nothing in about 30 million lines under constant saturation.
- A ring buffer large enough never to fill (4M) also avoids the loss, but only because the queue-full path is never taken.
- Ngôn ngữ chính
- Java
- Star
- 3.6k
- Fork
- 1.8k
- Merge trung bình
- 5 ngày 6 giờ
- Pull request đã merge (30 ngày)
- 24
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Không có hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của apache/logging-log4j2
-
additivity attributeĐang mởwaiting-for-maintainer
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 63/100
apache/logging-log4j2#4475 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Migrate `log4j-api-test` tests to JUnit 5Có thể làm lại được Pull request cho issue này đã bị đóng mà không được merge. Đang mởapi priority: P3 tests waiting-for-maintainer
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
apache/logging-log4j2#4430 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
`Rfc5424Layout` does not escape the `newLineEscape` replacement in SD-PARAM-VALUECó thể đã có người làm @lunarbae628 đã nhận 5 ngày trước. Đang mởbug layouts priority: P3
apache/logging-log4j2#4464 · 2 bình luận · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
-
configuration enhancement layouts
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 55/100
apache/logging-log4j2#4463 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
`evaluateOnStartup="true"` saves logs under wrong file names after missing scheduled rollCó thể làm lại được Pull request cho issue này đã bị đóng mà không được merge. Đang mởappenders:Rolling bug priority: P1 waiting-for-maintainer
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 30/100
apache/logging-log4j2#4449 ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của apache/logging-log4j2
Issue tương tự
-
[Bug]: Nullable implicit operator on string extensible enums still throws for a null stringCó thể đã có người làm Có pull request liên kết đang mở hoặc đã được merge. Đang mởbug emitter:client:csharp
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
microsoft/typespec#12572 · 1 bình luận · 1 reaction ·
Maintainer thường phản hồi trong vòng 1 ngày
-
[BUG]: Java SDK MountRequest always sends update-optional fields, resetting stored mount settingsĐang mởbug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100
Maintainer thường phản hồi trong vòng 1 ngày
-
GCC 15.2: -Wcast-qual warning - cast discards const qualifier from pointer target type (H5trace.c)Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 73/100
Maintainer thường phản hồi trong vòng 1 ngày
-
[Bug] RemotingHelper.connect swaps the socket send and receive buffer configurationCó thể đã có người làm @amalsaidov đã nhận hôm nay. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
Maintainer thường phản hồi trong vòng 1 ngày
-
needs-area-label
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Maintainer thường phản hồi trong vòng 2 ngày