Skip to content

Commit

Permalink
HTTPCLIENT-2277: Revision and simplification of the cache eviction lo…
Browse files Browse the repository at this point in the history
…gic; conformance to RFC 9111 section 4.4
  • Loading branch information
ok2c committed Jul 20, 2023
1 parent a77a926 commit e08c17e
Show file tree
Hide file tree
Showing 21 changed files with 1,327 additions and 2,135 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@
* that this exchange would invalidate.
*
* @since 5.0
*
* @deprecated Do not use.
*/
@Deprecated
@Internal
public interface HttpAsyncCacheInvalidator {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@
* that this exchange would invalidate.
*
* @since 4.3
*
* @deprecated Do not use.
*/
@Deprecated
@Contract(threading = ThreadingBehavior.STATELESS)
@Internal
public interface HttpCacheInvalidator {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@
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;
Expand Down Expand Up @@ -237,27 +236,7 @@ public void execute(

final RequestCacheControl requestCacheControl = CacheControlHeaderParser.INSTANCE.parse(request);

if (!cacheableRequestPolicy.isServableFromCache(requestCacheControl, request)) {
LOG.debug("Request is not servable from cache");
operation.setDependency(responseCache.flushCacheEntriesInvalidatedByRequest(target, request, new FutureCallback<Boolean>() {

@Override
public void completed(final Boolean result) {
callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
}

@Override
public void failed(final Exception cause) {
asyncExecCallback.failed(cause);
}

@Override
public void cancelled() {
asyncExecCallback.failed(new InterruptedIOException());
}

}));
} else {
if (cacheableRequestPolicy.isServableFromCache(requestCacheControl, request)) {
operation.setDependency(responseCache.match(target, request, new FutureCallback<CacheMatch>() {

@Override
Expand Down Expand Up @@ -291,6 +270,9 @@ public void cancelled() {

}));

} else {
LOG.debug("Request is not servable from cache");
callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
}
}

Expand Down Expand Up @@ -479,7 +461,7 @@ public AsyncDataConsumer handleResponse(
final HttpResponse backendResponse,
final EntityDetails entityDetails) throws HttpException, IOException {
responseCompliance.ensureProtocolCompliance(scope.originalRequest, request, backendResponse);
responseCache.flushCacheEntriesInvalidatedByExchange(target, request, backendResponse, new FutureCallback<Boolean>() {
responseCache.evictInvalidatedEntries(target, request, backendResponse, new FutureCallback<Boolean>() {

@Override
public void completed(final Boolean result) {
Expand All @@ -502,24 +484,6 @@ public void cancelled() {
storeRequestIfModifiedSinceFor304Response(request, backendResponse);
} else {
LOG.debug("Backend response is not cacheable");
if (!Method.isSafe(request.getMethod())) {
responseCache.flushCacheEntriesFor(target, request, new FutureCallback<Boolean>() {

@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 cancelled() {
}

});
}
}
final CachingAsyncDataConsumer cachingDataConsumer = cachingConsumerRef.get();
if (cachingDataConsumer != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,17 @@
*/
package org.apache.hc.client5.http.impl.cache;

import java.net.URI;
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.Objects;
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;
Expand All @@ -47,9 +48,12 @@
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;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.Method;
import org.apache.hc.core5.http.message.RequestLine;
import org.apache.hc.core5.http.message.StatusLine;
Expand All @@ -64,27 +68,24 @@ class BasicHttpAsyncCache implements HttpAsyncCache {
private final ResourceFactory resourceFactory;
private final HttpCacheEntryFactory cacheEntryFactory;
private final CacheKeyGenerator cacheKeyGenerator;
private final HttpAsyncCacheInvalidator cacheInvalidator;
private final HttpAsyncCacheStorage storage;

public BasicHttpAsyncCache(
final ResourceFactory resourceFactory,
final HttpCacheEntryFactory cacheEntryFactory,
final HttpAsyncCacheStorage storage,
final CacheKeyGenerator cacheKeyGenerator,
final HttpAsyncCacheInvalidator cacheInvalidator) {
final CacheKeyGenerator cacheKeyGenerator) {
this.resourceFactory = resourceFactory;
this.cacheEntryFactory = cacheEntryFactory;
this.cacheKeyGenerator = cacheKeyGenerator;
this.storage = storage;
this.cacheInvalidator = cacheInvalidator;
}

public BasicHttpAsyncCache(
final ResourceFactory resourceFactory,
final HttpAsyncCacheStorage storage,
final CacheKeyGenerator cacheKeyGenerator) {
this(resourceFactory, HttpCacheEntryFactory.INSTANCE, storage, cacheKeyGenerator, DefaultAsyncCacheInvalidator.INSTANCE);
this(resourceFactory, HttpCacheEntryFactory.INSTANCE, storage, cacheKeyGenerator);
}

public BasicHttpAsyncCache(final ResourceFactory resourceFactory, final HttpAsyncCacheStorage storage) {
Expand Down Expand Up @@ -456,57 +457,124 @@ public Cancellable storeReusing(
return store(request, originResponse, requestSent, responseReceived, rootKey, hit.entry, callback);
}

@Override
public Cancellable flushCacheEntriesFor(
final HttpHost host, final HttpRequest request, final FutureCallback<Boolean> callback) {
final String rootKey = cacheKeyGenerator.generateKey(host, request);
if (LOG.isDebugEnabled()) {
LOG.debug("Flush cache entries: {}", rootKey);
}
return storage.removeEntry(rootKey, new FutureCallback<Boolean>() {
private void evictEntry(final String cacheKey) {
storage.removeEntry(cacheKey, new FutureCallback<Boolean>() {

@Override
public void completed(final Boolean result) {
callback.completed(result);
}

@Override
public void failed(final Exception ex) {
if (ex instanceof ResourceIOException) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error removing cache entry with key {}", rootKey);
if (LOG.isWarnEnabled()) {
if (ex instanceof ResourceIOException) {
LOG.warn("I/O error removing cache entry with key {}", cacheKey);
} else {
LOG.warn("Unexpected error removing cache entry with key {}", cacheKey, ex);
}
callback.completed(Boolean.TRUE);
} else {
callback.failed(ex);
}
}

@Override
public void cancelled() {
callback.cancelled();
}

});
}

@Override
public Cancellable flushCacheEntriesInvalidatedByRequest(
final HttpHost host, final HttpRequest request, final FutureCallback<Boolean> callback) {
private void evictAll(final HttpCacheEntry root, final String rootKey) {
if (LOG.isDebugEnabled()) {
LOG.debug("Flush cache entries invalidated by request: {}; {}", host, new RequestLine(request));
LOG.debug("Evicting root cache entry {}", rootKey);
}
evictEntry(rootKey);
if (root.isVariantRoot()) {
for (final String variantKey : root.getVariantMap().values()) {
if (LOG.isDebugEnabled()) {
LOG.debug("Evicting variant cache entry {}", variantKey);
}
evictEntry(variantKey);
}
}
return cacheInvalidator.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyGenerator, storage, callback);
}

private Cancellable evict(final String rootKey) {
return storage.getEntry(rootKey, new FutureCallback<HttpCacheEntry>() {

@Override
public void completed(final HttpCacheEntry root) {
if (root != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Evicting root cache entry {}", rootKey);
}
evictAll(root, rootKey);
}
}

@Override
public void failed(final Exception ex) {
}

@Override
public void cancelled() {
}

});
}

private Cancellable evict(final String rootKey, final HttpResponse response) {
return storage.getEntry(rootKey, new FutureCallback<HttpCacheEntry>() {

@Override
public void completed(final HttpCacheEntry root) {
if (root != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Evicting root cache entry {}", rootKey);
}
final Header existingETag = root.getFirstHeader(HttpHeaders.ETAG);
final Header newETag = response.getFirstHeader(HttpHeaders.ETAG);
if (existingETag != null && newETag != null &&
!Objects.equals(existingETag.getValue(), newETag.getValue()) &&
!DateSupport.isBefore(response, root, HttpHeaders.DATE)) {
evictAll(root, rootKey);
}
}
}

@Override
public void failed(final Exception ex) {
}

@Override
public void cancelled() {
}

});
}

@Override
public Cancellable flushCacheEntriesInvalidatedByExchange(
public Cancellable evictInvalidatedEntries(
final HttpHost host, final HttpRequest request, final HttpResponse response, final FutureCallback<Boolean> callback) {
if (LOG.isDebugEnabled()) {
LOG.debug("Flush cache entries invalidated by exchange: {}; {} -> {}", host, new RequestLine(request), new StatusLine(response));
}
if (!Method.isSafe(request.getMethod())) {
return cacheInvalidator.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyGenerator, storage, callback);
final int status = response.getCode();
if (status >= HttpStatus.SC_SUCCESS && status < HttpStatus.SC_CLIENT_ERROR &&
!Method.isSafe(request.getMethod())) {
final String rootKey = cacheKeyGenerator.generateKey(host, request);
evict(rootKey);
final URI requestUri = CacheSupport.normalize(CacheSupport.getRequestUri(request, host));
if (requestUri != null) {
final URI contentLocation = CacheSupport.getLocationURI(requestUri, response, HttpHeaders.CONTENT_LOCATION);
if (contentLocation != null && CacheSupport.isSameOrigin(requestUri, contentLocation)) {
final String cacheKey = cacheKeyGenerator.generateKey(contentLocation);
evict(cacheKey, response);
}
final URI location = CacheSupport.getLocationURI(requestUri, response, HttpHeaders.LOCATION);
if (location != null && CacheSupport.isSameOrigin(requestUri, location)) {
final String cacheKey = cacheKeyGenerator.generateKey(location);
evict(cacheKey, response);
}
}
}
callback.completed(Boolean.TRUE);
return Operations.nonCancellable();
Expand Down
Loading

0 comments on commit e08c17e

Please sign in to comment.