Add every example from the post, plus edge cases, to the companion repo
Full companion repo for the ankurm.com post "Spring Security Context Propagation: The Complete Guide" -- every code example the post discusses now has a corresponding runnable, verified demo (JDK 25, Spring Security 7.1.1, Spring Boot 4.1.1 dependency versions), not just the virtual-thread/structured-concurrency sections: - Demo1PlainThreadLocal: InheritableThreadLocal across thread models (no Spring) - Demo2AsyncVirtualThreads: @Async on a virtual-thread SimpleAsyncTaskExecutor (DelegatingSecurityContextExecutor vs ContextPropagatingTaskDecorator) - Demo3StructuredConcurrency: StructuredTaskScope.fork() propagation - Demo4ExecutorWrapping: DelegatingSecurityContextExecutorService/Executor/ AsyncTaskExecutor on a classic pooled platform-thread executor -- the post's "Using @Async" / "Using ExecutorService" / "Using CompletableFuture" sections - Demo5ReactiveContext: ReactiveSecurityContextHolder vs. ThreadLocal across a Reactor scheduler hop -- the post's WebFlux/getProfile() section - Demo6ScheduledSystemIdentity: DelegatingSecurityContextTaskScheduler's actual per-call capture semantics (confirmed via bytecode before writing the demo) and the createSystemContext() pattern -- the post's scheduled-tasks section - Demo7ServletFilterPersistence: SecurityContextHolderFilter (load-only) vs. SecurityContextPersistenceFilter (load+auto-save), against real filter instances and a real HttpSession -- the post's servlet-environment section - SecurityContextPropagationContractTest: 10 JUnit tests pinning the above as assertions instead of printed lines, including a TestSecurityContextHolder-based test reproducing the post's own "Testing Security Context Propagation" section Thirteen edge cases discovered along the way are indexed in docs/08 with links into the chapter that reproduces each one -- a reused pool worker NOT leaking under the Delegating* wrappers (unlike Demo1's InheritableThreadLocal), the common ForkJoinPool trap, why there's no DelegatingSecurityContextStructuredTaskScope and never will be, a real NullPointerException from Reactor's map() hit while writing the reactive test, per-call (not per-construction) context capture in DelegatingSecurityContextTaskScheduler, and the precise load-vs-save split between the two servlet filters, among others. docs/01-08 are numbered, cross-linked chapters with prev/next navigation; README indexes all demos, chapters, captured output, and the edge-case list. scripts/run-all.sh regenerates every docs/output/*.txt and the test suite output from one command.
This commit is contained in:
39
docs/01-inheritable-threadlocal.md
Normal file
39
docs/01-inheritable-threadlocal.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# 1. Why InheritableThreadLocal behaves differently with virtual threads
|
||||
|
||||
[Next: Async + virtual threads →](02-async-virtual-threads.md)
|
||||
|
||||
`Demo1PlainThreadLocal.java` has no Spring in it at all. It exists to settle one question
|
||||
before Spring Security enters the picture: does `InheritableThreadLocal` actually behave
|
||||
differently once the thread on the other end is virtual?
|
||||
|
||||
## The three cases
|
||||
|
||||
Every `Thread` copies the creating thread's `InheritableThreadLocal` values **once, at
|
||||
construction time**. That single sentence explains everything Spring Security's concurrency
|
||||
support has ever had to work around:
|
||||
|
||||
- A **fresh platform `Thread`** picks up whatever was set on the thread that created it. Fine.
|
||||
- A **pooled platform thread** was constructed once, long ago, by the pool's internal thread
|
||||
factory. Every task submitted to it later runs on that same physical thread, so it keeps
|
||||
whatever `InheritableThreadLocal` value existed *when the pool created the worker*, not
|
||||
what the submitting thread had at submission time. `docs/output/demo1.txt` shows this
|
||||
directly: task 2 on a reused pool worker still reports `request-B`, not `request-C`, even
|
||||
though the caller updated the value in between.
|
||||
- A **virtual thread** is, in this respect, identical to the fresh-platform-thread case.
|
||||
`Executors.newVirtualThreadPerTaskExecutor()` and `Thread.ofVirtual().start(...)` both
|
||||
construct a brand new `Thread` object per task -- virtual threads are never pooled or
|
||||
reused the way platform worker threads are. So the "stale value from a reused thread"
|
||||
failure mode that made `SecurityContextHolder.MODE_INHERITABLETHREADLOCAL` dangerous with
|
||||
`ThreadPoolTaskExecutor` simply does not exist for virtual threads.
|
||||
|
||||
## Why this matters for the rest of the repo
|
||||
|
||||
Spring Security's docs (and the original version of the blog post this repo supports) warn
|
||||
against `MODE_INHERITABLETHREADLOCAL` because of the pooled-thread case above. That warning
|
||||
is correct for `ThreadPoolTaskExecutor`. It stops being the relevant risk once
|
||||
`spring.threads.virtual.enabled=true` swaps the executor for a `SimpleAsyncTaskExecutor`
|
||||
backed by virtual threads -- there is no pool left to go stale. [Chapter 2](02-async-virtual-threads.md)
|
||||
verifies that directly against `SecurityContextHolder`.
|
||||
|
||||
Run it yourself: `scripts/run-all.sh`, or just `demo1` from the output already captured in
|
||||
[`docs/output/demo1.txt`](output/demo1.txt).
|
||||
Reference in New Issue
Block a user