Add support for context propagation in task execution

Closes gh-48033
This commit is contained in:
Stéphane Nicoll
2025-12-22 10:24:41 +01:00
parent ec9901c337
commit 644ac08886
7 changed files with 129 additions and 4 deletions
+1 -1
View File
@@ -62,7 +62,7 @@
<subpackage name=".*\.metrics" regex="true">
<allow pkg="io.micrometer" />
</subpackage>
<subpackage name=".*\.autoconfigure" regex="true">
<subpackage name="(.*\.autoconfigure|autoconfigure)(\..*)?" regex="true">
<allow pkg="io.micrometer" />
</subpackage>
<subpackage name="docs">
@@ -30,6 +30,7 @@ dependencies {
optional("com.github.ben-manes.caffeine:caffeine")
optional("org.aspectj:aspectjweaver")
optional("io.micrometer:context-propagation")
optional("jakarta.servlet:jakarta.servlet-api")
optional("javax.money:money-api")
optional("org.springframework:spring-web")
@@ -35,7 +35,8 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@ConditionalOnClass(ThreadPoolTaskExecutor.class)
@AutoConfiguration
@EnableConfigurationProperties(TaskExecutionProperties.class)
@Import({ TaskExecutorConfigurations.ThreadPoolTaskExecutorBuilderConfiguration.class,
@Import({ TaskExecutorConfigurations.TaskExecutorContextPropagationConfiguration.class,
TaskExecutorConfigurations.ThreadPoolTaskExecutorBuilderConfiguration.class,
TaskExecutorConfigurations.SimpleAsyncTaskExecutorBuilderConfiguration.class,
TaskExecutorConfigurations.TaskExecutorConfiguration.class,
TaskExecutorConfigurations.BootstrapExecutorConfiguration.class })
@@ -44,6 +44,11 @@ public class TaskExecutionProperties {
*/
private Mode mode = Mode.AUTO;
/**
* Whether to propagate the current context to task executions.
*/
private boolean propagateContext;
/**
* Prefix to use for the names of newly created threads.
*/
@@ -69,6 +74,14 @@ public class TaskExecutionProperties {
this.mode = mode;
}
public boolean getPropagateContext() {
return this.propagateContext;
}
public void setPropagateContext(boolean propagateContext) {
this.propagateContext = propagateContext;
}
public String getThreadNamePrefix() {
return this.threadNamePrefix;
}
@@ -19,6 +19,7 @@ package org.springframework.boot.autoconfigure.task;
import java.util.List;
import java.util.concurrent.Executor;
import io.micrometer.context.ContextSnapshot;
import org.jspecify.annotations.Nullable;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
@@ -29,6 +30,7 @@ import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading;
@@ -47,6 +49,7 @@ import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.TaskDecorator;
import org.springframework.core.task.TaskExecutor;
import org.springframework.core.task.support.CompositeTaskDecorator;
import org.springframework.core.task.support.ContextPropagatingTaskDecorator;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@@ -68,6 +71,18 @@ class TaskExecutorConfigurations {
return (!taskDecorators.isEmpty()) ? new CompositeTaskDecorator(taskDecorators) : null;
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ContextSnapshot.class)
static class TaskExecutorContextPropagationConfiguration {
@Bean
@ConditionalOnProperty(name = "spring.task.execution.propagate-context", havingValue = "true")
ContextPropagatingTaskDecorator contextPropagatingTaskDecorator() {
return new ContextPropagatingTaskDecorator();
}
}
@Configuration(proxyBeanMethods = false)
@Conditional(OnExecutorCondition.class)
@Import({ AsyncConfigurerWrapperConfiguration.class, AsyncConfigurerConfiguration.class })
@@ -16,6 +16,10 @@
package org.springframework.boot.autoconfigure.task;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
@@ -24,6 +28,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import io.micrometer.context.ThreadLocalAccessor;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
@@ -42,6 +47,7 @@ import org.springframework.boot.test.context.assertj.AssertableApplicationContex
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.ContextConsumer;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.boot.testsupport.classpath.resources.WithResource;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -50,6 +56,7 @@ import org.springframework.core.task.SyncTaskExecutor;
import org.springframework.core.task.TaskDecorator;
import org.springframework.core.task.TaskExecutor;
import org.springframework.core.task.support.CompositeTaskDecorator;
import org.springframework.core.task.support.ContextPropagatingTaskDecorator;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncConfigurer;
@@ -253,6 +260,35 @@ class TaskExecutionAutoConfigurationTests {
});
}
@Test
@WithResource(name = "META-INF/services/io.micrometer.context.ThreadLocalAccessor",
content = "org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfigurationTests$TestThreadLocalAccessor")
void asyncTaskExecutorShouldNotNotRegisterContextPropagatingTaskDecoratorByDefault() {
this.contextRunner.withUserConfiguration(AsyncConfiguration.class, TestBean.class).run((context) -> {
assertThat(context).doesNotHaveBean(ContextPropagatingTaskDecorator.class);
TestBean bean = context.getBean(TestBean.class);
TestThreadLocalHolder.setValue("from-context");
String text = bean.echoContext().get();
assertThat(text).contains("task-").endsWith("null");
});
}
@Test
@WithResource(name = "META-INF/services/io.micrometer.context.ThreadLocalAccessor",
content = "org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfigurationTests$TestThreadLocalAccessor")
void asyncTaskExecutorWhenContextPropagationIsEnabledShouldRegisterBean() {
this.contextRunner.withUserConfiguration(AsyncConfiguration.class, TestBean.class)
.withPropertyValues("spring.task.execution.propagate-context=true")
.run((context) -> {
assertThat(context).hasSingleBean(ContextPropagatingTaskDecorator.class);
TestBean bean = context.getBean(TestBean.class);
TestThreadLocalHolder.setValue("from-context");
String text = bean.echoContext().get();
assertThat(text).contains("task-").endsWith("from-context");
});
}
@Test
void taskExecutorWhenHasCustomTaskExecutorShouldBackOff() {
this.contextRunner.withBean("customTaskExecutor", Executor.class, SyncTaskExecutor::new).run((context) -> {
@@ -591,6 +627,14 @@ class TaskExecutionAutoConfigurationTests {
return thread.getName();
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@WithResource(name = "META-INF/services/io.micrometer.context.ThreadLocalAccessor",
content = "org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfigurationTests.TestThreadLocalAccessor")
@interface WithThreadLocalAccessor {
}
@Configuration(proxyBeanMethods = false)
static class CustomThreadPoolTaskExecutorBuilderConfig {
@@ -622,6 +666,56 @@ class TaskExecutionAutoConfigurationTests {
return CompletableFuture.completedFuture(Thread.currentThread().getName() + " " + text);
}
@Async
Future<String> echoContext() {
return CompletableFuture
.completedFuture(Thread.currentThread().getName() + " " + TestThreadLocalHolder.getValue());
}
}
static class TestThreadLocalHolder {
private static final ThreadLocal<String> holder = new ThreadLocal<>();
static void setValue(String value) {
holder.set(value);
}
static String getValue() {
return holder.get();
}
static void reset() {
holder.remove();
}
}
public static class TestThreadLocalAccessor implements ThreadLocalAccessor<String> {
static final String KEY = "test.threadlocal";
@Override
public Object key() {
return KEY;
}
@Override
public String getValue() {
return TestThreadLocalHolder.getValue();
}
@Override
public void setValue(String value) {
TestThreadLocalHolder.setValue(value);
}
@Override
public void setValue() {
TestThreadLocalHolder.reset();
}
}
}
@@ -29,8 +29,9 @@ Observability support relies on the https://github.com/micrometer-metrics/contex
By default, javadoc:java.lang.ThreadLocal[] values are not automatically reinstated in reactive operators.
This behavior is controlled with the configprop:spring.reactor.context-propagation[] property, which can be set to `auto` to enable automatic propagation.
If you're working with javadoc:org.springframework.scheduling.annotation.Async[format=annotation] methods or use an javadoc:org.springframework.core.task.AsyncTaskExecutor[], you have to register the javadoc:org.springframework.core.task.support.ContextPropagatingTaskDecorator[] on the executor, otherwise the observability context is lost when switching threads.
This can be done using this configuration:
If you're working with javadoc:org.springframework.scheduling.annotation.Async[format=annotation] methods and the javadoc:org.springframework.core.task.AsyncTaskExecutor[] is auto-configured, you have to opt-in for context propagation using the configprop:spring.task.execution.propagate-context[] property.
If you are configuring the javadoc:org.springframework.core.task.AsyncTaskExecutor[] yourself, then you need to register a javadoc:org.springframework.core.task.support.ContextPropagatingTaskDecorator[] bean, as shown in the following example:
include-code::ContextPropagationConfiguration[]