CacheValidityPolicy.java
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl.cache;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.client5.http.cache.ResponseCacheControl;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.message.MessageSupport;
import org.apache.hc.core5.util.TimeValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class CacheValidityPolicy {
private static final Logger LOG = LoggerFactory.getLogger(CacheValidityPolicy.class);
private final boolean shared;
private final boolean useHeuristicCaching;
private final float heuristicCoefficient;
private final TimeValue heuristicDefaultLifetime;
/**
* Constructs a CacheValidityPolicy with the provided CacheConfig. If the config is null, it will use
* default heuristic coefficient and default heuristic lifetime from CacheConfig.DEFAULT.
*
* @param config The CacheConfig to use for this CacheValidityPolicy. If null, default values are used.
*/
CacheValidityPolicy(final CacheConfig config) {
super();
this.shared = config != null ? config.isSharedCache() : CacheConfig.DEFAULT.isSharedCache();
this.useHeuristicCaching = config != null ? config.isHeuristicCachingEnabled() : CacheConfig.DEFAULT.isHeuristicCachingEnabled();
this.heuristicCoefficient = config != null ? config.getHeuristicCoefficient() : CacheConfig.DEFAULT.getHeuristicCoefficient();
this.heuristicDefaultLifetime = config != null ? config.getHeuristicDefaultLifetime() : CacheConfig.DEFAULT.getHeuristicDefaultLifetime();
}
/**
* Default constructor for CacheValidityPolicy. Initializes the policy with default values.
*/
CacheValidityPolicy() {
this(null);
}
public TimeValue getCurrentAge(final HttpCacheEntry entry, final Instant now) {
return TimeValue.ofSeconds(getCorrectedInitialAge(entry).toSeconds() + getResidentTime(entry, now).toSeconds());
}
/**
* Calculate the freshness lifetime of a response based on the provided cache control and cache entry.
* <ul>
* <li>If the cache is shared and the s-maxage response directive is present, use its value.</li>
* <li>If the max-age response directive is present, use its value.</li>
* <li>If the Expires response header field is present, use its value minus the value of the Date response header field.</li>
* <li>Otherwise, a heuristic freshness lifetime might be applicable.</li>
* </ul>
*
* @param responseCacheControl the cache control directives associated with the response.
* @param entry the cache entry associated with the response.
* @return the calculated freshness lifetime as a {@link TimeValue}.
*/
public TimeValue getFreshnessLifetime(final ResponseCacheControl responseCacheControl, final HttpCacheEntry entry) {
// If the cache is shared and the s-maxage response directive is present, use its value
if (shared) {
final long sharedMaxAge = responseCacheControl.getSharedMaxAge();
if (sharedMaxAge > -1) {
if (LOG.isDebugEnabled()) {
LOG.debug("Using s-maxage directive for freshness lifetime calculation: {} seconds", sharedMaxAge);
}
return TimeValue.ofSeconds(sharedMaxAge);
}
}
// If the max-age response directive is present, use its value
final long maxAge = responseCacheControl.getMaxAge();
if (maxAge > -1) {
if (LOG.isDebugEnabled()) {
LOG.debug("Using max-age directive for freshness lifetime calculation: {} seconds", maxAge);
}
return TimeValue.ofSeconds(maxAge);
}
// If the Expires response header field is present, use its value minus the value of the Date response header field
final Instant dateValue = entry.getInstant();
if (dateValue != null) {
final Instant expiry = entry.getExpires();
if (expiry != null) {
final Duration diff = Duration.between(dateValue, expiry);
if (diff.isNegative()) {
if (LOG.isDebugEnabled()) {
LOG.debug("Negative freshness lifetime detected. Content is already expired. Returning zero freshness lifetime.");
}
return TimeValue.ZERO_MILLISECONDS;
}
return TimeValue.ofSeconds(diff.getSeconds());
}
}
if (useHeuristicCaching) {
// No explicit expiration time is present in the response. A heuristic freshness lifetime might be applicable
if (LOG.isDebugEnabled()) {
LOG.debug("No explicit expiration time present in the response. Using heuristic freshness lifetime calculation.");
}
return getHeuristicFreshnessLifetime(entry);
}
return TimeValue.ZERO_MILLISECONDS;
}
TimeValue getHeuristicFreshnessLifetime(final HttpCacheEntry entry) {
final Instant dateValue = entry.getInstant();
final Instant lastModifiedValue = entry.getLastModified();
if (dateValue != null && lastModifiedValue != null) {
final Duration diff = Duration.between(lastModifiedValue, dateValue);
if (diff.isNegative()) {
return TimeValue.ZERO_MILLISECONDS;
}
return TimeValue.ofSeconds((long) (heuristicCoefficient * diff.getSeconds()));
}
return heuristicDefaultLifetime;
}
TimeValue getApparentAge(final HttpCacheEntry entry) {
final Instant dateValue = entry.getInstant();
if (dateValue == null) {
return CacheSupport.MAX_AGE;
}
final Duration diff = Duration.between(dateValue, entry.getResponseInstant());
if (diff.isNegative()) {
return TimeValue.ZERO_MILLISECONDS;
}
return TimeValue.ofSeconds(diff.getSeconds());
}
/**
* Extracts and processes the Age value from an HttpCacheEntry by tokenizing the Age header value.
* The Age header value is interpreted as a sequence of tokens, and the first token is parsed into a number
* representing the age in delta-seconds. If the first token cannot be parsed into a number, the Age value is
* considered as invalid and this method returns 0. If the first token represents a negative number or a number
* that exceeds Integer.MAX_VALUE, the Age value is set to MAX_AGE (in seconds).
* This method uses CacheSupport.parseTokens to robustly handle the Age header value.
* <p>
* Note: If the HttpCacheEntry contains multiple Age headers, only the first one is considered.
*
* @param entry The HttpCacheEntry from which to extract the Age value.
* @return The Age value in delta-seconds, or MAX_AGE in seconds if the Age value exceeds Integer.MAX_VALUE or
* is negative. If the Age value is invalid (cannot be parsed into a number or contains non-numeric characters),
* this method returns 0.
*/
long getAgeValue(final HttpCacheEntry entry) {
final Header age = entry.getFirstHeader(HttpHeaders.AGE);
if (age != null) {
final AtomicReference<String> firstToken = new AtomicReference<>();
MessageSupport.parseTokens(age, token -> firstToken.compareAndSet(null, token));
final long delta = CacheSupport.deltaSeconds(firstToken.get());
if (delta == -1 && LOG.isDebugEnabled()) {
LOG.debug("Malformed Age value: {}", age);
}
return delta > 0 ? delta : 0;
}
// If we've got here, there were no valid Age headers
return 0;
}
TimeValue getCorrectedAgeValue(final HttpCacheEntry entry) {
final long ageValue = getAgeValue(entry);
final long responseDelay = getResponseDelay(entry).toSeconds();
return TimeValue.ofSeconds(ageValue + responseDelay);
}
TimeValue getResponseDelay(final HttpCacheEntry entry) {
final Duration diff = Duration.between(entry.getRequestInstant(), entry.getResponseInstant());
return TimeValue.ofSeconds(diff.getSeconds());
}
TimeValue getCorrectedInitialAge(final HttpCacheEntry entry) {
final long apparentAge = getApparentAge(entry).toSeconds();
final long correctedReceivedAge = getCorrectedAgeValue(entry).toSeconds();
return TimeValue.ofSeconds(Math.max(apparentAge, correctedReceivedAge));
}
TimeValue getResidentTime(final HttpCacheEntry entry, final Instant now) {
final Duration diff = Duration.between(entry.getResponseInstant(), now);
return TimeValue.ofSeconds(diff.getSeconds());
}
}