Skip to content

Commit 8b964a0

Browse files
committed
Expunge stale entries in InternalLoggerRegistry
Use ReferenceQueue to track and remove GC'd Logger WeakReferences from the registry map to prevent potential memory leaks. Includes improved unit tests.
1 parent 05b3f8f commit 8b964a0

File tree

2 files changed

+105
-56
lines changed

2 files changed

+105
-56
lines changed

log4j-core-test/src/main/java/org/apache/logging/log4j/core/test/util/InternalLoggerRegistryGCTest.java

Lines changed: 85 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
*/
1717
package org.apache.logging.log4j.core.test.util;
1818

19+
import static java.util.concurrent.TimeUnit.MILLISECONDS;
20+
import static java.util.concurrent.TimeUnit.SECONDS;
21+
import static org.awaitility.Awaitility.await;
1922
import static org.junit.jupiter.api.Assertions.assertEquals;
2023
import static org.junit.jupiter.api.Assertions.assertFalse;
2124
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -24,6 +27,8 @@
2427
import static org.junit.jupiter.api.Assertions.assertTrue;
2528

2629
import java.lang.ref.WeakReference;
30+
import java.lang.reflect.Field;
31+
import java.util.Map;
2732
import java.util.concurrent.CountDownLatch;
2833
import java.util.concurrent.ExecutorService;
2934
import java.util.concurrent.Executors;
@@ -32,17 +37,30 @@
3237
import org.apache.logging.log4j.core.util.internal.InternalLoggerRegistry;
3338
import org.apache.logging.log4j.message.MessageFactory;
3439
import org.apache.logging.log4j.message.SimpleMessageFactory;
40+
import org.junit.jupiter.api.AfterEach;
3541
import org.junit.jupiter.api.BeforeEach;
3642
import org.junit.jupiter.api.Test;
43+
import org.junit.jupiter.api.TestInfo;
3744

3845
class InternalLoggerRegistryTest {
46+
private LoggerContext loggerContext;
3947
private InternalLoggerRegistry registry;
4048
private MessageFactory messageFactory;
4149

4250
@BeforeEach
43-
void setUp() {
44-
registry = new InternalLoggerRegistry();
45-
messageFactory = new SimpleMessageFactory();
51+
void setUp(TestInfo testInfo) throws NoSuchFieldException, IllegalAccessException {
52+
loggerContext = new LoggerContext(testInfo.getDisplayName());
53+
Field registryField = loggerContext.getClass().getDeclaredField("loggerRegistry");
54+
registryField.setAccessible(true);
55+
registry = (InternalLoggerRegistry) registryField.get(loggerContext);
56+
messageFactory = SimpleMessageFactory.INSTANCE;
57+
}
58+
59+
@AfterEach
60+
void tearDown() {
61+
if (loggerContext != null) {
62+
loggerContext.stop();
63+
}
4664
}
4765

4866
@Test
@@ -52,54 +70,79 @@ void testGetLoggerReturnsNullForNonExistentLogger() {
5270

5371
@Test
5472
void testComputeIfAbsentCreatesLogger() {
55-
Logger logger =
56-
registry.computeIfAbsent("testLogger", messageFactory, (name, factory) -> LoggerContext.getContext()
57-
.getLogger(name, factory));
73+
Logger logger = registry.computeIfAbsent(
74+
"testLogger", messageFactory, (name, factory) -> loggerContext.getLogger(name, factory));
5875
assertNotNull(logger);
5976
assertEquals("testLogger", logger.getName());
6077
}
6178

6279
@Test
6380
void testGetLoggerRetrievesExistingLogger() {
64-
Logger logger =
65-
registry.computeIfAbsent("testLogger", messageFactory, (name, factory) -> LoggerContext.getContext()
66-
.getLogger(name, factory));
81+
Logger logger = registry.computeIfAbsent(
82+
"testLogger", messageFactory, (name, factory) -> loggerContext.getLogger(name, factory));
6783
assertSame(logger, registry.getLogger("testLogger", messageFactory));
6884
}
6985

7086
@Test
7187
void testHasLoggerReturnsCorrectStatus() {
7288
assertFalse(registry.hasLogger("testLogger", messageFactory));
73-
registry.computeIfAbsent("testLogger", messageFactory, (name, factory) -> LoggerContext.getContext()
74-
.getLogger(name, factory));
89+
registry.computeIfAbsent(
90+
"testLogger", messageFactory, (name, factory) -> loggerContext.getLogger(name, factory));
7591
assertTrue(registry.hasLogger("testLogger", messageFactory));
7692
}
7793

7894
@Test
79-
void testExpungeStaleEntriesRemovesGarbageCollectedLoggers() throws InterruptedException {
80-
Logger logger =
81-
registry.computeIfAbsent("testLogger", messageFactory, (name, factory) -> LoggerContext.getContext()
82-
.getLogger(name, factory));
83-
84-
WeakReference<Logger> weakRef = new WeakReference<>(logger);
85-
logger = null; // Dereference to allow GC
95+
void testExpungeStaleWeakReferenceEntries() {
96+
String loggerNamePrefix = "testLogger_";
97+
int numberOfLoggers = 1000;
98+
99+
for (int i = 0; i < numberOfLoggers; i++) {
100+
Logger logger = registry.computeIfAbsent(
101+
loggerNamePrefix + i, messageFactory, (name, factory) -> loggerContext.getLogger(name, factory));
102+
logger.info("Using logger {}", logger.getName());
103+
}
86104

87-
// Retry loop to give GC time to collect
88-
for (int i = 0; i < 10; i++) {
105+
await().atMost(10, SECONDS).pollInterval(100, MILLISECONDS).untilAsserted(() -> {
89106
System.gc();
90-
Thread.sleep(100);
91-
if (weakRef.get() == null) {
92-
break;
107+
System.runFinalization();
108+
registry.computeIfAbsent(
109+
"triggerExpunge", messageFactory, (name, factory) -> loggerContext.getLogger(name, factory));
110+
111+
Map<MessageFactory, Map<String, WeakReference<Logger>>> loggerRefByNameByMessageFactory =
112+
reflectAndGetLoggerMapFromRegistry();
113+
Map<String, WeakReference<Logger>> loggerRefByName = loggerRefByNameByMessageFactory.get(messageFactory);
114+
115+
boolean isExpungeStaleEntries = true;
116+
for (int i = 0; i < numberOfLoggers; i++) {
117+
if (loggerRefByName.containsKey(loggerNamePrefix + i)) {
118+
isExpungeStaleEntries = false;
119+
break;
120+
}
93121
}
94-
}
122+
assertTrue(
123+
isExpungeStaleEntries,
124+
"Stale WeakReference entries were not removed from the inner map for MessageFactory");
125+
});
126+
}
95127

96-
// Access the registry to potentially trigger cleanup
97-
registry.computeIfAbsent("tempLogger", messageFactory, (name, factory) -> LoggerContext.getContext()
98-
.getLogger(name, factory));
128+
@Test
129+
void testExpungeStaleMessageFactoryEntry() {
130+
Logger logger = registry.computeIfAbsent(
131+
"testLogger", messageFactory, (name, factory) -> loggerContext.getLogger(name, factory));
132+
logger.info("Using logger {}", logger.getName());
133+
logger = null;
99134

100-
assertNull(weakRef.get(), "Logger should have been garbage collected");
101-
assertNull(
102-
registry.getLogger("testLogger", messageFactory), "Stale logger should be removed from the registry");
135+
await().atMost(10, SECONDS).pollInterval(100, MILLISECONDS).untilAsserted(() -> {
136+
System.gc();
137+
System.runFinalization();
138+
registry.getLogger("testLogger", messageFactory);
139+
140+
Map<MessageFactory, Map<String, WeakReference<Logger>>> loggerRefByNameByMessageFactory =
141+
reflectAndGetLoggerMapFromRegistry();
142+
assertNull(
143+
loggerRefByNameByMessageFactory.get(messageFactory),
144+
"Stale MessageFactory entry was not removed from the outer map");
145+
});
103146
}
104147

105148
@Test
@@ -110,8 +153,8 @@ void testConcurrentAccess() throws InterruptedException {
110153

111154
for (int i = 0; i < threadCount; i++) {
112155
executor.submit(() -> {
113-
registry.computeIfAbsent("testLogger", messageFactory, (name, factory) -> LoggerContext.getContext()
114-
.getLogger(name, factory));
156+
registry.computeIfAbsent(
157+
"testLogger", messageFactory, (name, factory) -> loggerContext.getLogger(name, factory));
115158
latch.countDown();
116159
});
117160
}
@@ -124,4 +167,14 @@ void testConcurrentAccess() throws InterruptedException {
124167
registry.getLogger("testLogger", messageFactory),
125168
"Logger should be accessible after concurrent creation");
126169
}
170+
171+
private Map<MessageFactory, Map<String, WeakReference<Logger>>> reflectAndGetLoggerMapFromRegistry()
172+
throws NoSuchFieldException, IllegalAccessException {
173+
Field loggerMapField = registry.getClass().getDeclaredField("loggerRefByNameByMessageFactory");
174+
loggerMapField.setAccessible(true);
175+
@SuppressWarnings("unchecked")
176+
Map<MessageFactory, Map<String, WeakReference<Logger>>> loggerMap =
177+
(Map<MessageFactory, Map<String, WeakReference<Logger>>>) loggerMapField.get(registry);
178+
return loggerMap;
179+
}
127180
}

log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InternalLoggerRegistry.java

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.lang.ref.WeakReference;
2424
import java.util.Collection;
2525
import java.util.HashMap;
26+
import java.util.Iterator;
2627
import java.util.Map;
2728
import java.util.WeakHashMap;
2829
import java.util.concurrent.locks.Lock;
@@ -66,36 +67,31 @@ public final class InternalLoggerRegistry {
6667
public InternalLoggerRegistry() {}
6768

6869
/**
69-
* Expunges stale logger references from the registry.
70+
* Expunges stale entries for logger references and message factories.
7071
*/
7172
private void expungeStaleEntries() {
72-
Reference<? extends Logger> loggerRef;
73-
while ((loggerRef = staleLoggerRefs.poll()) != null) {
74-
removeLogger(loggerRef);
75-
}
76-
}
77-
78-
/**
79-
* Removes a logger from the registry.
80-
*/
81-
private void removeLogger(Reference<? extends Logger> loggerRef) {
82-
Logger logger = loggerRef.get();
83-
if (logger == null) return; // Logger already cleared
73+
Reference<? extends Logger> loggerRef = staleLoggerRefs.poll();
8474

85-
MessageFactory messageFactory = logger.getMessageFactory();
86-
String name = logger.getName();
75+
if (loggerRef != null) {
76+
writeLock.lock();
77+
try {
78+
while (staleLoggerRefs.poll() != null) {
79+
// Clear refQueue
80+
}
8781

88-
writeLock.lock();
89-
try {
90-
Map<String, WeakReference<Logger>> loggerRefByName = loggerRefByNameByMessageFactory.get(messageFactory);
91-
if (loggerRefByName != null) {
92-
loggerRefByName.remove(name);
93-
if (loggerRefByName.isEmpty()) {
94-
loggerRefByNameByMessageFactory.remove(messageFactory); // Cleanup
82+
Iterator<Map.Entry<MessageFactory, Map<String, WeakReference<Logger>>>> outerIt =
83+
loggerRefByNameByMessageFactory.entrySet().iterator();
84+
while (outerIt.hasNext()) {
85+
Map.Entry<MessageFactory, Map<String, WeakReference<Logger>>> outerEntry = outerIt.next();
86+
Map<String, WeakReference<Logger>> innerMap = outerEntry.getValue();
87+
innerMap.values().removeIf(weakRef -> weakRef.get() == null);
88+
if (innerMap.isEmpty()) {
89+
outerIt.remove();
90+
}
9591
}
92+
} finally {
93+
writeLock.unlock();
9694
}
97-
} finally {
98-
writeLock.unlock();
9995
}
10096
}
10197

0 commit comments

Comments
 (0)