mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-17 12:09:16 +00:00
Add support for context propagation in task execution
Closes gh-48033
This commit is contained in:
@@ -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")
|
||||
|
||||
+2
-1
@@ -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 })
|
||||
|
||||
+13
@@ -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;
|
||||
}
|
||||
|
||||
+15
@@ -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 })
|
||||
|
||||
+94
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-2
@@ -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[]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user