Skip to content

Commit

Permalink
Add support for thread context map propagation
Browse files Browse the repository at this point in the history
Adds a new `ExtendedThreadContext` SPI class to help external libraries
with an efficient propagation of the `ThreadContextMap`.

Currently integrators can retrieve and set the context map through a
combination of `ThreadContext#getImmutableContext`, `#clear` and
`#putAll`. This inevitable leads to the creation of temporary objects if
the underlying implementation does not use the JDK `Map` class
internally.

As a replacement we provide two `saveMap` and `restoreMap` methods that
can be used to access the `ThreadLocal`s directly. Due to the inherent
unsafety of such operations, these methods are only available from the
`o.a.l.l.spi` package.

We also reimplement `CloseableThreadContext.Instance` to take advantage
of this SPI.
  • Loading branch information
ppkarwasz committed Apr 4, 2024
1 parent 01fd727 commit bf9c703
Show file tree
Hide file tree
Showing 17 changed files with 496 additions and 89 deletions.
11 changes: 6 additions & 5 deletions log4j-api-test/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
<bnd-module-name>org.apache.logging.log4j.test</bnd-module-name>
<bnd-extra-package-options>
org.apache.commons.lang3.*;resolution:=optional,
org.assertj.*;resolution:=optional,
<!-- Both JUnit 4 and JUnit 5 are not required -->
org.junit.*;resolution:=optional,
org.hamcrest.*;resolution:=optional,
Expand All @@ -48,6 +49,7 @@
<bnd-extra-module-options>
<!-- Non-transitive static modules -->
junit;transitive=false,
org.assertj.core;transitive=false,
org.hamcrest;transitive=false,
org.junit.jupiter.api;transitive=false,
org.junitpioneer;transitive=false,
Expand All @@ -72,6 +74,10 @@
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
Expand Down Expand Up @@ -108,11 +114,6 @@
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-utils</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<!-- Required for JSON support -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.logging.log4j.test.spi;

import static org.assertj.core.api.Assertions.assertThat;

import java.time.Duration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.logging.log4j.spi.ThreadContextMap;

/**
* A simple suite of tests for {@link ThreadContextMap} implementations.
*/
public abstract class ThreadContextMapSuite {
private static final String KEY = "key";

/**
* Checks if new threads either inherit or do not inherit the context data of the current thread.
*
* @param threadContext A {@link ThreadContextMap implementation}.
* @param key The key to use.
* @param expectedValue The expected value on a new thread.
*/
protected static void assertThreadContextValueOnANewThread(
final ThreadContextMap threadContext, final String key, final String expectedValue) {
final ExecutorService executorService = Executors.newSingleThreadExecutor();
try {
assertThat(executorService.submit(() -> threadContext.get(key)))
.succeedsWithin(Duration.ofSeconds(1))
.isEqualTo(expectedValue);
} finally {
executorService.shutdown();
}
}

/**
* Ensures that {@code save/restore} works correctly on the current thread.
*/
protected static void assertContextDataCanBeSavedAndRestored(final ThreadContextMap threadContext) {
final String externalValue = "externalValue";
final String internalValue = "internalValue";
final RuntimeException throwable = new RuntimeException();
threadContext.put(KEY, externalValue);
final Object saved = threadContext.save();
try {
threadContext.put(KEY, internalValue);
assertThat(threadContext.get(KEY)).isEqualTo(internalValue);
throw throwable;
} catch (final RuntimeException e) {
assertThat(e).isSameAs(throwable);
assertThat(threadContext.get(KEY)).isEqualTo(internalValue);
} finally {
threadContext.restore(saved);
}
assertThat(threadContext.get(KEY)).isEqualTo(externalValue);
}

/**
* Ensures that the context data obtained through {@link ThreadContextMap#save} can be safely transferred to another
* thread.
*
* @param threadContext The {@link ThreadContextMap} to test.
*/
protected static void assertContextDataCanBeTransferred(final ThreadContextMap threadContext) {
final String mainThreadValue = "mainThreadValue";
final String newThreadValue = "newThreadValue";
final RuntimeException throwable = new RuntimeException();
threadContext.put(KEY, mainThreadValue);
final Object mainThreadSaved = threadContext.save();
threadContext.remove(KEY);
// Move to new thread
final ExecutorService executorService = Executors.newSingleThreadExecutor();
try {
assertThat(executorService.submit(() -> {
threadContext.put(KEY, newThreadValue);
final Object newThreadSaved = threadContext.restore(mainThreadSaved);
try {
assertThat(threadContext.get(KEY)).isEqualTo(mainThreadValue);
throw throwable;
} catch (final RuntimeException e) {
assertThat(e).isSameAs(throwable);
assertThat(threadContext.get(KEY)).isEqualTo(mainThreadValue);
} finally {
threadContext.restore(newThreadSaved);
}
assertThat(threadContext.get(KEY)).isEqualTo(newThreadValue);
}))
.succeedsWithin(Duration.ofSeconds(1));
} finally {
executorService.shutdown();
}
}

/**
* Ensures the {@code restore(null)} can be used to clear the context data.
*
* @param threadContext The {@link ThreadContextMap} to test.
*/
protected static void assertNullValueClearsTheContextData(final ThreadContextMap threadContext) {
final String value = "restoreAcceptsNull";
final RuntimeException throwable = new RuntimeException();
threadContext.put(KEY, value);
final Object saved = threadContext.restore(null);
try {
assertThat(threadContext.getImmutableMapOrNull()).isNullOrEmpty();
throw throwable;
} catch (final RuntimeException e) {
assertThat(e).isSameAs(throwable);
assertThat(threadContext.getImmutableMapOrNull()).isNullOrEmpty();
} finally {
threadContext.restore(saved);
}
assertThat(threadContext.get(KEY)).isEqualTo(value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache license, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the license for the specific language governing permissions and
* limitations under the license.
*/
/**
* Contains helper classes to blackbox test implementations of SPI interfaces.
* @since 2.24.0
*/
@Export
@Version("2.24.0")
package org.apache.logging.log4j.test.spi;

import org.osgi.annotation.bundle.Export;
import org.osgi.annotation.versioning.Version;
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,18 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.logging.log4j.test.junit.Log4jStaticResources;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.ResourceAccessMode;
import org.junit.jupiter.api.parallel.ResourceLock;
import org.junit.jupiter.api.parallel.Resources;

/**
* Tests {@link CloseableThreadContext}.
*
* @since 2.6
*/
@ResourceLock(value = Resources.SYSTEM_PROPERTIES, mode = ResourceAccessMode.READ)
@ResourceLock(Log4jStaticResources.THREAD_CONTEXT)
public class CloseableThreadContextTest {

private final String key = "key";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,61 +16,85 @@
*/
package org.apache.logging.log4j.spi;

import static org.assertj.core.api.Assertions.assertThat;

import java.time.Duration;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Stream;
import org.apache.logging.log4j.internal.map.StringArrayThreadContextMap;
import org.apache.logging.log4j.test.spi.ThreadContextMapSuite;
import org.apache.logging.log4j.util.Lazy;
import org.apache.logging.log4j.util.PropertiesUtil;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

class ThreadContextMapTest {
@Execution(ExecutionMode.CONCURRENT)
class ThreadContextMapTest extends ThreadContextMapSuite {

private static final String KEY = "key";
private static final Lazy<PropertiesUtil> defaultMapProperties = Lazy.pure(() -> createMapProperties(false));
private static final Lazy<PropertiesUtil> inheritableMapProperties = Lazy.pure(() -> createMapProperties(true));

private static PropertiesUtil createMapProperties(final boolean inheritable) {
final Properties props = new Properties();
// By specifying all the possible properties, the resulting thread context maps do not depend on other
// property sources like Java system properties.
props.setProperty("log4j2.threadContextInitialCapacity", "16");
props.setProperty("log4j2.isThreadContextMapInheritable", inheritable ? "true" : "false");
return new PropertiesUtil(props);
}

static Stream<ThreadContextMap> defaultMaps() {
return Stream.of(
new DefaultThreadContextMap(),
new CopyOnWriteSortedArrayThreadContextMap(),
new GarbageFreeSortedArrayThreadContextMap());
new GarbageFreeSortedArrayThreadContextMap(),
new StringArrayThreadContextMap());
}

static Stream<ThreadContextMap> localMaps() {
return Stream.of(
new DefaultThreadContextMap(true, defaultMapProperties.get()),
new CopyOnWriteSortedArrayThreadContextMap(defaultMapProperties.get()),
new GarbageFreeSortedArrayThreadContextMap(defaultMapProperties.get()),
new StringArrayThreadContextMap());
}

static Stream<ThreadContextMap> inheritableMaps() {
final Properties props = new Properties();
props.setProperty("log4j2.isThreadContextMapInheritable", "true");
final PropertiesUtil util = new PropertiesUtil(props);
return Stream.of(
new DefaultThreadContextMap(true, util),
new CopyOnWriteSortedArrayThreadContextMap(util),
new GarbageFreeSortedArrayThreadContextMap(util));
new DefaultThreadContextMap(true, inheritableMapProperties.get()),
new CopyOnWriteSortedArrayThreadContextMap(inheritableMapProperties.get()),
new GarbageFreeSortedArrayThreadContextMap(inheritableMapProperties.get()));
}

@ParameterizedTest
@MethodSource("defaultMaps")
void threadLocalNotInheritableByDefault(final ThreadContextMap contextMap) {
contextMap.put(KEY, "threadLocalNotInheritableByDefault");
verifyThreadContextValueFromANewThread(contextMap, null);
void threadLocalNotInheritableByDefault(final ThreadContextMap threadContext) {
threadContext.put(KEY, "threadLocalNotInheritableByDefault");
assertThreadContextValueOnANewThread(threadContext, KEY, null);
}

@ParameterizedTest
@MethodSource("inheritableMaps")
void threadLocalInheritableIfConfigured(final ThreadContextMap contextMap) {
contextMap.put(KEY, "threadLocalInheritableIfConfigured");
verifyThreadContextValueFromANewThread(contextMap, "threadLocalInheritableIfConfigured");
void threadLocalInheritableIfConfigured(final ThreadContextMap threadContext) {
threadContext.put(KEY, "threadLocalInheritableIfConfigured");
assertThreadContextValueOnANewThread(threadContext, KEY, "threadLocalInheritableIfConfigured");
}

private static void verifyThreadContextValueFromANewThread(
final ThreadContextMap contextMap, final String expected) {
final ExecutorService executorService = Executors.newSingleThreadExecutor();
try {
assertThat(executorService.submit(() -> contextMap.get(KEY)))
.succeedsWithin(Duration.ofSeconds(1))
.isEqualTo(expected);
} finally {
executorService.shutdown();
}
@ParameterizedTest
@MethodSource("localMaps")
void saveAndRestoreMap(final ThreadContextMap threadContext) {
assertContextDataCanBeSavedAndRestored(threadContext);
}

@ParameterizedTest
@MethodSource("localMaps")
void saveAndRestoreMapOnAnotherThread(final ThreadContextMap threadContext) {
assertContextDataCanBeTransferred(threadContext);
}

@ParameterizedTest
@MethodSource("localMaps")
void restoreAcceptsNull(final ThreadContextMap threadContext) {
assertNullValueClearsTheContextData(threadContext);
}
}

0 comments on commit bf9c703

Please sign in to comment.