1
0
mirror of https://github.com/square/okhttp.git synced 2025-08-08 23:42:08 +03:00

Cache Events (#6015)

Provide EventListener methods to understand the impact of caching.
This commit is contained in:
Yuri Schimke
2020-05-05 18:25:06 +01:00
committed by GitHub
parent af1eadfebf
commit 8cebf9012b
7 changed files with 269 additions and 5 deletions

View File

@@ -204,4 +204,24 @@ sealed class CallEvent {
override val call: Call, override val call: Call,
val ioe: IOException val ioe: IOException
) : CallEvent() ) : CallEvent()
data class SatisfactionFailure(
override val timestampNs: Long,
override val call: Call
) : CallEvent()
data class CacheHit(
override val timestampNs: Long,
override val call: Call
) : CallEvent()
data class CacheMiss(
override val timestampNs: Long,
override val call: Call
) : CallEvent()
data class CacheConditionalHit(
override val timestampNs: Long,
override val call: Call
) : CallEvent()
} }

View File

@@ -241,6 +241,30 @@ class ClientRuleEventListener(
delegate.canceled(call) delegate.canceled(call)
} }
override fun satisfactionFailure(call: Call, response: Response) {
logWithTime("satisfactionFailure")
delegate.satisfactionFailure(call, response)
}
override fun cacheMiss(call: Call) {
logWithTime("cacheMiss")
delegate.cacheMiss(call)
}
override fun cacheHit(call: Call, response: Response) {
logWithTime("cacheHit")
delegate.cacheHit(call, response)
}
override fun cacheConditionalHit(call: Call, cachedResponse: Response) {
logWithTime("cacheConditionalHit")
delegate.cacheConditionalHit(call, cachedResponse)
}
private fun logWithTime(message: String) { private fun logWithTime(message: String) {
val timeMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs) val timeMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs)
logger.invoke("[$timeMs ms] $message") logger.invoke("[$timeMs ms] $message")

View File

@@ -22,6 +22,9 @@ import java.net.Proxy
import java.util.Deque import java.util.Deque
import java.util.concurrent.ConcurrentLinkedDeque import java.util.concurrent.ConcurrentLinkedDeque
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import okhttp3.CallEvent.CacheConditionalHit
import okhttp3.CallEvent.CacheHit
import okhttp3.CallEvent.CacheMiss
import okhttp3.CallEvent.CallEnd import okhttp3.CallEvent.CallEnd
import okhttp3.CallEvent.CallFailed import okhttp3.CallEvent.CallFailed
import okhttp3.CallEvent.CallStart import okhttp3.CallEvent.CallStart
@@ -45,6 +48,7 @@ import okhttp3.CallEvent.ResponseBodyStart
import okhttp3.CallEvent.ResponseFailed import okhttp3.CallEvent.ResponseFailed
import okhttp3.CallEvent.ResponseHeadersEnd import okhttp3.CallEvent.ResponseHeadersEnd
import okhttp3.CallEvent.ResponseHeadersStart import okhttp3.CallEvent.ResponseHeadersStart
import okhttp3.CallEvent.SatisfactionFailure
import okhttp3.CallEvent.SecureConnectEnd import okhttp3.CallEvent.SecureConnectEnd
import okhttp3.CallEvent.SecureConnectStart import okhttp3.CallEvent.SecureConnectStart
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
@@ -260,4 +264,21 @@ open class RecordingEventListener : EventListener() {
override fun canceled( override fun canceled(
call: Call call: Call
) = logEvent(Canceled(System.nanoTime(), call)) ) = logEvent(Canceled(System.nanoTime(), call))
override fun satisfactionFailure(
call: Call,
response: Response
) = logEvent(SatisfactionFailure(System.nanoTime(), call))
override fun cacheMiss(
call: Call
) = logEvent(CacheMiss(System.nanoTime(), call))
override fun cacheHit(
call: Call,
response: Response
) = logEvent(CacheHit(System.nanoTime(), call))
override fun cacheConditionalHit(call: Call, cachedResponse: Response) =
logEvent(CacheConditionalHit(System.nanoTime(), call))
} }

View File

@@ -422,6 +422,41 @@ abstract class EventListener {
) { ) {
} }
/**
* Invoked when a call fails due to cache rules.
* For example, we're forbidden from using the network and the cache is insufficient
*/
open fun satisfactionFailure(call: Call, response: Response) {
}
/**
* Invoked when a result is served from the cache. The Response provided is the top level
* Response and normal event sequences will not be received.
*
* This event will only be received when a Cache is configured for the client.
*/
open fun cacheHit(call: Call, response: Response) {
}
/**
* Invoked when a response will be served from the network. The Response will be
* available from normal event sequences.
*
* This event will only be received when a Cache is configured for the client.
*/
open fun cacheMiss(call: Call) {
}
/**
* Invoked when a response will be served from the cache or network based on validating the
* cached Response freshness. Will be followed by cacheHit or cacheMiss after the network
* Response is available.
*
* This event will only be received when a Cache is configured for the client.
*/
open fun cacheConditionalHit(call: Call, cachedResponse: Response) {
}
interface Factory { interface Factory {
/** /**
* Creates an instance of the [EventListener] for a particular [Call]. The returned * Creates an instance of the [EventListener] for a particular [Call]. The returned

View File

@@ -21,12 +21,14 @@ import java.net.HttpURLConnection.HTTP_GATEWAY_TIMEOUT
import java.net.HttpURLConnection.HTTP_NOT_MODIFIED import java.net.HttpURLConnection.HTTP_NOT_MODIFIED
import java.util.concurrent.TimeUnit.MILLISECONDS import java.util.concurrent.TimeUnit.MILLISECONDS
import okhttp3.Cache import okhttp3.Cache
import okhttp3.EventListener
import okhttp3.Headers import okhttp3.Headers
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Protocol import okhttp3.Protocol
import okhttp3.Response import okhttp3.Response
import okhttp3.internal.EMPTY_RESPONSE import okhttp3.internal.EMPTY_RESPONSE
import okhttp3.internal.closeQuietly import okhttp3.internal.closeQuietly
import okhttp3.internal.connection.RealCall
import okhttp3.internal.discard import okhttp3.internal.discard
import okhttp3.internal.http.ExchangeCodec import okhttp3.internal.http.ExchangeCodec
import okhttp3.internal.http.HttpMethod import okhttp3.internal.http.HttpMethod
@@ -42,6 +44,7 @@ class CacheInterceptor(internal val cache: Cache?) : Interceptor {
@Throws(IOException::class) @Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val call = chain.call()
val cacheCandidate = cache?.get(chain.request()) val cacheCandidate = cache?.get(chain.request())
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
@@ -51,6 +54,7 @@ class CacheInterceptor(internal val cache: Cache?) : Interceptor {
val cacheResponse = strategy.cacheResponse val cacheResponse = strategy.cacheResponse
cache?.trackResponse(strategy) cache?.trackResponse(strategy)
val listener = (call as? RealCall)?.eventListener ?: EventListener.NONE
if (cacheCandidate != null && cacheResponse == null) { if (cacheCandidate != null && cacheResponse == null) {
// The cache candidate wasn't applicable. Close it. // The cache candidate wasn't applicable. Close it.
@@ -67,14 +71,24 @@ class CacheInterceptor(internal val cache: Cache?) : Interceptor {
.body(EMPTY_RESPONSE) .body(EMPTY_RESPONSE)
.sentRequestAtMillis(-1L) .sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis()) .receivedResponseAtMillis(System.currentTimeMillis())
.build() .build().also {
listener.satisfactionFailure(call, it)
}
} }
// If we don't need the network, we're done. // If we don't need the network, we're done.
if (networkRequest == null) { if (networkRequest == null) {
return cacheResponse!!.newBuilder() return cacheResponse!!.newBuilder()
.cacheResponse(stripBody(cacheResponse)) .cacheResponse(stripBody(cacheResponse))
.build() .build().also {
listener.cacheHit(call, it)
}
}
if (cacheResponse != null) {
listener.cacheConditionalHit(call, cacheResponse)
} else if (cache != null) {
listener.cacheMiss(call)
} }
var networkResponse: Response? = null var networkResponse: Response? = null
@@ -104,7 +118,9 @@ class CacheInterceptor(internal val cache: Cache?) : Interceptor {
// Content-Encoding header (as performed by initContentStream()). // Content-Encoding header (as performed by initContentStream()).
cache!!.trackConditionalCacheHit() cache!!.trackConditionalCacheHit()
cache.update(cacheResponse, response) cache.update(cacheResponse, response)
return response return response.also {
listener.cacheHit(call, it)
}
} else { } else {
cacheResponse.body?.closeQuietly() cacheResponse.body?.closeQuietly()
} }
@@ -119,7 +135,12 @@ class CacheInterceptor(internal val cache: Cache?) : Interceptor {
if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) { if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache. // Offer this request to the cache.
val cacheRequest = cache.put(response) val cacheRequest = cache.put(response)
return cacheWritingResponse(cacheRequest, response) return cacheWritingResponse(cacheRequest, response).also {
if (cacheResponse != null) {
// This will log a conditional cache miss only.
listener.cacheMiss(call)
}
}
} }
if (HttpMethod.invalidatesCache(networkRequest.method)) { if (HttpMethod.invalidatesCache(networkRequest.method)) {

View File

@@ -65,7 +65,7 @@ class RealCall(
) : Call { ) : Call {
private val connectionPool: RealConnectionPool = client.connectionPool.delegate private val connectionPool: RealConnectionPool = client.connectionPool.delegate
private val eventListener: EventListener = client.eventListenerFactory.create(this) internal val eventListener: EventListener = client.eventListenerFactory.create(this)
private val timeout = object : AsyncTimeout() { private val timeout = object : AsyncTimeout() {
override fun timedOut() { override fun timedOut() {

View File

@@ -15,6 +15,7 @@
*/ */
package okhttp3; package okhttp3;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InterruptedIOException; import java.io.InterruptedIOException;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
@@ -77,6 +78,7 @@ import static okhttp3.tls.internal.TlsUtil.localhost;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.any; import static org.hamcrest.CoreMatchers.any;
import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import static org.junit.Assume.assumeThat; import static org.junit.Assume.assumeThat;
@@ -96,6 +98,7 @@ public final class EventListenerTest {
.eventListenerFactory(clientTestRule.wrap(listener)) .eventListenerFactory(clientTestRule.wrap(listener))
.build(); .build();
private SocksProxy socksProxy; private SocksProxy socksProxy;
private Cache cache = null;
@Before public void setUp() { @Before public void setUp() {
platform.assumeNotOpenJSSE(); platform.assumeNotOpenJSSE();
@@ -109,6 +112,9 @@ public final class EventListenerTest {
if (socksProxy != null) { if (socksProxy != null) {
socksProxy.shutdown(); socksProxy.shutdown();
} }
if (cache != null) {
cache.delete();
}
} }
@Test public void successfulCallEventSequence() throws IOException { @Test public void successfulCallEventSequence() throws IOException {
@@ -1443,4 +1449,141 @@ public final class EventListenerTest {
listener.takeEvent(RequestBodyEnd.class, 0L); listener.takeEvent(RequestBodyEnd.class, 0L);
listener.takeEvent(ResponseHeadersEnd.class, responseHeadersStartDelay); listener.takeEvent(ResponseHeadersEnd.class, responseHeadersStartDelay);
} }
@Test public void cacheMiss() throws IOException {
enableCache();
server.enqueue(new MockResponse()
.setBody("abc"));
Call call = client.newCall(new Request.Builder()
.url(server.url("/"))
.build());
Response response = call.execute();
assertThat(response.code()).isEqualTo(200);
assertThat(response.body().string()).isEqualTo("abc");
response.close();
assertThat(listener.recordedEventTypes()).containsExactly("CallStart", "CacheMiss",
"ProxySelectStart", "ProxySelectEnd", "DnsStart", "DnsEnd",
"ConnectStart", "ConnectEnd", "ConnectionAcquired", "RequestHeadersStart",
"RequestHeadersEnd", "ResponseHeadersStart", "ResponseHeadersEnd",
"ResponseBodyStart", "ResponseBodyEnd", "ConnectionReleased", "CallEnd");
}
@Test public void conditionalCache() throws IOException {
enableCache();
server.enqueue(new MockResponse()
.addHeader("ETag: v1")
.setBody("abc"));
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
Call call = client.newCall(new Request.Builder()
.url(server.url("/"))
.build());
Response response = call.execute();
assertThat(response.code()).isEqualTo(200);
response.close();
listener.clearAllEvents();
call = call.clone();
response = call.execute();
assertThat(response.code()).isEqualTo(200);
assertThat(response.body().string()).isEqualTo("abc");
response.close();
assertThat(listener.recordedEventTypes()).containsExactly("CallStart", "CacheConditionalHit",
"ConnectionAcquired", "RequestHeadersStart",
"RequestHeadersEnd", "ResponseHeadersStart", "ResponseHeadersEnd",
"ResponseBodyStart", "ResponseBodyEnd", "CacheHit", "ConnectionReleased", "CallEnd");
}
@Test public void conditionalCacheMiss() throws IOException {
enableCache();
server.enqueue(new MockResponse()
.addHeader("ETag: v1")
.setBody("abc"));
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_OK)
.addHeader("ETag: v2")
.setBody("abd"));
Call call = client.newCall(new Request.Builder()
.url(server.url("/"))
.build());
Response response = call.execute();
assertThat(response.code()).isEqualTo(200);
response.close();
listener.clearAllEvents();
call = call.clone();
response = call.execute();
assertThat(response.code()).isEqualTo(200);
assertThat(response.body().string()).isEqualTo("abd");
response.close();
assertThat(listener.recordedEventTypes()).containsExactly("CallStart", "CacheConditionalHit",
"ConnectionAcquired", "RequestHeadersStart",
"RequestHeadersEnd", "ResponseHeadersStart", "ResponseHeadersEnd", "CacheMiss",
"ResponseBodyStart", "ResponseBodyEnd", "ConnectionReleased", "CallEnd");
}
@Test public void satisfactionFailure() throws IOException {
enableCache();
Call call = client.newCall(new Request.Builder()
.url(server.url("/"))
.cacheControl(CacheControl.FORCE_CACHE)
.build());
Response response = call.execute();
assertThat(response.code()).isEqualTo(504);
response.close();
assertThat(listener.recordedEventTypes()).containsExactly("CallStart", "SatisfactionFailure", "CallEnd");
}
@Test public void cacheHit() throws IOException {
enableCache();
server.enqueue(new MockResponse().setBody("abc").addHeader("cache-control: public, max-age=300"));
Call call = client.newCall(new Request.Builder()
.url(server.url("/"))
.build());
Response response = call.execute();
assertThat(response.code()).isEqualTo(200);
assertThat(response.body().string()).isEqualTo("abc");
response.close();
listener.clearAllEvents();
call = call.clone();
response = call.execute();
assertThat(response.code()).isEqualTo(200);
assertThat(response.body().string()).isEqualTo("abc");
response.close();
assertThat(listener.recordedEventTypes()).containsExactly("CallStart", "CacheHit", "CallEnd");
}
private Cache enableCache() throws IOException {
cache = makeCache();
client = client.newBuilder().cache(cache).build();
return cache;
}
private Cache makeCache() throws IOException {
File cacheDir = File.createTempFile("cache-", ".dir");
cacheDir.delete();
return new Cache(cacheDir, 1024 * 1024);
}
} }