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:
67
async/docs/07-which-executor-runs-it.md
Normal file
67
async/docs/07-which-executor-runs-it.md
Normal file
@@ -0,0 +1,67 @@
|
||||
prev: [Context propagation](06-context-propagation.md) · [README](../README.md) · next: [Virtual threads and pinning](08-virtual-threads-and-pinning.md)
|
||||
|
||||
# 7. Which executor actually runs it
|
||||
|
||||
Two independent decisions are involved, and conflating them is the source of most of the
|
||||
surprise.
|
||||
|
||||
**Decision one — does Boot create `applicationTaskExecutor`?** `TaskExecutorConfigurations` gates
|
||||
it on `OnExecutorCondition`, an `AnyNestedCondition` whose arms are "there is no `Executor` bean"
|
||||
and "`spring.task.execution.mode` is `force`". So *any* `Executor` bean of your own removes
|
||||
Boot's — along with its properties, its thread-name prefix, and its context-propagation
|
||||
decorator.
|
||||
|
||||
**Decision two — which executor does `@Async` resolve?** `AsyncExecutionAspectSupport` asks the
|
||||
bean factory for a unique bean of type `TaskExecutor`. Failing that, it looks for a bean named
|
||||
exactly `taskExecutor`. Failing that, it falls back to a plain `SimpleAsyncTaskExecutor`.
|
||||
|
||||
Three contexts, all captured:
|
||||
|
||||
| context | executor beans | `@Async` ran on |
|
||||
|---|---|---|
|
||||
| stock ([output](output/executor-report.txt)) | `applicationTaskExecutor` | `task-1` |
|
||||
| one custom `Executor` ([output](output/executor-one-custom.txt)) | `myExecutor` | `mine-1` |
|
||||
| two custom `Executor`s ([output](output/executor-two-custom.txt)) | `reportsExecutor`, `emailsExecutor` | **`SimpleAsyncTaskExecutor-1`** |
|
||||
|
||||
The third row is the one that hurts. Neither of your carefully sized two-thread pools is used.
|
||||
`SimpleAsyncTaskExecutor` starts **a brand new platform thread for every single call** and has no
|
||||
bound, so an application that was throttled to two concurrent report generations is now
|
||||
unthrottled, and the symptom is thread exhaustion under load rather than anything at the point of
|
||||
the change.
|
||||
|
||||
It is not entirely silent — the interceptor logs it, once, at `INFO`:
|
||||
|
||||
```
|
||||
o.s.a.i.AnnotationAsyncExecutionInterceptor : More than one TaskExecutor bean found within
|
||||
the context, and none is named 'taskExecutor'. Mark one of them as primary or name it
|
||||
'taskExecutor' (possibly as an alias) in order to use it for async processing:
|
||||
[reportsExecutor, emailsExecutor]
|
||||
```
|
||||
|
||||
`INFO`, on first use, in the middle of startup noise. In practice nobody sees it.
|
||||
|
||||
## The three ways out
|
||||
|
||||
- **`@Async("reportsExecutor")`** — name the executor at each call site. Explicit, and it
|
||||
survives someone adding a third executor later. This is the right answer when the executors
|
||||
genuinely differ in purpose.
|
||||
- **`spring.task.execution.mode=force`** — Boot creates `applicationTaskExecutor` alongside
|
||||
yours, and `@Async` resolves it ([output](output/executor-force-mode.txt)). Use it when your
|
||||
extra `Executor` beans exist for something other than `@Async` and you did not mean to disturb
|
||||
it.
|
||||
- **Name one of them `taskExecutor`**, or mark it `@Primary`. Works, and reads like an accident
|
||||
to the next person.
|
||||
|
||||
## The diagnostic
|
||||
|
||||
`ExecutorDiagnostics` prints every `Executor` bean, its class, and the real pool numbers off the
|
||||
live object. Two lines of it answer questions that otherwise take an afternoon:
|
||||
|
||||
```
|
||||
Bean named 'taskExecutor' present: false
|
||||
Bean named 'applicationTaskExecutor' present: false
|
||||
```
|
||||
|
||||
Delete it before shipping. It is a diagnostic, not a feature.
|
||||
|
||||
next: [Virtual threads and pinning](08-virtual-threads-and-pinning.md)
|
||||
Reference in New Issue
Block a user