Skip to content

Commit 1a5661d

Browse files
committed
Improve concurrent handling of result in WebAsyncManager
1. Use state transitions 2. Increase synchronized scope in setConcurrentResultAndDispatch See gh-32341
1 parent b208c63 commit 1a5661d

File tree

1 file changed

+87
-51
lines changed

1 file changed

+87
-51
lines changed

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

Lines changed: 87 additions & 51 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 jakarta.servlet.http.HttpServletRequest;
2728
import org.apache.commons.logging.Log;
@@ -34,7 +35,6 @@
3435
import org.springframework.util.Assert;
3536
import org.springframework.web.context.request.RequestAttributes;
3637
import org.springframework.web.context.request.async.DeferredResult.DeferredResultHandler;
37-
import org.springframework.web.util.DisconnectedClientHelper;
3838

3939
/**
4040
* The central class for managing asynchronous request processing, mainly intended
@@ -68,16 +68,6 @@ public final class WebAsyncManager {
6868

6969
private static final Log logger = LogFactory.getLog(WebAsyncManager.class);
7070

71-
/**
72-
* Log category to use for network failure after a client has gone away.
73-
* @see DisconnectedClientHelper
74-
*/
75-
private static final String DISCONNECTED_CLIENT_LOG_CATEGORY =
76-
"org.springframework.web.server.DisconnectedClient";
77-
78-
private static final DisconnectedClientHelper disconnectedClientHelper =
79-
new DisconnectedClientHelper(DISCONNECTED_CLIENT_LOG_CATEGORY);
80-
8171
private static final CallableProcessingInterceptor timeoutCallableInterceptor =
8272
new TimeoutCallableProcessingInterceptor();
8373

@@ -98,12 +88,7 @@ public final class WebAsyncManager {
9888
@Nullable
9989
private volatile Object[] concurrentResultContext;
10090

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

10893
private final Map<Object, CallableProcessingInterceptor> callableInterceptors = new LinkedHashMap<>();
10994

@@ -265,6 +250,12 @@ public void registerDeferredResultInterceptors(DeferredResultProcessingIntercept
265250
* {@linkplain #getConcurrentResultContext() concurrentResultContext}.
266251
*/
267252
public void clearConcurrentResult() {
253+
if (!this.state.compareAndSet(State.RESULT_SET, State.NOT_STARTED)) {
254+
if (logger.isDebugEnabled()) {
255+
logger.debug("Unexpected call to clear: [" + this.state.get() + "]");
256+
}
257+
return;
258+
}
268259
synchronized (WebAsyncManager.this) {
269260
this.concurrentResult = RESULT_NONE;
270261
this.concurrentResultContext = null;
@@ -305,6 +296,11 @@ public void startCallableProcessing(final WebAsyncTask<?> webAsyncTask, Object..
305296
Assert.notNull(webAsyncTask, "WebAsyncTask must not be null");
306297
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null");
307298

299+
if (!this.state.compareAndSet(State.NOT_STARTED, State.ASYNC_PROCESSING)) {
300+
throw new IllegalStateException(
301+
"Unexpected call to startCallableProcessing: [" + this.state.get() + "]");
302+
}
303+
308304
Long timeout = webAsyncTask.getTimeout();
309305
if (timeout != null) {
310306
this.asyncWebRequest.setTimeout(timeout);
@@ -328,7 +324,7 @@ public void startCallableProcessing(final WebAsyncTask<?> webAsyncTask, Object..
328324

329325
this.asyncWebRequest.addTimeoutHandler(() -> {
330326
if (logger.isDebugEnabled()) {
331-
logger.debug("Async request timeout for " + formatUri(this.asyncWebRequest));
327+
logger.debug("Servlet container timeout notification for " + formatUri(this.asyncWebRequest));
332328
}
333329
Object result = interceptorChain.triggerAfterTimeout(this.asyncWebRequest, callable);
334330
if (result != CallableProcessingInterceptor.RESULT_NONE) {
@@ -337,14 +333,12 @@ public void startCallableProcessing(final WebAsyncTask<?> webAsyncTask, Object..
337333
});
338334

339335
this.asyncWebRequest.addErrorHandler(ex -> {
340-
if (!this.errorHandlingInProgress) {
341-
if (logger.isDebugEnabled()) {
342-
logger.debug("Async request error for " + formatUri(this.asyncWebRequest) + ": " + ex);
343-
}
344-
Object result = interceptorChain.triggerAfterError(this.asyncWebRequest, callable, ex);
345-
result = (result != CallableProcessingInterceptor.RESULT_NONE ? result : ex);
346-
setConcurrentResultAndDispatch(result);
336+
if (logger.isDebugEnabled()) {
337+
logger.debug("Servlet container error notification for " + formatUri(this.asyncWebRequest) + ": " + ex);
347338
}
339+
Object result = interceptorChain.triggerAfterError(this.asyncWebRequest, callable, ex);
340+
result = (result != CallableProcessingInterceptor.RESULT_NONE ? result : ex);
341+
setConcurrentResultAndDispatch(result);
348342
});
349343

350344
this.asyncWebRequest.addCompletionHandler(() ->
@@ -396,31 +390,34 @@ private void logExecutorWarning(AsyncWebRequest asyncWebRequest) {
396390
}
397391

398392
private void setConcurrentResultAndDispatch(@Nullable Object result) {
393+
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null");
399394
synchronized (WebAsyncManager.this) {
400-
if (this.concurrentResult != RESULT_NONE) {
395+
if (!this.state.compareAndSet(State.ASYNC_PROCESSING, State.RESULT_SET)) {
396+
if (logger.isDebugEnabled()) {
397+
logger.debug("Async result already set: " +
398+
"[" + this.state.get() + "], ignored result: " + result +
399+
" for " + formatUri(this.asyncWebRequest));
400+
}
401401
return;
402402
}
403-
this.concurrentResult = result;
404-
this.errorHandlingInProgress = (result instanceof Throwable);
405-
}
406403

407-
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null");
408-
if (this.asyncWebRequest.isAsyncComplete()) {
404+
this.concurrentResult = result;
409405
if (logger.isDebugEnabled()) {
410-
logger.debug("Async result set but request already complete: " + formatUri(this.asyncWebRequest));
406+
logger.debug("Async result set to: " + result + " for " + formatUri(this.asyncWebRequest));
411407
}
412-
return;
413-
}
414408

415-
if (result instanceof Exception ex && disconnectedClientHelper.checkAndLogClientDisconnectedException(ex)) {
416-
return;
417-
}
409+
if (this.asyncWebRequest.isAsyncComplete()) {
410+
if (logger.isDebugEnabled()) {
411+
logger.debug("Async request already completed for " + formatUri(this.asyncWebRequest));
412+
}
413+
return;
414+
}
418415

419-
if (logger.isDebugEnabled()) {
420-
logger.debug("Async " + (this.errorHandlingInProgress ? "error" : "result set") +
421-
", dispatch to " + formatUri(this.asyncWebRequest));
416+
if (logger.isDebugEnabled()) {
417+
logger.debug("Performing async dispatch for " + formatUri(this.asyncWebRequest));
418+
}
419+
this.asyncWebRequest.dispatch();
422420
}
423-
this.asyncWebRequest.dispatch();
424421
}
425422

426423
/**
@@ -443,6 +440,11 @@ public void startDeferredResultProcessing(
443440
Assert.notNull(deferredResult, "DeferredResult must not be null");
444441
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null");
445442

443+
if (!this.state.compareAndSet(State.NOT_STARTED, State.ASYNC_PROCESSING)) {
444+
throw new IllegalStateException(
445+
"Unexpected call to startDeferredResultProcessing: [" + this.state.get() + "]");
446+
}
447+
446448
Long timeout = deferredResult.getTimeoutValue();
447449
if (timeout != null) {
448450
this.asyncWebRequest.setTimeout(timeout);
@@ -456,6 +458,9 @@ public void startDeferredResultProcessing(
456458
final DeferredResultInterceptorChain interceptorChain = new DeferredResultInterceptorChain(interceptors);
457459

458460
this.asyncWebRequest.addTimeoutHandler(() -> {
461+
if (logger.isDebugEnabled()) {
462+
logger.debug("Servlet container timeout notification for " + formatUri(this.asyncWebRequest));
463+
}
459464
try {
460465
interceptorChain.triggerAfterTimeout(this.asyncWebRequest, deferredResult);
461466
}
@@ -465,16 +470,17 @@ public void startDeferredResultProcessing(
465470
});
466471

467472
this.asyncWebRequest.addErrorHandler(ex -> {
468-
if (!this.errorHandlingInProgress) {
469-
try {
470-
if (!interceptorChain.triggerAfterError(this.asyncWebRequest, deferredResult, ex)) {
471-
return;
472-
}
473-
deferredResult.setErrorResult(ex);
474-
}
475-
catch (Throwable interceptorEx) {
476-
setConcurrentResultAndDispatch(interceptorEx);
473+
if (logger.isDebugEnabled()) {
474+
logger.debug("Servlet container error notification for " + formatUri(this.asyncWebRequest));
475+
}
476+
try {
477+
if (!interceptorChain.triggerAfterError(this.asyncWebRequest, deferredResult, ex)) {
478+
return;
477479
}
480+
deferredResult.setErrorResult(ex);
481+
}
482+
catch (Throwable interceptorEx) {
483+
setConcurrentResultAndDispatch(interceptorEx);
478484
}
479485
});
480486

@@ -500,10 +506,13 @@ private void startAsyncProcessing(Object[] processingContext) {
500506
synchronized (WebAsyncManager.this) {
501507
this.concurrentResult = RESULT_NONE;
502508
this.concurrentResultContext = processingContext;
503-
this.errorHandlingInProgress = false;
504509
}
505510

506511
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null");
512+
if (logger.isDebugEnabled()) {
513+
logger.debug("Started async request for " + formatUri(this.asyncWebRequest));
514+
}
515+
507516
this.asyncWebRequest.startAsync();
508517
if (logger.isDebugEnabled()) {
509518
logger.debug("Started async request");
@@ -515,4 +524,31 @@ private static String formatUri(AsyncWebRequest asyncWebRequest) {
515524
return (request != null ? request.getRequestURI() : "servlet container");
516525
}
517526

527+
528+
/**
529+
* Represents a state for {@link WebAsyncManager} to be in.
530+
* <p><pre>
531+
* NOT_STARTED <------+
532+
* | |
533+
* v |
534+
* ASYNC_PROCESSING |
535+
* | |
536+
* v |
537+
* RESULT_SET -------+
538+
* </pre>
539+
* @since 5.3.33
540+
*/
541+
private enum State {
542+
543+
/** No async processing in progress. */
544+
NOT_STARTED,
545+
546+
/** Async handling has started, but the result hasn't been set yet. */
547+
ASYNC_PROCESSING,
548+
549+
/** The result is set, and an async dispatch was performed, unless there is a network error. */
550+
RESULT_SET
551+
552+
}
553+
518554
}

0 commit comments

Comments
 (0)