Skip to content

Commit 91ce8ed

Browse files
authored
Improve Loggers.useConsoleLoggers() thread safety (#3174)
Although we expect that a huge majority of prod usages do use SLF4J and won't fallback to Console logging, the later is still the first choice of fallback logging factory. This commit ensures the backing cache of Loggers behind the ConsoleLoggerFactory is thread-safe, using a WeakHashMap and WeakReferences for the Logger values, as well as a synchronized block when obtaining a Logger. The documentation of Loggers.useCustomLogger(Function) is also amended to highlight the importance of thread-safety in the custom logger factories. Fixes #3170.
1 parent e94124d commit 91ce8ed

File tree

2 files changed

+127
-23
lines changed

2 files changed

+127
-23
lines changed

reactor-core/src/main/java/reactor/util/Loggers.java

Lines changed: 88 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved.
2+
* Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,7 +17,10 @@
1717
package reactor.util;
1818

1919
import java.io.PrintStream;
20-
import java.util.HashMap;
20+
import java.lang.ref.WeakReference;
21+
import java.util.Map;
22+
import java.util.Objects;
23+
import java.util.WeakHashMap;
2124
import java.util.function.Function;
2225
import java.util.logging.Level;
2326
import java.util.regex.Matcher;
@@ -129,6 +132,12 @@ public static void useVerboseConsoleLoggers() {
129132
* The previously active logger factory is simply replaced without
130133
* any particular clean-up.
131134
*
135+
* <h4>Thread-safety</h4>
136+
*
137+
* Given logger acquisition function <em>must</em> be thread-safe.
138+
* It means that it is user responsibility to ensure that any internal state and cache
139+
* used by the provided function is properly synchronized.
140+
*
132141
* @param loggerFactory the {@link Function} that provides a (possibly cached) {@link Logger}
133142
* given a name.
134143
*/
@@ -461,25 +470,27 @@ public Logger apply(String name) {
461470
*/
462471
static final class ConsoleLogger implements Logger {
463472

464-
private final String name;
473+
private final ConsoleLoggerKey identifier;
465474
private final PrintStream err;
466475
private final PrintStream log;
467-
private final boolean verbose;
468476

469-
ConsoleLogger(String name, PrintStream log, PrintStream err, boolean verbose) {
470-
this.name = name;
477+
ConsoleLogger(ConsoleLoggerKey identifier, PrintStream log, PrintStream err) {
478+
this.identifier = identifier;
471479
this.log = log;
472480
this.err = err;
473-
this.verbose = verbose;
474481
}
475482

476-
ConsoleLogger(String name, boolean verbose) {
477-
this(name, System.out, System.err, verbose);
483+
ConsoleLogger(String name, PrintStream log, PrintStream err, boolean verbose) {
484+
this(new ConsoleLoggerKey(name, verbose), log, err);
485+
}
486+
487+
ConsoleLogger(ConsoleLoggerKey identifier) {
488+
this(identifier, System.out, System.err);
478489
}
479490

480491
@Override
481492
public String getName() {
482-
return this.name;
493+
return identifier.name;
483494
}
484495

485496
@Nullable
@@ -498,27 +509,27 @@ final String format(@Nullable String from, @Nullable Object... arguments){
498509

499510
@Override
500511
public boolean isTraceEnabled() {
501-
return verbose;
512+
return identifier.verbose;
502513
}
503514

504515
@Override
505516
public synchronized void trace(String msg) {
506-
if (!verbose) {
517+
if (!identifier.verbose) {
507518
return;
508519
}
509520
this.log.format("[TRACE] (%s) %s\n", Thread.currentThread().getName(), msg);
510521
}
511522

512523
@Override
513524
public synchronized void trace(String format, Object... arguments) {
514-
if (!verbose) {
525+
if (!identifier.verbose) {
515526
return;
516527
}
517528
this.log.format("[TRACE] (%s) %s\n", Thread.currentThread().getName(), format(format, arguments));
518529
}
519530
@Override
520531
public synchronized void trace(String msg, Throwable t) {
521-
if (!verbose) {
532+
if (!identifier.verbose) {
522533
return;
523534
}
524535
this.log.format("[TRACE] (%s) %s - %s\n", Thread.currentThread().getName(), msg, t);
@@ -527,28 +538,28 @@ public synchronized void trace(String msg, Throwable t) {
527538

528539
@Override
529540
public boolean isDebugEnabled() {
530-
return verbose;
541+
return identifier.verbose;
531542
}
532543

533544
@Override
534545
public synchronized void debug(String msg) {
535-
if (!verbose) {
546+
if (!identifier.verbose) {
536547
return;
537548
}
538549
this.log.format("[DEBUG] (%s) %s\n", Thread.currentThread().getName(), msg);
539550
}
540551

541552
@Override
542553
public synchronized void debug(String format, Object... arguments) {
543-
if (!verbose) {
554+
if (!identifier.verbose) {
544555
return;
545556
}
546557
this.log.format("[DEBUG] (%s) %s\n", Thread.currentThread().getName(), format(format, arguments));
547558
}
548559

549560
@Override
550561
public synchronized void debug(String msg, Throwable t) {
551-
if (!verbose) {
562+
if (!identifier.verbose) {
552563
return;
553564
}
554565
this.log.format("[DEBUG] (%s) %s - %s\n", Thread.currentThread().getName(), msg, t);
@@ -617,21 +628,76 @@ public synchronized void error(String msg, Throwable t) {
617628
this.err.format("[ERROR] (%s) %s - %s\n", Thread.currentThread().getName(), msg, t);
618629
t.printStackTrace(this.err);
619630
}
631+
632+
@Override
633+
public String toString() {
634+
return "ConsoleLogger[name="+getName()+", verbose="+identifier.verbose+"]";
635+
}
620636
}
621637

622-
private static final class ConsoleLoggerFactory implements Function<String, Logger> {
638+
/**
639+
* A key object to serve a dual purpose:
640+
* <ul>
641+
* <li>Allow consistent identification of cached console loggers using not
642+
* only its name, but also its verbosity level</li>
643+
* <li>Provide an object eligible to cache eviction. Contrary to a logger or
644+
* a string (logger name) object, this is a good candidate for weak reference key,
645+
* because it should be held only internally by the attached logger and by the
646+
* logger cache (as evictable key).</li>
647+
* </ul>
648+
*/
649+
private static final class ConsoleLoggerKey {
623650

624-
private static final HashMap<String, Logger> consoleLoggers = new HashMap<>();
651+
private final String name;
652+
private final boolean verbose;
653+
654+
private ConsoleLoggerKey(String name, boolean verbose) {
655+
this.name = name;
656+
this.verbose = verbose;
657+
}
658+
659+
@Override
660+
public boolean equals(Object o) {
661+
if (this == o) {
662+
return true;
663+
}
664+
if (o == null || getClass() != o.getClass()) {
665+
return false;
666+
}
667+
ConsoleLoggerKey key = (ConsoleLoggerKey) o;
668+
return verbose == key.verbose && Objects.equals(name, key.name);
669+
}
670+
671+
@Override
672+
public int hashCode() {
673+
return Objects.hash(name, verbose);
674+
}
675+
}
676+
677+
static final class ConsoleLoggerFactory implements Function<String, Logger> {
678+
679+
private static final Map<ConsoleLoggerKey, WeakReference<Logger>> consoleLoggers =
680+
new WeakHashMap<>();
625681

626682
final boolean verbose;
627683

628-
private ConsoleLoggerFactory(boolean verbose) {
684+
ConsoleLoggerFactory(boolean verbose) {
629685
this.verbose = verbose;
630686
}
631687

632688
@Override
633689
public Logger apply(String name) {
634-
return consoleLoggers.computeIfAbsent(name, n -> new ConsoleLogger(n, verbose));
690+
final ConsoleLoggerKey key = new ConsoleLoggerKey(name, verbose);
691+
synchronized (consoleLoggers) {
692+
final WeakReference<Logger> ref = consoleLoggers.get(key);
693+
Logger cached = ref == null ? null : ref.get();
694+
if (cached == null) {
695+
cached = new ConsoleLogger(key);
696+
consoleLoggers.put(key, new WeakReference<>(cached));
697+
}
698+
699+
return cached;
700+
}
635701
}
636702
}
637703

reactor-core/src/test/java/reactor/util/ConsoleLoggerTest.java

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved.
2+
* Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,12 +18,15 @@
1818

1919
import java.io.ByteArrayOutputStream;
2020
import java.io.PrintStream;
21+
import java.util.stream.IntStream;
2122

2223
import org.junit.jupiter.api.AfterEach;
2324
import org.junit.jupiter.api.BeforeEach;
2425
import org.junit.jupiter.api.Test;
26+
import reactor.test.util.RaceTestUtils;
2527

2628
import static org.assertj.core.api.Assertions.assertThat;
29+
import static org.assertj.core.api.Assertions.fail;
2730

2831
public class ConsoleLoggerTest {
2932

@@ -296,4 +299,39 @@ public void formatNull() {
296299
.isEqualTo("[ INFO] (" + Thread.currentThread().getName() + ") null\n");
297300
}
298301

302+
/**
303+
* Ensure console logger factory synchronizes logger acquisition properly.
304+
*/
305+
@Test
306+
public void getConsoleLoggerShouldBeThreadSafe() {
307+
final Loggers.ConsoleLoggerFactory factory =
308+
new Loggers.ConsoleLoggerFactory(false);
309+
final String loggerName = "logger.thread-safety.test";
310+
final Runnable acquireLogger = () -> assertThat(factory.apply(loggerName))
311+
.isNotNull()
312+
.extracting(Logger::getName)
313+
.isEqualTo(loggerName);
314+
try {
315+
Runnable[] loggerAcquisitionFunctions =
316+
IntStream.range(0, 5)
317+
.mapToObj(i -> acquireLogger)
318+
.toArray(Runnable[]::new);
319+
RaceTestUtils.race(loggerAcquisitionFunctions);
320+
} catch (Exception e) {
321+
fail("Cannot acquire a console logger", e);
322+
}
323+
}
324+
325+
@Test
326+
public void consoleLoggerCacheDoesNotCorruptVerbosity() {
327+
final String loggerName = "console.cache.test";
328+
final Logger verboseLogger = new Loggers.ConsoleLoggerFactory(true)
329+
.apply(loggerName);
330+
final Logger notVerboseLogger = new Loggers.ConsoleLoggerFactory(false)
331+
.apply(loggerName);
332+
333+
assertThat(verboseLogger)
334+
.as("Logger verbosity should not match")
335+
.isNotEqualTo(notVerboseLogger);
336+
}
299337
}

0 commit comments

Comments
 (0)