|
16 | 16 | */
|
17 | 17 | package org.apache.logging.log4j.core.util.internal.instant;
|
18 | 18 |
|
| 19 | +import static java.util.concurrent.Executors.newSingleThreadExecutor; |
19 | 20 | import static org.assertj.core.api.Assertions.assertThat;
|
20 | 21 | import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
21 | 22 | import static org.mockito.ArgumentMatchers.any;
|
22 | 23 | import static org.mockito.ArgumentMatchers.eq;
|
23 | 24 | import static org.mockito.Mockito.doAnswer;
|
24 | 25 | import static org.mockito.Mockito.mock;
|
| 26 | +import static org.mockito.Mockito.times; |
25 | 27 | import static org.mockito.Mockito.verify;
|
26 | 28 | import static org.mockito.Mockito.verifyNoMoreInteractions;
|
27 | 29 | import static org.mockito.Mockito.when;
|
|
30 | 32 | import java.util.Locale;
|
31 | 33 | import java.util.Random;
|
32 | 34 | import java.util.TimeZone;
|
| 35 | +import java.util.concurrent.ExecutionException; |
| 36 | +import java.util.concurrent.ExecutorService; |
33 | 37 | import java.util.function.Function;
|
34 | 38 | import org.apache.logging.log4j.core.time.Instant;
|
35 | 39 | import org.apache.logging.log4j.core.time.MutableInstant;
|
36 | 40 | import org.junit.jupiter.api.Test;
|
37 | 41 | import org.junit.jupiter.params.ParameterizedTest;
|
38 | 42 | import org.junit.jupiter.params.provider.MethodSource;
|
39 | 43 | import org.junit.jupiter.params.provider.ValueSource;
|
| 44 | +import org.junitpioneer.jupiter.Issue; |
40 | 45 |
|
41 | 46 | class InstantPatternThreadLocalCachedFormatterTest {
|
42 | 47 |
|
@@ -289,4 +294,53 @@ private static MutableInstant createInstant(final long epochMillis, final int ep
|
289 | 294 | instant.initFromEpochMilli(epochMillis, epochMillisNanos);
|
290 | 295 | return instant;
|
291 | 296 | }
|
| 297 | + |
| 298 | + @Test |
| 299 | + @Issue("https://github.com/apache/logging-log4j2/issues/3792") |
| 300 | + void should_be_thread_safe() throws Exception { |
| 301 | + // Instead of randomly testing the thread safety, we test that the current implementation does not |
| 302 | + // cache results across threads. |
| 303 | + // |
| 304 | + // Modify this test if the implementation changes in the future. |
| 305 | + final InstantPatternFormatter patternFormatter = mock(InstantPatternFormatter.class); |
| 306 | + when(patternFormatter.getPrecision()).thenReturn(ChronoUnit.MILLIS); |
| 307 | + |
| 308 | + final Instant instant = INSTANT0; |
| 309 | + final String output = "thread-output"; |
| 310 | + doAnswer(invocation -> { |
| 311 | + StringBuilder buffer = invocation.getArgument(0); |
| 312 | + buffer.append(output) |
| 313 | + .append('-') |
| 314 | + .append(Thread.currentThread().getName()); |
| 315 | + return null; |
| 316 | + }) |
| 317 | + .when(patternFormatter) |
| 318 | + .formatTo(any(StringBuilder.class), eq(instant)); |
| 319 | + |
| 320 | + final InstantFormatter cachedFormatter = |
| 321 | + InstantPatternThreadLocalCachedFormatter.ofMilliPrecision(patternFormatter); |
| 322 | + |
| 323 | + final int threadCount = 2; |
| 324 | + for (int i = 0; i < threadCount; i++) { |
| 325 | + formatOnNewThread(cachedFormatter, instant, output); |
| 326 | + } |
| 327 | + verify(patternFormatter, times(threadCount)).formatTo(any(StringBuilder.class), eq(instant)); |
| 328 | + } |
| 329 | + |
| 330 | + private static void formatOnNewThread( |
| 331 | + final InstantFormatter formatter, final Instant instant, final String expectedOutput) |
| 332 | + throws ExecutionException, InterruptedException { |
| 333 | + ExecutorService executor = newSingleThreadExecutor(); |
| 334 | + try { |
| 335 | + executor.submit(() -> { |
| 336 | + String formatted = formatter.format(instant); |
| 337 | + assertThat(formatted) |
| 338 | + .isEqualTo(expectedOutput + "-" |
| 339 | + + Thread.currentThread().getName()); |
| 340 | + }) |
| 341 | + .get(); |
| 342 | + } finally { |
| 343 | + executor.shutdown(); |
| 344 | + } |
| 345 | + } |
292 | 346 | }
|
0 commit comments