work in progress to improve CachingContentFactory
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
parent
64a5b07424
commit
e910a8ea59
|
@ -16,6 +16,7 @@ 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;
|
||||
|
@ -27,6 +28,9 @@ import java.util.concurrent.atomic.AtomicLong;
|
|||
|
||||
import org.eclipse.jetty.util.NanoTime;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.eclipse.jetty.util.resource.Resource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* HttpContent.ContentFactory implementation that wraps any other HttpContent.ContentFactory instance
|
||||
|
@ -41,6 +45,8 @@ import org.eclipse.jetty.util.StringUtil;
|
|||
*/
|
||||
public class CachingContentFactory implements HttpContent.Factory
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CachingContentFactory.class);
|
||||
|
||||
private final HttpContent.Factory _authority;
|
||||
private final ConcurrentMap<String, CachingHttpContent> _cache = new ConcurrentHashMap<>();
|
||||
private final AtomicLong _cachedSize = new AtomicLong();
|
||||
|
@ -53,6 +59,11 @@ public class CachingContentFactory implements HttpContent.Factory
|
|||
_authority = authority;
|
||||
}
|
||||
|
||||
protected ConcurrentMap<String, CachingHttpContent> getCache()
|
||||
{
|
||||
return _cache;
|
||||
}
|
||||
|
||||
public long getCachedSize()
|
||||
{
|
||||
return _cachedSize.get();
|
||||
|
@ -110,15 +121,15 @@ public class CachingContentFactory 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._lastAccessed.get(), c1._lastAccessed.get());
|
||||
long delta = NanoTime.elapsed(c2.getLastAccessed().get(), c1.getLastAccessed().get());
|
||||
if (delta != 0)
|
||||
return delta < 0 ? -1 : 1;
|
||||
|
||||
delta = c1._contentLengthValue - c2._contentLengthValue;
|
||||
delta = c1.getContentLengthValue() - c2.getContentLengthValue();
|
||||
if (delta != 0)
|
||||
return delta < 0 ? -1 : 1;
|
||||
|
||||
return c1._cacheKey.compareTo(c2._cacheKey);
|
||||
return c1.getKey().compareTo(c2.getKey());
|
||||
});
|
||||
sorted.addAll(_cache.values());
|
||||
|
||||
|
@ -132,12 +143,13 @@ public class CachingContentFactory implements HttpContent.Factory
|
|||
}
|
||||
}
|
||||
|
||||
private void removeFromCache(CachingHttpContent content)
|
||||
protected void removeFromCache(CachingHttpContent content)
|
||||
{
|
||||
if (content == _cache.remove(content._cacheKey))
|
||||
CachingHttpContent removed = _cache.remove(content.getKey());
|
||||
if (removed != null)
|
||||
{
|
||||
content.release();
|
||||
_cachedSize.addAndGet(-content.getContentLengthValue());
|
||||
removed.release();
|
||||
_cachedSize.addAndGet(-removed.getContentLengthValue());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -158,7 +170,7 @@ public class CachingContentFactory implements HttpContent.Factory
|
|||
protected boolean isCacheable(HttpContent httpContent)
|
||||
{
|
||||
if (httpContent == null)
|
||||
return false;
|
||||
return true;
|
||||
|
||||
if (httpContent.getResource().isDirectory())
|
||||
return false;
|
||||
|
@ -175,14 +187,48 @@ public class CachingContentFactory implements HttpContent.Factory
|
|||
return (len <= _maxCachedFileSize && len <= _maxCacheSize);
|
||||
}
|
||||
|
||||
protected boolean isValid(CachingHttpContent content)
|
||||
{
|
||||
// Only check the FileSystem once per second, otherwise assume cached value is valid.
|
||||
// TODO: should the time between checks be configurable.
|
||||
long now = NanoTime.now();
|
||||
if (content.getLastAccessed().updateAndGet(lastChecked ->
|
||||
(NanoTime.since(lastChecked) > TimeUnit.SECONDS.toNanos(1)) ? now : lastChecked) != now)
|
||||
return true;
|
||||
|
||||
if (content instanceof CachedContent cachedContent)
|
||||
return Objects.equals(cachedContent.getWrapped().getLastModifiedInstant(), cachedContent.getLastModifiedInstant());
|
||||
|
||||
// TODO:
|
||||
|
||||
if (content instanceof NotFoundContent notFoundContent)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _authority.getContent(notFoundContent.getKey()) != null;
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("", e);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpContent getContent(String path) throws IOException
|
||||
{
|
||||
CachingHttpContent cachingHttpContent = _cache.get(path);
|
||||
if (cachingHttpContent != null)
|
||||
return cachingHttpContent;
|
||||
{
|
||||
if (isValid(cachingHttpContent))
|
||||
return (cachingHttpContent instanceof NotFoundContent) ? null : cachingHttpContent;
|
||||
else
|
||||
removeFromCache(cachingHttpContent);
|
||||
}
|
||||
|
||||
// TODO: record cache misses.
|
||||
HttpContent httpContent = _authority.getContent(path);
|
||||
if (isCacheable(httpContent))
|
||||
{
|
||||
|
@ -190,8 +236,9 @@ public class CachingContentFactory implements HttpContent.Factory
|
|||
cachingHttpContent = _cache.computeIfAbsent(path, p ->
|
||||
{
|
||||
wasAdded.set(true);
|
||||
_cachedSize.addAndGet(httpContent.getContentLengthValue());
|
||||
return new CachingHttpContent(p, httpContent);
|
||||
CachingHttpContent cachingContent = (httpContent == null) ? new NotFoundContent(p) : new CachedContent(p, httpContent);
|
||||
_cachedSize.addAndGet(cachingContent.getContentLengthValue());
|
||||
return cachingContent;
|
||||
});
|
||||
|
||||
if (wasAdded.get())
|
||||
|
@ -201,7 +248,13 @@ public class CachingContentFactory implements HttpContent.Factory
|
|||
return httpContent;
|
||||
}
|
||||
|
||||
private static class CachingHttpContent extends HttpContentWrapper
|
||||
protected interface CachingHttpContent extends HttpContent
|
||||
{
|
||||
AtomicLong getLastAccessed();
|
||||
String getKey();
|
||||
}
|
||||
|
||||
private static class CachedContent extends HttpContentWrapper implements CachingHttpContent
|
||||
{
|
||||
private final ByteBuffer _buffer;
|
||||
private final String _cacheKey;
|
||||
|
@ -210,16 +263,16 @@ public class CachingContentFactory implements HttpContent.Factory
|
|||
private final AtomicLong _lastAccessed = new AtomicLong();
|
||||
private final Set<CompressedContentFormat> _compressedFormats;
|
||||
private final String _lastModifiedValue;
|
||||
|
||||
private final String _characterEncoding;
|
||||
private final MimeTypes.Type _mimeType;
|
||||
private final HttpField _contentLength;
|
||||
private final Instant _lastModifiedInstant;
|
||||
private final HttpField _lastModified;
|
||||
|
||||
private CachingHttpContent(String key, HttpContent httpContent)
|
||||
private CachedContent(String key, HttpContent httpContent)
|
||||
{
|
||||
super(httpContent);
|
||||
_cacheKey = key;
|
||||
|
||||
// TODO: do all the following lazily and asynchronously.
|
||||
HttpField etagField = httpContent.getETag();
|
||||
|
@ -232,7 +285,6 @@ public class CachingContentFactory implements HttpContent.Factory
|
|||
|
||||
// Map the content into memory if possible.
|
||||
_buffer = httpContent.getBuffer();
|
||||
_cacheKey = key;
|
||||
_contentLengthValue = httpContent.getContentLengthValue();
|
||||
_lastModifiedValue = httpContent.getLastModifiedValue();
|
||||
_characterEncoding = httpContent.getCharacterEncoding();
|
||||
|
@ -260,16 +312,16 @@ public class CachingContentFactory implements HttpContent.Factory
|
|||
return _buffer.slice();
|
||||
}
|
||||
|
||||
public boolean isValid()
|
||||
@Override
|
||||
public AtomicLong getLastAccessed()
|
||||
{
|
||||
// Only check the FileSystem once per second, otherwise assume cached value is valid.
|
||||
// TODO: should the time between checks be configurable.
|
||||
long now = NanoTime.now();
|
||||
if (_lastAccessed.updateAndGet(lastChecked ->
|
||||
(NanoTime.since(lastChecked) > TimeUnit.SECONDS.toNanos(1)) ? now : lastChecked) != now)
|
||||
return true;
|
||||
return _lastAccessed;
|
||||
}
|
||||
|
||||
return getLastModifiedInstant().equals(_lastModifiedValue);
|
||||
@Override
|
||||
public String getKey()
|
||||
{
|
||||
return _cacheKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -332,4 +384,129 @@ public class CachingContentFactory implements HttpContent.Factory
|
|||
return _lastModifiedValue;
|
||||
}
|
||||
}
|
||||
|
||||
private static class NotFoundContent implements CachingHttpContent
|
||||
{
|
||||
private final AtomicLong _lastAccessed = new AtomicLong(NanoTime.now());
|
||||
|
||||
private final String _key;
|
||||
|
||||
public NotFoundContent(String key)
|
||||
{
|
||||
_key = key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey()
|
||||
{
|
||||
return _key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AtomicLong getLastAccessed()
|
||||
{
|
||||
return _lastAccessed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpField getContentType()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentTypeValue()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCharacterEncoding()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MimeTypes.Type getMimeType()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpField getContentEncoding()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentEncodingValue()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpField getContentLength()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getContentLengthValue()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getLastModifiedInstant()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpField getLastModified()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLastModifiedValue()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpField getETag()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getETagValue()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource getResource()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer getBuffer()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<CompressedContentFormat> getPreCompressedContentFormats()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jetty.util.thread.Scheduler;
|
||||
|
||||
public class EvictingCachingContentFactory extends CachingContentFactory implements Runnable
|
||||
{
|
||||
private final Scheduler _scheduler;
|
||||
private final long _delay;
|
||||
private final TimeUnit _timeUnit;
|
||||
|
||||
public EvictingCachingContentFactory(HttpContent.Factory authority, Scheduler scheduler)
|
||||
{
|
||||
this(authority, scheduler, 1, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
public EvictingCachingContentFactory(HttpContent.Factory authority, Scheduler scheduler, long delay, TimeUnit timeUnit)
|
||||
{
|
||||
super(authority);
|
||||
_scheduler = scheduler;
|
||||
_delay = delay;
|
||||
_timeUnit = timeUnit;
|
||||
}
|
||||
|
||||
private void schedule()
|
||||
{
|
||||
_scheduler.schedule(this, _delay, _timeUnit);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isValid(CachingHttpContent content)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
ConcurrentMap<String, CachingHttpContent> cache = getCache();
|
||||
for (Map.Entry<String, CachingHttpContent> entry : cache.entrySet())
|
||||
{
|
||||
if (!super.isValid(entry.getValue()))
|
||||
removeFromCache(entry.getValue());
|
||||
}
|
||||
schedule();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue