HTTPCLIENT-2277: optimization of internal cache operations

This commit is contained in:
Oleg Kalnichevski 2023-06-22 23:02:02 +02:00
parent e469cbb78d
commit 5fbef8fc7f
17 changed files with 1039 additions and 1068 deletions

View File

@ -30,6 +30,8 @@ import java.io.IOException;
import java.io.InterruptedIOException;
import java.nio.ByteBuffer;
import java.time.Instant;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
@ -44,7 +46,6 @@ import org.apache.hc.client5.http.async.methods.SimpleBody;
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
import org.apache.hc.client5.http.cache.CacheResponseStatus;
import org.apache.hc.client5.http.cache.HttpAsyncCacheStorage;
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.ExecSupport;
@ -64,6 +65,7 @@ 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.http.HttpStatus;
import org.apache.hc.core5.http.Method;
import org.apache.hc.core5.http.impl.BasicEntityDetails;
import org.apache.hc.core5.http.nio.AsyncDataConsumer;
import org.apache.hc.core5.http.nio.AsyncEntityProducer;
@ -256,22 +258,24 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
}));
} else {
operation.setDependency(responseCache.getCacheEntry(target, request, new FutureCallback<HttpCacheEntry>() {
operation.setDependency(responseCache.match(target, request, new FutureCallback<CacheMatch>() {
@Override
public void completed(final HttpCacheEntry entry) {
final SimpleHttpResponse fatalErrorResponse = getFatallyNonCompliantResponse(request, context, entry != null);
public void completed(final CacheMatch result) {
final CacheHit hit = result != null ? result.hit : null;
final CacheHit root = result != null ? result.root : null;
final SimpleHttpResponse fatalErrorResponse = getFatallyNonCompliantResponse(request, context, hit != null);
if (fatalErrorResponse != null) {
triggerResponse(fatalErrorResponse, scope, asyncExecCallback);
return;
}
if (entry == null) {
if (hit == null) {
LOG.debug("Cache miss");
handleCacheMiss(requestCacheControl, target, request, entityProducer, scope, chain, asyncExecCallback);
handleCacheMiss(requestCacheControl, root, target, request, entityProducer, scope, chain, asyncExecCallback);
} else {
final ResponseCacheControl responseCacheControl = CacheControlHeaderParser.INSTANCE.parse(entry);
handleCacheHit(requestCacheControl, responseCacheControl, target, request, entityProducer, scope, chain, asyncExecCallback, entry);
final ResponseCacheControl responseCacheControl = CacheControlHeaderParser.INSTANCE.parse(hit.entry);
handleCacheHit(requestCacheControl, responseCacheControl, hit, target, request, entityProducer, scope, chain, asyncExecCallback);
}
}
@ -498,22 +502,24 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
storeRequestIfModifiedSinceFor304Response(request, backendResponse);
} else {
LOG.debug("Backend response is not cacheable");
responseCache.flushCacheEntriesFor(target, request, new FutureCallback<Boolean>() {
if (!Method.isSafe(request.getMethod())) {
responseCache.flushCacheEntriesFor(target, request, new FutureCallback<Boolean>() {
@Override
public void completed(final Boolean result) {
}
@Override
public void completed(final Boolean result) {
}
@Override
public void failed(final Exception ex) {
LOG.warn("Unable to flush invalidated entries from cache", ex);
}
@Override
public void failed(final Exception ex) {
LOG.warn("Unable to flush invalidated entries from cache", ex);
}
@Override
public void cancelled() {
}
@Override
public void cancelled() {
}
});
});
}
}
final CachingAsyncDataConsumer cachingDataConsumer = cachingConsumerRef.get();
if (cachingDataConsumer != null) {
@ -530,20 +536,20 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
void triggerNewCacheEntryResponse(final HttpResponse backendResponse, final Instant responseDate, final ByteArrayBuffer buffer) {
final CancellableDependency operation = scope.cancellableDependency;
operation.setDependency(responseCache.createEntry(
operation.setDependency(responseCache.store(
target,
request,
backendResponse,
buffer,
requestDate,
responseDate,
new FutureCallback<HttpCacheEntry>() {
new FutureCallback<CacheHit>() {
@Override
public void completed(final HttpCacheEntry newEntry) {
public void completed(final CacheHit hit) {
LOG.debug("Backend response successfully cached");
try {
final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, newEntry);
final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry);
triggerResponse(cacheResponse, scope, asyncExecCallback);
} catch (final ResourceIOException ex) {
asyncExecCallback.failed(ex);
@ -572,14 +578,15 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
final HttpResponse backendResponse = cachingDataConsumer.backendResponse;
if (cacheConfig.isFreshnessCheckEnabled()) {
final CancellableDependency operation = scope.cancellableDependency;
operation.setDependency(responseCache.getCacheEntry(target, request, new FutureCallback<HttpCacheEntry>() {
operation.setDependency(responseCache.match(target, request, new FutureCallback<CacheMatch>() {
@Override
public void completed(final HttpCacheEntry existingEntry) {
if (DateSupport.isAfter(existingEntry, backendResponse, HttpHeaders.DATE)) {
public void completed(final CacheMatch result) {
final CacheHit hit = result != null ? result.hit : null;
if (DateSupport.isAfter(hit != null ? hit.entry : null, backendResponse, HttpHeaders.DATE)) {
LOG.debug("Backend already contains fresher cache entry");
try {
final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, existingEntry);
final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry);
triggerResponse(cacheResponse, scope, asyncExecCallback);
} catch (final ResourceIOException ex) {
asyncExecCallback.failed(ex);
@ -618,13 +625,13 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
private void handleCacheHit(
final RequestCacheControl requestCacheControl,
final ResponseCacheControl responseCacheControl,
final CacheHit hit,
final HttpHost target,
final HttpRequest request,
final AsyncEntityProducer entityProducer,
final AsyncExecChain.Scope scope,
final AsyncExecChain chain,
final AsyncExecCallback asyncExecCallback,
final HttpCacheEntry entry) {
final AsyncExecCallback asyncExecCallback) {
final HttpClientContext context = scope.clientContext;
recordCacheHit(target, request);
final Instant now = getCurrentDate();
@ -634,21 +641,19 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
if (LOG.isDebugEnabled()) {
LOG.debug("Revalidating with server due to no-cache directive in response.");
}
revalidateCacheEntry(requestCacheControl, responseCacheControl,
target, request, entityProducer, scope, chain, asyncExecCallback, entry);
revalidateCacheEntry(requestCacheControl, responseCacheControl, hit, target, request, entityProducer, scope, chain, asyncExecCallback);
return;
}
if (suitabilityChecker.canCachedResponseBeUsed(requestCacheControl, responseCacheControl, request, entry, now)) {
if (responseCachingPolicy.responseContainsNoCacheDirective(responseCacheControl, entry)) {
if (suitabilityChecker.canCachedResponseBeUsed(requestCacheControl, responseCacheControl, request, hit.entry, now)) {
if (responseCachingPolicy.responseContainsNoCacheDirective(responseCacheControl, hit.entry)) {
// Revalidate with the server due to no-cache directive in response
revalidateCacheEntry(requestCacheControl, responseCacheControl,
target, request, entityProducer, scope, chain, asyncExecCallback, entry);
revalidateCacheEntry(requestCacheControl, responseCacheControl, hit, target, request, entityProducer, scope, chain, asyncExecCallback);
return;
}
LOG.debug("Cache hit");
try {
final SimpleHttpResponse cacheResponse = generateCachedResponse(responseCacheControl, request, context, entry, now);
final SimpleHttpResponse cacheResponse = generateCachedResponse(responseCacheControl, hit.entry, request, context, now);
triggerResponse(cacheResponse, scope, asyncExecCallback);
} catch (final ResourceIOException ex) {
recordCacheFailure(target, request);
@ -668,15 +673,15 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
LOG.debug("Cache entry not suitable but only-if-cached requested");
final SimpleHttpResponse cacheResponse = generateGatewayTimeout(context);
triggerResponse(cacheResponse, scope, asyncExecCallback);
} else if (!(entry.getStatus() == HttpStatus.SC_NOT_MODIFIED && !suitabilityChecker.isConditional(request))) {
} else if (!(hit.entry.getStatus() == HttpStatus.SC_NOT_MODIFIED && !suitabilityChecker.isConditional(request))) {
LOG.debug("Revalidating cache entry");
final boolean staleIfErrorEnabled = responseCachingPolicy.isStaleIfErrorEnabled(responseCacheControl, entry);
final boolean staleIfErrorEnabled = responseCachingPolicy.isStaleIfErrorEnabled(responseCacheControl, hit.entry);
if (cacheRevalidator != null
&& !staleResponseNotAllowed(requestCacheControl, responseCacheControl, entry, now)
&& (validityPolicy.mayReturnStaleWhileRevalidating(responseCacheControl, entry, now) || staleIfErrorEnabled)) {
&& !staleResponseNotAllowed(requestCacheControl, responseCacheControl, hit.entry, now)
&& (validityPolicy.mayReturnStaleWhileRevalidating(responseCacheControl, hit.entry, now) || staleIfErrorEnabled)) {
LOG.debug("Serving stale with asynchronous revalidation");
try {
final SimpleHttpResponse cacheResponse = generateCachedResponse(responseCacheControl, request, context, entry, now);
final SimpleHttpResponse cacheResponse = generateCachedResponse(responseCacheControl, hit.entry, request, context, now);
final String exchangeId = ExecSupport.getNextExchangeId();
context.setExchangeId(exchangeId);
final AsyncExecChain.Scope fork = new AsyncExecChain.Scope(
@ -689,10 +694,10 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
scope.scheduler,
scope.execCount);
cacheRevalidator.revalidateCacheEntry(
responseCache.generateKey(target, request, entry),
hit.getEntryKey(),
asyncExecCallback,
asyncExecCallback1 -> revalidateCacheEntry(requestCacheControl, responseCacheControl,
target, request, entityProducer, fork, chain, asyncExecCallback1, entry));
hit, target, request, entityProducer, fork, chain, asyncExecCallback1));
triggerResponse(cacheResponse, scope, asyncExecCallback);
} catch (final ResourceIOException ex) {
if (staleIfErrorEnabled) {
@ -700,8 +705,7 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
LOG.debug("Serving stale response due to IOException and stale-if-error enabled");
}
try {
final SimpleHttpResponse cacheResponse = generateCachedResponse(responseCacheControl,
request, context, entry, now);
final SimpleHttpResponse cacheResponse = generateCachedResponse(responseCacheControl, hit.entry, request, context, now);
triggerResponse(cacheResponse, scope, asyncExecCallback);
} catch (final ResourceIOException ex2) {
if (LOG.isDebugEnabled()) {
@ -714,8 +718,7 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
}
}
} else {
revalidateCacheEntry(requestCacheControl, responseCacheControl,
target, request, entityProducer, scope, chain, asyncExecCallback, entry);
revalidateCacheEntry(requestCacheControl, responseCacheControl, hit, target, request, entityProducer, scope, chain, asyncExecCallback);
}
} else {
LOG.debug("Cache entry not usable; calling backend");
@ -726,18 +729,18 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
void revalidateCacheEntry(
final RequestCacheControl requestCacheControl,
final ResponseCacheControl responseCacheControl,
final CacheHit hit,
final HttpHost target,
final HttpRequest request,
final AsyncEntityProducer entityProducer,
final AsyncExecChain.Scope scope,
final AsyncExecChain chain,
final AsyncExecCallback asyncExecCallback,
final HttpCacheEntry cacheEntry) {
final AsyncExecCallback asyncExecCallback) {
final Instant requestDate = getCurrentDate();
final HttpRequest conditionalRequest = conditionalRequestBuilder.buildConditionalRequest(
responseCacheControl,
BasicRequestBuilder.copy(scope.originalRequest).build(),
cacheEntry);
hit.entry);
chainProceed(conditionalRequest, entityProducer, scope, chain, new AsyncExecCallback() {
final AtomicReference<AsyncExecCallback> callbackRef = new AtomicReference<>();
@ -745,24 +748,23 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
void triggerUpdatedCacheEntryResponse(final HttpResponse backendResponse, final Instant responseDate) {
final CancellableDependency operation = scope.cancellableDependency;
recordCacheUpdate(scope.clientContext);
operation.setDependency(responseCache.updateEntry(
target,
operation.setDependency(responseCache.update(
hit,
request,
cacheEntry,
backendResponse,
requestDate,
responseDate,
new FutureCallback<HttpCacheEntry>() {
new FutureCallback<CacheHit>() {
@Override
public void completed(final HttpCacheEntry updatedEntry) {
public void completed(final CacheHit updated) {
if (suitabilityChecker.isConditional(request)
&& suitabilityChecker.allConditionalsMatch(request, updatedEntry, Instant.now())) {
final SimpleHttpResponse cacheResponse = responseGenerator.generateNotModifiedResponse(updatedEntry);
&& suitabilityChecker.allConditionalsMatch(request, updated.entry, Instant.now())) {
final SimpleHttpResponse cacheResponse = responseGenerator.generateNotModifiedResponse(updated.entry);
triggerResponse(cacheResponse, scope, asyncExecCallback);
} else {
try {
final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, updatedEntry);
final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, updated.entry);
triggerResponse(cacheResponse, scope, asyncExecCallback);
} catch (final ResourceIOException ex) {
asyncExecCallback.failed(ex);
@ -785,7 +787,7 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
void triggerResponseStaleCacheEntry() {
try {
final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, cacheEntry);
final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry);
cacheResponse.addHeader(HttpHeaders.WARNING, "110 localhost \"Response is stale\"");
triggerResponse(cacheResponse, scope, asyncExecCallback);
} catch (final ResourceIOException ex) {
@ -804,8 +806,8 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
return new AsyncExecCallbackWrapper(asyncExecCallback, () -> triggerUpdatedCacheEntryResponse(backendResponse, responseDate));
}
if (staleIfErrorAppliesTo(statusCode)
&& !staleResponseNotAllowed(requestCacheControl, responseCacheControl, cacheEntry, getCurrentDate())
&& validityPolicy.mayReturnStaleIfError(requestCacheControl, responseCacheControl, cacheEntry, responseDate)) {
&& !staleResponseNotAllowed(requestCacheControl, responseCacheControl, hit.entry, getCurrentDate())
&& validityPolicy.mayReturnStaleIfError(requestCacheControl, responseCacheControl, hit.entry, responseDate)) {
return new AsyncExecCallbackWrapper(asyncExecCallback, this::triggerResponseStaleCacheEntry);
}
return new BackendResponseHandler(target, conditionalRequest, requestDate, responseDate, scope, asyncExecCallback);
@ -819,7 +821,7 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
final Instant responseDate1 = getCurrentDate();
final AsyncExecCallback callback1;
if (revalidationResponseIsTooOld(backendResponse1, cacheEntry)
if (revalidationResponseIsTooOld(backendResponse1, hit.entry)
&& (entityProducer == null || entityProducer.isRepeatable())) {
final HttpRequest unconditional = conditionalRequestBuilder.buildUnconditionalRequest(
@ -911,6 +913,7 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
private void handleCacheMiss(
final RequestCacheControl requestCacheControl,
final CacheHit partialMatch,
final HttpHost target,
final HttpRequest request,
final AsyncEntityProducer entityProducer,
@ -919,15 +922,19 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
final AsyncExecCallback asyncExecCallback) {
recordCacheMiss(target, request);
if (mayCallBackend(requestCacheControl)) {
final CancellableDependency operation = scope.cancellableDependency;
operation.setDependency(responseCache.getVariantCacheEntriesWithEtags(
target,
request,
new FutureCallback<Map<String, Variant>>() {
final CancellableDependency operation = scope.cancellableDependency;
if (!mayCallBackend(requestCacheControl)) {
final SimpleHttpResponse cacheResponse = SimpleHttpResponse.create(HttpStatus.SC_GATEWAY_TIMEOUT, "Gateway Timeout");
triggerResponse(cacheResponse, scope, asyncExecCallback);
}
if (partialMatch != null && partialMatch.entry.isVariantRoot()) {
operation.setDependency(responseCache.getVariants(
partialMatch,
new FutureCallback<Collection<CacheHit>>() {
@Override
public void completed(final Map<String, Variant> variants) {
public void completed(final Collection<CacheHit> variants) {
if (variants != null && !variants.isEmpty() && (entityProducer == null || entityProducer.isRepeatable())) {
negotiateResponseFromVariants(target, request, entityProducer, scope, chain, asyncExecCallback, variants);
} else {
@ -947,8 +954,7 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
}));
} else {
final SimpleHttpResponse cacheResponse = SimpleHttpResponse.create(HttpStatus.SC_GATEWAY_TIMEOUT, "Gateway Timeout");
triggerResponse(cacheResponse, scope, asyncExecCallback);
callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
}
}
@ -959,48 +965,52 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
final AsyncExecChain.Scope scope,
final AsyncExecChain chain,
final AsyncExecCallback asyncExecCallback,
final Map<String, Variant> variants) {
final Collection<CacheHit> variants) {
final CancellableDependency operation = scope.cancellableDependency;
final Map<String, CacheHit> variantMap = new HashMap<>();
for (final CacheHit variant : variants) {
final Header header = variant.entry.getFirstHeader(HttpHeaders.ETAG);
if (header != null) {
variantMap.put(header.getValue(), variant);
}
}
final HttpRequest conditionalRequest = conditionalRequestBuilder.buildConditionalRequestFromVariants(
BasicRequestBuilder.copy(request).build(),
variants);
variantMap.keySet());
final Instant requestDate = getCurrentDate();
chainProceed(conditionalRequest, entityProducer, scope, chain, new AsyncExecCallback() {
final AtomicReference<AsyncExecCallback> callbackRef = new AtomicReference<>();
void updateVariantCacheEntry(final HttpResponse backendResponse, final Instant responseDate, final Variant matchingVariant) {
void updateVariantCacheEntry(final HttpResponse backendResponse, final Instant responseDate, final CacheHit match) {
recordCacheUpdate(scope.clientContext);
final HttpCacheEntry variantEntry = matchingVariant.getEntry();
operation.setDependency(responseCache.updateVariantEntry(
target,
conditionalRequest,
operation.setDependency(responseCache.update(
match,
backendResponse,
variantEntry,
requestDate,
responseDate,
new FutureCallback<HttpCacheEntry>() {
new FutureCallback<CacheHit>() {
@Override
public void completed(final HttpCacheEntry responseEntry) {
if (shouldSendNotModifiedResponse(request, responseEntry)) {
final SimpleHttpResponse cacheResponse = responseGenerator.generateNotModifiedResponse(responseEntry);
public void completed(final CacheHit hit) {
if (shouldSendNotModifiedResponse(request, hit.entry)) {
final SimpleHttpResponse cacheResponse = responseGenerator.generateNotModifiedResponse(hit.entry);
triggerResponse(cacheResponse, scope, asyncExecCallback);
} else {
try {
final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, responseEntry);
operation.setDependency(responseCache.reuseVariantEntryFor(
final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry);
operation.setDependency(responseCache.storeReusing(
hit,
target,
request,
backendResponse,
responseEntry,
requestDate,
responseDate,
new FutureCallback<Boolean>() {
new FutureCallback<CacheHit>() {
@Override
public void completed(final Boolean result) {
public void completed(final CacheHit result) {
triggerResponse(cacheResponse, scope, asyncExecCallback);
}
@ -1044,29 +1054,29 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
final AsyncExecCallback callback;
// Handle 304 Not Modified responses
if (backendResponse.getCode() == HttpStatus.SC_NOT_MODIFIED) {
responseCache.getCacheEntry(target, request, new FutureCallback<HttpCacheEntry>() {
responseCache.match(target, request, new FutureCallback<CacheMatch>() {
@Override
public void completed(final HttpCacheEntry existingEntry) {
if (existingEntry != null) {
public void completed(final CacheMatch result) {
final CacheHit hit = result != null ? result.hit : null;
if (hit != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Existing cache entry found, updating cache entry");
}
responseCache.updateEntry(
target,
responseCache.update(
hit,
request,
existingEntry,
backendResponse,
requestDate,
responseDate,
new FutureCallback<HttpCacheEntry>() {
new FutureCallback<CacheHit>() {
@Override
public void completed(final HttpCacheEntry updatedEntry) {
public void completed(final CacheHit updated) {
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Cache entry updated, generating response from updated entry");
}
final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, updatedEntry);
final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, updated.entry);
triggerResponse(cacheResponse, scope, asyncExecCallback);
} catch (final ResourceIOException ex) {
asyncExecCallback.failed(ex);
@ -1113,18 +1123,18 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
callback = new AsyncExecCallbackWrapper(asyncExecCallback, () -> callBackend(target, request, entityProducer, scope, chain, asyncExecCallback));
} else {
final String resultEtag = resultEtagHeader.getValue();
final Variant matchingVariant = variants.get(resultEtag);
if (matchingVariant == null) {
final CacheHit match = variantMap.get(resultEtag);
if (match == null) {
LOG.debug("304 response did not contain ETag matching one sent in If-None-Match");
callback = new AsyncExecCallbackWrapper(asyncExecCallback, () -> callBackend(target, request, entityProducer, scope, chain, asyncExecCallback));
} else {
if (revalidationResponseIsTooOld(backendResponse, matchingVariant.getEntry())) {
if (revalidationResponseIsTooOld(backendResponse, match.entry)) {
final HttpRequest unconditional = conditionalRequestBuilder.buildUnconditionalRequest(
BasicRequestBuilder.copy(request).build());
scope.clientContext.setAttribute(HttpCoreContext.HTTP_REQUEST, unconditional);
callback = new AsyncExecCallbackWrapper(asyncExecCallback, () -> callBackend(target, request, entityProducer, scope, chain, asyncExecCallback));
} else {
callback = new AsyncExecCallbackWrapper(asyncExecCallback, () -> updateVariantCacheEntry(backendResponse, responseDate, matchingVariant));
callback = new AsyncExecCallbackWrapper(asyncExecCallback, () -> updateVariantCacheEntry(backendResponse, responseDate, match));
}
}
}

View File

@ -27,24 +27,26 @@
package org.apache.hc.client5.http.impl.cache;
import java.time.Instant;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.hc.client5.http.cache.HttpAsyncCacheInvalidator;
import org.apache.hc.client5.http.cache.HttpAsyncCacheStorage;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.client5.http.cache.HttpCacheEntryFactory;
import org.apache.hc.client5.http.cache.HttpCacheUpdateException;
import org.apache.hc.client5.http.cache.Resource;
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.ComplexCancellable;
import org.apache.hc.core5.concurrent.FutureCallback;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpResponse;
@ -90,111 +92,152 @@ class BasicHttpAsyncCache implements HttpAsyncCache {
}
@Override
public String generateKey(final HttpHost host, final HttpRequest request, final HttpCacheEntry cacheEntry) {
final String root = cacheKeyGenerator.generateKey(host, request);
if (cacheEntry != null && cacheEntry.isVariantRoot()) {
final List<String> variantNames = CacheKeyGenerator.variantNames(cacheEntry);
if (!variantNames.isEmpty()) {
final String variantKey = cacheKeyGenerator.generateVariantKey(request, variantNames);
return variantKey + root;
}
}
return root;
}
@Override
public Cancellable flushCacheEntriesFor(
final HttpHost host, final HttpRequest request, final FutureCallback<Boolean> callback) {
public Cancellable match(final HttpHost host, final HttpRequest request, final FutureCallback<CacheMatch> callback) {
final String rootKey = cacheKeyGenerator.generateKey(host, request);
if (LOG.isDebugEnabled()) {
LOG.debug("Flush cache entries: {}; {}", host, new RequestLine(request));
LOG.debug("Get cache entry: {}", rootKey);
}
if (!Method.isSafe(request.getMethod())) {
final String rootKey = cacheKeyGenerator.generateKey(host, request);
return storage.removeEntry(rootKey, new FutureCallback<Boolean>() {
@Override
public void completed(final Boolean result) {
callback.completed(result);
}
@Override
public void failed(final Exception ex) {
if (ex instanceof ResourceIOException) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error removing cache entry with key {}", rootKey);
}
callback.completed(Boolean.TRUE);
} else {
callback.failed(ex);
}
}
@Override
public void cancelled() {
callback.cancelled();
}
});
}
callback.completed(Boolean.TRUE);
return Operations.nonCancellable();
}
@Override
public Cancellable flushCacheEntriesInvalidatedByRequest(
final HttpHost host, final HttpRequest request, final FutureCallback<Boolean> callback) {
if (LOG.isDebugEnabled()) {
LOG.debug("Flush cache entries invalidated by request: {}; {}", host, new RequestLine(request));
}
return cacheInvalidator.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyGenerator, storage, callback);
}
@Override
public Cancellable flushCacheEntriesInvalidatedByExchange(
final HttpHost host, final HttpRequest request, final HttpResponse response, final FutureCallback<Boolean> callback) {
if (LOG.isDebugEnabled()) {
LOG.debug("Flush cache entries invalidated by exchange: {}; {} -> {}", host, new RequestLine(request), new StatusLine(response));
}
if (!Method.isSafe(request.getMethod())) {
return cacheInvalidator.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyGenerator, storage, callback);
}
callback.completed(Boolean.TRUE);
return Operations.nonCancellable();
}
Cancellable storeInCache(
final HttpRequest request,
final HttpResponse originResponse,
final Instant requestSent,
final Instant responseReceived,
final String rootKey,
final HttpCacheEntry entry,
final FutureCallback<Boolean> callback) {
if (entry.hasVariants()) {
return storeVariantEntry(request, originResponse, requestSent, responseReceived, rootKey, entry, callback);
} else {
return storeEntry(rootKey, entry, callback);
}
}
Cancellable storeEntry(
final String cacheKey,
final HttpCacheEntry entry,
final FutureCallback<Boolean> callback) {
return storage.putEntry(cacheKey, entry, new FutureCallback<Boolean>() {
final ComplexCancellable complexCancellable = new ComplexCancellable();
complexCancellable.setDependency(storage.getEntry(rootKey, new FutureCallback<HttpCacheEntry>() {
@Override
public void completed(final Boolean result) {
callback.completed(result);
public void completed(final HttpCacheEntry root) {
if (root != null) {
if (root.isVariantRoot()) {
final List<String> variantNames = CacheKeyGenerator.variantNames(root);
final String variantKey = cacheKeyGenerator.generateVariantKey(request, variantNames);
final String cacheKey = root.getVariantMap().get(variantKey);
if (LOG.isDebugEnabled()) {
LOG.debug("Get cache variant entry: {}", cacheKey);
}
if (cacheKey != null) {
complexCancellable.setDependency(storage.getEntry(
cacheKey,
new FutureCallback<HttpCacheEntry>() {
@Override
public void completed(final HttpCacheEntry entry) {
callback.completed(new CacheMatch(
entry != null ? new CacheHit(rootKey, cacheKey, entry) : null,
new CacheHit(rootKey, root)));
}
@Override
public void failed(final Exception ex) {
if (ex instanceof ResourceIOException) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error retrieving cache entry with key {}", cacheKey);
}
callback.completed(null);
} else {
callback.failed(ex);
}
}
@Override
public void cancelled() {
callback.cancelled();
}
}));
return;
}
}
callback.completed(new CacheMatch(new CacheHit(rootKey, root), null));
} else {
callback.completed(null);
}
}
@Override
public void failed(final Exception ex) {
if (ex instanceof ResourceIOException) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error storing cache entry with key {}", cacheKey);
LOG.warn("I/O error retrieving cache entry with key {}", rootKey);
}
callback.completed(Boolean.TRUE);
callback.completed(null);
} else {
callback.failed(ex);
}
}
@Override
public void cancelled() {
callback.cancelled();
}
}));
return complexCancellable;
}
@Override
public Cancellable getVariants(
final CacheHit hit, final FutureCallback<Collection<CacheHit>> callback) {
if (LOG.isDebugEnabled()) {
LOG.debug("Get variant cache entries: {}", hit.rootKey);
}
final ComplexCancellable complexCancellable = new ComplexCancellable();
final HttpCacheEntry root = hit.entry;
if (root != null && root.isVariantRoot()) {
final Set<String> variantCacheKeys = root.getVariantMap().keySet();
complexCancellable.setDependency(storage.getEntries(
variantCacheKeys,
new FutureCallback<Map<String, HttpCacheEntry>>() {
@Override
public void completed(final Map<String, HttpCacheEntry> resultMap) {
final List<CacheHit> cacheHits = resultMap.entrySet().stream()
.map(e -> new CacheHit(hit.rootKey, e.getKey(), e.getValue()))
.collect(Collectors.toList());
callback.completed(cacheHits);
}
@Override
public void failed(final Exception ex) {
if (ex instanceof ResourceIOException) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error retrieving cache entry with keys {}", variantCacheKeys);
}
callback.completed(Collections.emptyList());
} else {
callback.failed(ex);
}
}
@Override
public void cancelled() {
callback.cancelled();
}
}));
} else {
callback.completed(Collections.emptyList());
}
return complexCancellable;
}
Cancellable storeEntry(
final String rootKey,
final HttpCacheEntry entry,
final FutureCallback<CacheHit> callback) {
if (LOG.isDebugEnabled()) {
LOG.debug("Put entry in cache: {}", rootKey);
}
return storage.putEntry(rootKey, entry, new FutureCallback<Boolean>() {
@Override
public void completed(final Boolean result) {
callback.completed(new CacheHit(rootKey, entry));
}
@Override
public void failed(final Exception ex) {
if (ex instanceof ResourceIOException) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error storing cache entry with key {}", rootKey);
}
callback.completed(new CacheHit(rootKey, entry));
} else {
callback.failed(ex);
}
@ -208,21 +251,30 @@ class BasicHttpAsyncCache implements HttpAsyncCache {
});
}
Cancellable storeVariantEntry(
Cancellable storeVariant(
final HttpRequest request,
final HttpResponse originResponse,
final Instant requestSent,
final Instant responseReceived,
final String rootKey,
final HttpCacheEntry entry,
final FutureCallback<Boolean> callback) {
final FutureCallback<CacheHit> callback) {
final List<String> variantNames = CacheKeyGenerator.variantNames(entry);
final String variantKey = cacheKeyGenerator.generateVariantKey(request, variantNames);
final String variantCacheKey = variantKey + rootKey;
if (LOG.isDebugEnabled()) {
LOG.debug("Put variant entry in cache: {}", variantCacheKey);
}
return storage.putEntry(variantCacheKey, entry, new FutureCallback<Boolean>() {
@Override
public void completed(final Boolean result) {
if (LOG.isDebugEnabled()) {
LOG.debug("Update root entry: {}", rootKey);
}
storage.updateEntry(rootKey,
existing -> {
final Map<String,String> variantMap = existing != null ? new HashMap<>(existing.getVariantMap()) : new HashMap<>();
@ -233,7 +285,7 @@ class BasicHttpAsyncCache implements HttpAsyncCache {
@Override
public void completed(final Boolean result) {
callback.completed(result);
callback.completed(new CacheHit(rootKey, variantCacheKey, entry));
}
@Override
@ -265,6 +317,165 @@ class BasicHttpAsyncCache implements HttpAsyncCache {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error updating cache entry with key {}", variantCacheKey);
}
callback.completed(new CacheHit(rootKey, variantCacheKey, entry));
} else {
callback.failed(ex);
}
}
@Override
public void cancelled() {
callback.cancelled();
}
});
}
Cancellable store(
final HttpRequest request,
final HttpResponse originResponse,
final Instant requestSent,
final Instant responseReceived,
final String rootKey,
final HttpCacheEntry entry,
final FutureCallback<CacheHit> callback) {
if (entry.hasVariants()) {
return storeVariant(request, originResponse, requestSent, responseReceived, rootKey, entry, callback);
} else {
return storeEntry(rootKey, entry, callback);
}
}
@Override
public Cancellable store(
final HttpHost host,
final HttpRequest request,
final HttpResponse originResponse,
final ByteArrayBuffer content,
final Instant requestSent,
final Instant responseReceived,
final FutureCallback<CacheHit> callback) {
final String rootKey = cacheKeyGenerator.generateKey(host, request);
if (LOG.isDebugEnabled()) {
LOG.debug("Create cache entry: {}", rootKey);
}
final Resource resource;
try {
resource = content != null ? resourceFactory.generate(request.getRequestUri(), content.array(), 0, content.length()) : null;
} catch (final ResourceIOException ex) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error creating cache entry with key {}", rootKey);
}
final HttpCacheEntry backup = cacheEntryFactory.create(
requestSent,
responseReceived,
request,
originResponse,
content != null ? HeapResourceFactory.INSTANCE.generate(null, content.array(), 0, content.length()) : null);
callback.completed(new CacheHit(rootKey, backup));
return Operations.nonCancellable();
}
final HttpCacheEntry entry = cacheEntryFactory.create(requestSent, responseReceived, request, originResponse, resource);
return store(
request,
originResponse,
requestSent,
responseReceived,
rootKey,
entry,
callback);
}
@Override
public Cancellable update(
final CacheHit stale,
final HttpRequest request,
final HttpResponse originResponse,
final Instant requestSent,
final Instant responseReceived,
final FutureCallback<CacheHit> callback) {
final String entryKey = stale.getEntryKey();
if (LOG.isDebugEnabled()) {
LOG.debug("Update cache entry: {}", entryKey);
}
final HttpCacheEntry updatedEntry = cacheEntryFactory.createUpdated(
requestSent,
responseReceived,
originResponse,
stale.entry);
if (LOG.isDebugEnabled()) {
LOG.debug("Put entry in cache: {}", entryKey);
}
return store(
request,
originResponse,
requestSent,
responseReceived,
entryKey,
updatedEntry,
callback);
}
@Override
public Cancellable update(
final CacheHit stale,
final HttpResponse originResponse,
final Instant requestSent,
final Instant responseReceived,
final FutureCallback<CacheHit> callback) {
final String entryKey = stale.getEntryKey();
if (LOG.isDebugEnabled()) {
LOG.debug("Update cache entry (no root): {}", entryKey);
}
final HttpCacheEntry updatedEntry = cacheEntryFactory.createUpdated(
requestSent,
responseReceived,
originResponse,
stale.entry);
return storeEntry(
entryKey,
updatedEntry,
callback);
}
@Override
public Cancellable storeReusing(
final CacheHit hit,
final HttpHost host,
final HttpRequest request,
final HttpResponse originResponse,
final Instant requestSent,
final Instant responseReceived,
final FutureCallback<CacheHit> callback) {
final String rootKey = cacheKeyGenerator.generateKey(host, request);
if (LOG.isDebugEnabled()) {
LOG.debug("Store cache entry using existing entry: {} -> {}", rootKey, hit.rootKey);
}
return store(request, originResponse, requestSent, responseReceived, rootKey, hit.entry, callback);
}
@Override
public Cancellable flushCacheEntriesFor(
final HttpHost host, final HttpRequest request, final FutureCallback<Boolean> callback) {
final String rootKey = cacheKeyGenerator.generateKey(host, request);
if (LOG.isDebugEnabled()) {
LOG.debug("Flush cache entries: {}", rootKey);
}
return storage.removeEntry(rootKey, new FutureCallback<Boolean>() {
@Override
public void completed(final Boolean result) {
callback.completed(result);
}
@Override
public void failed(final Exception ex) {
if (ex instanceof ResourceIOException) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error removing cache entry with key {}", rootKey);
}
callback.completed(Boolean.TRUE);
} else {
callback.failed(ex);
@ -280,304 +491,25 @@ class BasicHttpAsyncCache implements HttpAsyncCache {
}
@Override
public Cancellable reuseVariantEntryFor(
final HttpHost host,
final HttpRequest request,
final HttpResponse originResponse,
final HttpCacheEntry entry,
final Instant requestSent,
final Instant responseReceived,
final FutureCallback<Boolean> callback) {
public Cancellable flushCacheEntriesInvalidatedByRequest(
final HttpHost host, final HttpRequest request, final FutureCallback<Boolean> callback) {
if (LOG.isDebugEnabled()) {
LOG.debug("Re-use variant entry: {}; {} / {}", host, new RequestLine(request), entry);
LOG.debug("Flush cache entries invalidated by request: {}; {}", host, new RequestLine(request));
}
final String rootKey = cacheKeyGenerator.generateKey(host, request);
return storeVariantEntry(request, originResponse, requestSent, responseReceived, rootKey, entry, callback);
return cacheInvalidator.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyGenerator, storage, callback);
}
@Override
public Cancellable updateEntry(
final HttpHost host,
final HttpRequest request,
final HttpCacheEntry stale,
final HttpResponse originResponse,
final Instant requestSent,
final Instant responseReceived,
final FutureCallback<HttpCacheEntry> callback) {
public Cancellable flushCacheEntriesInvalidatedByExchange(
final HttpHost host, final HttpRequest request, final HttpResponse response, final FutureCallback<Boolean> callback) {
if (LOG.isDebugEnabled()) {
LOG.debug("Update cache entry: {}; {}", host, new RequestLine(request));
LOG.debug("Flush cache entries invalidated by exchange: {}; {} -> {}", host, new RequestLine(request), new StatusLine(response));
}
final String rootKey = cacheKeyGenerator.generateKey(host, request);
final HttpCacheEntry updatedEntry = cacheEntryFactory.createUpdated(
requestSent,
responseReceived,
originResponse,
stale);
return storeInCache(
request,
originResponse,
requestSent,
responseReceived,
rootKey,
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();
}
});
}
@Override
public Cancellable updateVariantEntry(
final HttpHost host,
final HttpRequest request,
final HttpResponse originResponse,
final HttpCacheEntry entry,
final Instant requestSent,
final Instant responseReceived,
final FutureCallback<HttpCacheEntry> callback) {
if (LOG.isDebugEnabled()) {
LOG.debug("Update variant cache entry: {}; {} / {}", host, new RequestLine(request), entry);
if (!Method.isSafe(request.getMethod())) {
return cacheInvalidator.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyGenerator, storage, callback);
}
final String rootKey = cacheKeyGenerator.generateKey(host, request);
final HttpCacheEntry updatedEntry = cacheEntryFactory.createUpdated(
requestSent,
responseReceived,
originResponse,
entry);
return storeEntry(rootKey, 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();
}
});
}
@Override
public Cancellable createEntry(
final HttpHost host,
final HttpRequest request,
final HttpResponse originResponse,
final ByteArrayBuffer content,
final Instant requestSent,
final Instant responseReceived,
final FutureCallback<HttpCacheEntry> callback) {
if (LOG.isDebugEnabled()) {
LOG.debug("Create cache entry: {}; {}", host, new RequestLine(request));
}
final String rootKey = cacheKeyGenerator.generateKey(host, request);
try {
final HttpCacheEntry entry = cacheEntryFactory.create(requestSent, responseReceived, request, originResponse,
content != null ? resourceFactory.generate(request.getRequestUri(), content.array(), 0, content.length()) : null);
return storeInCache(
request,
originResponse,
requestSent,
responseReceived,
rootKey,
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) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error creating cache entry with key {}", rootKey);
}
callback.completed(cacheEntryFactory.create(
requestSent,
responseReceived,
request,
originResponse,
content != null ? HeapResourceFactory.INSTANCE.generate(null, content.array(), 0, content.length()) : null));
return Operations.nonCancellable();
}
}
@Override
public Cancellable getCacheEntry(final HttpHost host, final HttpRequest request, final FutureCallback<HttpCacheEntry> callback) {
if (LOG.isDebugEnabled()) {
LOG.debug("Get cache entry: {}; {}", host, new RequestLine(request));
}
final ComplexCancellable complexCancellable = new ComplexCancellable();
final String rootKey = cacheKeyGenerator.generateKey(host, request);
complexCancellable.setDependency(storage.getEntry(rootKey, new FutureCallback<HttpCacheEntry>() {
@Override
public void completed(final HttpCacheEntry root) {
if (root != null) {
if (root.isVariantRoot()) {
final List<String> variantNames = CacheKeyGenerator.variantNames(root);
final String variantKey = cacheKeyGenerator.generateVariantKey(request, variantNames);
final String variantCacheKey = root.getVariantMap().get(variantKey);
if (variantCacheKey != null) {
complexCancellable.setDependency(storage.getEntry(
variantCacheKey,
new FutureCallback<HttpCacheEntry>() {
@Override
public void completed(final HttpCacheEntry result) {
callback.completed(result);
}
@Override
public void failed(final Exception ex) {
if (ex instanceof ResourceIOException) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error retrieving cache entry with key {}", variantCacheKey);
}
callback.completed(null);
} else {
callback.failed(ex);
}
}
@Override
public void cancelled() {
callback.cancelled();
}
}));
return;
}
}
}
callback.completed(root);
}
@Override
public void failed(final Exception ex) {
if (ex instanceof ResourceIOException) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error retrieving cache entry with key {}", rootKey);
}
callback.completed(null);
} else {
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) {
if (LOG.isDebugEnabled()) {
LOG.debug("Get variant cache entries: {}; {}", host, new RequestLine(request));
}
final ComplexCancellable complexCancellable = new ComplexCancellable();
final String rootKey = cacheKeyGenerator.generateKey(host, request);
final Map<String, Variant> variants = new HashMap<>();
complexCancellable.setDependency(storage.getEntry(rootKey, new FutureCallback<HttpCacheEntry>() {
@Override
public void completed(final HttpCacheEntry rootEntry) {
if (rootEntry != null && rootEntry.isVariantRoot()) {
final Set<String> variantCacheKeys = rootEntry.getVariantMap().keySet();
complexCancellable.setDependency(storage.getEntries(
variantCacheKeys,
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(HttpHeaders.ETAG);
if (etagHeader != null) {
variants.put(etagHeader.getValue(), new Variant(cacheKey, cacheEntry));
}
}
callback.completed(variants);
}
@Override
public void failed(final Exception ex) {
if (ex instanceof ResourceIOException) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error retrieving cache entry with keys {}", variantCacheKeys);
}
callback.completed(variants);
} else {
callback.failed(ex);
}
}
@Override
public void cancelled() {
callback.cancelled();
}
}));
} else {
callback.completed(variants);
}
}
@Override
public void failed(final Exception ex) {
if (ex instanceof ResourceIOException) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error retrieving cache entry with key {}", rootKey);
}
callback.completed(variants);
} else {
callback.failed(ex);
}
}
@Override
public void cancelled() {
callback.cancelled();
}
}));
return complexCancellable;
callback.completed(Boolean.TRUE);
return Operations.nonCancellable();
}
}

View File

@ -27,19 +27,21 @@
package org.apache.hc.client5.http.impl.cache;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.hc.client5.http.cache.HttpCacheCASOperation;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.client5.http.cache.HttpCacheEntryFactory;
import org.apache.hc.client5.http.cache.HttpCacheInvalidator;
import org.apache.hc.client5.http.cache.HttpCacheStorage;
import org.apache.hc.client5.http.cache.HttpCacheUpdateException;
import org.apache.hc.client5.http.cache.Resource;
import org.apache.hc.client5.http.cache.ResourceFactory;
import org.apache.hc.client5.http.cache.ResourceIOException;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpResponse;
@ -92,32 +94,236 @@ class BasicHttpCache implements HttpCache {
this(CacheConfig.DEFAULT);
}
@Override
public String generateKey(final HttpHost host, final HttpRequest request, final HttpCacheEntry cacheEntry) {
final String root = cacheKeyGenerator.generateKey(host, request);
if (cacheEntry != null && cacheEntry.isVariantRoot()) {
final List<String> variantNames = CacheKeyGenerator.variantNames(cacheEntry);
if (!variantNames.isEmpty()) {
final String variantKey = cacheKeyGenerator.generateVariantKey(request, variantNames);
return variantKey + root;
void storeInternal(final String cacheKey, final HttpCacheEntry entry) {
try {
storage.putEntry(cacheKey, entry);
} catch (final ResourceIOException ex) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error storing cache entry with key {}", cacheKey);
}
}
return root;
}
void updateInternal(final String cacheKey, final HttpCacheCASOperation casOperation) {
try {
storage.updateEntry(cacheKey, casOperation);
} catch (final HttpCacheUpdateException ex) {
if (LOG.isWarnEnabled()) {
LOG.warn("Cannot update cache entry with key {}", cacheKey);
}
} catch (final ResourceIOException ex) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error updating cache entry with key {}", cacheKey);
}
}
}
HttpCacheEntry getInternal(final String cacheKey) {
try {
return storage.getEntry(cacheKey);
} catch (final ResourceIOException ex) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error retrieving cache entry with key {}", cacheKey);
}
return null;
}
}
@Override
public CacheMatch match(final HttpHost host, final HttpRequest request) {
final String rootKey = cacheKeyGenerator.generateKey(host, request);
if (LOG.isDebugEnabled()) {
LOG.debug("Get cache root entry: {}", rootKey);
}
final HttpCacheEntry root = getInternal(rootKey);
if (root == null) {
return null;
}
if (root.isVariantRoot()) {
final List<String> variantNames = CacheKeyGenerator.variantNames(root);
final String variantKey = cacheKeyGenerator.generateVariantKey(request, variantNames);
final String cacheKey = root.getVariantMap().get(variantKey);
if (LOG.isDebugEnabled()) {
LOG.debug("Get cache variant entry: {}", cacheKey);
}
if (cacheKey != null) {
final HttpCacheEntry entry = getInternal(cacheKey);
if (entry != null) {
return new CacheMatch(new CacheHit(rootKey, cacheKey, entry), new CacheHit(rootKey, root));
}
}
return new CacheMatch(null, new CacheHit(rootKey, root));
} else {
return new CacheMatch(new CacheHit(rootKey, root), null);
}
}
@Override
public List<CacheHit> getVariants(final CacheHit hit) {
if (LOG.isDebugEnabled()) {
LOG.debug("Get variant cache entries: {}", hit.rootKey);
}
final HttpCacheEntry root = hit.entry;
if (root != null && root.isVariantRoot()) {
final List<CacheHit> variants = new ArrayList<>();
for (final String variantKey : root.getVariantMap().values()) {
final HttpCacheEntry variant = getInternal(variantKey);
if (variant != null) {
variants.add(new CacheHit(hit.rootKey, variantKey, variant));
}
}
return variants;
}
return Collections.emptyList();
}
CacheHit store(
final HttpRequest request,
final HttpResponse originResponse,
final Instant requestSent,
final Instant responseReceived,
final String rootKey,
final HttpCacheEntry entry) {
if (entry.hasVariants()) {
return storeVariant(request, originResponse, requestSent, responseReceived, rootKey, entry);
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("Put entry in cache: {}", rootKey);
}
storeInternal(rootKey, entry);
return new CacheHit(rootKey, entry);
}
}
CacheHit storeVariant(
final HttpRequest request,
final HttpResponse originResponse,
final Instant requestSent,
final Instant responseReceived,
final String rootKey,
final HttpCacheEntry entry) {
final List<String> variantNames = CacheKeyGenerator.variantNames(entry);
final String variantKey = cacheKeyGenerator.generateVariantKey(request, variantNames);
final String variantCacheKey = variantKey + rootKey;
if (LOG.isDebugEnabled()) {
LOG.debug("Put variant entry in cache: {}", variantCacheKey);
}
storeInternal(variantCacheKey, entry);
if (LOG.isDebugEnabled()) {
LOG.debug("Update root entry: {}", rootKey);
}
updateInternal(rootKey, existing -> {
final Map<String, String> variantMap = existing != null ? new HashMap<>(existing.getVariantMap()) : new HashMap<>();
variantMap.put(variantKey, variantCacheKey);
return cacheEntryFactory.createRoot(requestSent, responseReceived, request, originResponse, variantMap);
});
return new CacheHit(rootKey, variantCacheKey, entry);
}
@Override
public CacheHit store(
final HttpHost host,
final HttpRequest request,
final HttpResponse originResponse,
final ByteArrayBuffer content,
final Instant requestSent,
final Instant responseReceived) {
final String rootKey = cacheKeyGenerator.generateKey(host, request);
if (LOG.isDebugEnabled()) {
LOG.debug("Store cache entry: {}", rootKey);
}
final Resource resource;
try {
resource = content != null ? resourceFactory.generate(request.getRequestUri(), content.array(), 0, content.length()) : null;
} catch (final ResourceIOException ex) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error creating cache entry with key {}", rootKey);
}
final HttpCacheEntry backup = cacheEntryFactory.create(
requestSent,
responseReceived,
request,
originResponse,
content != null ? HeapResourceFactory.INSTANCE.generate(null, content.array(), 0, content.length()) : null);
return new CacheHit(rootKey, backup);
}
final HttpCacheEntry entry = cacheEntryFactory.create(requestSent, responseReceived, request, originResponse, resource);
return store(request, originResponse, requestSent, responseReceived, rootKey, entry);
}
@Override
public CacheHit update(
final CacheHit stale,
final HttpRequest request,
final HttpResponse originResponse,
final Instant requestSent,
final Instant responseReceived) {
final String entryKey = stale.getEntryKey();
if (LOG.isDebugEnabled()) {
LOG.debug("Update cache entry: {}", entryKey);
}
final HttpCacheEntry updatedEntry = cacheEntryFactory.createUpdated(
requestSent,
responseReceived,
originResponse,
stale.entry);
return store(request, originResponse, requestSent, responseReceived, entryKey, updatedEntry);
}
@Override
public CacheHit update(
final CacheHit stale,
final HttpResponse originResponse,
final Instant requestSent,
final Instant responseReceived) {
final String entryKey = stale.getEntryKey();
if (LOG.isDebugEnabled()) {
LOG.debug("Update cache entry (no root)): {}", entryKey);
}
final HttpCacheEntry updatedEntry = cacheEntryFactory.createUpdated(
requestSent,
responseReceived,
originResponse,
stale.entry);
if (LOG.isDebugEnabled()) {
LOG.debug("Put entry in cache: {}", entryKey);
}
storeInternal(entryKey, updatedEntry);
return new CacheHit(stale.rootKey, stale.variantKey, updatedEntry);
}
@Override
public CacheHit storeReusing(
final CacheHit hit,
final HttpHost host,
final HttpRequest request,
final HttpResponse originResponse,
final Instant requestSent,
final Instant responseReceived) {
final String rootKey = cacheKeyGenerator.generateKey(host, request);
if (LOG.isDebugEnabled()) {
LOG.debug("Store cache entry using existing entry: {} -> {}", rootKey, hit.rootKey);
}
return store(request, originResponse, requestSent, responseReceived, rootKey, hit.entry);
}
@Override
public void flushCacheEntriesFor(final HttpHost host, final HttpRequest request) {
final String rootKey = cacheKeyGenerator.generateKey(host, request);
if (LOG.isDebugEnabled()) {
LOG.debug("Flush cache entries: {}; {}", host, new RequestLine(request));
LOG.debug("Flush cache entries: {}", rootKey);
}
if (!Method.isSafe(request.getMethod())) {
final String rootKey = cacheKeyGenerator.generateKey(host, request);
try {
storage.removeEntry(rootKey);
} catch (final ResourceIOException ex) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error removing cache entry with key {}", rootKey);
}
try {
storage.removeEntry(rootKey);
} catch (final ResourceIOException ex) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error removing cache entry with key {}", rootKey);
}
}
}
@ -140,218 +346,4 @@ class BasicHttpCache implements HttpCache {
}
}
void storeInCache(
final HttpRequest request,
final HttpResponse originResponse,
final Instant requestSent,
final Instant responseReceived,
final String rootKey,
final HttpCacheEntry entry) {
if (entry.hasVariants()) {
storeVariantEntry(request, originResponse, requestSent, responseReceived, rootKey, entry);
} else {
storeEntry(rootKey, entry);
}
}
void storeEntry(final String cacheKey, final HttpCacheEntry entry) {
try {
storage.putEntry(cacheKey, entry);
} catch (final ResourceIOException ex) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error storing cache entry with key {}", cacheKey);
}
}
}
void storeVariantEntry(
final HttpRequest request,
final HttpResponse originResponse,
final Instant requestSent,
final Instant responseReceived,
final String rootKey,
final HttpCacheEntry entry) {
final List<String> variantNames = CacheKeyGenerator.variantNames(entry);
final String variantKey = cacheKeyGenerator.generateVariantKey(request, variantNames);
final String variantCacheKey = variantKey + request;
storeEntry(variantCacheKey, entry);
try {
storage.updateEntry(rootKey, existing -> {
final Map<String,String> variantMap = existing != null ? new HashMap<>(existing.getVariantMap()) : new HashMap<>();
variantMap.put(variantKey, variantCacheKey);
return cacheEntryFactory.createRoot(requestSent, responseReceived, request, originResponse, variantMap);
});
} catch (final HttpCacheUpdateException ex) {
if (LOG.isWarnEnabled()) {
LOG.warn("Cannot update cache entry with key {}", rootKey);
}
} catch (final ResourceIOException ex) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error updating cache entry with key {}", rootKey);
}
}
}
@Override
public void reuseVariantEntryFor(
final HttpHost host,
final HttpRequest request,
final HttpResponse originResponse,
final HttpCacheEntry entry,
final Instant requestSent,
final Instant responseReceived) {
if (LOG.isDebugEnabled()) {
LOG.debug("Re-use variant entry: {}; {} / {}", host, new RequestLine(request), entry);
}
final String rootKey = cacheKeyGenerator.generateKey(host, request);
storeVariantEntry(request, originResponse, requestSent, responseReceived, rootKey, entry);
}
@Override
public HttpCacheEntry updateEntry(
final HttpHost host,
final HttpRequest request,
final HttpCacheEntry stale,
final HttpResponse originResponse,
final Instant requestSent,
final Instant responseReceived) {
if (LOG.isDebugEnabled()) {
LOG.debug("Update cache entry: {}; {}", host, new RequestLine(request));
}
final String rootKey = cacheKeyGenerator.generateKey(host, request);
final HttpCacheEntry updatedEntry = cacheEntryFactory.createUpdated(
requestSent,
responseReceived,
originResponse,
stale);
storeInCache(request, originResponse, requestSent, responseReceived, rootKey, updatedEntry);
return updatedEntry;
}
@Override
public HttpCacheEntry updateVariantEntry(
final HttpHost host,
final HttpRequest request,
final HttpResponse originResponse,
final HttpCacheEntry entry,
final Instant requestSent,
final Instant responseReceived) {
if (LOG.isDebugEnabled()) {
LOG.debug("Update variant cache entry: {}; {} / {}", host, new RequestLine(request), entry);
}
final String rootKey = cacheKeyGenerator.generateKey(host, request);
final HttpCacheEntry updatedEntry = cacheEntryFactory.createUpdated(
requestSent,
responseReceived,
originResponse,
entry);
storeEntry(rootKey, updatedEntry);
return updatedEntry;
}
@Override
public HttpCacheEntry createEntry(
final HttpHost host,
final HttpRequest request,
final HttpResponse originResponse,
final ByteArrayBuffer content,
final Instant requestSent,
final Instant responseReceived) {
if (LOG.isDebugEnabled()) {
LOG.debug("Create cache entry: {}; {}", host, new RequestLine(request));
}
final String rootKey = cacheKeyGenerator.generateKey(host, request);
try {
final HttpCacheEntry entry = cacheEntryFactory.create(requestSent, responseReceived, request, originResponse,
content != null ? resourceFactory.generate(request.getRequestUri(), content.array(), 0, content.length()) : null);
storeInCache(request, originResponse, requestSent, responseReceived, rootKey, entry);
return entry;
} catch (final ResourceIOException ex) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error creating cache entry with key {}", rootKey);
}
return cacheEntryFactory.create(
requestSent,
responseReceived,
request,
originResponse,
content != null ? HeapResourceFactory.INSTANCE.generate(null, content.array(), 0, content.length()) : null);
}
}
@Override
public HttpCacheEntry getCacheEntry(final HttpHost host, final HttpRequest request) {
if (LOG.isDebugEnabled()) {
LOG.debug("Get cache entry: {}; {}", host, new RequestLine(request));
}
final String rootKey = cacheKeyGenerator.generateKey(host, request);
final HttpCacheEntry root;
try {
root = storage.getEntry(rootKey);
} catch (final ResourceIOException ex) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error retrieving cache entry with key {}", rootKey);
}
return null;
}
if (root == null) {
return null;
}
if (root.isVariantRoot()) {
final List<String> variantNames = CacheKeyGenerator.variantNames(root);
final String variantKey = cacheKeyGenerator.generateVariantKey(request, variantNames);
final String variantCacheKey = root.getVariantMap().get(variantKey);
if (variantCacheKey != null) {
try {
return storage.getEntry(variantCacheKey);
} catch (final ResourceIOException ex) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error retrieving cache entry with key {}", variantCacheKey);
}
}
}
return null;
} else {
return root;
}
}
@Override
public Map<String, Variant> getVariantCacheEntriesWithEtags(final HttpHost host, final HttpRequest request) {
if (LOG.isDebugEnabled()) {
LOG.debug("Get variant cache entries: {}; {}", host, new RequestLine(request));
}
final Map<String,Variant> variants = new HashMap<>();
final String rootKey = cacheKeyGenerator.generateKey(host, request);
final HttpCacheEntry root;
try {
root = storage.getEntry(rootKey);
} catch (final ResourceIOException ex) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error retrieving cache entry with key {}", rootKey);
}
return variants;
}
if (root != null && root.isVariantRoot()) {
for(final Map.Entry<String, String> variant : root.getVariantMap().entrySet()) {
final String variantCacheKey = variant.getValue();
try {
final HttpCacheEntry entry = storage.getEntry(variantCacheKey);
if (entry != null) {
final Header etagHeader = entry.getFirstHeader(HttpHeaders.ETAG);
if (etagHeader != null) {
variants.put(etagHeader.getValue(), new Variant(variantCacheKey, entry));
}
}
} catch (final ResourceIOException ex) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error retrieving cache entry with key {}", variantCacheKey);
}
return variants;
}
}
}
return variants;
}
}

View File

@ -0,0 +1,60 @@
/*
* ====================================================================
* 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 org.apache.hc.client5.http.cache.HttpCacheEntry;
class CacheHit {
final String rootKey;
final String variantKey;
final HttpCacheEntry entry;
public CacheHit(final String rootKey, final String variantKey, final HttpCacheEntry entry) {
this.rootKey = rootKey;
this.variantKey = variantKey;
this.entry = entry;
}
public CacheHit(final String rootKey, final HttpCacheEntry entry) {
this(rootKey, null, entry);
}
public String getEntryKey() {
return variantKey != null ? variantKey : rootKey;
}
@Override
public String toString() {
return "CacheHit{" +
"rootKey='" + rootKey + '\'' +
", variantKey='" + variantKey + '\'' +
", entry=" + entry +
'}';
}
}

View File

@ -26,30 +26,26 @@
*/
package org.apache.hc.client5.http.impl.cache;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
/**
* Represents a full match or a partial match (variant options)
* result of a cache lookup operation.
*/
class CacheMatch {
/** Records a set of information describing a cached variant. */
class Variant {
final CacheHit hit;
final CacheHit root;
private final String cacheKey;
private final HttpCacheEntry entry;
public Variant(final String cacheKey, final HttpCacheEntry entry) {
this.cacheKey = cacheKey;
this.entry = entry;
}
public String getCacheKey() {
return cacheKey;
}
public HttpCacheEntry getEntry() {
return entry;
CacheMatch(final CacheHit hit, final CacheHit root) {
this.hit = hit;
this.root = root;
}
@Override
public String toString() {
return cacheKey;
return "CacheLookupResult{" +
"hit=" + hit +
", root=" + root +
'}';
}
}

View File

@ -64,7 +64,7 @@ class CachedHttpResponseGenerator {
* @return {@link SimpleHttpResponse} constructed response
*/
SimpleHttpResponse generateResponse(final HttpRequest request, final HttpCacheEntry entry) throws ResourceIOException {
final Instant now =Instant.now();
final Instant now = Instant.now();
final SimpleHttpResponse response = new SimpleHttpResponse(entry.getStatus());
response.setVersion(HttpVersion.DEFAULT);

View File

@ -29,7 +29,9 @@ package org.apache.hc.client5.http.impl.cache;
import java.io.IOException;
import java.io.InputStream;
import java.time.Instant;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
@ -37,7 +39,6 @@ import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.async.methods.SimpleBody;
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
import org.apache.hc.client5.http.cache.CacheResponseStatus;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.client5.http.cache.HttpCacheStorage;
import org.apache.hc.client5.http.cache.ResourceIOException;
import org.apache.hc.client5.http.classic.ExecChain;
@ -56,6 +57,7 @@ import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.HttpVersion;
import org.apache.hc.core5.http.Method;
import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
@ -165,9 +167,11 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE);
return new BasicClassicHttpResponse(HttpStatus.SC_NOT_IMPLEMENTED);
}
final HttpCacheEntry entry = responseCache.getCacheEntry(target, request);
final CacheMatch result = responseCache.match(target, request);
final CacheHit hit = result != null ? result.hit : null;
final CacheHit root = result != null ? result.root : null;
final SimpleHttpResponse fatalErrorResponse = getFatallyNonCompliantResponse(request, context, entry != null);
final SimpleHttpResponse fatalErrorResponse = getFatallyNonCompliantResponse(request, context, hit != null);
if (fatalErrorResponse != null) {
return convert(fatalErrorResponse, scope);
}
@ -183,12 +187,12 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
}
if (entry == null) {
if (hit == null) {
LOG.debug("Cache miss");
return handleCacheMiss(target, request, requestCacheControl, scope, chain);
return handleCacheMiss(requestCacheControl, root, target, request, scope, chain);
} else {
final ResponseCacheControl responseCacheControl = CacheControlHeaderParser.INSTANCE.parse(entry);
return handleCacheHit(requestCacheControl, responseCacheControl, target, request, scope, chain, entry);
final ResponseCacheControl responseCacheControl = CacheControlHeaderParser.INSTANCE.parse(hit.entry);
return handleCacheHit(requestCacheControl, responseCacheControl, hit, target, request, scope, chain);
}
}
@ -238,11 +242,11 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
private ClassicHttpResponse handleCacheHit(
final RequestCacheControl requestCacheControl,
final ResponseCacheControl responseCacheControl,
final CacheHit hit,
final HttpHost target,
final ClassicHttpRequest request,
final ExecChain.Scope scope,
final ExecChain chain,
final HttpCacheEntry entry) throws IOException, HttpException {
final ExecChain chain) throws IOException, HttpException {
final HttpClientContext context = scope.clientContext;
context.setAttribute(HttpCoreContext.HTTP_REQUEST, request);
recordCacheHit(target, request);
@ -250,20 +254,20 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
if (requestCacheControl.isNoCache()) {
// Revalidate with the server
return revalidateCacheEntry(requestCacheControl, responseCacheControl, target, request, scope, chain, entry);
return revalidateCacheEntry(requestCacheControl, responseCacheControl, hit, target, request, scope, chain);
}
if (suitabilityChecker.canCachedResponseBeUsed(requestCacheControl, responseCacheControl, request, entry, now)) {
if (responseCachingPolicy.responseContainsNoCacheDirective(responseCacheControl, entry)) {
if (suitabilityChecker.canCachedResponseBeUsed(requestCacheControl, responseCacheControl, request, hit.entry, now)) {
if (responseCachingPolicy.responseContainsNoCacheDirective(responseCacheControl, hit.entry)) {
// Revalidate with the server due to no-cache directive in response
if (LOG.isDebugEnabled()) {
LOG.debug("Revalidating with server due to no-cache directive in response.");
}
return revalidateCacheEntry(requestCacheControl, responseCacheControl, target, request, scope, chain, entry);
return revalidateCacheEntry(requestCacheControl, responseCacheControl, hit, target, request, scope, chain);
}
LOG.debug("Cache hit");
try {
return convert(generateCachedResponse(responseCacheControl, request, context, entry, now), scope);
return convert(generateCachedResponse(responseCacheControl, hit.entry, request, context, now), scope);
} catch (final ResourceIOException ex) {
recordCacheFailure(target, request);
if (!mayCallBackend(requestCacheControl)) {
@ -275,13 +279,13 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
} else if (!mayCallBackend(requestCacheControl)) {
LOG.debug("Cache entry not suitable but only-if-cached requested");
return convert(generateGatewayTimeout(context), scope);
} else if (!(entry.getStatus() == HttpStatus.SC_NOT_MODIFIED && !suitabilityChecker.isConditional(request))) {
} else if (!(hit.entry.getStatus() == HttpStatus.SC_NOT_MODIFIED && !suitabilityChecker.isConditional(request))) {
LOG.debug("Revalidating cache entry");
final boolean staleIfErrorEnabled = responseCachingPolicy.isStaleIfErrorEnabled(responseCacheControl, entry);
final boolean staleIfErrorEnabled = responseCachingPolicy.isStaleIfErrorEnabled(responseCacheControl, hit.entry);
try {
if (cacheRevalidator != null
&& !staleResponseNotAllowed(requestCacheControl, responseCacheControl, entry, now)
&& (validityPolicy.mayReturnStaleWhileRevalidating(responseCacheControl, entry, now) || staleIfErrorEnabled)) {
&& !staleResponseNotAllowed(requestCacheControl, responseCacheControl, hit.entry, now)
&& (validityPolicy.mayReturnStaleWhileRevalidating(responseCacheControl, hit.entry, now) || staleIfErrorEnabled)) {
LOG.debug("Serving stale with asynchronous revalidation");
final String exchangeId = ExecSupport.getNextExchangeId();
context.setExchangeId(exchangeId);
@ -291,21 +295,21 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
scope.originalRequest,
scope.execRuntime.fork(null),
HttpClientContext.create());
final SimpleHttpResponse response = generateCachedResponse(responseCacheControl, request, context, entry, now);
final SimpleHttpResponse response = generateCachedResponse(responseCacheControl, hit.entry, request, context, now);
cacheRevalidator.revalidateCacheEntry(
responseCache.generateKey(target, request, entry),
() -> revalidateCacheEntry(requestCacheControl, responseCacheControl, target, request, fork, chain, entry));
hit.getEntryKey(),
() -> revalidateCacheEntry(requestCacheControl, responseCacheControl, hit, target, request, fork, chain));
return convert(response, scope);
}
return revalidateCacheEntry(requestCacheControl, responseCacheControl, target, request, scope, chain, entry);
return revalidateCacheEntry(requestCacheControl, responseCacheControl, hit, target, request, scope, chain);
} catch (final IOException ioex) {
if (staleIfErrorEnabled) {
if (LOG.isDebugEnabled()) {
LOG.debug("Serving stale response due to IOException and stale-if-error enabled");
}
return convert(generateCachedResponse(responseCacheControl, request, context, entry, now), scope);
return convert(generateCachedResponse(responseCacheControl, hit.entry, request, context, now), scope);
}
return convert(handleRevalidationFailure(requestCacheControl, responseCacheControl, request, context, entry, now), scope);
return convert(handleRevalidationFailure(requestCacheControl, responseCacheControl, hit.entry, request, context, now), scope);
}
} else {
LOG.debug("Cache entry not usable; calling backend");
@ -316,20 +320,20 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
ClassicHttpResponse revalidateCacheEntry(
final RequestCacheControl requestCacheControl,
final ResponseCacheControl responseCacheControl,
final CacheHit hit,
final HttpHost target,
final ClassicHttpRequest request,
final ExecChain.Scope scope,
final ExecChain chain,
final HttpCacheEntry cacheEntry) throws IOException, HttpException {
final ExecChain chain) throws IOException, HttpException {
Instant requestDate = getCurrentDate();
final ClassicHttpRequest conditionalRequest = conditionalRequestBuilder.buildConditionalRequest(
responseCacheControl, scope.originalRequest, cacheEntry);
responseCacheControl, scope.originalRequest, hit.entry);
ClassicHttpResponse backendResponse = chain.proceed(conditionalRequest, scope);
try {
Instant responseDate = getCurrentDate();
if (revalidationResponseIsTooOld(backendResponse, cacheEntry)) {
if (revalidationResponseIsTooOld(backendResponse, hit.entry)) {
backendResponse.close();
final ClassicHttpRequest unconditional = conditionalRequestBuilder.buildUnconditionalRequest(
scope.originalRequest);
@ -346,20 +350,19 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
}
if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
final HttpCacheEntry updatedEntry = responseCache.updateEntry(
target, request, cacheEntry, backendResponse, requestDate, responseDate);
final CacheHit updated = responseCache.update(hit, request, backendResponse, requestDate, responseDate);
if (suitabilityChecker.isConditional(request)
&& suitabilityChecker.allConditionalsMatch(request, updatedEntry, Instant.now())) {
return convert(responseGenerator.generateNotModifiedResponse(updatedEntry), scope);
&& suitabilityChecker.allConditionalsMatch(request, updated.entry, Instant.now())) {
return convert(responseGenerator.generateNotModifiedResponse(updated.entry), scope);
}
return convert(responseGenerator.generateResponse(request, updatedEntry), scope);
return convert(responseGenerator.generateResponse(request, updated.entry), scope);
}
if (staleIfErrorAppliesTo(statusCode)
&& !staleResponseNotAllowed(requestCacheControl, responseCacheControl, cacheEntry, getCurrentDate())
&& validityPolicy.mayReturnStaleIfError(requestCacheControl, responseCacheControl, cacheEntry, responseDate)) {
&& !staleResponseNotAllowed(requestCacheControl, responseCacheControl, hit.entry, getCurrentDate())
&& validityPolicy.mayReturnStaleIfError(requestCacheControl, responseCacheControl, hit.entry, responseDate)) {
try {
final SimpleHttpResponse cachedResponse = responseGenerator.generateResponse(request, cacheEntry);
final SimpleHttpResponse cachedResponse = responseGenerator.generateResponse(request, hit.entry);
cachedResponse.addHeader(HttpHeaders.WARNING, "110 localhost \"Response is stale\"");
return convert(cachedResponse, scope);
} finally {
@ -391,7 +394,9 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
return cacheAndReturnResponse(target, request, backendResponse, scope, requestDate, responseDate);
}
LOG.debug("Backend response is not cacheable");
responseCache.flushCacheEntriesFor(target, request);
if (!Method.isSafe(request.getMethod())) {
responseCache.flushCacheEntriesFor(target, request);
}
return backendResponse;
}
@ -406,16 +411,16 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
// handle 304 Not Modified responses
if (backendResponse.getCode() == HttpStatus.SC_NOT_MODIFIED) {
final HttpCacheEntry existingEntry = responseCache.getCacheEntry(target, request);
if (existingEntry != null) {
final HttpCacheEntry updatedEntry = responseCache.updateEntry(
target,
final CacheMatch result = responseCache.match(target ,request);
final CacheHit hit = result != null ? result.hit : null;
if (hit != null) {
final CacheHit updated = responseCache.update(
hit,
request,
existingEntry,
backendResponse,
requestSent,
responseReceived);
return convert(responseGenerator.generateResponse(request, updatedEntry), scope);
return convert(responseGenerator.generateResponse(request, updated.entry), scope);
}
}
@ -441,27 +446,28 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
}
backendResponse.close();
final HttpCacheEntry cacheEntry;
CacheHit hit;
if (cacheConfig.isFreshnessCheckEnabled()) {
final HttpCacheEntry existingEntry = responseCache.getCacheEntry(target, request);
if (DateSupport.isAfter(existingEntry, backendResponse, HttpHeaders.DATE)) {
final CacheMatch result = responseCache.match(target ,request);
hit = result != null ? result.hit : null;
if (DateSupport.isAfter(hit != null ? hit.entry : null, backendResponse, HttpHeaders.DATE)) {
LOG.debug("Backend already contains fresher cache entry");
cacheEntry = existingEntry;
} else {
cacheEntry = responseCache.createEntry(target, request, backendResponse, buf, requestSent, responseReceived);
hit = responseCache.store(target, request, backendResponse, buf, requestSent, responseReceived);
LOG.debug("Backend response successfully cached");
}
} else {
cacheEntry = responseCache.createEntry(target, request, backendResponse, buf, requestSent, responseReceived);
hit = responseCache.store(target, request, backendResponse, buf, requestSent, responseReceived);
LOG.debug("Backend response successfully cached (freshness check skipped)");
}
return convert(responseGenerator.generateResponse(request, cacheEntry), scope);
return convert(responseGenerator.generateResponse(request, hit.entry), scope);
}
private ClassicHttpResponse handleCacheMiss(
final RequestCacheControl requestCacheControl,
final CacheHit partialMatch,
final HttpHost target,
final ClassicHttpRequest request,
final RequestCacheControl requestCacheControl,
final ExecChain.Scope scope,
final ExecChain chain) throws IOException, HttpException {
recordCacheMiss(target, request);
@ -469,10 +475,11 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
if (!mayCallBackend(requestCacheControl)) {
return new BasicClassicHttpResponse(HttpStatus.SC_GATEWAY_TIMEOUT, "Gateway Timeout");
}
final Map<String, Variant> variants = responseCache.getVariantCacheEntriesWithEtags(target, request);
if (variants != null && !variants.isEmpty()) {
return negotiateResponseFromVariants(target, request, scope, chain, variants);
if (partialMatch != null && partialMatch.entry.isVariantRoot()) {
final List<CacheHit> variants = responseCache.getVariants(partialMatch);
if (variants != null && !variants.isEmpty()) {
return negotiateResponseFromVariants(target, request, scope, chain, variants);
}
}
return callBackend(target, request, scope, chain);
@ -483,8 +490,16 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
final ClassicHttpRequest request,
final ExecChain.Scope scope,
final ExecChain chain,
final Map<String, Variant> variants) throws IOException, HttpException {
final ClassicHttpRequest conditionalRequest = conditionalRequestBuilder.buildConditionalRequestFromVariants(request, variants);
final List<CacheHit> variants) throws IOException, HttpException {
final Map<String, CacheHit> variantMap = new HashMap<>();
for (final CacheHit variant : variants) {
final Header header = variant.entry.getFirstHeader(HttpHeaders.ETAG);
if (header != null) {
variantMap.put(header.getValue(), variant);
}
}
final ClassicHttpRequest conditionalRequest = conditionalRequestBuilder.buildConditionalRequestFromVariants(request, variantMap.keySet());
final Instant requestDate = getCurrentDate();
final ClassicHttpResponse backendResponse = chain.proceed(conditionalRequest, scope);
@ -507,15 +522,13 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
}
final String resultEtag = resultEtagHeader.getValue();
final Variant matchingVariant = variants.get(resultEtag);
if (matchingVariant == null) {
final CacheHit match = variantMap.get(resultEtag);
if (match == null) {
LOG.debug("304 response did not contain ETag matching one sent in If-None-Match");
return callBackend(target, request, scope, chain);
}
final HttpCacheEntry variantEntry = matchingVariant.getEntry();
if (revalidationResponseIsTooOld(backendResponse, variantEntry)
if (revalidationResponseIsTooOld(backendResponse, match.entry)
&& (request.getEntity() == null || request.getEntity().isRepeatable())) {
final ClassicHttpRequest unconditional = conditionalRequestBuilder.buildUnconditionalRequest(request);
return callBackend(target, unconditional, scope, chain);
@ -523,13 +536,12 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
recordCacheUpdate(scope.clientContext);
final HttpCacheEntry responseEntry = responseCache.updateVariantEntry(
target, conditionalRequest, backendResponse, variantEntry, requestDate, responseDate);
if (shouldSendNotModifiedResponse(request, responseEntry)) {
return convert(responseGenerator.generateNotModifiedResponse(responseEntry), scope);
final CacheHit hit = responseCache.update(match, backendResponse, requestDate, responseDate);
if (shouldSendNotModifiedResponse(request, hit.entry)) {
return convert(responseGenerator.generateNotModifiedResponse(hit.entry), scope);
}
final SimpleHttpResponse response = responseGenerator.generateResponse(request, responseEntry);
responseCache.reuseVariantEntryFor(target, request, backendResponse, responseEntry, requestDate, responseDate);
final SimpleHttpResponse response = responseGenerator.generateResponse(request, hit.entry);
responseCache.storeReusing(hit, target, request, backendResponse, requestDate, responseDate);
return convert(response, scope);
} catch (final IOException | RuntimeException ex) {
backendResponse.close();

View File

@ -185,9 +185,9 @@ public class CachingExecBase {
SimpleHttpResponse generateCachedResponse(
final ResponseCacheControl responseCacheControl,
final HttpCacheEntry entry,
final HttpRequest request,
final HttpContext context,
final HttpCacheEntry entry,
final Instant now) throws ResourceIOException {
final SimpleHttpResponse cachedResponse;
if (request.containsHeader(HttpHeaders.IF_NONE_MATCH)
@ -223,9 +223,9 @@ public class CachingExecBase {
SimpleHttpResponse handleRevalidationFailure(
final RequestCacheControl requestCacheControl,
final ResponseCacheControl responseCacheControl,
final HttpCacheEntry entry,
final HttpRequest request,
final HttpContext context,
final HttpCacheEntry entry,
final Instant now) throws IOException {
if (staleResponseNotAllowed(requestCacheControl, responseCacheControl, entry, now)) {
return generateGatewayTimeout(context);
@ -259,8 +259,8 @@ public class CachingExecBase {
|| explicitFreshnessRequest(requestCacheControl, responseCacheControl, entry, now);
}
boolean mayCallBackend(final RequestCacheControl cacheControl) {
if (cacheControl.isOnlyIfCached()) {
boolean mayCallBackend(final RequestCacheControl requestCacheControl) {
if (requestCacheControl.isOnlyIfCached()) {
LOG.debug("Request marked only-if-cached");
return false;
}
@ -268,7 +268,8 @@ public class CachingExecBase {
}
boolean explicitFreshnessRequest(final RequestCacheControl requestCacheControl,
final ResponseCacheControl responseCacheControl, final HttpCacheEntry entry,
final ResponseCacheControl responseCacheControl,
final HttpCacheEntry entry,
final Instant now) {
if (requestCacheControl.getMaxStale() >= 0) {
final TimeValue age = validityPolicy.getCurrentAge(entry, now);

View File

@ -26,7 +26,7 @@
*/
package org.apache.hc.client5.http.impl.cache;
import java.util.Map;
import java.util.Set;
import org.apache.hc.client5.http.cache.HeaderConstants;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
@ -84,9 +84,9 @@ class ConditionalRequestBuilder<T extends HttpRequest> {
* @param variants
* @return the wrapped request
*/
public T buildConditionalRequestFromVariants(final T request, final Map<String, Variant> variants) {
public T buildConditionalRequestFromVariants(final T request, final Set<String> variants) {
final T newRequest = messageCopier.create(request);
newRequest.setHeader(MessageSupport.format(HttpHeaders.IF_NONE_MATCH, variants.keySet()));
newRequest.setHeader(MessageSupport.format(HttpHeaders.IF_NONE_MATCH, variants));
return newRequest;
}

View File

@ -27,7 +27,7 @@
package org.apache.hc.client5.http.impl.cache;
import java.time.Instant;
import java.util.Map;
import java.util.Collection;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.core5.concurrent.Cancellable;
@ -39,7 +39,64 @@ import org.apache.hc.core5.util.ByteArrayBuffer;
interface HttpAsyncCache {
String generateKey (HttpHost host, HttpRequest request, HttpCacheEntry cacheEntry);
/**
* Returns a result with either a fully matching {@link HttpCacheEntry}
* a partial match with a list of known variants or null if no match could be found.
*/
Cancellable match(HttpHost host, HttpRequest request, FutureCallback<CacheMatch> callback);
/**
* Retrieves variant {@link HttpCacheEntry}s for the given hit.
*/
Cancellable getVariants(
CacheHit hit, FutureCallback<Collection<CacheHit>> callback);
/**
* Stores {@link HttpRequest} / {@link HttpResponse} exchange details in the cache.
*/
Cancellable store(
HttpHost host,
HttpRequest request,
HttpResponse originResponse,
ByteArrayBuffer content,
Instant requestSent,
Instant responseReceived,
FutureCallback<CacheHit> callback);
/**
* Updates {@link HttpCacheEntry} using details from a 304 {@link HttpResponse} and
* updates the root entry if the given cache entry represents a variant.
*/
Cancellable update(
CacheHit stale,
HttpRequest request,
HttpResponse originResponse,
Instant requestSent,
Instant responseReceived,
FutureCallback<CacheHit> callback);
/**
* Updates {@link HttpCacheEntry} using details from a 304 {@link HttpResponse}.
*/
Cancellable update(
CacheHit stale,
HttpResponse originResponse,
Instant requestSent,
Instant responseReceived,
FutureCallback<CacheHit> callback);
/**
* Stores {@link HttpRequest} / {@link HttpResponse} exchange details in the cache
* re-using the resource of the existing {@link HttpCacheEntry}.
*/
Cancellable storeReusing(
CacheHit hit,
HttpHost host,
HttpRequest request,
HttpResponse originResponse,
Instant requestSent,
Instant responseReceived,
FutureCallback<CacheHit> callback);
/**
* Clear all matching {@link HttpCacheEntry}s.
@ -59,65 +116,4 @@ interface HttpAsyncCache {
Cancellable flushCacheEntriesInvalidatedByExchange(
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 createEntry(
HttpHost host,
HttpRequest request,
HttpResponse originResponse,
ByteArrayBuffer content,
Instant requestSent,
Instant responseReceived,
FutureCallback<HttpCacheEntry> callback);
/**
* Update a {@link HttpCacheEntry} using a 304 {@link HttpResponse}.
*/
Cancellable updateEntry(
HttpHost host,
HttpRequest request,
HttpCacheEntry stale,
HttpResponse originResponse,
Instant requestSent,
Instant responseReceived,
FutureCallback<HttpCacheEntry> callback);
/**
* Update a specific {@link HttpCacheEntry} representing a cached variant
* using a 304 {@link HttpResponse}.
*/
Cancellable updateVariantEntry(
HttpHost host,
HttpRequest request,
HttpResponse originResponse,
HttpCacheEntry entry,
Instant requestSent,
Instant responseReceived,
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 host,
HttpRequest request,
HttpResponse originResponse,
HttpCacheEntry entry,
Instant requestSent,
Instant responseReceived,
FutureCallback<Boolean> callback);
}

View File

@ -27,7 +27,7 @@
package org.apache.hc.client5.http.impl.cache;
import java.time.Instant;
import java.util.Map;
import java.util.List;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.core5.http.HttpHost;
@ -37,7 +37,59 @@ import org.apache.hc.core5.util.ByteArrayBuffer;
interface HttpCache {
String generateKey (HttpHost host, HttpRequest request, HttpCacheEntry cacheEntry);
/**
* Returns a result with either a fully matching {@link HttpCacheEntry}
* a partial match with a list of known variants or null if no match could be found.
*/
CacheMatch match(HttpHost host, HttpRequest request);
/**
* Retrieves variant {@link HttpCacheEntry}s for the given hit.
*/
List<CacheHit> getVariants(CacheHit hit);
/**
* Stores {@link HttpRequest} / {@link HttpResponse} exchange details in the cache.
*/
CacheHit store(
HttpHost host,
HttpRequest request,
HttpResponse originResponse,
ByteArrayBuffer content,
Instant requestSent,
Instant responseReceived);
/**
* Updates {@link HttpCacheEntry} using details from a 304 {@link HttpResponse} and
* updates the root entry if the given cache entry represents a variant.
*/
CacheHit update(
CacheHit stale,
HttpRequest request,
HttpResponse originResponse,
Instant requestSent,
Instant responseReceived);
/**
* Updates {@link HttpCacheEntry} using details from a 304 {@link HttpResponse}.
*/
CacheHit update(
CacheHit stale,
HttpResponse originResponse,
Instant requestSent,
Instant responseReceived);
/**
* Stores {@link HttpRequest} / {@link HttpResponse} exchange details in the cache
* re-using the resource of the existing {@link HttpCacheEntry}.
*/
CacheHit storeReusing(
CacheHit hit,
HttpHost host,
HttpRequest request,
HttpResponse originResponse,
Instant requestSent,
Instant responseReceived);
/**
* Clear all matching {@link HttpCacheEntry}s.
@ -54,60 +106,4 @@ interface HttpCache {
*/
void flushCacheEntriesInvalidatedByExchange(HttpHost host, HttpRequest request, HttpResponse response);
/**
* Retrieve matching {@link HttpCacheEntry} from the cache if it exists.
*/
HttpCacheEntry getCacheEntry(HttpHost host, HttpRequest request);
/**
* Retrieve all variants from the cache, if there are no variants then an empty
* {@link Map} is returned
*/
Map<String,Variant> getVariantCacheEntriesWithEtags(HttpHost host, HttpRequest request);
/**
* Store a {@link HttpResponse} in the cache if possible, and return
*/
HttpCacheEntry createEntry(
HttpHost host,
HttpRequest request,
HttpResponse originResponse,
ByteArrayBuffer content,
Instant requestSent,
Instant responseReceived);
/**
* Update a {@link HttpCacheEntry} using a 304 {@link HttpResponse}.
*/
HttpCacheEntry updateEntry(
HttpHost host,
HttpRequest request,
HttpCacheEntry stale,
HttpResponse originResponse,
Instant requestSent,
Instant responseReceived);
/**
* Update a specific {@link HttpCacheEntry} representing a cached variant
* using a 304 {@link HttpResponse}.
*/
HttpCacheEntry updateVariantEntry(
HttpHost host,
HttpRequest request,
HttpResponse originResponse,
HttpCacheEntry entry,
Instant requestSent,
Instant responseReceived);
/**
* Specifies cache should reuse the given cached variant to satisfy
* requests whose varying headers match those of the given client request.
*/
void reuseVariantEntryFor(
HttpHost host,
HttpRequest request,
HttpResponse originResponse,
HttpCacheEntry entry,
Instant requestSent,
Instant responseReceived);
}

View File

@ -61,7 +61,7 @@ public class ContainsHeaderMatcher extends BaseMatcher<MessageHeaders> {
@Override
public void describeTo(final Description description) {
description.appendText("contains header ").appendValue(headerValue).appendText(": ").appendValue(headerValue);
description.appendText("contains header ").appendValue(headerName).appendText(": ").appendValue(headerValue);
}
public static Matcher<MessageHeaders> contains(final String headerName, final Object headerValue) {

View File

@ -318,6 +318,11 @@ public class HttpTestUtils {
return makeCacheEntry(requestDate, responseDate, Method.GET, "/", null, HttpStatus.SC_OK, headers, variantMap);
}
public static HttpCacheEntry makeCacheEntry(final Map<String, String> variantMap) {
final Instant now = Instant.now();
return makeCacheEntry(now, now, new Header[] {}, variantMap);
}
public static HttpCacheEntry makeCacheEntry(final Header[] headers, final byte[] bytes) {
final Instant now = Instant.now();
return makeCacheEntry(now, now, headers, bytes);

View File

@ -34,15 +34,15 @@ import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.client5.http.classic.methods.HttpDelete;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpHead;
import org.apache.hc.client5.http.classic.methods.HttpOptions;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.classic.methods.HttpTrace;
import org.apache.hc.client5.http.utils.DateUtils;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
@ -52,6 +52,7 @@ import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.message.BasicHeader;
import org.apache.hc.core5.http.message.BasicHttpResponse;
import org.apache.hc.core5.util.ByteArrayBuffer;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -67,64 +68,7 @@ public class TestBasicHttpCache {
}
@Test
public void testDoNotFlushCacheEntriesOnGet() throws Exception {
final HttpHost host = new HttpHost("foo.example.com");
final HttpRequest req = new HttpGet("/bar");
final String key = CacheKeyGenerator.INSTANCE.generateKey(host, req);
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry();
backing.map.put(key, entry);
impl.flushCacheEntriesFor(host, req);
assertEquals(entry, backing.map.get(key));
}
@Test
public void testDoNotFlushCacheEntriesOnHead() throws Exception {
final HttpHost host = new HttpHost("foo.example.com");
final HttpRequest req = new HttpHead("/bar");
final String key = CacheKeyGenerator.INSTANCE.generateKey(host, req);
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry();
backing.map.put(key, entry);
impl.flushCacheEntriesFor(host, req);
assertEquals(entry, backing.map.get(key));
}
@Test
public void testDoNotFlushCacheEntriesOnOptions() throws Exception {
final HttpHost host = new HttpHost("foo.example.com");
final HttpRequest req = new HttpOptions("/bar");
final String key = CacheKeyGenerator.INSTANCE.generateKey(host, req);
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry();
backing.map.put(key, entry);
impl.flushCacheEntriesFor(host, req);
assertEquals(entry, backing.map.get(key));
}
@Test
public void testDoNotFlushCacheEntriesOnTrace() throws Exception {
final HttpHost host = new HttpHost("foo.example.com");
final HttpRequest req = new HttpTrace("/bar");
final String key = CacheKeyGenerator.INSTANCE.generateKey(host, req);
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry();
backing.map.put(key, entry);
impl.flushCacheEntriesFor(host, req);
assertEquals(entry, backing.map.get(key));
}
@Test
public void testFlushContentLocationEntryIfUnSafeRequest()
throws Exception {
public void testFlushContentLocationEntryIfUnSafeRequest() throws Exception {
final HttpHost host = new HttpHost("foo.example.com");
final HttpRequest req = new HttpPost("/foo");
final HttpResponse resp = HttpTestUtils.make200Response();
@ -144,8 +88,7 @@ public class TestBasicHttpCache {
}
@Test
public void testDoNotFlushContentLocationEntryIfSafeRequest()
throws Exception {
public void testDoNotFlushContentLocationEntryIfSafeRequest() throws Exception {
final HttpHost host = new HttpHost("foo.example.com");
final HttpRequest req = new HttpGet("/foo");
final HttpResponse resp = HttpTestUtils.make200Response();
@ -187,7 +130,7 @@ public class TestBasicHttpCache {
final String key = CacheKeyGenerator.INSTANCE.generateKey(host, req);
impl.storeInCache(req, resp, Instant.now(), Instant.now(), key, entry);
impl.store(req, resp, Instant.now(), Instant.now(), key, entry);
assertSame(entry, backing.map.get(key));
}
@ -195,7 +138,7 @@ public class TestBasicHttpCache {
public void testGetCacheEntryReturnsNullOnCacheMiss() throws Exception {
final HttpHost host = new HttpHost("foo.example.com");
final HttpRequest request = new HttpGet("http://foo.example.com/bar");
final HttpCacheEntry result = impl.getCacheEntry(host, request);
final CacheMatch result = impl.match(host, request);
assertNull(result);
}
@ -210,8 +153,10 @@ public class TestBasicHttpCache {
backing.map.put(key,entry);
final HttpCacheEntry result = impl.getCacheEntry(host, request);
assertSame(entry, result);
final CacheMatch result = impl.match(host, request);
assertNotNull(result);
assertNotNull(result.hit);
assertSame(entry, result.hit.entry);
}
@Test
@ -229,11 +174,12 @@ public class TestBasicHttpCache {
origResponse.setHeader("Vary", "Accept-Encoding");
origResponse.setHeader("Content-Encoding","gzip");
impl.createEntry(host, origRequest, origResponse, buf, Instant.now(), Instant.now());
impl.store(host, origRequest, origResponse, buf, Instant.now(), Instant.now());
final HttpRequest request = new HttpGet("http://foo.example.com/bar");
final HttpCacheEntry result = impl.getCacheEntry(host, request);
assertNull(result);
final CacheMatch result = impl.match(host, request);
assertNotNull(result);
assertNull(result.hit);
}
@Test
@ -251,12 +197,13 @@ public class TestBasicHttpCache {
origResponse.setHeader("Vary", "Accept-Encoding");
origResponse.setHeader("Content-Encoding","gzip");
impl.createEntry(host, origRequest, origResponse, buf, Instant.now(), Instant.now());
impl.store(host, origRequest, origResponse, buf, Instant.now(), Instant.now());
final HttpRequest request = new HttpGet("http://foo.example.com/bar");
request.setHeader("Accept-Encoding","gzip");
final HttpCacheEntry result = impl.getCacheEntry(host, request);
final CacheMatch result = impl.match(host, request);
assertNotNull(result);
assertNotNull(result.hit);
}
@Test
@ -282,28 +229,41 @@ public class TestBasicHttpCache {
origResponse2.setHeader(HttpHeaders.VARY, "Accept-Encoding");
// Store the two variants in cache
impl.createEntry(host, origRequest, origResponse1, buf, Instant.now(), Instant.now());
impl.createEntry(host, origRequest, origResponse2, buf, Instant.now(), Instant.now());
impl.store(host, origRequest, origResponse1, buf, Instant.now(), Instant.now());
impl.store(host, origRequest, origResponse2, buf, Instant.now(), Instant.now());
final HttpRequest request = new HttpGet("http://foo.example.com/bar");
request.setHeader("Accept-Encoding", "gzip");
final HttpCacheEntry result = impl.getCacheEntry(host, request);
final CacheMatch result = impl.match(host, request);
assertNotNull(result);
assertNotNull(result.hit);
final HttpCacheEntry entry = result.hit.entry;
assertNotNull(entry);
// Retrieve the ETag header value from the original response and assert that
// the returned cache entry has the same ETag value
final String expectedEtag = origResponse2.getFirstHeader(HttpHeaders.ETAG).getValue();
final String actualEtag = result.getFirstHeader(HttpHeaders.ETAG).getValue();
final String actualEtag = entry.getFirstHeader(HttpHeaders.ETAG).getValue();
assertEquals(expectedEtag, actualEtag);
}
@Test
public void testGetVariantCacheEntriesReturnsEmptySetOnNoVariants() throws Exception {
final HttpHost host = new HttpHost("foo.example.com");
final HttpRequest request = new HttpGet("http://foo.example.com/bar");
public void testGetVariantsRootNoVariants() throws Exception {
final HttpCacheEntry root = HttpTestUtils.makeCacheEntry();
final List<CacheHit> variants = impl.getVariants(new CacheHit("root-key", root));
final Map<String,Variant> variants = impl.getVariantCacheEntriesWithEtags(host, request);
assertNotNull(variants);
assertEquals(0, variants.size());
}
@Test
public void testGetVariantsRootNonExistentVariants() throws Exception {
final Map<String, String> variantMap = new HashMap<>();
variantMap.put("variant1", "variant-key-1");
variantMap.put("variant2", "variant-key-2");
final HttpCacheEntry root = HttpTestUtils.makeCacheEntry(variantMap);
final List<CacheHit> variants = impl.getVariants(new CacheHit("root-key", root));
assertNotNull(variants);
assertEquals(0, variants.size());
@ -334,14 +294,21 @@ public class TestBasicHttpCache {
resp2.setHeader("Content-Encoding","gzip");
resp2.setHeader("Vary", "Accept-Encoding");
impl.createEntry(host, req1, resp1, null, Instant.now(), Instant.now());
impl.createEntry(host, req2, resp2, null, Instant.now(), Instant.now());
final CacheHit hit1 = impl.store(host, req1, resp1, null, Instant.now(), Instant.now());
final CacheHit hit2 = impl.store(host, req2, resp2, null, Instant.now(), Instant.now());
final Map<String,Variant> variants = impl.getVariantCacheEntriesWithEtags(host, req1);
final Map<String, String> variantMap = new HashMap<>();
variantMap.put("variant-1", hit1.variantKey);
variantMap.put("variant-2", hit2.variantKey);
final Map<String, HttpCacheEntry> variants = impl.getVariants(new CacheHit(hit1.rootKey,
HttpTestUtils.makeCacheEntry(variantMap))).stream()
.collect(Collectors.toMap(CacheHit::getEntryKey, e -> e.entry));
assertNotNull(variants);
assertEquals(2, variants.size());
MatcherAssert.assertThat(variants.get(hit1.getEntryKey()), HttpCacheEntryMatcher.equivalent(hit1.entry));
MatcherAssert.assertThat(variants.get(hit2.getEntryKey()), HttpCacheEntryMatcher.equivalent(hit2.entry));
}
}

View File

@ -160,7 +160,7 @@ public class TestCachingExecChain {
execute(req2);
Mockito.verify(mockExecChain).proceed(Mockito.any(), Mockito.any());
Mockito.verify(cache).createEntry(Mockito.eq(host), RequestEquivalent.eq(req1),
Mockito.verify(cache).store(Mockito.eq(host), RequestEquivalent.eq(req1),
Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
}
@ -1070,7 +1070,7 @@ public class TestCachingExecChain {
final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, mockExecRuntime, context);
impl.cacheAndReturnResponse(host, request, originResponse, scope, requestSent, responseReceived);
Mockito.verify(cache, Mockito.never()).createEntry(
Mockito.verify(cache, Mockito.never()).store(
Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
}
@ -1096,18 +1096,18 @@ public class TestCachingExecChain {
final HttpCacheEntry httpCacheEntry = HttpTestUtils.makeCacheEntry();
final SimpleHttpResponse response = SimpleHttpResponse.create(HttpStatus.SC_OK);
Mockito.when(mockCache.createEntry(
Mockito.when(mockCache.store(
Mockito.eq(host),
RequestEquivalent.eq(request),
ResponseEquivalent.eq(response),
Mockito.any(),
Mockito.eq(requestSent),
Mockito.eq(responseReceived))).thenReturn(httpCacheEntry);
Mockito.eq(responseReceived))).thenReturn(new CacheHit("key", httpCacheEntry));
final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, mockExecRuntime, context);
impl.cacheAndReturnResponse(host, request, originResponse, scope, requestSent, responseReceived);
Mockito.verify(mockCache).createEntry(
Mockito.verify(mockCache).store(
Mockito.any(),
Mockito.any(),
Mockito.any(),
@ -1426,7 +1426,8 @@ public class TestCachingExecChain {
// Prepare original cache entry
final HttpCacheEntry originalEntry = HttpTestUtils.makeCacheEntry();
Mockito.when(mockCache.getCacheEntry(host, request)).thenReturn(originalEntry);
Mockito.when(mockCache.match(host, request)).thenReturn(
new CacheMatch(new CacheHit("key", originalEntry), null));
// Prepare 304 Not Modified response
final Instant now = Instant.now();
@ -1452,19 +1453,19 @@ public class TestCachingExecChain {
headers,
new HeapResource(body.getBytes(StandardCharsets.UTF_8)));
Mockito.when(mockCache.updateEntry(Mockito.eq(host), Mockito.eq(request), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(cacheEntry);
Mockito.when(mockCache.update(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()))
.thenReturn(new CacheHit("key", cacheEntry));
// Call cacheAndReturnResponse with 304 Not Modified response
final ClassicHttpResponse cachedResponse = impl.cacheAndReturnResponse(host, request, backendResponse, scope, requestSent, responseReceived);
// Verify cache entry is updated
Mockito.verify(mockCache).updateEntry(
host,
request,
originalEntry,
backendResponse,
requestSent,
responseReceived
Mockito.verify(mockCache).update(
Mockito.any(),
Mockito.same(request),
Mockito.same(backendResponse),
Mockito.eq(requestSent),
Mockito.eq(responseReceived)
);
// Verify response is generated from the updated cache entry
@ -1518,7 +1519,8 @@ public class TestCachingExecChain {
// Execute the first request and assert the response
final ClassicHttpResponse response1 = execute(req1);
final HttpCacheEntry httpCacheEntry = HttpTestUtils.makeCacheEntry();
Mockito.when(responseCache.getCacheEntry(Mockito.any(), Mockito.any())).thenReturn(httpCacheEntry);
Mockito.when(responseCache.match(Mockito.any(), Mockito.any())).thenReturn(
new CacheMatch(new CacheHit("key", httpCacheEntry), null));
Assertions.assertEquals(HttpStatus.SC_GATEWAY_TIMEOUT, response1.getCode());
Mockito.when(mockExecRuntime.fork(Mockito.any())).thenReturn(mockExecRuntime);
@ -1619,7 +1621,8 @@ public class TestCachingExecChain {
// Execute the first request and assert the response
final ClassicHttpResponse response1 = execute(req1);
final HttpCacheEntry httpCacheEntry = HttpTestUtils.makeCacheEntry();
Mockito.when(responseCache.getCacheEntry(Mockito.any(), Mockito.any())).thenReturn(httpCacheEntry);
Mockito.when(responseCache.match(Mockito.any(), Mockito.any())).thenReturn(
new CacheMatch(new CacheHit("key", httpCacheEntry), null));
Assertions.assertEquals(HttpStatus.SC_GATEWAY_TIMEOUT, response1.getCode());
Mockito.when(mockExecRuntime.fork(Mockito.any())).thenReturn(mockExecRuntime);

View File

@ -27,9 +27,10 @@
package org.apache.hc.client5.http.impl.cache;
import java.time.Instant;
import java.util.HashMap;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.client5.http.utils.DateUtils;
@ -280,10 +281,7 @@ public class TestConditionalRequestBuilder {
final String etag2 = "\"456\"";
final String etag3 = "\"789\"";
final Map<String,Variant> variantEntries = new HashMap<>();
variantEntries.put(etag1, new Variant("A", HttpTestUtils.makeCacheEntry(new BasicHeader("ETag", etag1))));
variantEntries.put(etag2, new Variant("B", HttpTestUtils.makeCacheEntry(new BasicHeader("ETag", etag2))));
variantEntries.put(etag3, new Variant("C", HttpTestUtils.makeCacheEntry(new BasicHeader("ETag", etag3))));
final Set<String> variantEntries = new HashSet<>(Arrays.asList(etag1, etag2, etag3));
final HttpRequest conditional = impl.buildConditionalRequestFromVariants(request, variantEntries);

View File

@ -1848,24 +1848,23 @@ public class TestProtocolRequirements {
notModified.setHeader("Date", DateUtils.formatStandardDate(now));
notModified.setHeader("ETag", "\"etag\"");
Mockito.when(mockCache.getCacheEntry(Mockito.eq(host), RequestEquivalent.eq(request))).thenReturn(entry);
Mockito.when(mockCache.match(Mockito.eq(host), RequestEquivalent.eq(request))).thenReturn(
new CacheMatch(new CacheHit("key", entry), null));
Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(validate), Mockito.any())).thenReturn(notModified);
Mockito.when(mockCache.updateEntry(
Mockito.eq(host),
RequestEquivalent.eq(request),
Mockito.eq(entry),
ResponseEquivalent.eq(notModified),
Mockito.any(),
Mockito.any()))
.thenReturn(HttpTestUtils.makeCacheEntry());
Mockito.when(mockCache.update(
Mockito.any(),
Mockito.any(),
Mockito.any(),
Mockito.any(),
Mockito.any()))
.thenReturn(new CacheHit("key", HttpTestUtils.makeCacheEntry()));
execute(request);
Mockito.verify(mockCache).updateEntry(
Mockito.any(),
Mockito.any(),
Mockito.any(),
Mockito.verify(mockCache).update(
Mockito.any(),
RequestEquivalent.eq(request),
ResponseEquivalent.eq(notModified),
Mockito.any(),
Mockito.any());
}
@ -1892,7 +1891,8 @@ public class TestProtocolRequirements {
impl = new CachingExec(mockCache, null, config);
request = new BasicClassicHttpRequest("GET", "/thing");
Mockito.when(mockCache.getCacheEntry(Mockito.eq(host), RequestEquivalent.eq(request))).thenReturn(entry);
Mockito.when(mockCache.match(Mockito.eq(host), RequestEquivalent.eq(request))).thenReturn(
new CacheMatch(new CacheHit("key", entry), null));
final ClassicHttpResponse result = execute(request);
@ -1936,7 +1936,8 @@ public class TestProtocolRequirements {
impl = new CachingExec(mockCache, null, config);
request = new BasicClassicHttpRequest("GET", "/thing");
Mockito.when(mockCache.getCacheEntry(Mockito.eq(host), RequestEquivalent.eq(request))).thenReturn(entry);
Mockito.when(mockCache.match(Mockito.eq(host), RequestEquivalent.eq(request))).thenReturn(
new CacheMatch(new CacheHit("key", entry), null));
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenThrow(
new IOException("can't talk to origin!"));
@ -2120,7 +2121,8 @@ public class TestProtocolRequirements {
impl = new CachingExec(mockCache, null, config);
request = new BasicClassicHttpRequest("GET", "/thing");
Mockito.when(mockCache.getCacheEntry(Mockito.eq(host), RequestEquivalent.eq(request))).thenReturn(entry);
Mockito.when(mockCache.match(Mockito.eq(host), RequestEquivalent.eq(request))).thenReturn(
new CacheMatch(new CacheHit("key", entry), null));
final ClassicHttpResponse result = execute(request);
@ -2183,15 +2185,16 @@ public class TestProtocolRequirements {
final HttpCacheEntry cacheEntry = HttpTestUtils.makeCacheEntry();
Mockito.when(mockCache.getCacheEntry(Mockito.eq(host), RequestEquivalent.eq(request))).thenReturn(entry);
Mockito.when(mockCache.match(Mockito.eq(host), RequestEquivalent.eq(request))).thenReturn(
new CacheMatch(new CacheHit("key", entry), null));
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(validated);
Mockito.when(mockCache.createEntry(
Mockito.when(mockCache.store(
Mockito.any(),
Mockito.any(),
ResponseEquivalent.eq(validated),
Mockito.any(),
Mockito.any(),
Mockito.any())).thenReturn(cacheEntry);
Mockito.any())).thenReturn(new CacheHit("key", cacheEntry));
final ClassicHttpResponse result = execute(request);
@ -2214,7 +2217,7 @@ public class TestProtocolRequirements {
}
Assertions.assertTrue(found113Warning);
}
Mockito.verify(mockCache).createEntry(
Mockito.verify(mockCache).store(
Mockito.any(),
Mockito.any(),
Mockito.any(),