|
16 | 16 |
|
17 | 17 | package org.springframework.web.context.request.async;
|
18 | 18 |
|
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; |
22 | 22 |
|
23 | 23 | import org.springframework.core.task.AsyncTaskExecutor;
|
24 | 24 | import org.springframework.util.Assert;
|
25 | 25 |
|
26 | 26 | /**
|
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. |
31 | 31 | *
|
32 |
| - * <p>The following sequence describes typical use of a DeferredResult: |
| 32 | + * <p>The following sequence describes the intended use scenario: |
33 | 33 | * <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 |
38 | 39 | * </ol>
|
39 | 40 | *
|
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> |
48 | 46 | *
|
49 | 47 | * @author Rossen Stoyanchev
|
50 | 48 | * @since 3.2
|
51 | 49 | */
|
52 | 50 | public final class DeferredResult {
|
53 | 51 |
|
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 | + } |
55 | 78 |
|
56 |
| - private final BlockingQueue<DeferredResultHandler> handlers = new ArrayBlockingQueue<DeferredResultHandler>(1); |
| 79 | + boolean canHandleTimeout() { |
| 80 | + return this.timeoutResult != TIMEOUT_RESULT_NONE; |
| 81 | + } |
57 | 82 |
|
58 | 83 | /**
|
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. |
63 | 87 | *
|
64 | 88 | * @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. |
66 | 90 | */
|
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; |
70 | 127 | try {
|
71 |
| - this.handlers.take().handle(value); |
| 128 | + this.readySignal.await(10, TimeUnit.SECONDS); |
72 | 129 | }
|
73 | 130 | 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"); |
75 | 136 | }
|
| 137 | + this.resultHandler.handle(result); |
76 | 138 | }
|
77 | 139 |
|
78 |
| - void setValueProcessor(DeferredResultHandler handler) { |
79 |
| - this.handlers.add(handler); |
| 140 | + void init(DeferredResultHandler handler) { |
| 141 | + this.resultHandler = handler; |
| 142 | + this.readySignal.countDown(); |
80 | 143 | }
|
81 | 144 |
|
82 | 145 |
|
83 | 146 | /**
|
84 |
| - * Puts the set value through processing wiht the async execution chain. |
| 147 | + * Completes processing when {@link DeferredResult#set(Object)} is called. |
85 | 148 | */
|
86 | 149 | interface DeferredResultHandler {
|
87 | 150 |
|
|
0 commit comments