Add the async module

Every @Async behaviour that surprises people, asserted by a test and captured
to docs/output/: the self-invocation trap, what CGLIB cannot override, the
IllegalArgumentException a plain return type throws, the unbounded queue that
makes max-size decoration, spring.task.execution.propagate-context (new in
Boot 4.1.0), the two Executor beans that leave @Async on an unpooled
SimpleAsyncTaskExecutor, and JEP 491 measured on JDK 21 against JDK 25.
This commit is contained in:
2026-09-01 23:27:47 +05:30
commit 9b3fe88813
53 changed files with 1891 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
prev: [Pool sizing](05-pool-sizing.md) · [README](../README.md) · next: [Which executor runs it](07-which-executor-runs-it.md)
# 6. `spring.task.execution.propagate-context` — new in Boot 4.1
Handing a `Callable` to another thread leaves every `ThreadLocal` behind. That is why the async
thread in a stock application sees no MDC entries, no `RequestAttributes`, and no
`SecurityContext` — and why "the trace id disappears in the async part" is such a common report.
Spring Boot 4.1.0 added a property for it:
```yaml
spring:
task:
execution:
propagate-context: true
```
It decorates the auto-configured executor with `ContextPropagatingTaskDecorator`, which takes a
snapshot of everything registered with micrometer's `ContextRegistry` at submission time and
restores it around the task on the executing thread.
## How new is it, exactly
New in **4.1.0**. It is absent from `TaskExecutionProperties` in 4.0.8 and in 3.5.9 — checked by
extracting `TaskExecutionProperties.class` from each `spring-boot-autoconfigure` jar and running
`javap` over it, rather than by reading release notes. The accessor pair
`getPropagateContext()`/`setPropagateContext(boolean)` appears first in the 4.1.0 jar.
Note also what it is *not*: there is no matching `spring.task.scheduling.propagate-context`. The
scheduler is not covered.
## Measured
`RequestId` in this module is a `ThreadLocal<String>` with a `ThreadLocalAccessor` registered
against `ContextRegistry`. Two tests differ only in the property:
- `ContextNotPropagatedTest` — the async thread reads `null`.
- `ContextPropagatedTest` — the async thread reads `req-4711`
([output](output/context-propagation.txt)).
## The three things that make it silently do nothing
1. **micrometer's `context-propagation` is not on the classpath.** The property is still bound
and still accepted; nothing decorates the executor. This module declares
`io.micrometer:context-propagation` explicitly for that reason.
2. **Nothing registered an accessor.** The snapshot only carries what `ContextRegistry` knows
about. Libraries that ship accessors (Micrometer tracing, Reactor) register their own; a
`ThreadLocal` of your own does not register itself.
3. **The executor is not the auto-configured one.** The decorator is applied by Boot's
auto-configuration. Declare your own `Executor` bean (chapter 7) and you have opted out of
the property along with everything else Boot was doing.
`SecurityContext` propagation is a related but separate mechanism, with its own set of ways to
get it wrong; that is covered in
[Spring Security Context Propagation: The Complete Guide](https://ankurm.com/spring-security-context-propagation-complete-guide/).
next: [Which executor runs it](07-which-executor-runs-it.md)