Skip to content

Commit b2bb59a

Browse files
uhagerjpd236
authored andcommitted
Fix soft TTL for duplicate requests (google#73)
* Fix soft TTL for duplicate requests: when a soft TTL cache entry exists, that response should be served immediately rather than having duplicate requests wait for the first request's network response.
1 parent b33a53f commit b2bb59a

File tree

6 files changed

+263
-75
lines changed

6 files changed

+263
-75
lines changed

src/main/java/com/android/volley/CacheDispatcher.java

Lines changed: 140 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818

1919
import android.os.Process;
2020

21+
import java.util.HashMap;
22+
import java.util.LinkedList;
23+
import java.util.Map;
24+
import java.util.Queue;
2125
import java.util.concurrent.BlockingQueue;
2226

2327
/**
@@ -48,6 +52,9 @@ public class CacheDispatcher extends Thread {
4852
/** Used for telling us to die. */
4953
private volatile boolean mQuit = false;
5054

55+
/** Manage list of waiting requests and de-duplicate requests with same cache key. */
56+
private final WaitingRequestManager mWaitingRequestManager;
57+
5158
/**
5259
* Creates a new cache triage dispatcher thread. You must call {@link #start()}
5360
* in order to begin processing.
@@ -64,6 +71,7 @@ public CacheDispatcher(
6471
mNetworkQueue = networkQueue;
6572
mCache = cache;
6673
mDelivery = delivery;
74+
mWaitingRequestManager = new WaitingRequestManager(this);
6775
}
6876

6977
/**
@@ -101,15 +109,19 @@ public void run() {
101109
if (entry == null) {
102110
request.addMarker("cache-miss");
103111
// Cache miss; send off to the network dispatcher.
104-
mNetworkQueue.put(request);
112+
if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
113+
mNetworkQueue.put(request);
114+
}
105115
continue;
106116
}
107117

108118
// If it is completely expired, just send it to the network.
109119
if (entry.isExpired()) {
110120
request.addMarker("cache-hit-expired");
111121
request.setCacheEntry(entry);
112-
mNetworkQueue.put(request);
122+
if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
123+
mNetworkQueue.put(request);
124+
}
113125
continue;
114126
}
115127

@@ -128,22 +140,28 @@ public void run() {
128140
// refreshing.
129141
request.addMarker("cache-hit-refresh-needed");
130142
request.setCacheEntry(entry);
131-
132143
// Mark the response as intermediate.
133144
response.intermediate = true;
134145

135-
// Post the intermediate response back to the user and have
136-
// the delivery then forward the request along to the network.
137-
mDelivery.postResponse(request, response, new Runnable() {
138-
@Override
139-
public void run() {
140-
try {
141-
mNetworkQueue.put(request);
142-
} catch (InterruptedException e) {
143-
// Not much we can do about this.
146+
if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
147+
// Post the intermediate response back to the user and have
148+
// the delivery then forward the request along to the network.
149+
mDelivery.postResponse(request, response, new Runnable() {
150+
@Override
151+
public void run() {
152+
try {
153+
mNetworkQueue.put(request);
154+
} catch (InterruptedException e) {
155+
// Restore the interrupted status
156+
Thread.currentThread().interrupt();
157+
}
144158
}
145-
}
146-
});
159+
});
160+
} else {
161+
// request has been added to list of waiting requests
162+
// to receive the network response from the first request once it returns.
163+
mDelivery.postResponse(request, response);
164+
}
147165
}
148166

149167
} catch (InterruptedException e) {
@@ -154,4 +172,112 @@ public void run() {
154172
}
155173
}
156174
}
175+
176+
private static class WaitingRequestManager implements Request.NetworkRequestCompleteListener {
177+
178+
/**
179+
* Staging area for requests that already have a duplicate request in flight.
180+
*
181+
* <ul>
182+
* <li>containsKey(cacheKey) indicates that there is a request in flight for the given cache
183+
* key.</li>
184+
* <li>get(cacheKey) returns waiting requests for the given cache key. The in flight request
185+
* is <em>not</em> contained in that list. Is null if no requests are staged.</li>
186+
* </ul>
187+
*/
188+
private final Map<String, Queue<Request<?>>> mWaitingRequests = new HashMap<>();
189+
190+
private final CacheDispatcher mCacheDispatcher;
191+
192+
WaitingRequestManager(CacheDispatcher cacheDispatcher) {
193+
mCacheDispatcher = cacheDispatcher;
194+
}
195+
196+
/** Request received a valid response that can be used by other waiting requests. */
197+
@Override
198+
public void onResponseReceived(Request<?> request, Response<?> response) {
199+
if (response.cacheEntry == null || response.cacheEntry.isExpired()) {
200+
onNoUsableResponseReceived(request);
201+
return;
202+
}
203+
String cacheKey = request.getCacheKey();
204+
Queue<Request<?>> waitingRequests;
205+
synchronized (this) {
206+
waitingRequests = mWaitingRequests.remove(cacheKey);
207+
}
208+
if (waitingRequests != null) {
209+
if (VolleyLog.DEBUG) {
210+
VolleyLog.v("Releasing %d waiting requests for cacheKey=%s.",
211+
waitingRequests.size(), cacheKey);
212+
}
213+
// Process all queued up requests.
214+
for (Request<?> waiting : waitingRequests) {
215+
mCacheDispatcher.mDelivery.postResponse(waiting, response);
216+
}
217+
}
218+
}
219+
220+
/** No valid response received from network, release waiting requests. */
221+
@Override
222+
public synchronized void onNoUsableResponseReceived(Request<?> request) {
223+
String cacheKey = request.getCacheKey();
224+
Queue<Request<?>> waitingRequests = mWaitingRequests.remove(cacheKey);
225+
if (waitingRequests != null) {
226+
if (VolleyLog.DEBUG) {
227+
VolleyLog.v("%d waiting requests for cacheKey=%s; resend to network",
228+
waitingRequests.size(), cacheKey);
229+
}
230+
Request<?> nextInLine = waitingRequests.remove();
231+
if (nextInLine == null) {
232+
return;
233+
}
234+
mWaitingRequests.put(cacheKey, waitingRequests);
235+
try {
236+
mCacheDispatcher.mNetworkQueue.put(nextInLine);
237+
} catch (InterruptedException iex) {
238+
VolleyLog.e("Couldn't add request to queue. %s", iex.toString());
239+
// Restore the interrupted status of the calling thread (i.e. NetworkDispatcher)
240+
Thread.currentThread().interrupt();
241+
// Quit the current CacheDispatcher thread.
242+
mCacheDispatcher.quit();
243+
}
244+
}
245+
}
246+
247+
/**
248+
* For cacheable requests, if a request for the same cache key is already in flight,
249+
* add it to a queue to wait for that in-flight request to finish.
250+
* @return whether the request was queued. If false, we should continue issuing the request
251+
* over the network. If true, we should put the request on hold to be processed when
252+
* the in-flight request finishes.
253+
*/
254+
private synchronized boolean maybeAddToWaitingRequests(Request<?> request) {
255+
String cacheKey = request.getCacheKey();
256+
// Insert request into stage if there's already a request with the same cache key
257+
// in flight.
258+
if (mWaitingRequests.containsKey(cacheKey)) {
259+
// There is already a request in flight. Queue up.
260+
Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
261+
if (stagedRequests == null) {
262+
stagedRequests = new LinkedList<Request<?>>();
263+
}
264+
request.addMarker("waiting-for-response");
265+
stagedRequests.add(request);
266+
mWaitingRequests.put(cacheKey, stagedRequests);
267+
if (VolleyLog.DEBUG) {
268+
VolleyLog.d("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
269+
}
270+
return true;
271+
} else {
272+
// Insert 'null' queue for this cacheKey, indicating there is now a request in
273+
// flight.
274+
mWaitingRequests.put(cacheKey, null);
275+
request.setNetworkRequestCompleteListener(this);
276+
if (VolleyLog.DEBUG) {
277+
VolleyLog.d("new request, sending to network %s", cacheKey);
278+
}
279+
return false;
280+
}
281+
}
282+
}
157283
}

src/main/java/com/android/volley/NetworkDispatcher.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
* errors are posted back to the caller via a {@link ResponseDelivery}.
3434
*/
3535
public class NetworkDispatcher extends Thread {
36+
3637
/** The queue of requests to service. */
3738
private final BlockingQueue<Request<?>> mQueue;
3839
/** The network interface for processing requests. */
@@ -54,8 +55,7 @@ public class NetworkDispatcher extends Thread {
5455
* @param delivery Delivery interface to use for posting responses
5556
*/
5657
public NetworkDispatcher(BlockingQueue<Request<?>> queue,
57-
Network network, Cache cache,
58-
ResponseDelivery delivery) {
58+
Network network, Cache cache, ResponseDelivery delivery) {
5959
mQueue = queue;
6060
mNetwork = network;
6161
mCache = cache;
@@ -103,6 +103,7 @@ public void run() {
103103
// network request.
104104
if (request.isCanceled()) {
105105
request.finish("network-discard-cancelled");
106+
request.notifyListenerResponseNotUsable();
106107
continue;
107108
}
108109

@@ -116,6 +117,7 @@ public void run() {
116117
// we're done -- don't deliver a second identical response.
117118
if (networkResponse.notModified && request.hasHadResponseDelivered()) {
118119
request.finish("not-modified");
120+
request.notifyListenerResponseNotUsable();
119121
continue;
120122
}
121123

@@ -133,14 +135,17 @@ public void run() {
133135
// Post the response back.
134136
request.markDelivered();
135137
mDelivery.postResponse(request, response);
138+
request.notifyListenerResponseReceived(response);
136139
} catch (VolleyError volleyError) {
137140
volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
138141
parseAndDeliverNetworkError(request, volleyError);
142+
request.notifyListenerResponseNotUsable();
139143
} catch (Exception e) {
140144
VolleyLog.e(e, "Unhandled exception %s", e.toString());
141145
VolleyError volleyError = new VolleyError(e);
142146
volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
143147
mDelivery.postError(request, volleyError);
148+
request.notifyListenerResponseNotUsable();
144149
}
145150
}
146151
}

src/main/java/com/android/volley/Request.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,18 @@ public interface Method {
5656
int PATCH = 7;
5757
}
5858

59+
/**
60+
* Callback to notify when the network request returns.
61+
*/
62+
/* package */ interface NetworkRequestCompleteListener {
63+
64+
/** Callback when a network response has been received. */
65+
void onResponseReceived(Request<?> request, Response<?> response);
66+
67+
/** Callback when request returns from network without valid response. */
68+
void onNoUsableResponseReceived(Request<?> request);
69+
}
70+
5971
/** An event log tracing the lifetime of this request; for debugging. */
6072
private final MarkerLog mEventLog = MarkerLog.ENABLED ? new MarkerLog() : null;
6173

@@ -105,6 +117,12 @@ public interface Method {
105117
/** An opaque token tagging this request; used for bulk cancellation. */
106118
private Object mTag;
107119

120+
/** Listener that will be notified when a response has been delivered. */
121+
private NetworkRequestCompleteListener mRequestCompleteListener;
122+
123+
/** Object to guard access to mRequestCompleteListener. */
124+
private final Object mLock = new Object();
125+
108126
/**
109127
* Creates a new request with the given URL and error listener. Note that
110128
* the normal response listener is not provided here as delivery of responses
@@ -584,6 +602,42 @@ public void deliverError(VolleyError error) {
584602
}
585603
}
586604

605+
/**
606+
* {@link NetworkRequestCompleteListener} that will receive callbacks when the request
607+
* returns from the network.
608+
*/
609+
/* package */ void setNetworkRequestCompleteListener(
610+
NetworkRequestCompleteListener requestCompleteListener) {
611+
synchronized (mLock) {
612+
mRequestCompleteListener = requestCompleteListener;
613+
}
614+
}
615+
616+
/**
617+
* Notify NetworkRequestCompleteListener that a valid response has been received
618+
* which can be used for other, waiting requests.
619+
* @param response received from the network
620+
*/
621+
/* package */ void notifyListenerResponseReceived(Response<?> response) {
622+
synchronized (mLock) {
623+
if (mRequestCompleteListener != null) {
624+
mRequestCompleteListener.onResponseReceived(this, response);
625+
}
626+
}
627+
}
628+
629+
/**
630+
* Notify NetworkRequestCompleteListener that the network request did not result in
631+
* a response which can be used for other, waiting requests.
632+
*/
633+
/* package */ void notifyListenerResponseNotUsable() {
634+
synchronized (mLock) {
635+
if (mRequestCompleteListener != null) {
636+
mRequestCompleteListener.onNoUsableResponseReceived(this);
637+
}
638+
}
639+
}
640+
587641
/**
588642
* Our comparator sorts from high to low priority, and secondarily by
589643
* sequence number to provide FIFO ordering.

0 commit comments

Comments
 (0)