Skip to content

Commit 644ac08

Browse files
committed
Add support for context propagation in task execution
Closes gh-48033
1 parent ec9901c commit 644ac08

File tree

7 files changed

+129
-4
lines changed

7 files changed

+129
-4
lines changed

config/checkstyle/import-control.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
<subpackage name=".*\.metrics" regex="true">
6363
<allow pkg="io.micrometer" />
6464
</subpackage>
65-
<subpackage name=".*\.autoconfigure" regex="true">
65+
<subpackage name="(.*\.autoconfigure|autoconfigure)(\..*)?" regex="true">
6666
<allow pkg="io.micrometer" />
6767
</subpackage>
6868
<subpackage name="docs">

core/spring-boot-autoconfigure/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ dependencies {
3030

3131
optional("com.github.ben-manes.caffeine:caffeine")
3232
optional("org.aspectj:aspectjweaver")
33+
optional("io.micrometer:context-propagation")
3334
optional("jakarta.servlet:jakarta.servlet-api")
3435
optional("javax.money:money-api")
3536
optional("org.springframework:spring-web")

core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
@ConditionalOnClass(ThreadPoolTaskExecutor.class)
3636
@AutoConfiguration
3737
@EnableConfigurationProperties(TaskExecutionProperties.class)
38-
@Import({ TaskExecutorConfigurations.ThreadPoolTaskExecutorBuilderConfiguration.class,
38+
@Import({ TaskExecutorConfigurations.TaskExecutorContextPropagationConfiguration.class,
39+
TaskExecutorConfigurations.ThreadPoolTaskExecutorBuilderConfiguration.class,
3940
TaskExecutorConfigurations.SimpleAsyncTaskExecutorBuilderConfiguration.class,
4041
TaskExecutorConfigurations.TaskExecutorConfiguration.class,
4142
TaskExecutorConfigurations.BootstrapExecutorConfiguration.class })

core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ public class TaskExecutionProperties {
4444
*/
4545
private Mode mode = Mode.AUTO;
4646

47+
/**
48+
* Whether to propagate the current context to task executions.
49+
*/
50+
private boolean propagateContext;
51+
4752
/**
4853
* Prefix to use for the names of newly created threads.
4954
*/
@@ -69,6 +74,14 @@ public void setMode(Mode mode) {
6974
this.mode = mode;
7075
}
7176

77+
public boolean getPropagateContext() {
78+
return this.propagateContext;
79+
}
80+
81+
public void setPropagateContext(boolean propagateContext) {
82+
this.propagateContext = propagateContext;
83+
}
84+
7285
public String getThreadNamePrefix() {
7386
return this.threadNamePrefix;
7487
}

core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.List;
2020
import java.util.concurrent.Executor;
2121

22+
import io.micrometer.context.ContextSnapshot;
2223
import org.jspecify.annotations.Nullable;
2324

2425
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
@@ -29,6 +30,7 @@
2930
import org.springframework.beans.factory.config.BeanPostProcessor;
3031
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
3132
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
33+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
3234
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
3335
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
3436
import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading;
@@ -47,6 +49,7 @@
4749
import org.springframework.core.task.TaskDecorator;
4850
import org.springframework.core.task.TaskExecutor;
4951
import org.springframework.core.task.support.CompositeTaskDecorator;
52+
import org.springframework.core.task.support.ContextPropagatingTaskDecorator;
5053
import org.springframework.scheduling.annotation.AsyncConfigurer;
5154
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
5255

@@ -68,6 +71,18 @@ class TaskExecutorConfigurations {
6871
return (!taskDecorators.isEmpty()) ? new CompositeTaskDecorator(taskDecorators) : null;
6972
}
7073

74+
@Configuration(proxyBeanMethods = false)
75+
@ConditionalOnClass(ContextSnapshot.class)
76+
static class TaskExecutorContextPropagationConfiguration {
77+
78+
@Bean
79+
@ConditionalOnProperty(name = "spring.task.execution.propagate-context", havingValue = "true")
80+
ContextPropagatingTaskDecorator contextPropagatingTaskDecorator() {
81+
return new ContextPropagatingTaskDecorator();
82+
}
83+
84+
}
85+
7186
@Configuration(proxyBeanMethods = false)
7287
@Conditional(OnExecutorCondition.class)
7388
@Import({ AsyncConfigurerWrapperConfiguration.class, AsyncConfigurerConfiguration.class })

core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

1717
package org.springframework.boot.autoconfigure.task;
1818

19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
1923
import java.util.concurrent.CompletableFuture;
2024
import java.util.concurrent.CountDownLatch;
2125
import java.util.concurrent.Executor;
@@ -24,6 +28,7 @@
2428
import java.util.concurrent.atomic.AtomicReference;
2529
import java.util.function.Consumer;
2630

31+
import io.micrometer.context.ThreadLocalAccessor;
2732
import org.assertj.core.api.InstanceOfAssertFactories;
2833
import org.jspecify.annotations.Nullable;
2934
import org.junit.jupiter.api.Test;
@@ -42,6 +47,7 @@
4247
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
4348
import org.springframework.boot.test.context.runner.ContextConsumer;
4449
import org.springframework.boot.test.system.OutputCaptureExtension;
50+
import org.springframework.boot.testsupport.classpath.resources.WithResource;
4551
import org.springframework.context.ConfigurableApplicationContext;
4652
import org.springframework.context.annotation.Bean;
4753
import org.springframework.context.annotation.Configuration;
@@ -50,6 +56,7 @@
5056
import org.springframework.core.task.TaskDecorator;
5157
import org.springframework.core.task.TaskExecutor;
5258
import org.springframework.core.task.support.CompositeTaskDecorator;
59+
import org.springframework.core.task.support.ContextPropagatingTaskDecorator;
5360
import org.springframework.scheduling.TaskScheduler;
5461
import org.springframework.scheduling.annotation.Async;
5562
import org.springframework.scheduling.annotation.AsyncConfigurer;
@@ -253,6 +260,35 @@ void simpleAsyncTaskExecutorBuilderUsesVirtualThreadsWhenEnabled() {
253260
});
254261
}
255262

263+
@Test
264+
@WithResource(name = "META-INF/services/io.micrometer.context.ThreadLocalAccessor",
265+
content = "org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfigurationTests$TestThreadLocalAccessor")
266+
void asyncTaskExecutorShouldNotNotRegisterContextPropagatingTaskDecoratorByDefault() {
267+
this.contextRunner.withUserConfiguration(AsyncConfiguration.class, TestBean.class).run((context) -> {
268+
assertThat(context).doesNotHaveBean(ContextPropagatingTaskDecorator.class);
269+
TestBean bean = context.getBean(TestBean.class);
270+
TestThreadLocalHolder.setValue("from-context");
271+
String text = bean.echoContext().get();
272+
assertThat(text).contains("task-").endsWith("null");
273+
});
274+
275+
}
276+
277+
@Test
278+
@WithResource(name = "META-INF/services/io.micrometer.context.ThreadLocalAccessor",
279+
content = "org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfigurationTests$TestThreadLocalAccessor")
280+
void asyncTaskExecutorWhenContextPropagationIsEnabledShouldRegisterBean() {
281+
this.contextRunner.withUserConfiguration(AsyncConfiguration.class, TestBean.class)
282+
.withPropertyValues("spring.task.execution.propagate-context=true")
283+
.run((context) -> {
284+
assertThat(context).hasSingleBean(ContextPropagatingTaskDecorator.class);
285+
TestBean bean = context.getBean(TestBean.class);
286+
TestThreadLocalHolder.setValue("from-context");
287+
String text = bean.echoContext().get();
288+
assertThat(text).contains("task-").endsWith("from-context");
289+
});
290+
}
291+
256292
@Test
257293
void taskExecutorWhenHasCustomTaskExecutorShouldBackOff() {
258294
this.contextRunner.withBean("customTaskExecutor", Executor.class, SyncTaskExecutor::new).run((context) -> {
@@ -591,6 +627,14 @@ private String virtualThreadName(SimpleAsyncTaskExecutor taskExecutor) throws In
591627
return thread.getName();
592628
}
593629

630+
@Target(ElementType.METHOD)
631+
@Retention(RetentionPolicy.RUNTIME)
632+
@WithResource(name = "META-INF/services/io.micrometer.context.ThreadLocalAccessor",
633+
content = "org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfigurationTests.TestThreadLocalAccessor")
634+
@interface WithThreadLocalAccessor {
635+
636+
}
637+
594638
@Configuration(proxyBeanMethods = false)
595639
static class CustomThreadPoolTaskExecutorBuilderConfig {
596640

@@ -622,6 +666,56 @@ Future<String> echo(String text) {
622666
return CompletableFuture.completedFuture(Thread.currentThread().getName() + " " + text);
623667
}
624668

669+
@Async
670+
Future<String> echoContext() {
671+
return CompletableFuture
672+
.completedFuture(Thread.currentThread().getName() + " " + TestThreadLocalHolder.getValue());
673+
}
674+
675+
}
676+
677+
static class TestThreadLocalHolder {
678+
679+
private static final ThreadLocal<String> holder = new ThreadLocal<>();
680+
681+
static void setValue(String value) {
682+
holder.set(value);
683+
}
684+
685+
static String getValue() {
686+
return holder.get();
687+
}
688+
689+
static void reset() {
690+
holder.remove();
691+
}
692+
693+
}
694+
695+
public static class TestThreadLocalAccessor implements ThreadLocalAccessor<String> {
696+
697+
static final String KEY = "test.threadlocal";
698+
699+
@Override
700+
public Object key() {
701+
return KEY;
702+
}
703+
704+
@Override
705+
public String getValue() {
706+
return TestThreadLocalHolder.getValue();
707+
}
708+
709+
@Override
710+
public void setValue(String value) {
711+
TestThreadLocalHolder.setValue(value);
712+
}
713+
714+
@Override
715+
public void setValue() {
716+
TestThreadLocalHolder.reset();
717+
}
718+
625719
}
626720

627721
}

documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ Observability support relies on the https://github.com/micrometer-metrics/contex
2929
By default, javadoc:java.lang.ThreadLocal[] values are not automatically reinstated in reactive operators.
3030
This behavior is controlled with the configprop:spring.reactor.context-propagation[] property, which can be set to `auto` to enable automatic propagation.
3131

32-
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.
33-
This can be done using this configuration:
32+
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.
33+
34+
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:
3435

3536
include-code::ContextPropagationConfiguration[]
3637

0 commit comments

Comments
 (0)