Skip to content

Commit bad8b56

Browse files
committed
Fix ConcurrentModificationException in updateLoggers()
The `InternalLoggerRegistry` implementation introduced in version `2.24.2` did not return a copy of the registry, when `InternalLoggerRegistry.getLoggers()` was called. This could lead to a `ConcurrentModificationException` if a thread creates a new logger, while another thread calls `LoggerContext.updateLoggers()`. Closes #3234
1 parent 11a3fc3 commit bad8b56

File tree

5 files changed

+65
-12
lines changed

5 files changed

+65
-12
lines changed

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

+8-8
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

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

21-
import java.util.ArrayList;
2221
import java.util.Collection;
2322
import java.util.Collections;
2423
import java.util.HashMap;
@@ -28,6 +27,7 @@
2827
import java.util.concurrent.locks.Lock;
2928
import java.util.concurrent.locks.ReadWriteLock;
3029
import java.util.concurrent.locks.ReentrantReadWriteLock;
30+
import java.util.stream.Collectors;
3131
import org.apache.logging.log4j.Logger;
3232
import org.apache.logging.log4j.message.MessageFactory;
3333
import org.apache.logging.log4j.message.ParameterizedMessageFactory;
@@ -158,19 +158,19 @@ public LoggerRegistry(final MapFactory<T> mapFactory) {
158158
}
159159

160160
public Collection<T> getLoggers() {
161-
return getLoggers(new ArrayList<>());
162-
}
163-
164-
public Collection<T> getLoggers(final Collection<T> destination) {
165-
requireNonNull(destination, "destination");
166161
readLock.lock();
167162
try {
168-
loggerByMessageFactoryByName.values().stream()
163+
return loggerByMessageFactoryByName.values().stream()
169164
.flatMap(loggerByMessageFactory -> loggerByMessageFactory.values().stream())
170-
.forEach(destination::add);
165+
.collect(Collectors.toList());
171166
} finally {
172167
readLock.unlock();
173168
}
169+
}
170+
171+
public Collection<T> getLoggers(final Collection<T> destination) {
172+
requireNonNull(destination, "destination");
173+
destination.addAll(getLoggers());
174174
return destination;
175175
}
176176

log4j-core-test/src/test/java/org/apache/logging/log4j/core/LoggerContextTest.java

+38
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,23 @@
1919
import static org.assertj.core.api.Assertions.assertThat;
2020
import static org.mockito.Mockito.mock;
2121

22+
import java.util.Collection;
23+
import java.util.concurrent.ExecutorService;
24+
import java.util.concurrent.Executors;
25+
import java.util.concurrent.Future;
26+
import java.util.stream.Collectors;
27+
import java.util.stream.IntStream;
2228
import org.apache.logging.log4j.message.MessageFactory;
2329
import org.apache.logging.log4j.message.MessageFactory2;
30+
import org.junit.jupiter.api.Assertions;
2431
import org.junit.jupiter.api.Test;
2532
import org.junit.jupiter.api.TestInfo;
2633

2734
class LoggerContextTest {
2835

36+
private static final int LOGGER_COUNT = 1024;
37+
private static final int CONCURRENCY_LEVEL = 16;
38+
2939
@Test
3040
void newInstance_should_honor_name_and_message_factory(final TestInfo testInfo) {
3141
final String testName = testInfo.getDisplayName();
@@ -37,4 +47,32 @@ void newInstance_should_honor_name_and_message_factory(final TestInfo testInfo)
3747
assertThat((MessageFactory) logger.getMessageFactory()).isSameAs(messageFactory);
3848
}
3949
}
50+
51+
@Test
52+
void getLoggers_can_be_updated_concurrently(final TestInfo testInfo) {
53+
final String testName = testInfo.getDisplayName();
54+
final ExecutorService executorService = Executors.newFixedThreadPool(CONCURRENCY_LEVEL);
55+
try (LoggerContext loggerContext = new LoggerContext(testName)) {
56+
// Create a logger
57+
Collection<Future<?>> tasks = IntStream.range(0, CONCURRENCY_LEVEL)
58+
.mapToObj(i -> executorService.submit(() -> {
59+
// Iterates over loggers
60+
loggerContext.updateLoggers();
61+
// Create some loggers
62+
for (int j = 0; j < LOGGER_COUNT; j++) {
63+
loggerContext.getLogger(testName + "-" + i + "-" + j);
64+
}
65+
// Iterate over loggers again
66+
loggerContext.updateLoggers();
67+
}))
68+
.collect(Collectors.toList());
69+
Assertions.assertDoesNotThrow(() -> {
70+
for (Future<?> task : tasks) {
71+
task.get();
72+
}
73+
});
74+
} finally {
75+
executorService.shutdown();
76+
}
77+
}
4078
}

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
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;
3736
import org.apache.logging.log4j.LogManager;
3837
import org.apache.logging.log4j.core.config.Configuration;
3938
import org.apache.logging.log4j.core.config.ConfigurationFactory;
@@ -513,7 +512,7 @@ public Logger getLogger(final String name) {
513512
* @return a collection of the current loggers.
514513
*/
515514
public Collection<Logger> getLoggers() {
516-
return loggerRegistry.getLoggers().collect(Collectors.toList());
515+
return loggerRegistry.getLoggers();
517516
}
518517

519518
/**

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

+8-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static java.util.Objects.requireNonNull;
2020

2121
import java.lang.ref.WeakReference;
22+
import java.util.Collection;
2223
import java.util.HashMap;
2324
import java.util.Map;
2425
import java.util.Optional;
@@ -27,6 +28,7 @@
2728
import java.util.concurrent.locks.ReadWriteLock;
2829
import java.util.concurrent.locks.ReentrantReadWriteLock;
2930
import java.util.function.BiFunction;
31+
import java.util.stream.Collectors;
3032
import java.util.stream.Stream;
3133
import org.apache.logging.log4j.core.Logger;
3234
import org.apache.logging.log4j.message.MessageFactory;
@@ -78,15 +80,19 @@ public InternalLoggerRegistry() {}
7880
}
7981
}
8082

81-
public Stream<Logger> getLoggers() {
83+
public Collection<Logger> getLoggers() {
8284
readLock.lock();
8385
try {
86+
// Return a new collection to allow concurrent iteration over the loggers
87+
//
88+
// https://github.com/apache/logging-log4j2/issues/3234
8489
return loggerRefByNameByMessageFactory.values().stream()
8590
.flatMap(loggerRefByName -> loggerRefByName.values().stream())
8691
.flatMap(loggerRef -> {
8792
@Nullable Logger logger = loggerRef.get();
8893
return logger != null ? Stream.of(logger) : Stream.empty();
89-
});
94+
})
95+
.collect(Collectors.toList());
9096
} finally {
9197
readLock.unlock();
9298
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xmlns="https://logging.apache.org/xml/ns"
4+
xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
5+
type="fixed">
6+
<issue id="3234" link="https://github.com/apache/logging-log4j2/issues/3234"/>
7+
<description format="asciidoc">
8+
Fix `ConcurrentModificationException`, if multiple threads iterate over the loggers at the same time.
9+
</description>
10+
</entry>

0 commit comments

Comments
 (0)