Skip to content

Commit 3478a70

Browse files
committed
Improve concurrent handling of result in WebAsyncManager
1. Use state transitions 2. Increase synchronized scope in setConcurrentResultAndDispatch See gh-32342
1 parent b31550f commit 3478a70

File tree

1 file changed

+91
-40
lines changed

1 file changed

+91
-40
lines changed

spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java

Lines changed: 91 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.Map;
2323
import java.util.concurrent.Callable;
2424
import java.util.concurrent.Future;
25+
import java.util.concurrent.atomic.AtomicReference;
2526

2627
import javax.servlet.http.HttpServletRequest;
2728

@@ -99,12 +100,7 @@ public final class WebAsyncManager {
99100
@Nullable
100101
private volatile Object[] concurrentResultContext;
101102

102-
/*
103-
* Whether the concurrentResult is an error. If such errors remain unhandled, some
104-
* Servlet containers will call AsyncListener#onError at the end, after the ASYNC
105-
* and/or the ERROR dispatch (Boot's case), and we need to ignore those.
106-
*/
107-
private volatile boolean errorHandlingInProgress;
103+
private final AtomicReference<State> state = new AtomicReference<>(State.NOT_STARTED);
108104

109105
private final Map<Object, CallableProcessingInterceptor> callableInterceptors = new LinkedHashMap<>();
110106

@@ -257,6 +253,12 @@ public void registerDeferredResultInterceptors(DeferredResultProcessingIntercept
257253
* {@linkplain #getConcurrentResultContext() concurrentResultContext}.
258254
*/
259255
public void clearConcurrentResult() {
256+
if (!this.state.compareAndSet(State.RESULT_SET, State.NOT_STARTED)) {
257+
if (logger.isDebugEnabled()) {
258+
logger.debug("Unexpected call to clear: [" + this.state.get() + "]");
259+
}
260+
return;
261+
}
260262
synchronized (WebAsyncManager.this) {
261263
this.concurrentResult = RESULT_NONE;
262264
this.concurrentResultContext = null;
@@ -297,6 +299,11 @@ public void startCallableProcessing(final WebAsyncTask<?> webAsyncTask, Object..
297299
Assert.notNull(webAsyncTask, "WebAsyncTask must not be null");
298300
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null");
299301

302+
if (!this.state.compareAndSet(State.NOT_STARTED, State.ASYNC_PROCESSING)) {
303+
throw new IllegalStateException(
304+
"Unexpected call to startCallableProcessing: [" + this.state.get() + "]");
305+
}
306+
300307
Long timeout = webAsyncTask.getTimeout();
301308
if (timeout != null) {
302309
this.asyncWebRequest.setTimeout(timeout);
@@ -320,7 +327,7 @@ public void startCallableProcessing(final WebAsyncTask<?> webAsyncTask, Object..
320327

321328
this.asyncWebRequest.addTimeoutHandler(() -> {
322329
if (logger.isDebugEnabled()) {
323-
logger.debug("Async request timeout for " + formatUri(this.asyncWebRequest));
330+
logger.debug("Servlet container timeout notification for " + formatUri(this.asyncWebRequest));
324331
}
325332
Object result = interceptorChain.triggerAfterTimeout(this.asyncWebRequest, callable);
326333
if (result != CallableProcessingInterceptor.RESULT_NONE) {
@@ -329,14 +336,12 @@ public void startCallableProcessing(final WebAsyncTask<?> webAsyncTask, Object..
329336
});
330337

331338
this.asyncWebRequest.addErrorHandler(ex -> {
332-
if (!this.errorHandlingInProgress) {
333-
if (logger.isDebugEnabled()) {
334-
logger.debug("Async request error for " + formatUri(this.asyncWebRequest) + ": " + ex);
335-
}
336-
Object result = interceptorChain.triggerAfterError(this.asyncWebRequest, callable, ex);
337-
result = (result != CallableProcessingInterceptor.RESULT_NONE ? result : ex);
338-
setConcurrentResultAndDispatch(result);
339+
if (logger.isDebugEnabled()) {
340+
logger.debug("Servlet container error notification for " + formatUri(this.asyncWebRequest) + ": " + ex);
339341
}
342+
Object result = interceptorChain.triggerAfterError(this.asyncWebRequest, callable, ex);
343+
result = (result != CallableProcessingInterceptor.RESULT_NONE ? result : ex);
344+
setConcurrentResultAndDispatch(result);
340345
});
341346

342347
this.asyncWebRequest.addCompletionHandler(() ->
@@ -388,33 +393,40 @@ private void logExecutorWarning(AsyncWebRequest asyncWebRequest) {
388393
}
389394

390395
private void setConcurrentResultAndDispatch(@Nullable Object result) {
396+
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null");
391397
synchronized (WebAsyncManager.this) {
392-
if (this.concurrentResult != RESULT_NONE) {
398+
if (!this.state.compareAndSet(State.ASYNC_PROCESSING, State.RESULT_SET)) {
399+
if (logger.isDebugEnabled()) {
400+
logger.debug("Async result already set: " +
401+
"[" + this.state.get() + "], ignored result: " + result +
402+
" for " + formatUri(this.asyncWebRequest));
403+
}
393404
return;
394405
}
395-
this.concurrentResult = result;
396-
this.errorHandlingInProgress = (result instanceof Throwable);
397-
}
398406

399-
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null");
400-
if (this.asyncWebRequest.isAsyncComplete()) {
407+
this.concurrentResult = result;
401408
if (logger.isDebugEnabled()) {
402-
logger.debug("Async result set but request already complete: " + formatUri(this.asyncWebRequest));
409+
logger.debug("Async result set to: " + result + " for " + formatUri(this.asyncWebRequest));
410+
}
411+
412+
if (result instanceof Exception) {
413+
if (disconnectedClientHelper.checkAndLogClientDisconnectedException((Exception) result)) {
414+
return;
415+
}
403416
}
404-
return;
405-
}
406417

407-
if (result instanceof Exception) {
408-
if (disconnectedClientHelper.checkAndLogClientDisconnectedException((Exception) result)) {
418+
if (this.asyncWebRequest.isAsyncComplete()) {
419+
if (logger.isDebugEnabled()) {
420+
logger.debug("Async request already completed for " + formatUri(this.asyncWebRequest));
421+
}
409422
return;
410423
}
411-
}
412424

413-
if (logger.isDebugEnabled()) {
414-
logger.debug("Async " + (this.errorHandlingInProgress ? "error" : "result set") +
415-
", dispatch to " + formatUri(this.asyncWebRequest));
425+
if (logger.isDebugEnabled()) {
426+
logger.debug("Performing async dispatch for " + formatUri(this.asyncWebRequest));
427+
}
428+
this.asyncWebRequest.dispatch();
416429
}
417-
this.asyncWebRequest.dispatch();
418430
}
419431

420432
/**
@@ -437,6 +449,11 @@ public void startDeferredResultProcessing(
437449
Assert.notNull(deferredResult, "DeferredResult must not be null");
438450
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null");
439451

452+
if (!this.state.compareAndSet(State.NOT_STARTED, State.ASYNC_PROCESSING)) {
453+
throw new IllegalStateException(
454+
"Unexpected call to startDeferredResultProcessing: [" + this.state.get() + "]");
455+
}
456+
440457
Long timeout = deferredResult.getTimeoutValue();
441458
if (timeout != null) {
442459
this.asyncWebRequest.setTimeout(timeout);
@@ -450,6 +467,9 @@ public void startDeferredResultProcessing(
450467
final DeferredResultInterceptorChain interceptorChain = new DeferredResultInterceptorChain(interceptors);
451468

452469
this.asyncWebRequest.addTimeoutHandler(() -> {
470+
if (logger.isDebugEnabled()) {
471+
logger.debug("Servlet container timeout notification for " + formatUri(this.asyncWebRequest));
472+
}
453473
try {
454474
interceptorChain.triggerAfterTimeout(this.asyncWebRequest, deferredResult);
455475
}
@@ -459,16 +479,17 @@ public void startDeferredResultProcessing(
459479
});
460480

461481
this.asyncWebRequest.addErrorHandler(ex -> {
462-
if (!this.errorHandlingInProgress) {
463-
try {
464-
if (!interceptorChain.triggerAfterError(this.asyncWebRequest, deferredResult, ex)) {
465-
return;
466-
}
467-
deferredResult.setErrorResult(ex);
468-
}
469-
catch (Throwable interceptorEx) {
470-
setConcurrentResultAndDispatch(interceptorEx);
482+
if (logger.isDebugEnabled()) {
483+
logger.debug("Servlet container error notification for " + formatUri(this.asyncWebRequest));
484+
}
485+
try {
486+
if (!interceptorChain.triggerAfterError(this.asyncWebRequest, deferredResult, ex)) {
487+
return;
471488
}
489+
deferredResult.setErrorResult(ex);
490+
}
491+
catch (Throwable interceptorEx) {
492+
setConcurrentResultAndDispatch(interceptorEx);
472493
}
473494
});
474495

@@ -494,10 +515,13 @@ private void startAsyncProcessing(Object[] processingContext) {
494515
synchronized (WebAsyncManager.this) {
495516
this.concurrentResult = RESULT_NONE;
496517
this.concurrentResultContext = processingContext;
497-
this.errorHandlingInProgress = false;
498518
}
499519

500520
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null");
521+
if (logger.isDebugEnabled()) {
522+
logger.debug("Started async request for " + formatUri(this.asyncWebRequest));
523+
}
524+
501525
this.asyncWebRequest.startAsync();
502526
if (logger.isDebugEnabled()) {
503527
logger.debug("Started async request");
@@ -509,4 +533,31 @@ private static String formatUri(AsyncWebRequest asyncWebRequest) {
509533
return (request != null ? request.getRequestURI() : "servlet container");
510534
}
511535

536+
537+
/**
538+
* Represents a state for {@link WebAsyncManager} to be in.
539+
* <p><pre>
540+
* NOT_STARTED <------+
541+
* | |
542+
* v |
543+
* ASYNC_PROCESSING |
544+
* | |
545+
* v |
546+
* RESULT_SET -------+
547+
* </pre>
548+
* @since 5.3.33
549+
*/
550+
private enum State {
551+
552+
/** No async processing in progress. */
553+
NOT_STARTED,
554+
555+
/** Async handling has started, but the result hasn't been set yet. */
556+
ASYNC_PROCESSING,
557+
558+
/** The result is set, and an async dispatch was performed, unless there is a network error. */
559+
RESULT_SET
560+
561+
}
562+
512563
}

0 commit comments

Comments
 (0)