Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.concurrent.CancellableDependency;
import org.apache.hc.core5.concurrent.ComplexCancellable;
import org.apache.hc.core5.concurrent.ComplexFuture;
import org.apache.hc.core5.concurrent.FutureCallback;
import org.apache.hc.core5.http.ContentType;
Expand Down Expand Up @@ -98,13 +99,17 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
private final HttpAsyncCache responseCache;
private final DefaultAsyncCacheRevalidator cacheRevalidator;
private final ConditionalRequestBuilder<HttpRequest> conditionalRequestBuilder;
private final boolean requestCollapsingEnabled;
private final CacheRequestCollapser collapser;

AsyncCachingExec(final HttpAsyncCache cache, final DefaultAsyncCacheRevalidator cacheRevalidator, final CacheConfig config) {
super(config);
this.responseCache = Args.notNull(cache, "Response cache");
this.cacheRevalidator = cacheRevalidator;
this.conditionalRequestBuilder = new ConditionalRequestBuilder<>(request ->
BasicRequestBuilder.copy(request).build());
this.requestCollapsingEnabled = config.isRequestCollapsingEnabled();
this.collapser = this.requestCollapsingEnabled ? new CacheRequestCollapser() : null;
}

AsyncCachingExec(
Expand Down Expand Up @@ -274,6 +279,96 @@ public void completed(final CacheMatch result) {
final CacheHit hit = result != null ? result.hit : null;
final CacheHit root = result != null ? result.root : null;
if (hit == null) {
if (requestCollapsingEnabled && root == null && entityProducer == null && !requestCacheControl.isOnlyIfCached()) {
final String cacheKey = CacheKeyGenerator.INSTANCE.generateKey(target, request);
final CacheRequestCollapser.Token token = collapser.enter(cacheKey);
if (token.isLeader()) {
handleCacheMiss(requestCacheControl, null, target, request, null, scope, chain, new AsyncExecCallback() {

@Override
public AsyncDataConsumer handleResponse(
final HttpResponse response,
final EntityDetails entityDetails) throws HttpException, IOException {
try {
return asyncExecCallback.handleResponse(response, entityDetails);
} catch (final HttpException | IOException ex) {
token.complete();
throw ex;
}
}

@Override
public void handleInformationResponse(final HttpResponse response) throws HttpException, IOException {
try {
asyncExecCallback.handleInformationResponse(response);
} catch (final HttpException | IOException ex) {
token.complete();
throw ex;
}
}

@Override
public void completed() {
try {
asyncExecCallback.completed();
} finally {
token.complete();
}
}

@Override
public void failed(final Exception cause) {
try {
asyncExecCallback.failed(cause);
} finally {
token.complete();
}
}

});
} else {
// Stable holder owned by the follower: registered with the outer operation
// exactly once so the cache-lookup dependency installed by the await task
// is never overwritten from the outside.
final ComplexCancellable follower = new ComplexCancellable();
operation.setDependency(follower);
collapser.await(token, follower, () -> {
if (follower.isCancelled()) {
return;
}
follower.setDependency(responseCache.match(target, request, new FutureCallback<CacheMatch>() {

@Override
public void completed(final CacheMatch result) {
final CacheHit hit = result != null ? result.hit : null;
final CacheHit root = result != null ? result.root : null;
if (hit == null) {
handleCacheMiss(requestCacheControl, root, target, request, entityProducer, scope, chain, asyncExecCallback);
} else {
final ResponseCacheControl responseCacheControl = CacheControlHeaderParser.INSTANCE.parse(hit.entry);
if (LOG.isDebugEnabled()) {
LOG.debug("{} response cache control: {}", exchangeId, responseCacheControl);
}
context.setResponseCacheControl(responseCacheControl);
handleCacheHit(requestCacheControl, responseCacheControl, hit, target, request, entityProducer, scope, chain, asyncExecCallback);
}
}

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

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

}));
});
}
return;
}
handleCacheMiss(requestCacheControl, root, target, request, entityProducer, scope, chain, asyncExecCallback);
} else {
final ResponseCacheControl responseCacheControl = CacheControlHeaderParser.INSTANCE.parse(hit.entry);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ public class CacheConfig implements Cloneable {
*/
public static final int DEFAULT_ASYNCHRONOUS_WORKERS = 1;

/** Default setting for the request-collapsing hint. */
public static final boolean DEFAULT_REQUEST_COLLAPSING_ENABLED = false;

public static final CacheConfig DEFAULT = new Builder().build();

private final long maxObjectSize;
Expand All @@ -133,6 +136,7 @@ public class CacheConfig implements Cloneable {
private final int asynchronousWorkers;
private final boolean neverCacheHTTP10ResponsesWithQuery;
private final boolean staleIfErrorEnabled;
private final boolean requestCollapsingEnabled;


/**
Expand All @@ -153,7 +157,8 @@ public class CacheConfig implements Cloneable {
final int asynchronousWorkers,
final boolean neverCacheHTTP10ResponsesWithQuery,
final boolean neverCacheHTTP11ResponsesWithQuery,
final boolean staleIfErrorEnabled) {
final boolean staleIfErrorEnabled,
final boolean requestCollapsingEnabled) {
super();
this.maxObjectSize = maxObjectSize;
this.maxCacheEntries = maxCacheEntries;
Expand All @@ -167,6 +172,7 @@ public class CacheConfig implements Cloneable {
this.neverCacheHTTP10ResponsesWithQuery = neverCacheHTTP10ResponsesWithQuery;
this.neverCacheHTTP11ResponsesWithQuery = neverCacheHTTP11ResponsesWithQuery;
this.staleIfErrorEnabled = staleIfErrorEnabled;
this.requestCollapsingEnabled = requestCollapsingEnabled;
}

/**
Expand Down Expand Up @@ -301,6 +307,21 @@ public int getAsynchronousWorkers() {
return asynchronousWorkers;
}

/**
* Returns whether the caching module should attempt to collapse concurrent
* requests for the same cache key so that only one request goes to the
* backend while the others wait and then re-check the cache.
* <p>
* This is a hint. Individual caching implementations may choose to honour
* it or ignore it; the asynchronous caching exec honours it, while the
* classic caching exec currently ignores it.
*
* @since 5.7
*/
public boolean isRequestCollapsingEnabled() {
return requestCollapsingEnabled;
}

@Override
protected CacheConfig clone() throws CloneNotSupportedException {
return (CacheConfig) super.clone();
Expand All @@ -323,7 +344,8 @@ public static Builder copy(final CacheConfig config) {
.setAsynchronousWorkers(config.getAsynchronousWorkers())
.setNeverCacheHTTP10ResponsesWithQueryString(config.isNeverCacheHTTP10ResponsesWithQuery())
.setNeverCacheHTTP11ResponsesWithQueryString(config.isNeverCacheHTTP11ResponsesWithQuery())
.setStaleIfErrorEnabled(config.isStaleIfErrorEnabled());
.setStaleIfErrorEnabled(config.isStaleIfErrorEnabled())
.setRequestCollapsingEnabled(config.isRequestCollapsingEnabled());
}

public static class Builder {
Expand All @@ -340,6 +362,7 @@ public static class Builder {
private boolean neverCacheHTTP10ResponsesWithQuery;
private boolean neverCacheHTTP11ResponsesWithQuery;
private boolean staleIfErrorEnabled;
private boolean requestCollapsingEnabled;

Builder() {
this.maxObjectSize = DEFAULT_MAX_OBJECT_SIZE_BYTES;
Expand All @@ -352,6 +375,7 @@ public static class Builder {
this.freshnessCheckEnabled = true;
this.asynchronousWorkers = DEFAULT_ASYNCHRONOUS_WORKERS;
this.staleIfErrorEnabled = false;
this.requestCollapsingEnabled = DEFAULT_REQUEST_COLLAPSING_ENABLED;
}

/**
Expand Down Expand Up @@ -518,6 +542,23 @@ public Builder setNeverCacheHTTP11ResponsesWithQueryString(
return this;
}

/**
* Enables request collapsing for cacheable requests. When enabled, concurrent
* requests for the same cache key are coalesced so that only one request goes
* to the backend while the others wait and then re-check the cache.
* <p>
* This setting is a hint. Individual caching implementations may honour it or
* ignore it; the asynchronous caching exec honours it, while the classic
* caching exec currently ignores it.
*
* @return this instance.
* @since 5.7
*/
public Builder setRequestCollapsingEnabled(final boolean requestCollapsingEnabled) {
this.requestCollapsingEnabled = requestCollapsingEnabled;
return this;
}

public CacheConfig build() {
return new CacheConfig(
maxObjectSize,
Expand All @@ -531,7 +572,8 @@ public CacheConfig build() {
asynchronousWorkers,
neverCacheHTTP10ResponsesWithQuery,
neverCacheHTTP11ResponsesWithQuery,
staleIfErrorEnabled);
staleIfErrorEnabled,
requestCollapsingEnabled);
}

}
Expand All @@ -551,6 +593,7 @@ public String toString() {
.append(", neverCacheHTTP10ResponsesWithQuery=").append(this.neverCacheHTTP10ResponsesWithQuery)
.append(", neverCacheHTTP11ResponsesWithQuery=").append(this.neverCacheHTTP11ResponsesWithQuery)
.append(", staleIfErrorEnabled=").append(this.staleIfErrorEnabled)
.append(", requestCollapsingEnabled=").append(this.requestCollapsingEnabled)
.append("]");
return builder.toString();
}
Expand Down
Loading
Loading