/* * 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); // 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); // 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; } } } }