Add heuristic expiration warning to cached responses"

Implemented an enhancement to add a heuristic expiration warning to cached responses when the freshness lifetime and current age are both greater than one day. This warning, indicated by the '113' code, helps in identifying potential staleness in cached data and aligns with the HTTP caching specifications.
This commit is contained in:
Arturo Bernal 2023-05-16 22:25:05 +02:00 committed by Oleg Kalnichevski
parent 46fe5a6a81
commit f0d76de66d
2 changed files with 104 additions and 2 deletions

View File

@ -40,6 +40,7 @@ import org.apache.hc.client5.http.cache.HeaderConstants;
import org.apache.hc.client5.http.cache.HttpCacheContext;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.client5.http.cache.ResourceIOException;
import org.apache.hc.client5.http.utils.DateUtils;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HeaderElement;
import org.apache.hc.core5.http.HttpHeaders;
@ -78,6 +79,8 @@ public class CachingExecBase {
private static final Logger LOG = LoggerFactory.getLogger(CachingExecBase.class);
private static final TimeValue ONE_DAY = TimeValue.ofHours(24);
CachingExecBase(
final CacheValidityPolicy validityPolicy,
final ResponseCachingPolicy responseCachingPolicy,
@ -198,6 +201,23 @@ public class CachingExecBase {
if (TimeValue.isPositive(validityPolicy.getStaleness(entry, now))) {
cachedResponse.addHeader(HeaderConstants.WARNING,"110 localhost \"Response is stale\"");
}
// Adding Warning: 113 - "Heuristic Expiration"
if (!entry.containsHeader(HeaderConstants.WARNING)) {
final Header header = entry.getFirstHeader(HttpHeaders.DATE);
if (header != null) {
final Instant responseDate = DateUtils.parseStandardDate(header.getValue());
final TimeValue freshnessLifetime = validityPolicy.getFreshnessLifetime(entry);
final TimeValue currentAge = validityPolicy.getCurrentAge(entry, responseDate);
if (freshnessLifetime.compareTo(ONE_DAY) > 0 && currentAge.compareTo(ONE_DAY) > 0) {
cachedResponse.addHeader(HeaderConstants.WARNING,"113 localhost \"Heuristic expiration\"");
if (LOG.isDebugEnabled()) {
LOG.debug("Added Warning 113 - Heuristic expiration to the response header.");
}
}
}
}
return cachedResponse;
}

View File

@ -39,10 +39,12 @@ import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
@ -1500,6 +1502,14 @@ public class TestCachingExecChain {
Mockito.when(cacheableRequestPolicy.isServableFromCache(Mockito.any())).thenReturn(true);
Mockito.when(validityPolicy.getStaleness(Mockito.any(), Mockito.any())).thenReturn(TimeValue.MAX_VALUE);
// Assuming validityPolicy is a Mockito mock
Mockito.when(validityPolicy.getCurrentAge(Mockito.any(), Mockito.any()))
.thenReturn(TimeValue.ofMilliseconds(0));
// Assuming validityPolicy is a Mockito mock
Mockito.when(validityPolicy.getFreshnessLifetime(Mockito.any()))
.thenReturn(TimeValue.ofMilliseconds(0));
final SimpleHttpResponse response = SimpleHttpResponse.create(HttpStatus.SC_OK);
final AtomicInteger callCount = new AtomicInteger(0);
@ -1558,6 +1568,7 @@ public class TestCachingExecChain {
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
execute(req1);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
@ -1569,4 +1580,75 @@ public class TestCachingExecChain {
Mockito.verify(mockExecChain, Mockito.times(5)).proceed(Mockito.any(), Mockito.any());
}
@Test
public void testHeuristicExpirationWarning() throws Exception {
impl = new CachingExec(responseCache, validityPolicy, responseCachingPolicy,
responseGenerator, cacheableRequestPolicy, suitabilityChecker,
responseCompliance, requestCompliance, cacheRevalidator,
conditionalRequestBuilder, customConfig);
// Create the first request and response
final BasicClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "http://foo.example.com/");
final BasicClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "http://foo.example.com/");
final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_GATEWAY_TIMEOUT, "OK");
resp1.setEntity(HttpTestUtils.makeBody(128));
resp1.setHeader("Content-Length", "128");
resp1.setHeader("ETag", "\"etag\"");
resp1.setHeader("Date", DateUtils.formatStandardDate(Instant.now().minus(Duration.ofHours(10))));
resp1.setHeader("Cache-Control", "public, max-age=-1, stale-while-revalidate=1");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
// Set up the mock response chain
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
Mockito.when(responseCachingPolicy.isStaleIfErrorEnabled(Mockito.any())).thenReturn(true);
Mockito.when(cacheableRequestPolicy.isServableFromCache(Mockito.any())).thenReturn(true);
Mockito.when(validityPolicy.getStaleness(Mockito.any(), Mockito.any())).thenReturn(TimeValue.MAX_VALUE);
// Assuming validityPolicy is a Mockito mock
Mockito.when(validityPolicy.getCurrentAge(Mockito.any(), Mockito.any()))
.thenReturn(TimeValue.ofDays(2));
// Assuming validityPolicy is a Mockito mock
Mockito.when(validityPolicy.getFreshnessLifetime(Mockito.any()))
.thenReturn(TimeValue.ofDays(2));
final SimpleHttpResponse response = SimpleHttpResponse.create(HttpStatus.SC_OK);
final AtomicInteger callCount = new AtomicInteger(0);
Mockito.doAnswer(invocation -> {
if (callCount.getAndIncrement() == 0) {
throw new ResourceIOException("ResourceIOException");
} else {
// Replace this with the actual return value for the second call
return response;
}
}).when(responseGenerator).generateResponse(Mockito.any(), Mockito.any());
// 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);
Assertions.assertEquals(HttpStatus.SC_GATEWAY_TIMEOUT, response1.getCode());
Mockito.when(mockExecRuntime.fork(Mockito.any())).thenReturn(mockExecRuntime);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
final ClassicHttpResponse response2 = execute(req2);
// Verify that the response has the expected status code (e.g., 200)
Assertions.assertEquals(HttpStatus.SC_OK, response2.getCode());
// For example, you can check for the "Warning" header indicating a stale response:
final List<Header> warningHeaders = Arrays.stream(response.getHeaders())
.filter(header -> header.getName().equalsIgnoreCase("Warning"))
.collect(Collectors.toList());
Assertions.assertFalse(warningHeaders.isEmpty());
final boolean found113 = warningHeaders.stream().anyMatch(header -> header.getValue().contains("113 localhost \"Heuristic expiration\""));
Assertions.assertTrue(found113);
final boolean found110 = warningHeaders.stream().anyMatch(header -> header.getValue().contains("110 localhost \"Response is stale\""));
Assertions.assertTrue(found110);
}
}