Skip to content

Commit

Permalink
Merge pull request #49 from nstdio/cache-stats
Browse files Browse the repository at this point in the history
feat(cache): Cache statistics.
  • Loading branch information
nstdio authored Mar 26, 2022
2 parents 28fefbf + 3ad0d64 commit 5d3fd79
Show file tree
Hide file tree
Showing 15 changed files with 218 additions and 17 deletions.
24 changes: 24 additions & 0 deletions src/main/java/io/github/nstdio/http/ext/Cache.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ static Cache noop() {
*/
void evictAll();

/**
* Gets the statistics for this cache.
*
* @return The statistics for this cache.
* @see CacheStats
*/
CacheStats stats();

/**
* Creates a {@code Writer}.
*
Expand Down Expand Up @@ -131,6 +139,22 @@ interface CacheBuilder {
Cache build();
}

interface CacheStats {
/**
* The number the cache serves stored response.
*
* @return The number of times the cache serves stored response.
*/
long hit();

/**
* The number the cache does not have stored response which resulted in network call.
*
* @return The number the cache does not have stored response which resulted in network call.
*/
long miss();
}

/**
* The builder for in memory cache.
*/
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/io/github/nstdio/http/ext/CachingInterceptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import static java.util.stream.Collectors.toList;

import io.github.nstdio.http.ext.Cache.CacheEntry;
import io.github.nstdio.http.ext.Cache.CacheStats;
import lombok.RequiredArgsConstructor;

import java.net.http.HttpRequest;
Expand All @@ -33,6 +34,7 @@
import java.time.Clock;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Stream;

@RequiredArgsConstructor
Expand Down Expand Up @@ -147,6 +149,7 @@ private <T> BodyHandler<T> cacheAware(RequestContext ctx) {
info, ctx.request(), clock);

if (metadata.isApplicable()) {
trackMiss();
var writer = cache.writer(metadata);
sub = new CachingBodySubscriber<>(sub, writer.subscriber(), writer.finisher());
}
Expand Down Expand Up @@ -238,6 +241,23 @@ private <T> HttpResponse<T> possiblyCached(RequestContext ctx, CacheEntry entry,
}

private <T> HttpResponse<T> createCachedResponse(RequestContext ctx, CacheEntry entry) {
trackHit();
return new CachedHttpResponse<>(ctx.bodyHandler(), ctx.request(), entry);
}


private void trackMiss() {
executeIfTrackable(TrackableCacheStats::trackMiss);
}

private void trackHit() {
executeIfTrackable(TrackableCacheStats::trackHit);
}

private void executeIfTrackable(Consumer<TrackableCacheStats> action) {
CacheStats stats = cache.stats();
if (stats instanceof TrackableCacheStats) {
action.accept((TrackableCacheStats) stats);
}
}
}
44 changes: 44 additions & 0 deletions src/main/java/io/github/nstdio/http/ext/DefaultCacheStats.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright (C) 2022 Edgar Asatryan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.github.nstdio.http.ext;

import java.util.concurrent.atomic.LongAdder;

class DefaultCacheStats implements TrackableCacheStats {
private final LongAdder hit = new LongAdder();
private final LongAdder miss = new LongAdder();

@Override
public long hit() {
return hit.longValue();
}

@Override
public long miss() {
return miss.longValue();
}

@Override
public void trackHit() {
hit.increment();
}

@Override
public void trackMiss() {
miss.increment();
}
}
5 changes: 5 additions & 0 deletions src/main/java/io/github/nstdio/http/ext/FilteringCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ public void evictAll() {
delegate.evictAll();
}

@Override
public CacheStats stats() {
return delegate.stats();
}

@Override
public <T> Writer<T> writer(CacheEntryMetadata metadata) {
if (responseFilter.test(metadata.response()) && requestFilter.test(metadata.request())) {
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/io/github/nstdio/http/ext/NullCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ public void evictAll() {
// intentional noop
}

@Override
public CacheStats stats() {
return null;
}

@Override
public <T> Writer<T> writer(CacheEntryMetadata metadata) {
return writer();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

abstract class SizeConstrainedCache implements Cache {
private final LruMultimap<URI, CacheEntry> cache;
private final CacheStats stats = new DefaultCacheStats();
private final long maxBytes;
private long size;

Expand Down Expand Up @@ -68,6 +69,11 @@ public void evictAll() {
cache.clear();
}

@Override
public CacheStats stats() {
return stats;
}

private void putInternal(HttpRequest k, CacheEntry e) {
size += e.bodySize();
cache.putSingle(k.uri(), e, idxFn(k));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ public synchronized void evictAll() {
delegate.evictAll();
}

@Override
public /* synchronized */ CacheStats stats() {
return delegate.stats();
}

@Override
public <T> Writer<T> writer(CacheEntryMetadata metadata) {
Writer<T> writer = delegate.writer(metadata);
Expand Down
13 changes: 0 additions & 13 deletions src/main/java/io/github/nstdio/http/ext/Throwables.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@

package io.github.nstdio.http.ext;

import java.io.IOException;
import java.util.concurrent.CompletionException;

class Throwables {
private Throwables() {
}
Expand All @@ -32,14 +29,4 @@ private static <T extends Throwable> T sneakyThrow0(Throwable t) throws T {
throw (T) t;
}

static void unwrap(CompletionException e) throws IOException, InterruptedException {
Throwable cause = e.getCause();
if (cause instanceof IOException) {
throw (IOException) cause;
} else if (cause instanceof InterruptedException) {
throw (InterruptedException) cause;
}

throw e;
}
}
25 changes: 25 additions & 0 deletions src/main/java/io/github/nstdio/http/ext/TrackableCacheStats.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (C) 2022 Edgar Asatryan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.github.nstdio.http.ext;

import io.github.nstdio.http.ext.Cache.CacheStats;

interface TrackableCacheStats extends CacheStats {
void trackHit();

void trackMiss();
}
35 changes: 35 additions & 0 deletions src/test/java/io/github/nstdio/http/ext/Assertions.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ public static HttpHeadersAssertion assertThat(HttpHeaders h) {
return new HttpHeadersAssertion(h);
}

public static CacheAssertion assertThat(Cache c) {
return new CacheAssertion(c);
}

public static <K, V> LruMultimapAssertion<K, V> assertThat(LruMultimap<K, V> m) {
return new LruMultimapAssertion<>(m);
}
Expand Down Expand Up @@ -234,4 +238,35 @@ public HttpResponseAssertion<T> hasNoHeader(String headers) {
return this;
}
}

public static class CacheAssertion extends ObjectAssert<Cache> {
public CacheAssertion(Cache cache) {
super(cache);
}

public CacheAssertion hasHits(long expectedHit) {
assertEquals(expectedHit, stats().hit(), "cache does not have expected hit count");
return this;
}

private Cache.CacheStats stats() {
return actual.stats();
}

public CacheAssertion hasNoHits() {
assertEquals(0, stats().hit(), "cache does expected to have hits, but there is some");
return this;
}

public CacheAssertion hasMiss(long expectedMiss) {
assertEquals(expectedMiss, stats().miss(), "cache does not have expected miss count");
return this;
}

public CacheAssertion hasAtLeastMiss(long expectedMiss) {
org.assertj.core.api.Assertions.assertThat(stats().miss())
.isGreaterThanOrEqualTo(expectedMiss);
return this;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ public ExtendedHttpClient client() {
return client;
}

@Override
public Cache cache() {
return cache;
}

@Override
public ExtendedHttpClient client(Clock clock) {
return new ExtendedHttpClient(HttpClient.newHttpClient(), cache, clock);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ static URI resolve(String path) {
*/
ExtendedHttpClient client();

/**
* The cache (if any) used by {@linkplain #client()}.
*/
Cache cache();

/**
* The client created with {@code clock} under the test.
*/
Expand Down Expand Up @@ -112,6 +117,7 @@ default HttpRequest.Builder requestBuilder() {
@Test
default void shouldSupportETagForCaching() throws Exception {
//given
Cache cache = cache();
var etag = "v1";
stubFor(get(urlEqualTo(path()))
.willReturn(ok()
Expand All @@ -127,18 +133,21 @@ default void shouldSupportETagForCaching() throws Exception {
//when + then
var r1 = send(requestBuilder().build());
assertThat(r1).isNotCached();
assertThat(cache).hasNoHits().hasMiss(1);

awaitFor(() -> {
var r2 = send(requestBuilder().build());
assertThat(r2).isCached()
.hasStatusCode(200)
.hasBody("abc");
});
assertThat(cache).hasHits(1).hasMiss(1);
}

@Test
default void shouldApplyHeuristicFreshness() throws Exception {
//given
Cache cache = cache();
stubFor(get(urlEqualTo(path()))
.willReturn(ok()
.withHeader(HEADER_LAST_MODIFIED, Headers.toRFC1123(Instant.now().minusSeconds(60)))
Expand All @@ -149,11 +158,10 @@ default void shouldApplyHeuristicFreshness() throws Exception {
//when + then
var r1 = send(requestBuilder().build());
assertThat(r1).isNotCached();
assertThat(cache).hasNoHits().hasMiss(1);

awaitFor(() -> {
var r2 = send(requestBuilder().build());
assertThat(r2).isCached();
});
awaitFor(() -> assertThat(send(requestBuilder().build())).isCached());
assertThat(cache).hasHits(1).hasAtLeastMiss(1);
}

@Test
Expand Down Expand Up @@ -263,6 +271,7 @@ default void shouldRespectMinFreshRequests() throws Exception {
})
default void shouldNotRespondWithCacheWhenNoCacheProvided(String cacheControl) throws IOException, InterruptedException {
//given
var cache = cache();
var urlPattern = urlEqualTo(path());
stubFor(get(urlPattern)
.willReturn(ok()
Expand All @@ -280,13 +289,17 @@ default void shouldNotRespondWithCacheWhenNoCacheProvided(String cacheControl) t
var noCacheControlRequest = requestBuilder().build();

//when + then
assertThat(cache).hasHits(i).hasMiss(i);

var r1 = send(request);
assertThat(r1).isNotCached().hasBody("abc");

awaitFor(() -> {
var r2 = send(noCacheControlRequest);
assertThat(r2).isCached().hasBody("abc");
});

assertThat(cache).hasHits(i + 1).hasAtLeastMiss(i + 1);
}

verify(count, getRequestedFor(urlPattern).withHeader(HEADER_CACHE_CONTROL, equalTo(cacheControl)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ public ExtendedHttpClient client() {
return client;
}

@Override
public Cache cache() {
return cache;
}

@Override
public ExtendedHttpClient client(Clock clock) {
return new ExtendedHttpClient(HttpClient.newHttpClient(), cache, clock);
Expand Down
Loading

0 comments on commit 5d3fd79

Please sign in to comment.