Skip to content

Commit a1dfa85

Browse files
ppkarwaszvy
andauthored
Removes weak references from LoggerRepository (2.24.x branch) (#3209)
This is a port of #3199 to the 2.24.x branch. Removes weak references to `Logger`s in `LoggerRepository`. The usage of weak references in `LoggerRepository` might cause `null` to be returned by `LogManager.getLogger()` of all Log4j Core versions up to `2.24.1`. Versions of Log4j API up to `2.24.0` did hold **hard** references to all the registered loggers, so the change will not alter the previous behavior. This PR also inverts the order of the `String` and `MessageFactory` keys to the `LoggerRepository` multi-map to limit the number of internal maps. The external map is a `WeakHashMap` to allow logger-specific message factories to be GC-ed. Closes #3143. Co-authored-by: Volkan Yazıcı <volkan@yazi.ci>
1 parent 3fc2088 commit a1dfa85

File tree

7 files changed

+310
-51
lines changed

7 files changed

+310
-51
lines changed

log4j-api-test/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@
144144
<artifactId>mockito-inline</artifactId>
145145
<scope>test</scope>
146146
</dependency>
147+
<dependency>
148+
<groupId>org.jspecify</groupId>
149+
<artifactId>jspecify</artifactId>
150+
<scope>test</scope>
151+
</dependency>
147152
<!-- Used by ServiceLoaderUtilTest -->
148153
<dependency>
149154
<groupId>org.osgi</groupId>
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.logging.log4j.spi;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
21+
import java.lang.ref.WeakReference;
22+
import java.util.stream.Stream;
23+
import org.apache.logging.log4j.message.MessageFactory;
24+
import org.apache.logging.log4j.message.ParameterizedMessageFactory;
25+
import org.apache.logging.log4j.message.ReusableMessageFactory;
26+
import org.apache.logging.log4j.test.TestLogger;
27+
import org.jspecify.annotations.Nullable;
28+
import org.junit.jupiter.params.ParameterizedTest;
29+
import org.junit.jupiter.params.provider.MethodSource;
30+
31+
class LoggerRegistryTest {
32+
33+
private static final String LOGGER_NAME = LoggerRegistryTest.class.getName();
34+
35+
static Stream<@Nullable MessageFactory> doesNotLoseLoggerReferences() {
36+
return Stream.of(
37+
ParameterizedMessageFactory.INSTANCE,
38+
ReusableMessageFactory.INSTANCE,
39+
new ParameterizedMessageFactory(),
40+
null);
41+
}
42+
43+
/**
44+
* @see <a href="https://github.com/apache/logging-log4j2/issues/3143>Issue #3143</a>
45+
*/
46+
@ParameterizedTest
47+
@MethodSource
48+
void doesNotLoseLoggerReferences(@Nullable MessageFactory messageFactory) {
49+
LoggerRegistry<TestLogger> loggerRegistry = new LoggerRegistry<>();
50+
TestLogger logger = new TestLogger(LOGGER_NAME, messageFactory);
51+
WeakReference<TestLogger> loggerRef = new WeakReference<>(logger);
52+
// Register logger
53+
loggerRegistry.putIfAbsent(LOGGER_NAME, messageFactory, logger);
54+
// The JIT compiler/optimizer might figure out by himself the `logger` and `messageFactory` are no longer used:
55+
// https://shipilev.net/jvm/anatomy-quarks/8-local-var-reachability/
56+
// We help him with the task though.
57+
logger = null;
58+
// Trigger a GC run
59+
System.gc();
60+
// Check if the logger is still there
61+
assertThat(loggerRef.get()).isNotNull();
62+
assertThat(loggerRegistry.getLogger(LOGGER_NAME, messageFactory)).isInstanceOf(TestLogger.class);
63+
}
64+
}

log4j-api/src/main/java/org/apache/logging/log4j/spi/LoggerRegistry.java

Lines changed: 33 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,27 @@
1818

1919
import static java.util.Objects.requireNonNull;
2020

21-
import java.lang.ref.WeakReference;
2221
import java.util.ArrayList;
2322
import java.util.Collection;
2423
import java.util.Collections;
2524
import java.util.HashMap;
2625
import java.util.Map;
27-
import java.util.Objects;
2826
import java.util.WeakHashMap;
2927
import java.util.concurrent.ConcurrentHashMap;
3028
import java.util.concurrent.locks.Lock;
3129
import java.util.concurrent.locks.ReadWriteLock;
3230
import java.util.concurrent.locks.ReentrantReadWriteLock;
31+
import org.apache.logging.log4j.Logger;
3332
import org.apache.logging.log4j.message.MessageFactory;
3433
import org.apache.logging.log4j.message.ParameterizedMessageFactory;
34+
import org.jspecify.annotations.Nullable;
3535

3636
/**
3737
* Convenience class to be used as an {@link ExtendedLogger} registry by {@code LoggerContext} implementations.
3838
*/
3939
public class LoggerRegistry<T extends ExtendedLogger> {
4040

41-
private final Map<String, Map<MessageFactory, WeakReference<T>>> loggerRefByMessageFactoryByName = new HashMap<>();
41+
private final Map<String, Map<MessageFactory, T>> loggerByMessageFactoryByName = new HashMap<>();
4242

4343
private final ReadWriteLock lock = new ReentrantReadWriteLock();
4444

@@ -127,7 +127,7 @@ public LoggerRegistry(final MapFactory<T> mapFactory) {
127127
* @param name a logger name
128128
* @return the logger associated with the name
129129
*/
130-
public T getLogger(final String name) {
130+
public @Nullable T getLogger(final String name) {
131131
requireNonNull(name, "name");
132132
return getLogger(name, null);
133133
}
@@ -144,39 +144,29 @@ public T getLogger(final String name) {
144144
* @param messageFactory a message factory
145145
* @return the logger associated with the given name and message factory
146146
*/
147-
public T getLogger(final String name, final MessageFactory messageFactory) {
147+
public @Nullable T getLogger(final String name, @Nullable final MessageFactory messageFactory) {
148148
requireNonNull(name, "name");
149149
readLock.lock();
150150
try {
151-
final Map<MessageFactory, WeakReference<T>> loggerRefByMessageFactory =
152-
loggerRefByMessageFactoryByName.get(name);
153-
if (loggerRefByMessageFactory == null) {
154-
return null;
155-
}
151+
final @Nullable Map<MessageFactory, T> loggerByMessageFactory = loggerByMessageFactoryByName.get(name);
156152
final MessageFactory effectiveMessageFactory =
157153
messageFactory != null ? messageFactory : ParameterizedMessageFactory.INSTANCE;
158-
final WeakReference<T> loggerRef = loggerRefByMessageFactory.get(effectiveMessageFactory);
159-
if (loggerRef == null) {
160-
return null;
161-
}
162-
return loggerRef.get();
154+
return loggerByMessageFactory == null ? null : loggerByMessageFactory.get(effectiveMessageFactory);
163155
} finally {
164156
readLock.unlock();
165157
}
166158
}
167159

168160
public Collection<T> getLoggers() {
169-
return getLoggers(new ArrayList<T>());
161+
return getLoggers(new ArrayList<>());
170162
}
171163

172164
public Collection<T> getLoggers(final Collection<T> destination) {
173165
requireNonNull(destination, "destination");
174166
readLock.lock();
175167
try {
176-
loggerRefByMessageFactoryByName.values().stream()
177-
.flatMap(loggerRefByMessageFactory ->
178-
loggerRefByMessageFactory.values().stream().map(WeakReference::get))
179-
.filter(Objects::nonNull)
168+
loggerByMessageFactoryByName.values().stream()
169+
.flatMap(loggerByMessageFactory -> loggerByMessageFactory.values().stream())
180170
.forEach(destination::add);
181171
} finally {
182172
readLock.unlock();
@@ -196,7 +186,7 @@ public Collection<T> getLoggers(final Collection<T> destination) {
196186
*/
197187
public boolean hasLogger(final String name) {
198188
requireNonNull(name, "name");
199-
final T logger = getLogger(name);
189+
final @Nullable T logger = getLogger(name);
200190
return logger != null;
201191
}
202192

@@ -215,7 +205,7 @@ public boolean hasLogger(final String name) {
215205
*/
216206
public boolean hasLogger(final String name, final MessageFactory messageFactory) {
217207
requireNonNull(name, "name");
218-
final T logger = getLogger(name, messageFactory);
208+
final @Nullable T logger = getLogger(name, messageFactory);
219209
return logger != null;
220210
}
221211

@@ -232,7 +222,7 @@ public boolean hasLogger(final String name, final Class<? extends MessageFactory
232222
requireNonNull(messageFactoryClass, "messageFactoryClass");
233223
readLock.lock();
234224
try {
235-
return loggerRefByMessageFactoryByName.getOrDefault(name, Collections.emptyMap()).keySet().stream()
225+
return loggerByMessageFactoryByName.getOrDefault(name, Collections.emptyMap()).keySet().stream()
236226
.anyMatch(messageFactory -> messageFactoryClass.equals(messageFactory.getClass()));
237227
} finally {
238228
readLock.unlock();
@@ -241,34 +231,42 @@ public boolean hasLogger(final String name, final Class<? extends MessageFactory
241231

242232
/**
243233
* Registers the provided logger.
244-
* <b>Logger name and message factory parameters are ignored</b>, those will be obtained from the logger instead.
234+
* <p>
235+
* The logger will be registered using the keys provided by the {@code name} and {@code messageFactory} parameters
236+
* and the values of {@link Logger#getName()} and {@link Logger#getMessageFactory()}.
237+
* </p>
245238
*
246-
* @param name ignored – kept for backward compatibility
247-
* @param messageFactory ignored – kept for backward compatibility
239+
* @param name a logger name
240+
* @param messageFactory a message factory
248241
* @param logger a logger instance
249242
*/
250-
public void putIfAbsent(final String name, final MessageFactory messageFactory, final T logger) {
243+
public void putIfAbsent(final String name, @Nullable final MessageFactory messageFactory, final T logger) {
251244

252245
// Check arguments
246+
requireNonNull(name, "name");
253247
requireNonNull(logger, "logger");
254248

255249
// Insert the logger
256250
writeLock.lock();
257251
try {
258-
final Map<MessageFactory, WeakReference<T>> loggerRefByMessageFactory =
259-
loggerRefByMessageFactoryByName.computeIfAbsent(
260-
logger.getName(), this::createLoggerRefByMessageFactoryMap);
261-
final MessageFactory loggerMessageFactory = logger.getMessageFactory();
262-
final WeakReference<T> loggerRef = loggerRefByMessageFactory.get(loggerMessageFactory);
263-
if (loggerRef == null || loggerRef.get() == null) {
264-
loggerRefByMessageFactory.put(loggerMessageFactory, new WeakReference<>(logger));
252+
final MessageFactory effectiveMessageFactory =
253+
messageFactory != null ? messageFactory : ParameterizedMessageFactory.INSTANCE;
254+
// Register using the keys provided by the caller
255+
loggerByMessageFactoryByName
256+
.computeIfAbsent(name, this::createLoggerRefByMessageFactoryMap)
257+
.putIfAbsent(effectiveMessageFactory, logger);
258+
// Also register using the values extracted from `logger`
259+
if (!name.equals(logger.getName()) || !effectiveMessageFactory.equals(logger.getMessageFactory())) {
260+
loggerByMessageFactoryByName
261+
.computeIfAbsent(logger.getName(), this::createLoggerRefByMessageFactoryMap)
262+
.putIfAbsent(logger.getMessageFactory(), logger);
265263
}
266264
} finally {
267265
writeLock.unlock();
268266
}
269267
}
270268

271-
private Map<MessageFactory, WeakReference<T>> createLoggerRefByMessageFactoryMap(final String ignored) {
269+
private Map<MessageFactory, T> createLoggerRefByMessageFactoryMap(final String ignored) {
272270
return new WeakHashMap<>();
273271
}
274272
}

log4j-api/src/main/java/org/apache/logging/log4j/spi/package-info.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
* API classes.
2020
*/
2121
@Export
22-
@Version("2.24.1")
22+
@Version("2.24.2")
2323
package org.apache.logging.log4j.spi;
2424

2525
import org.osgi.annotation.bundle.Export;

log4j-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.util.concurrent.TimeUnit;
3434
import java.util.concurrent.locks.Lock;
3535
import java.util.concurrent.locks.ReentrantLock;
36+
import java.util.stream.Collectors;
3637
import org.apache.logging.log4j.LogManager;
3738
import org.apache.logging.log4j.core.config.Configuration;
3839
import org.apache.logging.log4j.core.config.ConfigurationFactory;
@@ -47,11 +48,11 @@
4748
import org.apache.logging.log4j.core.util.ExecutorServices;
4849
import org.apache.logging.log4j.core.util.NetUtils;
4950
import org.apache.logging.log4j.core.util.ShutdownCallbackRegistry;
51+
import org.apache.logging.log4j.core.util.internal.InternalLoggerRegistry;
5052
import org.apache.logging.log4j.message.MessageFactory;
5153
import org.apache.logging.log4j.spi.LoggerContextFactory;
5254
import org.apache.logging.log4j.spi.LoggerContextShutdownAware;
5355
import org.apache.logging.log4j.spi.LoggerContextShutdownEnabled;
54-
import org.apache.logging.log4j.spi.LoggerRegistry;
5556
import org.apache.logging.log4j.spi.Terminable;
5657
import org.apache.logging.log4j.spi.ThreadContextMapFactory;
5758
import org.apache.logging.log4j.util.PropertiesUtil;
@@ -82,7 +83,7 @@ public class LoggerContext extends AbstractLifeCycle
8283
*/
8384
private static final MessageFactory DEFAULT_MESSAGE_FACTORY = Logger.getEffectiveMessageFactory(null);
8485

85-
private final LoggerRegistry<Logger> loggerRegistry = new LoggerRegistry<>();
86+
private final InternalLoggerRegistry loggerRegistry = new InternalLoggerRegistry();
8687
private final CopyOnWriteArrayList<PropertyChangeListener> propertyChangeListeners = new CopyOnWriteArrayList<>();
8788
private volatile List<LoggerContextShutdownAware> listeners;
8889

@@ -512,7 +513,7 @@ public Logger getLogger(final String name) {
512513
* @return a collection of the current loggers.
513514
*/
514515
public Collection<Logger> getLoggers() {
515-
return loggerRegistry.getLoggers();
516+
return loggerRegistry.getLoggers().collect(Collectors.toList());
516517
}
517518

518519
/**
@@ -526,13 +527,7 @@ public Collection<Logger> getLoggers() {
526527
public Logger getLogger(final String name, final MessageFactory messageFactory) {
527528
final MessageFactory effectiveMessageFactory =
528529
messageFactory != null ? messageFactory : DEFAULT_MESSAGE_FACTORY;
529-
final Logger oldLogger = loggerRegistry.getLogger(name, effectiveMessageFactory);
530-
if (oldLogger != null) {
531-
return oldLogger;
532-
}
533-
final Logger newLogger = newInstance(name, effectiveMessageFactory);
534-
loggerRegistry.putIfAbsent(name, effectiveMessageFactory, newLogger);
535-
return loggerRegistry.getLogger(name, effectiveMessageFactory);
530+
return loggerRegistry.computeIfAbsent(name, effectiveMessageFactory, this::newInstance);
536531
}
537532

538533
/**
@@ -541,8 +536,11 @@ public Logger getLogger(final String name, final MessageFactory messageFactory)
541536
* @return the LoggerRegistry.
542537
* @since 2.17.2
543538
*/
544-
public LoggerRegistry<Logger> getLoggerRegistry() {
545-
return loggerRegistry;
539+
public org.apache.logging.log4j.spi.LoggerRegistry<Logger> getLoggerRegistry() {
540+
org.apache.logging.log4j.spi.LoggerRegistry<Logger> result =
541+
new org.apache.logging.log4j.spi.LoggerRegistry<>();
542+
loggerRegistry.getLoggers().forEach(l -> result.putIfAbsent(l.getName(), l.getMessageFactory(), l));
543+
return result;
546544
}
547545

548546
/**
@@ -775,9 +773,7 @@ public void updateLoggers() {
775773
*/
776774
public void updateLoggers(final Configuration config) {
777775
final Configuration old = this.configuration;
778-
for (final Logger logger : loggerRegistry.getLoggers()) {
779-
logger.updateConfiguration(config);
780-
}
776+
loggerRegistry.getLoggers().forEach(logger -> logger.updateConfiguration(config));
781777
firePropertyChangeEvent(new PropertyChangeEvent(this, PROPERTY_CONFIG, old, config));
782778
}
783779

0 commit comments

Comments
 (0)