HTTPCLIENT-1824: asynchronous HTTP response cache

This commit is contained in:
Oleg Kalnichevski 2017-12-20 10:22:38 +01:00
parent 3f52d0bf90
commit b4768fc86b
6 changed files with 520 additions and 63 deletions

View File

@ -0,0 +1,386 @@
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl.cache;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.apache.hc.client5.http.StandardMethods;
import org.apache.hc.client5.http.cache.HeaderConstants;
import org.apache.hc.client5.http.cache.HttpAsyncCacheInvalidator;
import org.apache.hc.client5.http.cache.HttpAsyncCacheStorage;
import org.apache.hc.client5.http.cache.HttpCacheCASOperation;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.client5.http.cache.ResourceFactory;
import org.apache.hc.client5.http.cache.ResourceIOException;
import org.apache.hc.client5.http.impl.Operations;
import org.apache.hc.core5.concurrent.Cancellable;
import org.apache.hc.core5.concurrent.FutureCallback;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.util.ByteArrayBuffer;
class BasicHttpAsyncCache implements HttpAsyncCache {
private final CacheUpdateHandler cacheUpdateHandler;
private final CacheKeyGenerator cacheKeyGenerator;
private final HttpAsyncCacheInvalidator cacheInvalidator;
private final HttpAsyncCacheStorage storage;
public BasicHttpAsyncCache(
final ResourceFactory resourceFactory,
final HttpAsyncCacheStorage storage,
final CacheKeyGenerator cacheKeyGenerator,
final HttpAsyncCacheInvalidator cacheInvalidator) {
this.cacheUpdateHandler = new CacheUpdateHandler(resourceFactory);
this.cacheKeyGenerator = cacheKeyGenerator;
this.storage = storage;
this.cacheInvalidator = cacheInvalidator;
}
public BasicHttpAsyncCache(
final ResourceFactory resourceFactory,
final HttpAsyncCacheStorage storage,
final CacheKeyGenerator cacheKeyGenerator) {
this(resourceFactory, storage, cacheKeyGenerator, DefaultAsyncCacheInvalidator.INSTANCE);
}
public BasicHttpAsyncCache(final ResourceFactory resourceFactory, final HttpAsyncCacheStorage storage) {
this( resourceFactory, storage, CacheKeyGenerator.INSTANCE);
}
@Override
public Cancellable flushCacheEntriesFor(
final HttpHost host, final HttpRequest request, final FutureCallback<Boolean> callback) {
if (!StandardMethods.isSafe(request.getMethod())) {
final String uri = cacheKeyGenerator.generateKey(host, request);
return storage.removeEntry(uri, callback);
} else {
callback.completed(Boolean.TRUE);
return Operations.nonCancellable();
}
}
@Override
public Cancellable flushInvalidatedCacheEntriesFor(
final HttpHost host, final HttpRequest request, final HttpResponse response, final FutureCallback<Boolean> callback) {
if (!StandardMethods.isSafe(request.getMethod())) {
return cacheInvalidator.flushInvalidatedCacheEntries(host, request, response, cacheKeyGenerator, storage, callback);
} else {
callback.completed(Boolean.TRUE);
return Operations.nonCancellable();
}
}
@Override
public Cancellable flushInvalidatedCacheEntriesFor(
final HttpHost host, final HttpRequest request, final FutureCallback<Boolean> callback) {
return cacheInvalidator.flushInvalidatedCacheEntries(host, request, cacheKeyGenerator, storage, callback);
}
Cancellable storeInCache(
final HttpHost target, final HttpRequest request, final HttpCacheEntry entry, final FutureCallback<Boolean> callback) {
if (entry.hasVariants()) {
return storeVariantEntry(target, request, entry, callback);
} else {
return storeNonVariantEntry(target, request, entry, callback);
}
}
Cancellable storeNonVariantEntry(
final HttpHost target,
final HttpRequest req,
final HttpCacheEntry entry,
final FutureCallback<Boolean> callback) {
final String uri = cacheKeyGenerator.generateKey(target, req);
return storage.putEntry(uri, entry, callback);
}
Cancellable storeVariantEntry(
final HttpHost target,
final HttpRequest req,
final HttpCacheEntry entry,
final FutureCallback<Boolean> callback) {
final String parentCacheKey = cacheKeyGenerator.generateKey(target, req);
final String variantKey = cacheKeyGenerator.generateVariantKey(req, entry);
final String variantURI = cacheKeyGenerator.generateVariantURI(target, req, entry);
return storage.putEntry(variantURI, entry, new FutureCallback<Boolean>() {
@Override
public void completed(final Boolean result) {
storage.updateEntry(parentCacheKey, new HttpCacheCASOperation() {
@Override
public HttpCacheEntry execute(final HttpCacheEntry existing) throws ResourceIOException {
return cacheUpdateHandler.updateParentCacheEntry(req.getRequestUri(), existing, entry, variantKey, variantURI);
}
}, callback);
}
@Override
public void failed(final Exception ex) {
callback.failed(ex);
}
@Override
public void cancelled() {
callback.cancelled();
}
});
}
@Override
public Cancellable reuseVariantEntryFor(
final HttpHost target, final HttpRequest req, final Variant variant, final FutureCallback<Boolean> callback) {
final String parentCacheKey = cacheKeyGenerator.generateKey(target, req);
final HttpCacheEntry entry = variant.getEntry();
final String variantKey = cacheKeyGenerator.generateVariantKey(req, entry);
final String variantCacheKey = variant.getCacheKey();
return storage.updateEntry(parentCacheKey, new HttpCacheCASOperation() {
@Override
public HttpCacheEntry execute(final HttpCacheEntry existing) throws ResourceIOException {
return cacheUpdateHandler.updateParentCacheEntry(req.getRequestUri(), existing, entry, variantKey, variantCacheKey);
}
}, callback);
}
@Override
public Cancellable updateCacheEntry(
final HttpHost target,
final HttpRequest request,
final HttpCacheEntry stale,
final HttpResponse originResponse,
final Date requestSent,
final Date responseReceived,
final FutureCallback<HttpCacheEntry> callback) {
try {
final HttpCacheEntry updatedEntry = cacheUpdateHandler.updateCacheEntry(
request.getRequestUri(),
stale,
requestSent,
responseReceived,
originResponse);
return storeInCache(target, request, updatedEntry, new FutureCallback<Boolean>() {
@Override
public void completed(final Boolean result) {
callback.completed(updatedEntry);
}
@Override
public void failed(final Exception ex) {
callback.failed(ex);
}
@Override
public void cancelled() {
callback.cancelled();
}
});
} catch (final ResourceIOException ex) {
callback.failed(ex);
return Operations.nonCancellable();
}
}
@Override
public Cancellable updateVariantCacheEntry(
final HttpHost target,
final HttpRequest request,
final HttpCacheEntry stale,
final HttpResponse originResponse,
final Date requestSent,
final Date responseReceived,
final String cacheKey,
final FutureCallback<HttpCacheEntry> callback) {
try {
final HttpCacheEntry updatedEntry = cacheUpdateHandler.updateCacheEntry(
request.getRequestUri(),
stale,
requestSent,
responseReceived,
originResponse);
return storage.putEntry(cacheKey, updatedEntry, new FutureCallback<Boolean>() {
@Override
public void completed(final Boolean result) {
callback.completed(updatedEntry);
}
@Override
public void failed(final Exception ex) {
callback.failed(ex);
}
@Override
public void cancelled() {
callback.cancelled();
}
});
} catch (final ResourceIOException ex) {
callback.failed(ex);
return Operations.nonCancellable();
}
}
@Override
public Cancellable createCacheEntry(
final HttpHost host,
final HttpRequest request,
final HttpResponse originResponse,
final ByteArrayBuffer content,
final Date requestSent,
final Date responseReceived,
final FutureCallback<HttpCacheEntry> callback) {
try {
final HttpCacheEntry entry = cacheUpdateHandler.createtCacheEntry(request, originResponse, content, requestSent, responseReceived);
return storeInCache(host, request, entry, new FutureCallback<Boolean>() {
@Override
public void completed(final Boolean result) {
callback.completed(entry);
}
@Override
public void failed(final Exception ex) {
callback.failed(ex);
}
@Override
public void cancelled() {
callback.cancelled();
}
});
} catch (final ResourceIOException ex) {
callback.failed(ex);
return Operations.nonCancellable();
}
}
@Override
public Cancellable getCacheEntry(final HttpHost host, final HttpRequest request, final FutureCallback<HttpCacheEntry> callback) {
final ComplexCancellable complexCancellable = new ComplexCancellable();
final String cacheKey = cacheKeyGenerator.generateKey(host, request);
complexCancellable.setDependency(storage.getEntry(cacheKey, new FutureCallback<HttpCacheEntry>() {
@Override
public void completed(final HttpCacheEntry root) {
if (root != null) {
if (root.hasVariants()) {
final String variantCacheKey = root.getVariantMap().get(cacheKeyGenerator.generateVariantKey(request, root));
if (variantCacheKey != null) {
complexCancellable.setDependency(storage.getEntry(variantCacheKey, callback));
return;
}
}
}
callback.completed(root);
}
@Override
public void failed(final Exception ex) {
callback.failed(ex);
}
@Override
public void cancelled() {
callback.cancelled();
}
}));
return complexCancellable;
}
@Override
public Cancellable getVariantCacheEntriesWithEtags(
final HttpHost host, final HttpRequest request, final FutureCallback<Map<String, Variant>> callback) {
final ComplexCancellable complexCancellable = new ComplexCancellable();
final String cacheKey = cacheKeyGenerator.generateKey(host, request);
complexCancellable.setDependency(storage.getEntry(cacheKey, new FutureCallback<HttpCacheEntry>() {
@Override
public void completed(final HttpCacheEntry rootEntry) {
final Map<String, Variant> variants = new HashMap<>();
if (rootEntry != null && rootEntry.hasVariants()) {
complexCancellable.setDependency(storage.getEntries(
rootEntry.getVariantMap().keySet(),
new FutureCallback<Map<String, HttpCacheEntry>>() {
@Override
public void completed(final Map<String, HttpCacheEntry> resultMap) {
for (final Map.Entry<String, HttpCacheEntry> resultMapEntry : resultMap.entrySet()) {
final String cacheKey = resultMapEntry.getKey();
final HttpCacheEntry cacheEntry = resultMapEntry.getValue();
final Header etagHeader = cacheEntry.getFirstHeader(HeaderConstants.ETAG);
if (etagHeader != null) {
variants.put(etagHeader.getValue(), new Variant(cacheKey, cacheEntry));
}
}
callback.completed(variants);
}
@Override
public void failed(final Exception ex) {
callback.failed(ex);
}
@Override
public void cancelled() {
callback.cancelled();
}
}));
} else {
callback.completed(variants);
}
}
@Override
public void failed(final Exception ex) {
callback.failed(ex);
}
@Override
public void cancelled() {
callback.cancelled();
}
}));
return complexCancellable;
}
}

View File

@ -193,6 +193,7 @@ class BasicHttpCache implements HttpCache {
return updatedEntry;
}
@Override
public HttpCacheEntry createCacheEntry(
final HttpHost host,
final HttpRequest request,
@ -236,16 +237,14 @@ class BasicHttpCache implements HttpCache {
return variants;
}
for(final Map.Entry<String, String> variant : root.getVariantMap().entrySet()) {
final String variantKey = variant.getKey();
final String variantCacheKey = variant.getValue();
addVariantWithEtag(variantKey, variantCacheKey, variants);
addVariantWithEtag(variantCacheKey, variants);
}
return variants;
}
private void addVariantWithEtag(final String variantKey,
final String variantCacheKey, final Map<String, Variant> variants)
throws ResourceIOException {
private void addVariantWithEtag(
final String variantCacheKey, final Map<String, Variant> variants) throws ResourceIOException {
final HttpCacheEntry entry = storage.getEntry(variantCacheKey);
if (entry == null) {
return;
@ -254,7 +253,7 @@ class BasicHttpCache implements HttpCache {
if (etagHeader == null) {
return;
}
variants.put(etagHeader.getValue(), new Variant(variantKey, variantCacheKey, entry));
variants.put(etagHeader.getValue(), new Variant(variantCacheKey, entry));
}
}

View File

@ -0,0 +1,124 @@
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl.cache;
import java.util.Date;
import java.util.Map;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.core5.annotation.Internal;
import org.apache.hc.core5.concurrent.Cancellable;
import org.apache.hc.core5.concurrent.FutureCallback;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.util.ByteArrayBuffer;
/**
* @since 5.0
*/
@Internal
interface HttpAsyncCache {
/**
* Clear all matching {@link HttpCacheEntry}s.
*/
Cancellable flushCacheEntriesFor(
HttpHost host, HttpRequest request, FutureCallback<Boolean> callback);
/**
* Clear invalidated matching {@link HttpCacheEntry}s
*/
Cancellable flushInvalidatedCacheEntriesFor(
HttpHost host, HttpRequest request, FutureCallback<Boolean> callback);
/** Clear any entries that may be invalidated by the given response to
* a particular request.
*/
Cancellable flushInvalidatedCacheEntriesFor(
HttpHost host, HttpRequest request, HttpResponse response, FutureCallback<Boolean> callback);
/**
* Retrieve matching {@link HttpCacheEntry} from the cache if it exists
*/
Cancellable getCacheEntry(
HttpHost host, HttpRequest request, FutureCallback<HttpCacheEntry> callback);
/**
* Retrieve all variants from the cache, if there are no variants then an empty
*/
Cancellable getVariantCacheEntriesWithEtags(
HttpHost host, HttpRequest request, FutureCallback<Map<String,Variant>> callback);
/**
* Store a {@link HttpResponse} in the cache if possible, and return
*/
Cancellable createCacheEntry(
HttpHost host,
HttpRequest request,
HttpResponse originResponse,
ByteArrayBuffer content,
Date requestSent,
Date responseReceived,
FutureCallback<HttpCacheEntry> callback);
/**
* Update a {@link HttpCacheEntry} using a 304 {@link HttpResponse}.
*/
Cancellable updateCacheEntry(
HttpHost target,
HttpRequest request,
HttpCacheEntry stale,
HttpResponse originResponse,
Date requestSent,
Date responseReceived,
FutureCallback<HttpCacheEntry> callback);
/**
* Update a specific {@link HttpCacheEntry} representing a cached variant
* using a 304 {@link HttpResponse}.
*/
Cancellable updateVariantCacheEntry(
HttpHost target,
HttpRequest request,
HttpCacheEntry stale,
HttpResponse originResponse,
Date requestSent,
Date responseReceived,
String cacheKey,
FutureCallback<HttpCacheEntry> callback);
/**
* Specifies cache should reuse the given cached variant to satisfy
* requests whose varying headers match those of the given client request.
*/
Cancellable reuseVariantEntryFor(
HttpHost target,
HttpRequest req,
Variant variant,
FutureCallback<Boolean> callback);
}

View File

@ -43,57 +43,32 @@ interface HttpCache {
/**
* Clear all matching {@link HttpCacheEntry}s.
* @param host
* @param request
* @throws ResourceIOException
*/
void flushCacheEntriesFor(HttpHost host, HttpRequest request) throws ResourceIOException;
/**
* Clear invalidated matching {@link HttpCacheEntry}s
* @param host
* @param request
* @throws ResourceIOException
*/
void flushInvalidatedCacheEntriesFor(HttpHost host, HttpRequest request) throws ResourceIOException;
/** Clear any entries that may be invalidated by the given response to
* a particular request.
* @param host
* @param request
* @param response
*/
void flushInvalidatedCacheEntriesFor(HttpHost host, HttpRequest request, HttpResponse response);
/**
* Retrieve matching {@link HttpCacheEntry} from the cache if it exists
* @param host
* @param request
* @return the matching {@link HttpCacheEntry} or {@code null}
* @throws ResourceIOException
* Retrieve matching {@link HttpCacheEntry} from the cache if it exists.
*/
HttpCacheEntry getCacheEntry(HttpHost host, HttpRequest request) throws ResourceIOException;
/**
* Retrieve all variants from the cache, if there are no variants then an empty
* {@link Map} is returned
* @param host
* @param request
* @return a {@code Map} mapping Etags to variant cache entries
* @throws ResourceIOException
*/
Map<String,Variant> getVariantCacheEntriesWithEtags(HttpHost host, HttpRequest request) throws ResourceIOException;
/**
* Store a {@link HttpResponse} in the cache if possible, and return
* @param host
* @param request
* @param originResponse
* @param content
* @param requestSent
* @param responseReceived
* @return new {@link HttpCacheEntry}
* @throws ResourceIOException
*/
HttpCacheEntry createCacheEntry(
HttpHost host,
@ -105,14 +80,6 @@ interface HttpCache {
/**
* Update a {@link HttpCacheEntry} using a 304 {@link HttpResponse}.
* @param target
* @param request
* @param stale
* @param originResponse
* @param requestSent
* @param responseReceived
* @return the updated {@link HttpCacheEntry}
* @throws ResourceIOException
*/
HttpCacheEntry updateCacheEntry(
HttpHost target,
@ -125,15 +92,6 @@ interface HttpCache {
/**
* Update a specific {@link HttpCacheEntry} representing a cached variant
* using a 304 {@link HttpResponse}.
* @param target host for client request
* @param request actual request from upstream client
* @param stale current variant cache entry
* @param originResponse 304 response received from origin
* @param requestSent when the validating request was sent
* @param responseReceived when the validating response was received
* @param cacheKey where in the cache this entry is currently stored
* @return the updated {@link HttpCacheEntry}
* @throws ResourceIOException
*/
HttpCacheEntry updateVariantCacheEntry(
HttpHost target,
@ -147,10 +105,6 @@ interface HttpCache {
/**
* Specifies cache should reuse the given cached variant to satisfy
* requests whose varying headers match those of the given client request.
* @param target host of the upstream client request
* @param req request sent by upstream client
* @param variant variant cache entry to reuse
* @throws ResourceIOException may be thrown during cache processChallenge
*/
void reuseVariantEntryFor(
HttpHost target,

View File

@ -31,20 +31,14 @@ import org.apache.hc.client5.http.cache.HttpCacheEntry;
/** Records a set of information describing a cached variant. */
class Variant {
private final String variantKey;
private final String cacheKey;
private final HttpCacheEntry entry;
public Variant(final String variantKey, final String cacheKey, final HttpCacheEntry entry) {
this.variantKey = variantKey;
public Variant(final String cacheKey, final HttpCacheEntry entry) {
this.cacheKey = cacheKey;
this.entry = entry;
}
public String getVariantKey() {
return variantKey;
}
public String getCacheKey() {
return cacheKey;
}

View File

@ -302,9 +302,9 @@ public class TestConditionalRequestBuilder {
final String etag3 = "\"789\"";
final Map<String,Variant> variantEntries = new HashMap<>();
variantEntries.put(etag1, new Variant("A","B",HttpTestUtils.makeCacheEntry(new Header[] { new BasicHeader("ETag", etag1) })));
variantEntries.put(etag2, new Variant("C","D",HttpTestUtils.makeCacheEntry(new Header[] { new BasicHeader("ETag", etag2) })));
variantEntries.put(etag3, new Variant("E","F",HttpTestUtils.makeCacheEntry(new Header[] { new BasicHeader("ETag", etag3) })));
variantEntries.put(etag1, new Variant("A", HttpTestUtils.makeCacheEntry(new Header[] { new BasicHeader("ETag", etag1) })));
variantEntries.put(etag2, new Variant("B", HttpTestUtils.makeCacheEntry(new Header[] { new BasicHeader("ETag", etag2) })));
variantEntries.put(etag3, new Variant("C", HttpTestUtils.makeCacheEntry(new Header[] { new BasicHeader("ETag", etag3) })));
final HttpRequest conditional = impl.buildConditionalRequestFromVariants(request, variantEntries);