Explain the WebAsyncManager mechanism: how is the servlet thread released and re-attached, and what happens to thread-locals like the SecurityContext during async MVC?
answer
- WebAsyncManager + request.startAsync() = release thread
- two dispatches: REQUEST then ASYNC (asyncContext.dispatch)
- OncePerRequestFilter runs on async dispatch by default
- SecurityContext auto-propagated to Callable via WebAsyncManagerIntegrationFilter
- DeferredResult / other thread-locals: capture & restore yourself
basics
~20 sWebAsyncManager calls request.startAsync() to free the container thread, runs the work on an executor, then triggers an async dispatch to resume. Thread-locals (SecurityContext, request scope) don't automatically move to the executor thread — Spring Security propagates them for Callable via a special filter, but you must handle it yourself for DeferredResult.
solid answer
~40 sEvery async request is coordinated by a WebAsyncManager, retrieved via WebAsyncUtils.getAsyncManager(request). It calls the Servlet 3.0 request.startAsync() to detach processing from the container thread, then either submits your Callable to the AsyncTaskExecutor or holds your DeferredResult. The container thread returns to the pool; the response is not committed. When the concurrent result is ready, Spring performs an ASYNC dispatch back through the DispatcherServlet, on which normal return-value/exception handling runs. The catch is thread-locals: SecurityContextHolder, RequestContextHolder and any request/thread-bound state live on the *original* thread, not the executor thread. Spring Security's WebAsyncManagerIntegrationFilter registers a SecurityContextCallableProcessingInterceptor so Callables inherit the SecurityContext — but that only covers Callable/WebAsyncTask. For DeferredResult, whose completion runs on your own thread, you must propagate context yourself. Filters must also be async-aware.
code
java · 19 lines// TaskDecorator to carry request attributes + MDC onto the async executor thread
class ContextCopyingDecorator implements TaskDecorator {
@Override public Runnable decorate(Runnable task) {
var attrs = RequestContextHolder.getRequestAttributes();
var mdc = MDC.getCopyOfContextMap();
return () -> {
try {
if (attrs != null) RequestContextHolder.setRequestAttributes(attrs);
if (mdc != null) MDC.setContextMap(mdc);
task.run();
} finally {
RequestContextHolder.resetRequestAttributes();
MDC.clear();
}
};
}
}
// Register on the executor used by configureAsyncSupport(...).setTaskExecutor(...)
// SecurityContext for Callable is handled by WebAsyncManagerIntegrationFilter automatically.go deeper
Know the container thread is released and reattached later; details of context propagation are beyond junior.
Understand startAsync + async dispatch and that context must be considered when work moves threads.
Explain WebAsyncManager, the two dispatches, filter behavior, and precisely how SecurityContext propagates for Callable but not DeferredResult.
Design a robust context-propagation strategy (TaskDecorator, security integration, MDC/tracing) and reason about correctness across both dispatches.
## The coordinator: WebAsyncManager Each request has one `WebAsyncManager`, obtained with `WebAsyncUtils.getAsyncManager(request)`. It drives the whole async lifecycle: - `startCallableProcessing(...)` for a `Callable`/`WebAsyncTask`, - `startDeferredResultProcessing(...)` for a `DeferredResult`/future. It holds the configured `AsyncTaskExecutor`, the timeout, the registered interceptors, and the eventual *concurrent result*. ## The two dispatches (Servlet 3.0 async) 1. **Initial dispatch** (`DispatcherType.REQUEST`): the controller returns an async type. Spring calls `request.startAsync()`, obtaining an `AsyncContext`. This tells the container: *don't commit the response when this thread returns.* The container thread runs to the end of the filter chain and **returns to the pool**. The socket stays open. 2. **Work phase**: `Callable` runs on the executor thread; or a `DeferredResult` waits with no thread. 3. **Async dispatch** (`DispatcherType.ASYNC`): when the result is set (Callable returns / `setResult` called / timeout), Spring calls `asyncContext.dispatch()`. The **container re-dispatches the same request** into the `DispatcherServlet`, which detects the ready concurrent result and runs it through the *normal* `HandlerMethodReturnValueHandler` path — message converters, view resolution, `@ExceptionHandler`. Then the response is committed. So the request passes through the servlet/filter stack **twice**. This is why `asyncSupported=true` must be set on the servlet and every filter in the path (Spring Boot does this automatically for its registered filters). ## Filters and async dispatches: OncePerRequestFilter Filters extending `OncePerRequestFilter` decide whether to run again on the async dispatch via `shouldNotFilterAsyncDispatch()`, which **returns `false` by default** — meaning the filter is *not* skipped, so it **runs on both** the initial and the async dispatch. That's usually what you want (e.g. security must be present on the dispatch that writes the response). Override it to `true` only for filters that must run once. ## Thread-locals: the core senior gotcha Much Spring machinery uses `ThreadLocal`: - `SecurityContextHolder` (current authentication), - `RequestContextHolder` (request-scoped beans, `@RequestScope`), - `LocaleContextHolder`, MDC logging context, transaction sync, etc. These are bound to the **original container thread**. When your `Callable` runs on an **executor thread**, that thread has *none of them* unless something copies them across. ### Callable / WebAsyncTask — Spring Security handles it Spring Security registers **`WebAsyncManagerIntegrationFilter`** in the chain. It installs a **`SecurityContextCallableProcessingInterceptor`** on the request's `WebAsyncManager`. Before the `Callable` runs, this interceptor sets the original `SecurityContext` on the executor thread (`beforeConcurrentHandling` captures it, `preProcess` applies it), and clears it afterwards. So inside a `Callable`, `SecurityContextHolder.getContext()` correctly returns the caller's authentication. `@PreAuthorize` and friends work. Other thread-locals are **not** automatically propagated. For `RequestContextHolder`, if you use `RequestContextFilter` / expose request beans, you may need `threadContextInheritable` or manual copying. `LocaleContextHolder` similarly needs care. For MDC logging you typically wrap the executor with a `TaskDecorator` that copies and restores the MDC map. ### DeferredResult — you are on your own Because you complete a `DeferredResult` from a thread Spring never scheduled (a message listener, a `CompletableFuture` callback), **no interceptor runs your code with the caller's context**. If your completion logic needs the `SecurityContext`, you must capture it while still on the request thread and re-establish it yourself: ```java @GetMapping("/x") public DeferredResult<Foo> x() { SecurityContext ctx = SecurityContextHolder.getContext(); // capture now DeferredResult<Foo> dr = new DeferredResult<>(); externalBus.subscribe(evt -> { SecurityContextHolder.setContext(ctx); // restore on the callback thread try { dr.setResult(service.build(evt)); } finally { SecurityContextHolder.clearContext(); } }); return dr; } ``` (Spring Security's `DelegatingSecurityContextExecutor` / `SecurityContextHolder`'s strategy can help, but the responsibility is yours.) ## Propagating context to Callable's executor cleanly Use a `TaskDecorator` on your `ThreadPoolTaskExecutor` to copy MDC / RequestContext, and rely on `WebAsyncManagerIntegrationFilter` for security. Example decorator copies `RequestContextHolder.getRequestAttributes()` and `MDC.getCopyOfContextMap()` into the worker and clears them after. ## Summary of the trap - Container thread is released and the request is dispatched twice. - Filters run on both dispatches by default (`OncePerRequestFilter`). - SecurityContext auto-propagates to `Callable` (via `WebAsyncManagerIntegrationFilter`) but **not** to `DeferredResult` completions and **not** for arbitrary other thread-locals — copy those yourself.
- Why does @PreAuthorize still work inside a returned Callable but you can lose the authentication inside a DeferredResult callback?Spring Security's WebAsyncManagerIntegrationFilter installs a SecurityContextCallableProcessingInterceptor that copies the SecurityContext onto the Callable's executor thread. DeferredResult completions run on threads Spring didn't schedule, so no interceptor applies — you must capture and restore the context yourself.
- How many times does the request pass through the servlet filter chain in async MVC, and what must filters support?Twice: the initial DispatcherType.REQUEST dispatch and the DispatcherType.ASYNC dispatch after the result is ready. All filters in the path must have asyncSupported=true; OncePerRequestFilter runs on both dispatches by default via shouldNotFilterAsyncDispatch() returning false.
saying these in an interview costs you the question
- Assuming all thread-locals automatically follow the work to the executor thread
- Thinking the same thread handles the whole async request
- Believing SecurityContext propagates automatically to DeferredResult callbacks
- Not knowing the request is dispatched twice / filters need asyncSupported