separate evicting logic from CachingHttpContentFactory

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
Lachlan Roberts 2022-10-20 10:37:38 +11:00
parent 86da43a54a
commit 8768725de9
6 changed files with 121 additions and 117 deletions

View File

@ -16,13 +16,11 @@ package org.eclipse.jetty.http;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.time.Instant;
import java.util.Objects;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
@ -36,16 +34,6 @@ import org.slf4j.LoggerFactory;
* {@link HttpContent.Factory} implementation that wraps any other {@link HttpContent.Factory} instance
* using it as a caching authority.
* Only HttpContent instances whose path is not a directory are cached.
*
* <p>
* Eviction behaviours are configured through the {@link #setValidationTime(int)} method. A value of -1 means that the cached
* {@link HttpContent} will never be checked if it is still valid, a value of 0 means it is checked on every request, and a
* positive value indicates the number of milliseconds of the minimum time between validation checks.
* </p>
* <p>
* This also caches misses, and will remember a missed entry for the time set by {@link #setValidationTime(int)}. After this has
* elapsed the entry will be invalid and will be evicted from the cache after the next access.
* </p>
*/
public class CachingHttpContentFactory implements HttpContent.Factory
{
@ -57,7 +45,6 @@ public class CachingHttpContentFactory implements HttpContent.Factory
private int _maxCachedFileSize = 128 * 1024 * 1024;
private int _maxCachedFiles = 2048;
private int _maxCacheSize = 256 * 1024 * 1024;
private long _validationTime = 0;
public CachingHttpContentFactory(HttpContent.Factory authority)
{
@ -118,16 +105,6 @@ public class CachingHttpContentFactory implements HttpContent.Factory
shrinkCache();
}
public long getValidationTime()
{
return _validationTime;
}
public void setValidationTime(int evictionTime)
{
_validationTime = evictionTime;
}
private void shrinkCache()
{
// While we need to shrink
@ -136,7 +113,7 @@ public class CachingHttpContentFactory implements HttpContent.Factory
// Scan the entire cache and generate an ordered list by last accessed time.
SortedSet<CachingHttpContent> sorted = new TreeSet<>((c1, c2) ->
{
long delta = NanoTime.elapsed(c2.getLastAccessed().get(), c1.getLastAccessed().get());
long delta = NanoTime.elapsed(c2.getLastAccessedNanos(), c1.getLastAccessedNanos());
if (delta != 0)
return delta < 0 ? -1 : 1;
@ -185,7 +162,7 @@ public class CachingHttpContentFactory implements HttpContent.Factory
protected boolean isCacheable(HttpContent httpContent)
{
if (httpContent == null)
return (_validationTime != 0);
return true;
if (httpContent.getResource().isDirectory())
return false;
@ -208,7 +185,7 @@ public class CachingHttpContentFactory implements HttpContent.Factory
CachingHttpContent cachingHttpContent = _cache.get(path);
if (cachingHttpContent != null)
{
cachingHttpContent.getLastAccessed().set(NanoTime.now());
cachingHttpContent.setLastAccessedNanos(NanoTime.now());
if (cachingHttpContent.isValid())
return (cachingHttpContent instanceof NotFoundHttpContent) ? null : cachingHttpContent;
else
@ -222,8 +199,7 @@ public class CachingHttpContentFactory implements HttpContent.Factory
cachingHttpContent = _cache.computeIfAbsent(path, p ->
{
wasAdded.set(true);
CachingHttpContent cachingContent = (httpContent == null)
? newNotFoundContent(p, _validationTime) : newCachedContent(p, httpContent, _validationTime);
CachingHttpContent cachingContent = (httpContent == null) ? newNotFoundContent(p) : newCachedContent(p, httpContent);
_cachedSize.addAndGet(cachingContent.getContentLengthValue());
return cachingContent;
});
@ -235,19 +211,21 @@ public class CachingHttpContentFactory implements HttpContent.Factory
return httpContent;
}
protected CachingHttpContent newCachedContent(String p, HttpContent httpContent, long evictionTime)
protected CachingHttpContent newCachedContent(String p, HttpContent httpContent)
{
return new CachedHttpContent(p, httpContent, evictionTime);
return new CachedHttpContent(p, httpContent);
}
protected CachingHttpContent newNotFoundContent(String p, long evictionTime)
protected CachingHttpContent newNotFoundContent(String p)
{
return new NotFoundHttpContent(p, evictionTime);
return new NotFoundHttpContent(p);
}
protected interface CachingHttpContent extends HttpContent
{
AtomicLong getLastAccessed();
long getLastAccessedNanos();
void setLastAccessedNanos(long nanosTime);
String getKey();
@ -258,11 +236,9 @@ public class CachingHttpContentFactory implements HttpContent.Factory
{
private final ByteBuffer _buffer;
private final String _cacheKey;
private final long _validationTime;
private final HttpField _etagField;
private final long _contentLengthValue;
private final AtomicLong _lastAccessed = new AtomicLong();
private final AtomicLong _lastValidated = new AtomicLong();
private final Set<CompressedContentFormat> _compressedFormats;
private final String _lastModifiedValue;
private final String _characterEncoding;
@ -271,11 +247,10 @@ public class CachingHttpContentFactory implements HttpContent.Factory
private final Instant _lastModifiedInstant;
private final HttpField _lastModified;
public CachedHttpContent(String key, HttpContent httpContent, long validationTime)
public CachedHttpContent(String key, HttpContent httpContent)
{
super(httpContent);
_cacheKey = key;
_validationTime = validationTime;
// TODO: do all the following lazily and asynchronously.
HttpField etagField = httpContent.getETag();
@ -297,9 +272,7 @@ public class CachingHttpContentFactory implements HttpContent.Factory
_lastModifiedInstant = httpContent.getLastModifiedInstant();
_lastModified = httpContent.getLastModified();
long now = NanoTime.now();
_lastAccessed.set(now);
_lastValidated.set(now);
_lastAccessed.set(NanoTime.now());
}
@Override
@ -319,9 +292,15 @@ public class CachingHttpContentFactory implements HttpContent.Factory
}
@Override
public AtomicLong getLastAccessed()
public long getLastAccessedNanos()
{
return _lastAccessed;
return _lastAccessed.get();
}
@Override
public void setLastAccessedNanos(long nanosTime)
{
_lastAccessed.set(nanosTime);
}
@Override
@ -393,43 +372,20 @@ public class CachingHttpContentFactory implements HttpContent.Factory
@Override
public boolean isValid()
{
if (_validationTime < 0)
{
return true;
}
else if (_validationTime > 0)
{
long now = NanoTime.now();
if (_lastValidated.updateAndGet(lastChecked ->
(NanoTime.elapsed(lastChecked, now) > TimeUnit.MILLISECONDS.toNanos(_validationTime)) ? now : lastChecked) != now)
return true;
}
return Objects.equals(getLastModifiedInstant(), getWrapped().getLastModifiedInstant());
return true;
}
}
protected static class NotFoundHttpContent implements CachingHttpContent
{
private final AtomicLong _lastAccessed = new AtomicLong();
private final AtomicLong _lastValidated = new AtomicLong();
private final String _key;
private final long _evictionTime;
public NotFoundHttpContent(String key)
{
this(key, -1);
}
public NotFoundHttpContent(String key, long evictionTime)
{
_key = key;
_evictionTime = evictionTime;
long now = NanoTime.now();
_lastAccessed.set(now);
_lastValidated.set(now);
_lastAccessed.set(NanoTime.now());
}
@Override
@ -439,9 +395,15 @@ public class CachingHttpContentFactory implements HttpContent.Factory
}
@Override
public AtomicLong getLastAccessed()
public long getLastAccessedNanos()
{
return _lastAccessed;
return _lastAccessed.get();
}
@Override
public void setLastAccessedNanos(long nanosTime)
{
_lastAccessed.set(nanosTime);
}
@Override
@ -548,11 +510,7 @@ public class CachingHttpContentFactory implements HttpContent.Factory
@Override
public boolean isValid()
{
if (_evictionTime < 0)
return true;
if (_evictionTime > 0)
return NanoTime.since(_lastValidated.get()) < TimeUnit.MILLISECONDS.toNanos(_evictionTime);
return false;
return true;
}
}
}

View File

@ -14,22 +14,59 @@
package org.eclipse.jetty.http;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import org.eclipse.jetty.util.NanoTime;
import org.eclipse.jetty.util.thread.Scheduler;
/**
* <p>
* {@link HttpContent.Factory} implementation of {@link CachingHttpContentFactory} which evicts invalid entries from the cache.
* Uses a validationTime parameter to check files when they are accessed and/or sweepDelay parameter for a periodic sweep
* for invalid entries in the cache.
* </p>
* <p>
* {@link HttpContent} validation checks are configured through the {@code validationTime} parameter in the constructor.
* If an {@link HttpContent} is found to be invalid it will be removed from the cache.
* A value of -1 means that the cached {@link HttpContent} will never be checked if it is still valid,
* a value of 0 means it is checked on every request,
* a positive value indicates the number of milliseconds of the minimum time between validation checks.
* </p>
* <p>
* This also remember a missed entry for the time set by {@code validationTime}ms. After this has
* elapsed the entry will be invalid and will be evicted from the cache at the next access.
* </p>
*/
public class EvictingCachingContentFactory extends CachingHttpContentFactory implements Runnable
{
private final Scheduler _scheduler;
private final long _delay;
private final long _validationTime;
public EvictingCachingContentFactory(HttpContent.Factory authority, Scheduler scheduler, long delay)
public EvictingCachingContentFactory(HttpContent.Factory authority, long validationTime)
{
this(authority, validationTime, null, -1);
}
public EvictingCachingContentFactory(HttpContent.Factory authority, long validationTime, Scheduler scheduler, long sweepDelay)
{
super(authority);
_validationTime = validationTime;
_scheduler = scheduler;
_delay = delay;
setEvictionTime(0);
_delay = sweepDelay;
if (scheduler != null && sweepDelay > 0)
schedule();
}
@Override
protected boolean isCacheable(HttpContent httpContent)
{
if (httpContent == null)
return (_validationTime != 0);
return super.isCacheable(httpContent);
}
private void schedule()
@ -44,72 +81,75 @@ public class EvictingCachingContentFactory extends CachingHttpContentFactory imp
for (Map.Entry<String, CachingHttpContent> entry : cache.entrySet())
{
CachingHttpContent value = entry.getValue();
if (value instanceof EvictingCachedContent content)
{
if (!content.checkValid())
removeFromCache(content);
}
else if (value instanceof EvictingNotFoundContent content)
{
if (!content.checkValid())
removeFromCache(content);
}
else
{
if (!value.isValid())
removeFromCache(value);
}
if (!value.isValid())
removeFromCache(value);
}
schedule();
}
@Override
protected CachingHttpContent newCachedContent(String p, HttpContent httpContent, long evictionTime)
protected CachingHttpContent newCachedContent(String p, HttpContent httpContent)
{
return new EvictingCachedContent(p, httpContent, evictionTime);
return new EvictingCachedContent(p, httpContent, _validationTime);
}
@Override
protected CachingHttpContent newNotFoundContent(String p, long evictionTime)
protected CachingHttpContent newNotFoundContent(String p)
{
return new NotFoundHttpContent(p, evictionTime);
return new EvictingNotFoundContent(p, _validationTime);
}
protected static class EvictingCachedContent extends CachedHttpContent
{
public EvictingCachedContent(String key, HttpContent httpContent, long evictionTime)
private final long _validationTime;
private final AtomicLong _lastValidated = new AtomicLong();
public EvictingCachedContent(String key, HttpContent httpContent, long validationTime)
{
super(key, httpContent, evictionTime);
super(key, httpContent);
_lastValidated.set(NanoTime.now());
_validationTime = validationTime;
}
@Override
public boolean isValid()
{
return true;
}
if (_validationTime < 0)
{
return true;
}
else if (_validationTime > 0)
{
long now = NanoTime.now();
if (_lastValidated.updateAndGet(lastChecked ->
(NanoTime.elapsed(lastChecked, now) > TimeUnit.MILLISECONDS.toNanos(_validationTime)) ? now : lastChecked) != now)
return true;
}
public boolean checkValid()
{
return super.isValid();
return Objects.equals(getLastModifiedInstant(), getWrapped().getLastModifiedInstant());
}
}
protected static class EvictingNotFoundContent extends NotFoundHttpContent
{
public EvictingNotFoundContent(String key, long evictionTime)
private final long _validationTime;
private final AtomicLong _lastValidated = new AtomicLong();
public EvictingNotFoundContent(String key, long validationTime)
{
super(key, evictionTime);
super(key);
_validationTime = validationTime;
_lastValidated.set(NanoTime.now());
}
@Override
public boolean isValid()
{
return true;
}
public boolean checkValid()
{
return super.isValid();
if (_validationTime < 0)
return true;
if (_validationTime > 0)
return NanoTime.since(_lastValidated.get()) < TimeUnit.MILLISECONDS.toNanos(_validationTime);
return false;
}
}
}

View File

@ -13,10 +13,11 @@
package org.eclipse.jetty.server.handler;
import java.time.Duration;
import java.util.List;
import org.eclipse.jetty.http.CachingHttpContentFactory;
import org.eclipse.jetty.http.CompressedContentFormat;
import org.eclipse.jetty.http.EvictingCachingContentFactory;
import org.eclipse.jetty.http.FileMappedHttpContentFactory;
import org.eclipse.jetty.http.HttpContent;
import org.eclipse.jetty.http.HttpMethod;
@ -86,7 +87,7 @@ public class ResourceHandler extends Handler.Wrapper
HttpContent.Factory contentFactory = new ResourceHttpContentFactory(ResourceFactory.of(_resourceBase), _mimeTypes);
contentFactory = new PreCompressedHttpContentFactory(contentFactory, _resourceService.getPrecompressedFormats());
contentFactory = new FileMappedHttpContentFactory(contentFactory);
contentFactory = new CachingHttpContentFactory(contentFactory);
contentFactory = new EvictingCachingContentFactory(contentFactory, Duration.ofSeconds(1).toMillis());
return contentFactory;
}

View File

@ -18,6 +18,7 @@ import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.nio.file.InvalidPathException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.Enumeration;
@ -45,6 +46,7 @@ import jakarta.servlet.http.HttpServletResponseWrapper;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.CachingHttpContentFactory;
import org.eclipse.jetty.http.CompressedContentFormat;
import org.eclipse.jetty.http.EvictingCachingContentFactory;
import org.eclipse.jetty.http.FileMappedHttpContentFactory;
import org.eclipse.jetty.http.HttpContent;
import org.eclipse.jetty.http.HttpContentWrapper;
@ -136,7 +138,7 @@ public class DefaultServlet extends HttpServlet
if (getInitBoolean("useFileMappedBuffer", false))
contentFactory = new FileMappedHttpContentFactory(contentFactory);
CachingHttpContentFactory cached = new CachingHttpContentFactory(contentFactory);
CachingHttpContentFactory cached = new EvictingCachingContentFactory(contentFactory, Duration.ofSeconds(1).toMillis());
int maxCacheSize = getInitInt("maxCacheSize", -2);
int maxCachedFileSize = getInitInt("maxCachedFileSize", -2);
int maxCachedFiles = getInitInt("maxCachedFiles", -2);

View File

@ -16,6 +16,7 @@ package org.eclipse.jetty.ee9.nested;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Path;
import java.time.Duration;
import java.util.List;
import jakarta.servlet.ServletException;
@ -23,8 +24,8 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.ee9.nested.ContextHandler.APIContext;
import org.eclipse.jetty.ee9.nested.ResourceService.WelcomeFactory;
import org.eclipse.jetty.http.CachingHttpContentFactory;
import org.eclipse.jetty.http.CompressedContentFormat;
import org.eclipse.jetty.http.EvictingCachingContentFactory;
import org.eclipse.jetty.http.FileMappedHttpContentFactory;
import org.eclipse.jetty.http.HttpContent;
import org.eclipse.jetty.http.HttpHeader;
@ -110,7 +111,7 @@ public class ResourceHandler extends HandlerWrapper implements ResourceFactory,
HttpContent.Factory contentFactory = new ResourceHttpContentFactory(this, _mimeTypes);
contentFactory = new PreCompressedHttpContentFactory(contentFactory, _resourceService.getPrecompressedFormats());
contentFactory = new FileMappedHttpContentFactory(contentFactory);
contentFactory = new CachingHttpContentFactory(contentFactory);
contentFactory = new EvictingCachingContentFactory(contentFactory, Duration.ofSeconds(1).toMillis());
return contentFactory;
}

View File

@ -15,6 +15,7 @@ package org.eclipse.jetty.ee9.servlet;
import java.io.IOException;
import java.net.URI;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@ -31,6 +32,7 @@ import org.eclipse.jetty.ee9.nested.ResourceService;
import org.eclipse.jetty.ee9.nested.ResourceService.WelcomeFactory;
import org.eclipse.jetty.http.CachingHttpContentFactory;
import org.eclipse.jetty.http.CompressedContentFormat;
import org.eclipse.jetty.http.EvictingCachingContentFactory;
import org.eclipse.jetty.http.FileMappedHttpContentFactory;
import org.eclipse.jetty.http.HttpContent;
import org.eclipse.jetty.http.HttpHeader;
@ -248,7 +250,7 @@ public class DefaultServlet extends HttpServlet implements ResourceFactory, Welc
if (_useFileMappedBuffer)
contentFactory = new FileMappedHttpContentFactory(contentFactory);
_cachingContentFactory = new CachingHttpContentFactory(contentFactory);
_cachingContentFactory = new EvictingCachingContentFactory(contentFactory, Duration.ofSeconds(1).toMillis());
int maxCacheSize = getInitInt("maxCacheSize", -2);
int maxCachedFileSize = getInitInt("maxCachedFileSize", -2);
int maxCachedFiles = getInitInt("maxCachedFiles", -2);