Skip to content

Commit 8b7a670

Browse files
committed
Cancel WebAsyncManager thread on request timeout
Issue: SPR-15852
1 parent 9c3bd8c commit 8b7a670

File tree

3 files changed

+51
-1
lines changed

3 files changed

+51
-1
lines changed

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.List;
2020
import java.util.concurrent.Callable;
21+
import java.util.concurrent.Future;
2122

2223
import org.apache.commons.logging.Log;
2324
import org.apache.commons.logging.LogFactory;
@@ -39,11 +40,19 @@ class CallableInterceptorChain {
3940

4041
private int preProcessIndex = -1;
4142

43+
private volatile Future<?> taskFuture;
44+
4245

4346
public CallableInterceptorChain(List<CallableProcessingInterceptor> interceptors) {
4447
this.interceptors = interceptors;
4548
}
4649

50+
51+
public void setTaskFuture(Future<?> taskFuture) {
52+
this.taskFuture = taskFuture;
53+
}
54+
55+
4756
public void applyBeforeConcurrentHandling(NativeWebRequest request, Callable<?> task) throws Exception {
4857
for (CallableProcessingInterceptor interceptor : this.interceptors) {
4958
interceptor.beforeConcurrentHandling(request, task);
@@ -77,6 +86,7 @@ public Object applyPostProcess(NativeWebRequest request, Callable<?> task, Objec
7786
}
7887

7988
public Object triggerAfterTimeout(NativeWebRequest request, Callable<?> task) {
89+
cancelTask();
8090
for (CallableProcessingInterceptor interceptor : this.interceptors) {
8191
try {
8292
Object result = interceptor.handleTimeout(request, task);
@@ -94,7 +104,20 @@ else if (result != CallableProcessingInterceptor.RESULT_NONE) {
94104
return CallableProcessingInterceptor.RESULT_NONE;
95105
}
96106

107+
private void cancelTask() {
108+
Future<?> future = this.taskFuture;
109+
if (future != null) {
110+
try {
111+
future.cancel(true);
112+
}
113+
catch (Throwable ex) {
114+
// Ignore
115+
}
116+
}
117+
}
118+
97119
public Object triggerAfterError(NativeWebRequest request, Callable<?> task, Throwable throwable) {
120+
cancelTask();
98121
for (CallableProcessingInterceptor interceptor : this.interceptors) {
99122
try {
100123
Object result = interceptor.handleError(request, task, throwable);

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.List;
2222
import java.util.Map;
2323
import java.util.concurrent.Callable;
24+
import java.util.concurrent.Future;
2425
import java.util.concurrent.RejectedExecutionException;
2526
import javax.servlet.http.HttpServletRequest;
2627

@@ -314,7 +315,7 @@ public void startCallableProcessing(final WebAsyncTask<?> webAsyncTask, Object..
314315
interceptorChain.applyBeforeConcurrentHandling(this.asyncWebRequest, callable);
315316
startAsyncProcessing(processingContext);
316317
try {
317-
this.taskExecutor.submit(() -> {
318+
Future<?> future = this.taskExecutor.submit(() -> {
318319
Object result = null;
319320
try {
320321
interceptorChain.applyPreProcess(this.asyncWebRequest, callable);
@@ -328,6 +329,7 @@ public void startCallableProcessing(final WebAsyncTask<?> webAsyncTask, Object..
328329
}
329330
setConcurrentResultAndDispatch(result);
330331
});
332+
interceptorChain.setTaskFuture(future);
331333
}
332334
catch (RejectedExecutionException ex) {
333335
Object result = interceptorChain.applyPostProcess(this.asyncWebRequest, callable, ex);

spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTimeoutTests.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.web.context.request.async;
1818

1919
import java.util.concurrent.Callable;
20+
import java.util.concurrent.Future;
2021
import javax.servlet.AsyncEvent;
2122

2223
import org.junit.Before;
@@ -30,9 +31,12 @@
3031

3132
import static org.junit.Assert.assertEquals;
3233
import static org.junit.Assert.assertTrue;
34+
import static org.mockito.ArgumentMatchers.any;
3335
import static org.mockito.BDDMockito.given;
3436
import static org.mockito.BDDMockito.mock;
3537
import static org.mockito.BDDMockito.verify;
38+
import static org.mockito.Mockito.verifyNoMoreInteractions;
39+
import static org.mockito.Mockito.when;
3640
import static org.springframework.web.context.request.async.CallableProcessingInterceptor.RESULT_NONE;
3741

3842
/**
@@ -151,6 +155,27 @@ public void startCallableProcessingAfterTimeoutException() throws Exception {
151155
verify(interceptor).beforeConcurrentHandling(this.asyncWebRequest, callable);
152156
}
153157

158+
@SuppressWarnings("unchecked")
159+
@Test
160+
public void startCallableProcessingTimeoutAndCheckThreadInterrupted() throws Exception {
161+
162+
StubCallable callable = new StubCallable();
163+
Future future = mock(Future.class);
164+
165+
AsyncTaskExecutor executor = mock(AsyncTaskExecutor.class);
166+
when(executor.submit(any(Runnable.class))).thenReturn(future);
167+
168+
this.asyncManager.setTaskExecutor(executor);
169+
this.asyncManager.startCallableProcessing(callable);
170+
171+
this.asyncWebRequest.onTimeout(ASYNC_EVENT);
172+
173+
assertTrue(this.asyncManager.hasConcurrentResult());
174+
175+
verify(future).cancel(true);
176+
verifyNoMoreInteractions(future);
177+
}
178+
154179
@Test
155180
public void startDeferredResultProcessingTimeoutAndComplete() throws Exception {
156181

0 commit comments

Comments
 (0)