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:
64
async/README.md
Normal file
64
async/README.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# async
|
||||
|
||||
Companion project for **@Async in Spring Boot 4: Executors, Virtual Threads and the
|
||||
Self-Invocation Trap** on [ankurm.com](https://ankurm.com).
|
||||
|
||||
Everything here is asserted by a test and captured under [`docs/output/`](docs/output). The
|
||||
measurement is always the same: the name of the thread the method body actually ran on, returned
|
||||
by the method itself. Timing cannot distinguish a fast synchronous call from an asynchronous one;
|
||||
a thread name can.
|
||||
|
||||
## Verified stack
|
||||
|
||||
| Component | Version | Source of the number |
|
||||
|---|---|---|
|
||||
| JDK | 25.0.4.1+1 (Temurin) | `java -version` |
|
||||
| Spring Boot | 4.1.1 | `maven-metadata.xml` on Maven Central |
|
||||
| Spring Framework | 7.0.9 | `spring-boot-dependencies-4.1.1.pom` |
|
||||
| micrometer context-propagation | Boot-managed | `spring-boot-dependencies-4.1.1.pom` |
|
||||
| JDK used for the pinning comparison | 21.0.12.1+1 (Temurin) | `java -version` |
|
||||
|
||||
## Quickstart
|
||||
|
||||
```bash
|
||||
mvn test # 22 tests, all of the transcripts
|
||||
scripts/run-all.sh # regenerates every docs/output/ file
|
||||
scripts/run-all.sh /path/to/jdk21/bin/java # adds the JDK 21 vs 25 pinning comparison
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
| Chapter | What it settles |
|
||||
|---|---|
|
||||
| [01 What `@Async` actually does](docs/01-what-async-actually-does.md) | The proxy is the whole mechanism, and `@EnableAsync` is not automatic |
|
||||
| [02 The self-invocation trap](docs/02-the-self-invocation-trap.md) | Why an internal call runs inline, and the three fixes |
|
||||
| [03 What the proxy cannot see](docs/03-what-the-proxy-cannot-see.md) | `final` and `private` methods, and how to tell that case from a self-invocation |
|
||||
| [04 Return types and exceptions](docs/04-return-types-and-exceptions.md) | A plain return type throws; it does not return null |
|
||||
| [05 Pool sizing](docs/05-pool-sizing.md) | Why `max-size` does nothing until `queue-capacity` is bounded |
|
||||
| [06 Context propagation](docs/06-context-propagation.md) | `spring.task.execution.propagate-context`, new in Boot 4.1.0 |
|
||||
| [07 Which executor runs it](docs/07-which-executor-runs-it.md) | Two `Executor` beans and `@Async` quietly uses neither |
|
||||
| [08 Virtual threads and pinning](docs/08-virtual-threads-and-pinning.md) | JEP 491 measured on JDK 21 against JDK 25 |
|
||||
|
||||
## Captured output
|
||||
|
||||
| File | What it shows |
|
||||
|---|---|
|
||||
| [`self-invocation.txt`](docs/output/self-invocation.txt) | The same method on `task-5`, on `main`, and on `task-6` |
|
||||
| [`visibility.txt`](docs/output/visibility.txt) | `final` is inert; `protected` is not |
|
||||
| [`return-types.txt`](docs/output/return-types.txt) | The `IllegalArgumentException` a plain return type throws |
|
||||
| [`executor-report.txt`](docs/output/executor-report.txt) | Stock pool numbers, read off the live bean |
|
||||
| [`executor-one-custom.txt`](docs/output/executor-one-custom.txt) | Boot backing off in favour of one custom `Executor` |
|
||||
| [`executor-two-custom.txt`](docs/output/executor-two-custom.txt) | Two custom executors and neither is used |
|
||||
| [`executor-force-mode.txt`](docs/output/executor-force-mode.txt) | `spring.task.execution.mode=force` restoring it |
|
||||
| [`pool-unbounded-queue.txt`](docs/output/pool-unbounded-queue.txt) | 16 tasks, 4 threads, `max-size=12` ignored |
|
||||
| [`pool-bounded-queue.txt`](docs/output/pool-bounded-queue.txt) | The same 16 tasks on 12 threads |
|
||||
| [`virtual-threads.txt`](docs/output/virtual-threads.txt) | `SimpleAsyncTaskExecutor`, `virtual=true`, pool properties inert |
|
||||
| [`context-propagation.txt`](docs/output/context-propagation.txt) | A `ThreadLocal` surviving the hop |
|
||||
| [`pinning-probe.txt`](docs/output/pinning-probe.txt) | 4806 ms on JDK 21, 301 ms on JDK 25 |
|
||||
| [`tests.txt`](docs/output/tests.txt) | The test run behind all of the above |
|
||||
|
||||
## The one diagnostic
|
||||
|
||||
`ExecutorDiagnostics` prints every `Executor` bean in the context with its real core size, max
|
||||
size and queue capacity. It is the fastest way to answer "which executor is actually running
|
||||
this". Delete it before shipping.
|
||||
Reference in New Issue
Block a user