From 5fbef8fc7fe1f2632dccb88402164121dcc3c407 Mon Sep 17 00:00:00 2001 From: Oleg Kalnichevski Date: Thu, 22 Jun 2023 23:02:02 +0200 Subject: [PATCH] HTTPCLIENT-2277: optimization of internal cache operations --- .../http/impl/cache/AsyncCachingExec.java | 214 +++--- .../http/impl/cache/BasicHttpAsyncCache.java | 710 ++++++++---------- .../http/impl/cache/BasicHttpCache.java | 460 ++++++------ .../hc/client5/http/impl/cache/CacheHit.java | 60 ++ .../cache/{Variant.java => CacheMatch.java} | 32 +- .../cache/CachedHttpResponseGenerator.java | 2 +- .../client5/http/impl/cache/CachingExec.java | 148 ++-- .../http/impl/cache/CachingExecBase.java | 11 +- .../impl/cache/ConditionalRequestBuilder.java | 6 +- .../http/impl/cache/HttpAsyncCache.java | 122 ++- .../hc/client5/http/impl/cache/HttpCache.java | 112 ++- .../impl/cache/ContainsHeaderMatcher.java | 2 +- .../http/impl/cache/HttpTestUtils.java | 5 + .../http/impl/cache/TestBasicHttpCache.java | 135 ++-- .../http/impl/cache/TestCachingExecChain.java | 35 +- .../cache/TestConditionalRequestBuilder.java | 10 +- .../impl/cache/TestProtocolRequirements.java | 43 +- 17 files changed, 1039 insertions(+), 1068 deletions(-) create mode 100644 httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheHit.java rename httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/{Variant.java => CacheMatch.java} (71%) diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsyncCachingExec.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsyncCachingExec.java index 24562c3ac..d113861cd 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsyncCachingExec.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsyncCachingExec.java @@ -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() { + operation.setDependency(responseCache.match(target, request, new FutureCallback() { @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() { + if (!Method.isSafe(request.getMethod())) { + responseCache.flushCacheEntriesFor(target, request, new FutureCallback() { - @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() { + new FutureCallback() { @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() { + operation.setDependency(responseCache.match(target, request, new FutureCallback() { @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 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() { + new FutureCallback() { @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>() { + 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>() { @Override - public void completed(final Map variants) { + public void completed(final Collection 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 variants) { + final Collection variants) { final CancellableDependency operation = scope.cancellableDependency; + final Map 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 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() { + new FutureCallback() { @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() { + new FutureCallback() { @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() { + responseCache.match(target, request, new FutureCallback() { @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() { + new FutureCallback() { @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)); } } } diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/BasicHttpAsyncCache.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/BasicHttpAsyncCache.java index 4c3f84046..f3a07c6af 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/BasicHttpAsyncCache.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/BasicHttpAsyncCache.java @@ -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 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 callback) { + public Cancellable match(final HttpHost host, final HttpRequest request, final FutureCallback 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() { - - @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 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 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 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 callback) { - return storage.putEntry(cacheKey, entry, new FutureCallback() { + final ComplexCancellable complexCancellable = new ComplexCancellable(); + complexCancellable.setDependency(storage.getEntry(rootKey, new FutureCallback() { @Override - public void completed(final Boolean result) { - callback.completed(result); + public void completed(final HttpCacheEntry root) { + if (root != null) { + if (root.isVariantRoot()) { + final List 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() { + + @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> 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 variantCacheKeys = root.getVariantMap().keySet(); + complexCancellable.setDependency(storage.getEntries( + variantCacheKeys, + new FutureCallback>() { + + @Override + public void completed(final Map resultMap) { + final List 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 callback) { + if (LOG.isDebugEnabled()) { + LOG.debug("Put entry in cache: {}", rootKey); + } + + return storage.putEntry(rootKey, entry, new FutureCallback() { + + @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 callback) { + final FutureCallback callback) { final List 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() { @Override public void completed(final Boolean result) { + if (LOG.isDebugEnabled()) { + LOG.debug("Update root entry: {}", rootKey); + } + storage.updateEntry(rootKey, existing -> { final Map 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 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 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 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 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 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 callback) { + final String rootKey = cacheKeyGenerator.generateKey(host, request); + if (LOG.isDebugEnabled()) { + LOG.debug("Flush cache entries: {}", rootKey); + } + return storage.removeEntry(rootKey, new FutureCallback() { + + @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 callback) { + public Cancellable flushCacheEntriesInvalidatedByRequest( + final HttpHost host, final HttpRequest request, final FutureCallback 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 callback) { + public Cancellable flushCacheEntriesInvalidatedByExchange( + final HttpHost host, final HttpRequest request, final HttpResponse response, final FutureCallback 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() { - - @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 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() { - - @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 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() { - - @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 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() { - - @Override - public void completed(final HttpCacheEntry root) { - if (root != null) { - if (root.isVariantRoot()) { - final List 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() { - - @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> 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 variants = new HashMap<>(); - complexCancellable.setDependency(storage.getEntry(rootKey, new FutureCallback() { - - @Override - public void completed(final HttpCacheEntry rootEntry) { - if (rootEntry != null && rootEntry.isVariantRoot()) { - final Set variantCacheKeys = rootEntry.getVariantMap().keySet(); - complexCancellable.setDependency(storage.getEntries( - variantCacheKeys, - new FutureCallback>() { - - @Override - public void completed(final Map resultMap) { - for (final Map.Entry 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(); } } diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/BasicHttpCache.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/BasicHttpCache.java index e2cf0562a..0eecf016e 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/BasicHttpCache.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/BasicHttpCache.java @@ -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 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 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 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 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 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 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 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 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 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 getVariantCacheEntriesWithEtags(final HttpHost host, final HttpRequest request) { - if (LOG.isDebugEnabled()) { - LOG.debug("Get variant cache entries: {}; {}", host, new RequestLine(request)); - } - final Map 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 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; - } - } diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheHit.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheHit.java new file mode 100644 index 000000000..2c96df0d2 --- /dev/null +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheHit.java @@ -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 + * . + * + */ +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 + + '}'; + } + +} diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/Variant.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheMatch.java similarity index 71% rename from httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/Variant.java rename to httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheMatch.java index c5fd4c1ca..35e84a4b6 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/Variant.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheMatch.java @@ -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 + + '}'; } } diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachedHttpResponseGenerator.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachedHttpResponseGenerator.java index 360dfd077..293d7e584 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachedHttpResponseGenerator.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachedHttpResponseGenerator.java @@ -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); diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExec.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExec.java index 869db843a..f76785db3 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExec.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExec.java @@ -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 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 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 variants) throws IOException, HttpException { - final ClassicHttpRequest conditionalRequest = conditionalRequestBuilder.buildConditionalRequestFromVariants(request, variants); + final List variants) throws IOException, HttpException { + final Map 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(); diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExecBase.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExecBase.java index 41aaafdbe..af8d4ea10 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExecBase.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExecBase.java @@ -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); diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ConditionalRequestBuilder.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ConditionalRequestBuilder.java index eb2a67f93..22562fbfa 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ConditionalRequestBuilder.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ConditionalRequestBuilder.java @@ -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 { * @param variants * @return the wrapped request */ - public T buildConditionalRequestFromVariants(final T request, final Map variants) { + public T buildConditionalRequestFromVariants(final T request, final Set 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; } diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/HttpAsyncCache.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/HttpAsyncCache.java index f4d0bba59..ba59f83d4 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/HttpAsyncCache.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/HttpAsyncCache.java @@ -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 callback); + + /** + * Retrieves variant {@link HttpCacheEntry}s for the given hit. + */ + Cancellable getVariants( + CacheHit hit, FutureCallback> 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 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 callback); + + /** + * Updates {@link HttpCacheEntry} using details from a 304 {@link HttpResponse}. + */ + Cancellable update( + CacheHit stale, + HttpResponse originResponse, + Instant requestSent, + Instant responseReceived, + FutureCallback 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 callback); /** * Clear all matching {@link HttpCacheEntry}s. @@ -59,65 +116,4 @@ interface HttpAsyncCache { Cancellable flushCacheEntriesInvalidatedByExchange( HttpHost host, HttpRequest request, HttpResponse response, FutureCallback callback); - /** - * Retrieve matching {@link HttpCacheEntry} from the cache if it exists - */ - Cancellable getCacheEntry( - HttpHost host, HttpRequest request, FutureCallback callback); - - /** - * Retrieve all variants from the cache, if there are no variants then an empty - */ - Cancellable getVariantCacheEntriesWithEtags( - HttpHost host, HttpRequest request, FutureCallback> 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 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 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 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 callback); } diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/HttpCache.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/HttpCache.java index 4747ca4d1..fa73d3e74 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/HttpCache.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/HttpCache.java @@ -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 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 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); } diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/ContainsHeaderMatcher.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/ContainsHeaderMatcher.java index d7f786ce5..0ec9a53b9 100644 --- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/ContainsHeaderMatcher.java +++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/ContainsHeaderMatcher.java @@ -61,7 +61,7 @@ public class ContainsHeaderMatcher extends BaseMatcher { @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 contains(final String headerName, final Object headerValue) { diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpTestUtils.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpTestUtils.java index 2f1ca8494..82be89d68 100644 --- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpTestUtils.java +++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpTestUtils.java @@ -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 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); diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestBasicHttpCache.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestBasicHttpCache.java index f89936bf1..24189bb3a 100644 --- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestBasicHttpCache.java +++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestBasicHttpCache.java @@ -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 variants = impl.getVariants(new CacheHit("root-key", root)); - final Map variants = impl.getVariantCacheEntriesWithEtags(host, request); + assertNotNull(variants); + assertEquals(0, variants.size()); + } + + @Test + public void testGetVariantsRootNonExistentVariants() throws Exception { + final Map variantMap = new HashMap<>(); + variantMap.put("variant1", "variant-key-1"); + variantMap.put("variant2", "variant-key-2"); + final HttpCacheEntry root = HttpTestUtils.makeCacheEntry(variantMap); + final List 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 variants = impl.getVariantCacheEntriesWithEtags(host, req1); + final Map variantMap = new HashMap<>(); + variantMap.put("variant-1", hit1.variantKey); + variantMap.put("variant-2", hit2.variantKey); + + final Map 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)); } } diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCachingExecChain.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCachingExecChain.java index cbfbca25e..ef39d990c 100644 --- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCachingExecChain.java +++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCachingExecChain.java @@ -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); diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestConditionalRequestBuilder.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestConditionalRequestBuilder.java index 3ec083065..beb0f0dd9 100644 --- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestConditionalRequestBuilder.java +++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestConditionalRequestBuilder.java @@ -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 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 variantEntries = new HashSet<>(Arrays.asList(etag1, etag2, etag3)); final HttpRequest conditional = impl.buildConditionalRequestFromVariants(request, variantEntries); diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestProtocolRequirements.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestProtocolRequirements.java index f5f43cb20..a727be2a9 100644 --- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestProtocolRequirements.java +++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestProtocolRequirements.java @@ -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(),