init
This commit is contained in:
329
java/org/apache/catalina/webresources/Cache.java
Normal file
329
java/org/apache/catalina/webresources/Cache.java
Normal file
@@ -0,0 +1,329 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
package org.apache.catalina.webresources;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import org.apache.catalina.WebResource;
|
||||
import org.apache.juli.logging.Log;
|
||||
import org.apache.juli.logging.LogFactory;
|
||||
import org.apache.tomcat.util.res.StringManager;
|
||||
|
||||
public class Cache {
|
||||
|
||||
private static final Log log = LogFactory.getLog(Cache.class);
|
||||
protected static final StringManager sm = StringManager.getManager(Cache.class);
|
||||
|
||||
private static final long TARGET_FREE_PERCENT_GET = 5;
|
||||
private static final long TARGET_FREE_PERCENT_BACKGROUND = 10;
|
||||
|
||||
// objectMaxSize must be < maxSize/20
|
||||
private static final int OBJECT_MAX_SIZE_FACTOR = 20;
|
||||
|
||||
private final StandardRoot root;
|
||||
private final AtomicLong size = new AtomicLong(0);
|
||||
|
||||
private long ttl = 5000;
|
||||
private long maxSize = 10 * 1024 * 1024;
|
||||
private int objectMaxSize = (int) maxSize/OBJECT_MAX_SIZE_FACTOR;
|
||||
|
||||
private AtomicLong lookupCount = new AtomicLong(0);
|
||||
private AtomicLong hitCount = new AtomicLong(0);
|
||||
|
||||
private final ConcurrentMap<String,CachedResource> resourceCache =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
public Cache(StandardRoot root) {
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
protected WebResource getResource(String path, boolean useClassLoaderResources) {
|
||||
|
||||
if (noCache(path)) {
|
||||
return root.getResourceInternal(path, useClassLoaderResources);
|
||||
}
|
||||
|
||||
lookupCount.incrementAndGet();
|
||||
|
||||
CachedResource cacheEntry = resourceCache.get(path);
|
||||
|
||||
if (cacheEntry != null && !cacheEntry.validateResource(useClassLoaderResources)) {
|
||||
removeCacheEntry(path);
|
||||
cacheEntry = null;
|
||||
}
|
||||
|
||||
if (cacheEntry == null) {
|
||||
// Local copy to ensure consistency
|
||||
int objectMaxSizeBytes = getObjectMaxSizeBytes();
|
||||
CachedResource newCacheEntry = new CachedResource(this, root, path, getTtl(),
|
||||
objectMaxSizeBytes, useClassLoaderResources);
|
||||
|
||||
// Concurrent callers will end up with the same CachedResource
|
||||
// instance
|
||||
cacheEntry = resourceCache.putIfAbsent(path, newCacheEntry);
|
||||
|
||||
if (cacheEntry == null) {
|
||||
// newCacheEntry was inserted into the cache - validate it
|
||||
cacheEntry = newCacheEntry;
|
||||
cacheEntry.validateResource(useClassLoaderResources);
|
||||
|
||||
// Even if the resource content larger than objectMaxSizeBytes
|
||||
// there is still benefit in caching the resource metadata
|
||||
|
||||
long delta = cacheEntry.getSize();
|
||||
size.addAndGet(delta);
|
||||
|
||||
if (size.get() > maxSize) {
|
||||
// Process resources unordered for speed. Trades cache
|
||||
// efficiency (younger entries may be evicted before older
|
||||
// ones) for speed since this is on the critical path for
|
||||
// request processing
|
||||
long targetSize = maxSize * (100 - TARGET_FREE_PERCENT_GET) / 100;
|
||||
long newSize = evict(targetSize, resourceCache.values().iterator());
|
||||
if (newSize > maxSize) {
|
||||
// Unable to create sufficient space for this resource
|
||||
// Remove it from the cache
|
||||
removeCacheEntry(path);
|
||||
log.warn(sm.getString("cache.addFail", path, root.getContext().getName()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Another thread added the entry to the cache
|
||||
// Make sure it is validated
|
||||
cacheEntry.validateResource(useClassLoaderResources);
|
||||
}
|
||||
} else {
|
||||
hitCount.incrementAndGet();
|
||||
}
|
||||
|
||||
return cacheEntry;
|
||||
}
|
||||
|
||||
protected WebResource[] getResources(String path, boolean useClassLoaderResources) {
|
||||
lookupCount.incrementAndGet();
|
||||
|
||||
// Don't call noCache(path) since the class loader only caches
|
||||
// individual resources. Therefore, always cache collections here
|
||||
|
||||
CachedResource cacheEntry = resourceCache.get(path);
|
||||
|
||||
if (cacheEntry != null && !cacheEntry.validateResources(useClassLoaderResources)) {
|
||||
removeCacheEntry(path);
|
||||
cacheEntry = null;
|
||||
}
|
||||
|
||||
if (cacheEntry == null) {
|
||||
// Local copy to ensure consistency
|
||||
int objectMaxSizeBytes = getObjectMaxSizeBytes();
|
||||
CachedResource newCacheEntry = new CachedResource(this, root, path, getTtl(),
|
||||
objectMaxSizeBytes, useClassLoaderResources);
|
||||
|
||||
// Concurrent callers will end up with the same CachedResource
|
||||
// instance
|
||||
cacheEntry = resourceCache.putIfAbsent(path, newCacheEntry);
|
||||
|
||||
if (cacheEntry == null) {
|
||||
// newCacheEntry was inserted into the cache - validate it
|
||||
cacheEntry = newCacheEntry;
|
||||
cacheEntry.validateResources(useClassLoaderResources);
|
||||
|
||||
// Content will not be cached but we still need metadata size
|
||||
long delta = cacheEntry.getSize();
|
||||
size.addAndGet(delta);
|
||||
|
||||
if (size.get() > maxSize) {
|
||||
// Process resources unordered for speed. Trades cache
|
||||
// efficiency (younger entries may be evicted before older
|
||||
// ones) for speed since this is on the critical path for
|
||||
// request processing
|
||||
long targetSize = maxSize * (100 - TARGET_FREE_PERCENT_GET) / 100;
|
||||
long newSize = evict(targetSize, resourceCache.values().iterator());
|
||||
if (newSize > maxSize) {
|
||||
// Unable to create sufficient space for this resource
|
||||
// Remove it from the cache
|
||||
removeCacheEntry(path);
|
||||
log.warn(sm.getString("cache.addFail", path));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Another thread added the entry to the cache
|
||||
// Make sure it is validated
|
||||
cacheEntry.validateResources(useClassLoaderResources);
|
||||
}
|
||||
} else {
|
||||
hitCount.incrementAndGet();
|
||||
}
|
||||
|
||||
return cacheEntry.getWebResources();
|
||||
}
|
||||
|
||||
protected void backgroundProcess() {
|
||||
// Create an ordered set of all cached resources with the least recently
|
||||
// used first. This is a background process so we can afford to take the
|
||||
// time to order the elements first
|
||||
TreeSet<CachedResource> orderedResources =
|
||||
new TreeSet<>(new EvictionOrder());
|
||||
orderedResources.addAll(resourceCache.values());
|
||||
|
||||
Iterator<CachedResource> iter = orderedResources.iterator();
|
||||
|
||||
long targetSize =
|
||||
maxSize * (100 - TARGET_FREE_PERCENT_BACKGROUND) / 100;
|
||||
long newSize = evict(targetSize, iter);
|
||||
|
||||
if (newSize > targetSize) {
|
||||
log.info(sm.getString("cache.backgroundEvictFail",
|
||||
Long.valueOf(TARGET_FREE_PERCENT_BACKGROUND),
|
||||
root.getContext().getName(),
|
||||
Long.valueOf(newSize / 1024)));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean noCache(String path) {
|
||||
// Don't cache classes. The class loader handles this.
|
||||
// Don't cache JARs. The ResourceSet handles this.
|
||||
if ((path.endsWith(".class") &&
|
||||
(path.startsWith("/WEB-INF/classes/") || path.startsWith("/WEB-INF/lib/")))
|
||||
||
|
||||
(path.startsWith("/WEB-INF/lib/") && path.endsWith(".jar"))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private long evict(long targetSize, Iterator<CachedResource> iter) {
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
long newSize = size.get();
|
||||
|
||||
while (newSize > targetSize && iter.hasNext()) {
|
||||
CachedResource resource = iter.next();
|
||||
|
||||
// Don't expire anything that has been checked within the TTL
|
||||
if (resource.getNextCheck() > now) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remove the entry from the cache
|
||||
removeCacheEntry(resource.getWebappPath());
|
||||
|
||||
newSize = size.get();
|
||||
}
|
||||
|
||||
return newSize;
|
||||
}
|
||||
|
||||
void removeCacheEntry(String path) {
|
||||
// With concurrent calls for the same path, the entry is only removed
|
||||
// once and the cache size is only updated (if required) once.
|
||||
CachedResource cachedResource = resourceCache.remove(path);
|
||||
if (cachedResource != null) {
|
||||
long delta = cachedResource.getSize();
|
||||
size.addAndGet(-delta);
|
||||
}
|
||||
}
|
||||
|
||||
public long getTtl() {
|
||||
return ttl;
|
||||
}
|
||||
|
||||
public void setTtl(long ttl) {
|
||||
this.ttl = ttl;
|
||||
}
|
||||
|
||||
public long getMaxSize() {
|
||||
// Internally bytes, externally kilobytes
|
||||
return maxSize / 1024;
|
||||
}
|
||||
|
||||
public void setMaxSize(long maxSize) {
|
||||
// Internally bytes, externally kilobytes
|
||||
this.maxSize = maxSize * 1024;
|
||||
}
|
||||
|
||||
public long getLookupCount() {
|
||||
return lookupCount.get();
|
||||
}
|
||||
|
||||
public long getHitCount() {
|
||||
return hitCount.get();
|
||||
}
|
||||
|
||||
public void setObjectMaxSize(int objectMaxSize) {
|
||||
if (objectMaxSize * 1024L > Integer.MAX_VALUE) {
|
||||
log.warn(sm.getString("cache.objectMaxSizeTooBigBytes", Integer.valueOf(objectMaxSize)));
|
||||
this.objectMaxSize = Integer.MAX_VALUE;
|
||||
}
|
||||
// Internally bytes, externally kilobytes
|
||||
this.objectMaxSize = objectMaxSize * 1024;
|
||||
}
|
||||
|
||||
public int getObjectMaxSize() {
|
||||
// Internally bytes, externally kilobytes
|
||||
return objectMaxSize / 1024;
|
||||
}
|
||||
|
||||
public int getObjectMaxSizeBytes() {
|
||||
return objectMaxSize;
|
||||
}
|
||||
|
||||
void enforceObjectMaxSizeLimit() {
|
||||
long limit = maxSize / OBJECT_MAX_SIZE_FACTOR;
|
||||
if (limit > Integer.MAX_VALUE) {
|
||||
return;
|
||||
}
|
||||
if (objectMaxSize > limit) {
|
||||
log.warn(sm.getString("cache.objectMaxSizeTooBig",
|
||||
Integer.valueOf(objectMaxSize / 1024), Integer.valueOf((int)limit / 1024)));
|
||||
objectMaxSize = (int) limit;
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
resourceCache.clear();
|
||||
size.set(0);
|
||||
}
|
||||
|
||||
public long getSize() {
|
||||
return size.get() / 1024;
|
||||
}
|
||||
|
||||
private static class EvictionOrder implements Comparator<CachedResource> {
|
||||
|
||||
@Override
|
||||
public int compare(CachedResource cr1, CachedResource cr2) {
|
||||
long nc1 = cr1.getNextCheck();
|
||||
long nc2 = cr2.getNextCheck();
|
||||
|
||||
// Oldest resource should be first (so iterator goes from oldest to
|
||||
// youngest.
|
||||
if (nc1 == nc2) {
|
||||
return 0;
|
||||
} else if (nc1 > nc2) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user