1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */

17 package org.apache.catalina.webresources;
18
19 import java.util.Comparator;
20 import java.util.Iterator;
21 import java.util.TreeSet;
22 import java.util.concurrent.ConcurrentHashMap;
23 import java.util.concurrent.ConcurrentMap;
24 import java.util.concurrent.atomic.AtomicLong;
25
26 import org.apache.catalina.WebResource;
27 import org.apache.juli.logging.Log;
28 import org.apache.juli.logging.LogFactory;
29 import org.apache.tomcat.util.res.StringManager;
30
31 public class Cache {
32
33     private static final Log log = LogFactory.getLog(Cache.class);
34     protected static final StringManager sm = StringManager.getManager(Cache.class);
35
36     private static final long TARGET_FREE_PERCENT_GET = 5;
37     private static final long TARGET_FREE_PERCENT_BACKGROUND = 10;
38
39     // objectMaxSize must be < maxSize/20
40     private static final int OBJECT_MAX_SIZE_FACTOR = 20;
41
42     private final StandardRoot root;
43     private final AtomicLong size = new AtomicLong(0);
44
45     private long ttl = 5000;
46     private long maxSize = 10 * 1024 * 1024;
47     private int objectMaxSize = (int) maxSize/OBJECT_MAX_SIZE_FACTOR;
48
49     private AtomicLong lookupCount = new AtomicLong(0);
50     private AtomicLong hitCount = new AtomicLong(0);
51
52     private final ConcurrentMap<String,CachedResource> resourceCache =
53             new ConcurrentHashMap<>();
54
55     public Cache(StandardRoot root) {
56         this.root = root;
57     }
58
59     protected WebResource getResource(String path, boolean useClassLoaderResources) {
60
61         if (noCache(path)) {
62             return root.getResourceInternal(path, useClassLoaderResources);
63         }
64
65         lookupCount.incrementAndGet();
66
67         CachedResource cacheEntry = resourceCache.get(path);
68
69         if (cacheEntry != null && !cacheEntry.validateResource(useClassLoaderResources)) {
70             removeCacheEntry(path);
71             cacheEntry = null;
72         }
73
74         if (cacheEntry == null) {
75             // Local copy to ensure consistency
76             int objectMaxSizeBytes = getObjectMaxSizeBytes();
77             CachedResource newCacheEntry = new CachedResource(this, root, path, getTtl(),
78                     objectMaxSizeBytes, useClassLoaderResources);
79
80             // Concurrent callers will end up with the same CachedResource
81             // instance
82             cacheEntry = resourceCache.putIfAbsent(path, newCacheEntry);
83
84             if (cacheEntry == null) {
85                 // newCacheEntry was inserted into the cache - validate it
86                 cacheEntry = newCacheEntry;
87                 cacheEntry.validateResource(useClassLoaderResources);
88
89                 // Even if the resource content larger than objectMaxSizeBytes
90                 // there is still benefit in caching the resource metadata
91
92                 long delta = cacheEntry.getSize();
93                 size.addAndGet(delta);
94
95                 if (size.get() > maxSize) {
96                     // Process resources unordered for speed. Trades cache
97                     // efficiency (younger entries may be evicted before older
98                     // ones) for speed since this is on the critical path for
99                     // request processing
100                     long targetSize = maxSize * (100 - TARGET_FREE_PERCENT_GET) / 100;
101                     long newSize = evict(targetSize, resourceCache.values().iterator());
102                     if (newSize > maxSize) {
103                         // Unable to create sufficient space for this resource
104                         // Remove it from the cache
105                         removeCacheEntry(path);
106                         log.warn(sm.getString("cache.addFail", path, root.getContext().getName()));
107                     }
108                 }
109             } else {
110                 // Another thread added the entry to the cache
111                 // Make sure it is validated
112                 cacheEntry.validateResource(useClassLoaderResources);
113             }
114         } else {
115             hitCount.incrementAndGet();
116         }
117
118         return cacheEntry;
119     }
120
121     protected WebResource[] getResources(String path, boolean useClassLoaderResources) {
122         lookupCount.incrementAndGet();
123
124         // Don't call noCache(path) since the class loader only caches
125         // individual resources. Therefore, always cache collections here
126
127         CachedResource cacheEntry = resourceCache.get(path);
128
129         if (cacheEntry != null && !cacheEntry.validateResources(useClassLoaderResources)) {
130             removeCacheEntry(path);
131             cacheEntry = null;
132         }
133
134         if (cacheEntry == null) {
135             // Local copy to ensure consistency
136             int objectMaxSizeBytes = getObjectMaxSizeBytes();
137             CachedResource newCacheEntry = new CachedResource(this, root, path, getTtl(),
138                     objectMaxSizeBytes, useClassLoaderResources);
139
140             // Concurrent callers will end up with the same CachedResource
141             // instance
142             cacheEntry = resourceCache.putIfAbsent(path, newCacheEntry);
143
144             if (cacheEntry == null) {
145                 // newCacheEntry was inserted into the cache - validate it
146                 cacheEntry = newCacheEntry;
147                 cacheEntry.validateResources(useClassLoaderResources);
148
149                 // Content will not be cached but we still need metadata size
150                 long delta = cacheEntry.getSize();
151                 size.addAndGet(delta);
152
153                 if (size.get() > maxSize) {
154                     // Process resources unordered for speed. Trades cache
155                     // efficiency (younger entries may be evicted before older
156                     // ones) for speed since this is on the critical path for
157                     // request processing
158                     long targetSize = maxSize * (100 - TARGET_FREE_PERCENT_GET) / 100;
159                     long newSize = evict(targetSize, resourceCache.values().iterator());
160                     if (newSize > maxSize) {
161                         // Unable to create sufficient space for this resource
162                         // Remove it from the cache
163                         removeCacheEntry(path);
164                         log.warn(sm.getString("cache.addFail", path));
165                     }
166                 }
167             } else {
168                 // Another thread added the entry to the cache
169                 // Make sure it is validated
170                 cacheEntry.validateResources(useClassLoaderResources);
171             }
172         } else {
173             hitCount.incrementAndGet();
174         }
175
176         return cacheEntry.getWebResources();
177     }
178
179     protected void backgroundProcess() {
180         // Create an ordered set of all cached resources with the least recently
181         // used first. This is a background process so we can afford to take the
182         // time to order the elements first
183         TreeSet<CachedResource> orderedResources =
184                 new TreeSet<>(new EvictionOrder());
185         orderedResources.addAll(resourceCache.values());
186
187         Iterator<CachedResource> iter = orderedResources.iterator();
188
189         long targetSize =
190                 maxSize * (100 - TARGET_FREE_PERCENT_BACKGROUND) / 100;
191         long newSize = evict(targetSize, iter);
192
193         if (newSize > targetSize) {
194             log.info(sm.getString("cache.backgroundEvictFail",
195                     Long.valueOf(TARGET_FREE_PERCENT_BACKGROUND),
196                     root.getContext().getName(),
197                     Long.valueOf(newSize / 1024)));
198         }
199     }
200
201     private boolean noCache(String path) {
202         // Don't cache classes. The class loader handles this.
203         // Don't cache JARs. The ResourceSet handles this.
204         if ((path.endsWith(".class") &&
205                 (path.startsWith("/WEB-INF/classes/") || path.startsWith("/WEB-INF/lib/")))
206                 ||
207                 (path.startsWith("/WEB-INF/lib/") && path.endsWith(".jar"))) {
208             return true;
209         }
210         return false;
211     }
212
213     private long evict(long targetSize, Iterator<CachedResource> iter) {
214
215         long now = System.currentTimeMillis();
216
217         long newSize = size.get();
218
219         while (newSize > targetSize && iter.hasNext()) {
220             CachedResource resource = iter.next();
221
222             // Don't expire anything that has been checked within the TTL
223             if (resource.getNextCheck() > now) {
224                 continue;
225             }
226
227             // Remove the entry from the cache
228             removeCacheEntry(resource.getWebappPath());
229
230             newSize = size.get();
231         }
232
233         return newSize;
234     }
235
236     void removeCacheEntry(String path) {
237         // With concurrent calls for the same path, the entry is only removed
238         // once and the cache size is only updated (if required) once.
239         CachedResource cachedResource = resourceCache.remove(path);
240         if (cachedResource != null) {
241             long delta = cachedResource.getSize();
242             size.addAndGet(-delta);
243         }
244     }
245
246     public long getTtl() {
247         return ttl;
248     }
249
250     public void setTtl(long ttl) {
251         this.ttl = ttl;
252     }
253
254     public long getMaxSize() {
255         // Internally bytes, externally kilobytes
256         return maxSize / 1024;
257     }
258
259     public void setMaxSize(long maxSize) {
260         // Internally bytes, externally kilobytes
261         this.maxSize = maxSize * 1024;
262     }
263
264     public long getLookupCount() {
265         return lookupCount.get();
266     }
267
268     public long getHitCount() {
269         return hitCount.get();
270     }
271
272     public void setObjectMaxSize(int objectMaxSize) {
273         if (objectMaxSize * 1024L > Integer.MAX_VALUE) {
274             log.warn(sm.getString("cache.objectMaxSizeTooBigBytes", Integer.valueOf(objectMaxSize)));
275             this.objectMaxSize = Integer.MAX_VALUE;
276         }
277         // Internally bytes, externally kilobytes
278         this.objectMaxSize = objectMaxSize * 1024;
279     }
280
281     public int getObjectMaxSize() {
282         // Internally bytes, externally kilobytes
283         return objectMaxSize / 1024;
284     }
285
286     public int getObjectMaxSizeBytes() {
287         return objectMaxSize;
288     }
289
290     void enforceObjectMaxSizeLimit() {
291         long limit = maxSize / OBJECT_MAX_SIZE_FACTOR;
292         if (limit > Integer.MAX_VALUE) {
293             return;
294         }
295         if (objectMaxSize > limit) {
296             log.warn(sm.getString("cache.objectMaxSizeTooBig",
297                     Integer.valueOf(objectMaxSize / 1024), Integer.valueOf((int)limit / 1024)));
298             objectMaxSize = (int) limit;
299         }
300     }
301
302     public void clear() {
303         resourceCache.clear();
304         size.set(0);
305     }
306
307     public long getSize() {
308         return size.get() / 1024;
309     }
310
311     private static class EvictionOrder implements Comparator<CachedResource> {
312
313         @Override
314         public int compare(CachedResource cr1, CachedResource cr2) {
315             long nc1 = cr1.getNextCheck();
316             long nc2 = cr2.getNextCheck();
317
318             // Oldest resource should be first (so iterator goes from oldest to
319             // youngest.
320             if (nc1 == nc2) {
321                 return 0;
322             } else if (nc1 > nc2) {
323                 return -1;
324             } else {
325                 return 1;
326             }
327         }
328     }
329 }
330