Skip to content

Commit 20035c4

Browse files
authored
Removes weak references from LoggerRepository (#3199)
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.
1 parent 3646eb6 commit 20035c4

File tree

10 files changed

+317
-110
lines changed

10 files changed

+317
-110
lines changed
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/simple/SimpleLoggerContext.java

+7-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,13 @@ public ExtendedLogger getLogger(final String name) {
102102
public ExtendedLogger getLogger(final String name, @Nullable final MessageFactory messageFactory) {
103103
final MessageFactory effectiveMessageFactory =
104104
messageFactory != null ? messageFactory : DEFAULT_MESSAGE_FACTORY;
105-
return loggerRegistry.computeIfAbsent(name, effectiveMessageFactory, this::createLogger);
105+
final ExtendedLogger oldLogger = loggerRegistry.getLogger(name, effectiveMessageFactory);
106+
if (oldLogger != null) {
107+
return oldLogger;
108+
}
109+
final ExtendedLogger newLogger = createLogger(name, effectiveMessageFactory);
110+
loggerRegistry.putIfAbsent(name, effectiveMessageFactory, newLogger);
111+
return loggerRegistry.getLogger(name, effectiveMessageFactory);
106112
}
107113

108114
private ExtendedLogger createLogger(final String name, @Nullable final MessageFactory messageFactory) {

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

+21-96
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,18 @@
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;
33-
import java.util.function.BiFunction;
3431
import org.apache.logging.log4j.message.MessageFactory;
3532
import org.apache.logging.log4j.message.ParameterizedMessageFactory;
36-
import org.apache.logging.log4j.status.StatusLogger;
3733
import org.jspecify.annotations.NullMarked;
3834
import org.jspecify.annotations.Nullable;
3935

@@ -43,7 +39,7 @@
4339
@NullMarked
4440
public class LoggerRegistry<T extends ExtendedLogger> {
4541

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

4844
private final ReadWriteLock lock = new ReentrantReadWriteLock();
4945

@@ -143,7 +139,7 @@ public LoggerRegistry(@Nullable final MapFactory<T> mapFactory) {
143139
* Use {@link #getLogger(String, MessageFactory)} instead.
144140
*/
145141
@Deprecated
146-
public T getLogger(final String name) {
142+
public @Nullable T getLogger(final String name) {
147143
requireNonNull(name, "name");
148144
return getLogger(name, null);
149145
}
@@ -160,39 +156,29 @@ public T getLogger(final String name) {
160156
* @param messageFactory a message factory
161157
* @return the logger associated with the given name and message factory
162158
*/
163-
public T getLogger(final String name, @Nullable final MessageFactory messageFactory) {
159+
public @Nullable T getLogger(final String name, @Nullable final MessageFactory messageFactory) {
164160
requireNonNull(name, "name");
165161
readLock.lock();
166162
try {
167-
final Map<MessageFactory, WeakReference<T>> loggerRefByMessageFactory =
168-
loggerRefByMessageFactoryByName.get(name);
169-
if (loggerRefByMessageFactory == null) {
170-
return null;
171-
}
163+
final @Nullable Map<MessageFactory, T> loggerByMessageFactory = loggerByMessageFactoryByName.get(name);
172164
final MessageFactory effectiveMessageFactory =
173165
messageFactory != null ? messageFactory : ParameterizedMessageFactory.INSTANCE;
174-
final WeakReference<T> loggerRef = loggerRefByMessageFactory.get(effectiveMessageFactory);
175-
if (loggerRef == null) {
176-
return null;
177-
}
178-
return loggerRef.get();
166+
return loggerByMessageFactory == null ? null : loggerByMessageFactory.get(effectiveMessageFactory);
179167
} finally {
180168
readLock.unlock();
181169
}
182170
}
183171

184172
public Collection<T> getLoggers() {
185-
return getLoggers(new ArrayList<T>());
173+
return getLoggers(new ArrayList<>());
186174
}
187175

188176
public Collection<T> getLoggers(final Collection<T> destination) {
189177
requireNonNull(destination, "destination");
190178
readLock.lock();
191179
try {
192-
loggerRefByMessageFactoryByName.values().stream()
193-
.flatMap(loggerRefByMessageFactory ->
194-
loggerRefByMessageFactory.values().stream().map(WeakReference::get))
195-
.filter(Objects::nonNull)
180+
loggerByMessageFactoryByName.values().stream()
181+
.flatMap(loggerByMessageFactory -> loggerByMessageFactory.values().stream())
196182
.forEach(destination::add);
197183
} finally {
198184
readLock.unlock();
@@ -215,7 +201,7 @@ public Collection<T> getLoggers(final Collection<T> destination) {
215201
@Deprecated
216202
public boolean hasLogger(final String name) {
217203
requireNonNull(name, "name");
218-
final T logger = getLogger(name);
204+
final @Nullable T logger = getLogger(name);
219205
return logger != null;
220206
}
221207

@@ -234,7 +220,7 @@ public boolean hasLogger(final String name) {
234220
*/
235221
public boolean hasLogger(final String name, @Nullable final MessageFactory messageFactory) {
236222
requireNonNull(name, "name");
237-
final T logger = getLogger(name, messageFactory);
223+
final @Nullable T logger = getLogger(name, messageFactory);
238224
return logger != null;
239225
}
240226

@@ -251,7 +237,7 @@ public boolean hasLogger(final String name, final Class<? extends MessageFactory
251237
requireNonNull(messageFactoryClass, "messageFactoryClass");
252238
readLock.lock();
253239
try {
254-
return loggerRefByMessageFactoryByName.getOrDefault(name, Collections.emptyMap()).keySet().stream()
240+
return loggerByMessageFactoryByName.getOrDefault(name, Collections.emptyMap()).keySet().stream()
255241
.anyMatch(messageFactory -> messageFactoryClass.equals(messageFactory.getClass()));
256242
} finally {
257243
readLock.unlock();
@@ -262,91 +248,30 @@ public boolean hasLogger(final String name, final Class<? extends MessageFactory
262248
* Registers the provided logger.
263249
* <b>Logger name and message factory parameters are ignored</b>, those will be obtained from the logger instead.
264250
*
265-
* @param name ignored – kept for backward compatibility
266-
* @param messageFactory ignored – kept for backward compatibility
251+
* @param name a logger name
252+
* @param messageFactory a message factory
267253
* @param logger a logger instance
268-
* @deprecated As of version {@code 2.25.0}, planned to be removed!
269-
* Use {@link #computeIfAbsent(String, MessageFactory, BiFunction)} instead.
270254
*/
271-
@Deprecated
272-
public void putIfAbsent(
273-
@Nullable final String name, @Nullable final MessageFactory messageFactory, final T logger) {
255+
public void putIfAbsent(final String name, @Nullable final MessageFactory messageFactory, final T logger) {
274256

275257
// Check arguments
258+
requireNonNull(name, "name");
276259
requireNonNull(logger, "logger");
277260

278261
// Insert the logger
279262
writeLock.lock();
280263
try {
281-
final String loggerName = logger.getName();
282-
final Map<MessageFactory, WeakReference<T>> loggerRefByMessageFactory =
283-
loggerRefByMessageFactoryByName.computeIfAbsent(
284-
loggerName, this::createLoggerRefByMessageFactoryMap);
285-
final MessageFactory loggerMessageFactory = logger.getMessageFactory();
286-
final WeakReference<T> loggerRef = loggerRefByMessageFactory.get(loggerMessageFactory);
287-
if (loggerRef == null || loggerRef.get() == null) {
288-
loggerRefByMessageFactory.put(loggerMessageFactory, new WeakReference<>(logger));
289-
}
290-
} finally {
291-
writeLock.unlock();
292-
}
293-
}
294-
295-
public T computeIfAbsent(
296-
final String name,
297-
final MessageFactory messageFactory,
298-
final BiFunction<String, MessageFactory, T> loggerSupplier) {
299-
300-
// Check arguments
301-
requireNonNull(name, "name");
302-
requireNonNull(messageFactory, "messageFactory");
303-
requireNonNull(loggerSupplier, "loggerSupplier");
304-
305-
// Read lock fast path: See if logger already exists
306-
T logger = getLogger(name, messageFactory);
307-
if (logger != null) {
308-
return logger;
309-
}
310-
311-
// Write lock slow path: Insert the logger
312-
writeLock.lock();
313-
try {
314-
315-
// See if the logger is created by another thread in the meantime
316-
final Map<MessageFactory, WeakReference<T>> loggerRefByMessageFactory =
317-
loggerRefByMessageFactoryByName.computeIfAbsent(name, this::createLoggerRefByMessageFactoryMap);
318-
final WeakReference<T> loggerRef;
319-
if ((loggerRef = loggerRefByMessageFactory.get(messageFactory)) != null
320-
&& (logger = loggerRef.get()) != null) {
321-
return logger;
322-
}
323-
324-
// Create the logger
325-
logger = loggerSupplier.apply(name, messageFactory);
326-
327-
// Report message factory mismatches, if there is any
328-
final MessageFactory loggerMessageFactory = logger.getMessageFactory();
329-
if (!loggerMessageFactory.equals(messageFactory)) {
330-
StatusLogger.getLogger()
331-
.error(
332-
"Newly registered logger with name `{}` and message factory `{}`, is requested to be associated with a different message factory: `{}`.\n"
333-
+ "Effectively the message factory of the logger will be used and the other one will be ignored.\n"
334-
+ "This generally hints a problem at the logger context implementation.\n"
335-
+ "Please report this using the Log4j project issue tracker.",
336-
name,
337-
loggerMessageFactory,
338-
messageFactory);
339-
}
340-
341-
// Insert the logger
342-
loggerRefByMessageFactory.put(loggerMessageFactory, new WeakReference<>(logger));
343-
return logger;
264+
final MessageFactory effectiveMessageFactory =
265+
messageFactory != null ? messageFactory : ParameterizedMessageFactory.INSTANCE;
266+
loggerByMessageFactoryByName
267+
.computeIfAbsent(name, this::createLoggerRefByMessageFactoryMap)
268+
.putIfAbsent(effectiveMessageFactory, logger);
344269
} finally {
345270
writeLock.unlock();
346271
}
347272
}
348273

349-
private Map<MessageFactory, WeakReference<T>> createLoggerRefByMessageFactoryMap(final String ignored) {
274+
private Map<MessageFactory, T> createLoggerRefByMessageFactoryMap(final String ignored) {
350275
return new WeakHashMap<>();
351276
}
352277
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
* API classes.
2020
*/
2121
@Export
22-
@Version("2.25.0")
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

+12-8
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;
@@ -83,7 +84,7 @@ public class LoggerContext extends AbstractLifeCycle
8384
*/
8485
private static final MessageFactory DEFAULT_MESSAGE_FACTORY = Logger.getEffectiveMessageFactory(null);
8586

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

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

519520
/**
@@ -535,9 +536,14 @@ public Logger getLogger(final String name, @Nullable final MessageFactory messag
535536
*
536537
* @return the LoggerRegistry.
537538
* @since 2.17.2
539+
* @deprecated since 2.25.0 without a replacement.
538540
*/
539-
public LoggerRegistry<Logger> getLoggerRegistry() {
540-
return loggerRegistry;
541+
@Deprecated
542+
public org.apache.logging.log4j.spi.LoggerRegistry<Logger> getLoggerRegistry() {
543+
org.apache.logging.log4j.spi.LoggerRegistry<Logger> result =
544+
new org.apache.logging.log4j.spi.LoggerRegistry<>();
545+
loggerRegistry.getLoggers().forEach(l -> result.putIfAbsent(l.getName(), l.getMessageFactory(), l));
546+
return result;
541547
}
542548

543549
/**
@@ -770,9 +776,7 @@ public void updateLoggers() {
770776
*/
771777
public void updateLoggers(final Configuration config) {
772778
final Configuration old = this.configuration;
773-
for (final Logger logger : loggerRegistry.getLoggers()) {
774-
logger.updateConfiguration(config);
775-
}
779+
loggerRegistry.getLoggers().forEach(logger -> logger.updateConfiguration(config));
776780
firePropertyChangeEvent(new PropertyChangeEvent(this, PROPERTY_CONFIG, old, config));
777781
}
778782

0 commit comments

Comments
 (0)