Skip to content

Commit 7ee821d

Browse files
committed
Add ability to handle a timeout to DeferredResult
When a controller returns a DeferredResult, the underlying async request will eventually time out. Until now the default behavior was to send a 503 (SERVICE_UNAVAILABLE). However, this is not desirable in all cases. For example if waiting on an event, a timeout simply means there is no new information to send. To handle those cases a DeferredResult now accespts a timeout result Object in its constructor. If the timeout occurs before the DeferredResult is set, the timeout result provided to the constructor is used instead. Issue: SPR-8617
1 parent f37efb4 commit 7ee821d

File tree

8 files changed

+270
-53
lines changed

8 files changed

+270
-53
lines changed

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

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ private void startAsync() {
143143
}
144144

145145
private Callable<Object> buildChain() {
146-
Assert.state(this.callable != null, "The callable field is required to complete the chain");
146+
Assert.state(this.callable != null, "The last callable is required to build the async chain");
147147
this.delegatingCallables.add(new StaleAsyncRequestCheckingCallable(asyncWebRequest));
148148
Callable<Object> result = this.callable;
149149
for (int i = this.delegatingCallables.size() - 1; i >= 0; i--) {
@@ -165,25 +165,39 @@ private Callable<Object> buildChain() {
165165
* the threading model, i.e. whether a TaskExecutor is used.
166166
* @see DeferredResult
167167
*/
168-
public void startDeferredResultProcessing(DeferredResult deferredResult) {
169-
Assert.notNull(deferredResult, "A DeferredResult is required");
168+
public void startDeferredResultProcessing(final DeferredResult deferredResult) {
169+
Assert.notNull(deferredResult, "DeferredResult is required");
170170
startAsync();
171-
deferredResult.setValueProcessor(new DeferredResultHandler() {
172-
public void handle(Object value) {
171+
deferredResult.init(new DeferredResultHandler() {
172+
public void handle(Object result) {
173173
if (asyncWebRequest.isAsyncCompleted()) {
174174
throw new StaleAsyncWebRequestException("Async request processing already completed");
175175
}
176-
setCallable(getSimpleCallable(value));
176+
setCallable(new PassThroughCallable(result));
177177
new AsyncExecutionChainRunnable(asyncWebRequest, buildChain()).run();
178178
}
179179
});
180+
if (deferredResult.canHandleTimeout()) {
181+
this.asyncWebRequest.setTimeoutHandler(new Runnable() {
182+
public void run() {
183+
deferredResult.handleTimeout();
184+
}
185+
});
186+
}
180187
}
181188

182-
private Callable<Object> getSimpleCallable(final Object value) {
183-
return new Callable<Object>() {
184-
public Object call() throws Exception {
185-
return value;
186-
}
187-
};
189+
190+
private static class PassThroughCallable implements Callable<Object> {
191+
192+
private final Object value;
193+
194+
public PassThroughCallable(Object value) {
195+
this.value = value;
196+
}
197+
198+
public Object call() throws Exception {
199+
return this.value;
200+
}
188201
}
202+
189203
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ public class AsyncExecutionChainRunnable implements Runnable {
4949
public AsyncExecutionChainRunnable(AsyncWebRequest asyncWebRequest, Callable<?> callable) {
5050
Assert.notNull(asyncWebRequest, "An AsyncWebRequest is required");
5151
Assert.notNull(callable, "A Callable is required");
52-
Assert.state(asyncWebRequest.isAsyncStarted(), "Not an async request");
5352
this.asyncWebRequest = asyncWebRequest;
5453
this.callable = callable;
5554
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ public interface AsyncWebRequest extends NativeWebRequest {
3535
*/
3636
void setTimeout(Long timeout);
3737

38+
/**
39+
* Invoked on a timeout to complete the response instead of the default
40+
* behavior that sets the status to 503 (SERVICE_UNAVAILABLE).
41+
*/
42+
void setTimeoutHandler(Runnable runnable);
43+
3844
/**
3945
* Mark the start of async request processing for example ensuring the
4046
* request remains open in order to be completed in a separate thread.

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

Lines changed: 98 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,72 +16,135 @@
1616

1717
package org.springframework.web.context.request.async;
1818

19-
import java.util.concurrent.ArrayBlockingQueue;
20-
import java.util.concurrent.BlockingQueue;
21-
import java.util.concurrent.atomic.AtomicReference;
19+
import java.util.concurrent.CountDownLatch;
20+
import java.util.concurrent.TimeUnit;
21+
import java.util.concurrent.locks.ReentrantLock;
2222

2323
import org.springframework.core.task.AsyncTaskExecutor;
2424
import org.springframework.util.Assert;
2525

2626
/**
27-
* DeferredResult provides an alternative to using a Callable to complete async
28-
* request processing. Whereas with a Callable the framework manages a thread on
29-
* behalf of the application through an {@link AsyncTaskExecutor}, with a
30-
* DeferredResult the application can produce a value using a thread of its choice.
27+
* DeferredResult provides an alternative to using a Callable for async request
28+
* processing. With a Callable the framework manages a thread on behalf of the
29+
* application through an {@link AsyncTaskExecutor}. With a DeferredResult the
30+
* application sets the result in a thread of its choice.
3131
*
32-
* <p>The following sequence describes typical use of a DeferredResult:
32+
* <p>The following sequence describes the intended use scenario:
3333
* <ol>
34-
* <li>Application method (e.g. controller method) returns a DeferredResult instance
35-
* <li>The framework completes initialization of the returned DeferredResult in the same thread
36-
* <li>The application calls {@link DeferredResult#set(Object)} from another thread
37-
* <li>The framework completes request processing in the thread in which it is invoked
34+
* <li>thread-1: framework calls application method
35+
* <li>thread-1: application method returns a DeferredResult
36+
* <li>thread-1: framework initializes DeferredResult
37+
* <li>thread-2: application calls {@link #set(Object)}
38+
* <li>thread-2: framework completes async processing with given result
3839
* </ol>
3940
*
40-
* <p><strong>Note:</strong> {@link DeferredResult#set(Object)} will block if
41-
* called before the DeferredResult is fully initialized (by the framework).
42-
* Application code should never create a DeferredResult and set it immediately:
43-
*
44-
* <pre>
45-
* DeferredResult value = new DeferredResult();
46-
* value.set(1); // blocks
47-
* </pre>
41+
* <p>If the application calls {@link #set(Object)} in thread-2 before the
42+
* DeferredResult is initialized by the framework in thread-1, then thread-2
43+
* will block and wait for the initialization to complete. Therefore an
44+
* application should never create and set the DeferredResult in the same
45+
* thread because the initialization will never complete.</p>
4846
*
4947
* @author Rossen Stoyanchev
5048
* @since 3.2
5149
*/
5250
public final class DeferredResult {
5351

54-
private final AtomicReference<Object> value = new AtomicReference<Object>();
52+
private final static Object TIMEOUT_RESULT_NONE = new Object();
53+
54+
private Object result;
55+
56+
private final Object timeoutResult;
57+
58+
private DeferredResultHandler resultHandler;
59+
60+
private final CountDownLatch readySignal = new CountDownLatch(1);
61+
62+
private final ReentrantLock timeoutLock = new ReentrantLock();
63+
64+
/**
65+
* Create a new instance.
66+
*/
67+
public DeferredResult() {
68+
this(TIMEOUT_RESULT_NONE);
69+
}
70+
71+
/**
72+
* Create a new instance and also provide a default result to use if a
73+
* timeout occurs before {@link #set(Object)} is called.
74+
*/
75+
public DeferredResult(Object timeoutResult) {
76+
this.timeoutResult = timeoutResult;
77+
}
5578

56-
private final BlockingQueue<DeferredResultHandler> handlers = new ArrayBlockingQueue<DeferredResultHandler>(1);
79+
boolean canHandleTimeout() {
80+
return this.timeoutResult != TIMEOUT_RESULT_NONE;
81+
}
5782

5883
/**
59-
* Provide a value to use to complete async request processing.
60-
* This method should be invoked only once and usually from a separate
61-
* thread to allow the framework to fully initialize the created
62-
* DeferrredValue. See the class level documentation for more details.
84+
* Complete async processing with the given result. If the DeferredResult is
85+
* not yet fully initialized, this method will block and wait for that to
86+
* occur before proceeding. See the class level javadoc for more details.
6387
*
6488
* @throws StaleAsyncWebRequestException if the underlying async request
65-
* ended due to a timeout or an error before the value was set.
89+
* has already timed out or ended due to a network error.
6690
*/
67-
public void set(Object value) throws StaleAsyncWebRequestException {
68-
Assert.isNull(this.value.get(), "Value already set");
69-
this.value.set(value);
91+
public void set(Object result) throws StaleAsyncWebRequestException {
92+
if (this.timeoutLock.tryLock() && (this.result != this.timeoutResult)) {
93+
try {
94+
handle(result);
95+
}
96+
finally {
97+
this.timeoutLock.unlock();
98+
}
99+
}
100+
else {
101+
// A timeout is in progress
102+
throw new StaleAsyncWebRequestException("Async request already timed out");
103+
}
104+
}
105+
106+
/**
107+
* Invoked to complete async processing when a timeout occurs before
108+
* {@link #set(Object)} is called. Or if {@link #set(Object)} is already in
109+
* progress, this method blocks, waits for it to complete, and then returns.
110+
*/
111+
void handleTimeout() {
112+
Assert.state(canHandleTimeout(), "Can't handle timeout");
113+
this.timeoutLock.lock();
114+
try {
115+
if (this.result == null) {
116+
handle(this.timeoutResult);
117+
}
118+
}
119+
finally {
120+
this.timeoutLock.unlock();
121+
}
122+
}
123+
124+
private void handle(Object result) throws StaleAsyncWebRequestException {
125+
Assert.isNull(this.result, "A deferred result can be set once only");
126+
this.result = result;
70127
try {
71-
this.handlers.take().handle(value);
128+
this.readySignal.await(10, TimeUnit.SECONDS);
72129
}
73130
catch (InterruptedException e) {
74-
throw new IllegalStateException("Failed to process deferred return value: " + value, e);
131+
throw new IllegalStateException(
132+
"Gave up on waiting for DeferredResult to be initialized. " +
133+
"Are you perhaps creating and setting a DeferredResult in the same thread? " +
134+
"The DeferredResult must be fully initialized before you can set it. " +
135+
"See the class javadoc for more details");
75136
}
137+
this.resultHandler.handle(result);
76138
}
77139

78-
void setValueProcessor(DeferredResultHandler handler) {
79-
this.handlers.add(handler);
140+
void init(DeferredResultHandler handler) {
141+
this.resultHandler = handler;
142+
this.readySignal.countDown();
80143
}
81144

82145

83146
/**
84-
* Puts the set value through processing wiht the async execution chain.
147+
* Completes processing when {@link DeferredResult#set(Object)} is called.
85148
*/
86149
interface DeferredResultHandler {
87150

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ public NoOpAsyncWebRequest(HttpServletRequest request, HttpServletResponse respo
3939
public void setTimeout(Long timeout) {
4040
}
4141

42+
public void setTimeoutHandler(Runnable runnable) {
43+
}
44+
4245
public boolean isAsyncStarted() {
4346
return false;
4447
}

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
4848

4949
private AtomicBoolean asyncCompleted = new AtomicBoolean(false);
5050

51+
private Runnable timeoutHandler;
52+
5153
public StandardServletAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) {
5254
super(request, response);
5355
}
@@ -64,6 +66,10 @@ public boolean isAsyncCompleted() {
6466
return this.asyncCompleted.get();
6567
}
6668

69+
public void setTimeoutHandler(Runnable timeoutHandler) {
70+
this.timeoutHandler = timeoutHandler;
71+
}
72+
6773
public void startAsync() {
6874
Assert.state(getRequest().isAsyncSupported(),
6975
"Async support must be enabled on a servlet and for all filters involved " +
@@ -111,8 +117,13 @@ private void assertNotStale() {
111117
// ---------------------------------------------------------------------
112118

113119
public void onTimeout(AsyncEvent event) throws IOException {
120+
if (this.timeoutHandler == null) {
121+
getResponse().sendError(HttpStatus.SERVICE_UNAVAILABLE.value());
122+
}
123+
else {
124+
this.timeoutHandler.run();
125+
}
114126
completeInternal();
115-
getResponse().sendError(HttpStatus.SERVICE_UNAVAILABLE.value());
116127
}
117128

118129
public void onError(AsyncEvent event) throws IOException {

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ public void startCallableChainProcessing_requiredCallable() {
123123
fail("Expected exception");
124124
}
125125
catch (IllegalStateException ex) {
126-
assertThat(ex.getMessage(), containsString("The callable field is required"));
126+
assertThat(ex.getMessage(), containsString("last callable is required"));
127127
}
128128
}
129129

@@ -171,7 +171,7 @@ public void startDeferredResultProcessing_requiredDeferredResult() {
171171
fail("Expected exception");
172172
}
173173
catch (IllegalArgumentException ex) {
174-
assertThat(ex.getMessage(), containsString("A DeferredResult is required"));
174+
assertThat(ex.getMessage(), containsString("DeferredResult is required"));
175175
}
176176
}
177177

@@ -186,6 +186,10 @@ public SimpleAsyncWebRequest(HttpServletRequest request, HttpServletResponse res
186186
super(request, response);
187187
}
188188

189+
public void setTimeout(Long timeout) { }
190+
191+
public void setTimeoutHandler(Runnable runnable) { }
192+
189193
public void startAsync() {
190194
this.asyncStarted = true;
191195
}
@@ -194,8 +198,6 @@ public boolean isAsyncStarted() {
194198
return this.asyncStarted;
195199
}
196200

197-
public void setTimeout(Long timeout) { }
198-
199201
public void complete() {
200202
this.asyncStarted = false;
201203
this.asyncCompleted = true;

0 commit comments

Comments
 (0)