Removed spurious static_path.
[smonitor.git] / monitor / cherrypy / lib / caching.py
blob435b9dc184dfe08a068d58b0f1c690a4113eb98c
1 """
2 CherryPy implements a simple caching system as a pluggable Tool. This tool tries
3 to be an (in-process) HTTP/1.1-compliant cache. It's not quite there yet, but
4 it's probably good enough for most sites.
6 In general, GET responses are cached (along with selecting headers) and, if
7 another request arrives for the same resource, the caching Tool will return 304
8 Not Modified if possible, or serve the cached response otherwise. It also sets
9 request.cached to True if serving a cached representation, and sets
10 request.cacheable to False (so it doesn't get cached again).
12 If POST, PUT, or DELETE requests are made for a cached resource, they invalidate
13 (delete) any cached response.
15 Usage
16 =====
18 Configuration file example::
20 [/]
21 tools.caching.on = True
22 tools.caching.delay = 3600
24 You may use a class other than the default
25 :class:`MemoryCache<cherrypy.lib.caching.MemoryCache>` by supplying the config
26 entry ``cache_class``; supply the full dotted name of the replacement class
27 as the config value. It must implement the basic methods ``get``, ``put``,
28 ``delete``, and ``clear``.
30 You may set any attribute, including overriding methods, on the cache
31 instance by providing them in config. The above sets the
32 :attr:`delay<cherrypy.lib.caching.MemoryCache.delay>` attribute, for example.
33 """
35 import datetime
36 import sys
37 import threading
38 import time
40 import cherrypy
41 from cherrypy.lib import cptools, httputil
42 from cherrypy._cpcompat import copyitems, ntob, set_daemon, sorted
45 class Cache(object):
46 """Base class for Cache implementations."""
48 def get(self):
49 """Return the current variant if in the cache, else None."""
50 raise NotImplemented
52 def put(self, obj, size):
53 """Store the current variant in the cache."""
54 raise NotImplemented
56 def delete(self):
57 """Remove ALL cached variants of the current resource."""
58 raise NotImplemented
60 def clear(self):
61 """Reset the cache to its initial, empty state."""
62 raise NotImplemented
66 # ------------------------------- Memory Cache ------------------------------- #
69 class AntiStampedeCache(dict):
70 """A storage system for cached items which reduces stampede collisions."""
72 def wait(self, key, timeout=5, debug=False):
73 """Return the cached value for the given key, or None.
75 If timeout is not None, and the value is already
76 being calculated by another thread, wait until the given timeout has
77 elapsed. If the value is available before the timeout expires, it is
78 returned. If not, None is returned, and a sentinel placed in the cache
79 to signal other threads to wait.
81 If timeout is None, no waiting is performed nor sentinels used.
82 """
83 value = self.get(key)
84 if isinstance(value, threading._Event):
85 if timeout is None:
86 # Ignore the other thread and recalc it ourselves.
87 if debug:
88 cherrypy.log('No timeout', 'TOOLS.CACHING')
89 return None
91 # Wait until it's done or times out.
92 if debug:
93 cherrypy.log('Waiting up to %s seconds' % timeout, 'TOOLS.CACHING')
94 value.wait(timeout)
95 if value.result is not None:
96 # The other thread finished its calculation. Use it.
97 if debug:
98 cherrypy.log('Result!', 'TOOLS.CACHING')
99 return value.result
100 # Timed out. Stick an Event in the slot so other threads wait
101 # on this one to finish calculating the value.
102 if debug:
103 cherrypy.log('Timed out', 'TOOLS.CACHING')
104 e = threading.Event()
105 e.result = None
106 dict.__setitem__(self, key, e)
108 return None
109 elif value is None:
110 # Stick an Event in the slot so other threads wait
111 # on this one to finish calculating the value.
112 if debug:
113 cherrypy.log('Timed out', 'TOOLS.CACHING')
114 e = threading.Event()
115 e.result = None
116 dict.__setitem__(self, key, e)
117 return value
119 def __setitem__(self, key, value):
120 """Set the cached value for the given key."""
121 existing = self.get(key)
122 dict.__setitem__(self, key, value)
123 if isinstance(existing, threading._Event):
124 # Set Event.result so other threads waiting on it have
125 # immediate access without needing to poll the cache again.
126 existing.result = value
127 existing.set()
130 class MemoryCache(Cache):
131 """An in-memory cache for varying response content.
133 Each key in self.store is a URI, and each value is an AntiStampedeCache.
134 The response for any given URI may vary based on the values of
135 "selecting request headers"; that is, those named in the Vary
136 response header. We assume the list of header names to be constant
137 for each URI throughout the lifetime of the application, and store
138 that list in ``self.store[uri].selecting_headers``.
140 The items contained in ``self.store[uri]`` have keys which are tuples of
141 request header values (in the same order as the names in its
142 selecting_headers), and values which are the actual responses.
145 maxobjects = 1000
146 """The maximum number of cached objects; defaults to 1000."""
148 maxobj_size = 100000
149 """The maximum size of each cached object in bytes; defaults to 100 KB."""
151 maxsize = 10000000
152 """The maximum size of the entire cache in bytes; defaults to 10 MB."""
154 delay = 600
155 """Seconds until the cached content expires; defaults to 600 (10 minutes)."""
157 antistampede_timeout = 5
158 """Seconds to wait for other threads to release a cache lock."""
160 expire_freq = 0.1
161 """Seconds to sleep between cache expiration sweeps."""
163 debug = False
165 def __init__(self):
166 self.clear()
168 # Run self.expire_cache in a separate daemon thread.
169 t = threading.Thread(target=self.expire_cache, name='expire_cache')
170 self.expiration_thread = t
171 set_daemon(t, True)
172 t.start()
174 def clear(self):
175 """Reset the cache to its initial, empty state."""
176 self.store = {}
177 self.expirations = {}
178 self.tot_puts = 0
179 self.tot_gets = 0
180 self.tot_hist = 0
181 self.tot_expires = 0
182 self.tot_non_modified = 0
183 self.cursize = 0
185 def expire_cache(self):
186 """Continuously examine cached objects, expiring stale ones.
188 This function is designed to be run in its own daemon thread,
189 referenced at ``self.expiration_thread``.
191 # It's possible that "time" will be set to None
192 # arbitrarily, so we check "while time" to avoid exceptions.
193 # See tickets #99 and #180 for more information.
194 while time:
195 now = time.time()
196 # Must make a copy of expirations so it doesn't change size
197 # during iteration
198 for expiration_time, objects in copyitems(self.expirations):
199 if expiration_time <= now:
200 for obj_size, uri, sel_header_values in objects:
201 try:
202 del self.store[uri][tuple(sel_header_values)]
203 self.tot_expires += 1
204 self.cursize -= obj_size
205 except KeyError:
206 # the key may have been deleted elsewhere
207 pass
208 del self.expirations[expiration_time]
209 time.sleep(self.expire_freq)
211 def get(self):
212 """Return the current variant if in the cache, else None."""
213 request = cherrypy.serving.request
214 self.tot_gets += 1
216 uri = cherrypy.url(qs=request.query_string)
217 uricache = self.store.get(uri)
218 if uricache is None:
219 return None
221 header_values = [request.headers.get(h, '')
222 for h in uricache.selecting_headers]
223 variant = uricache.wait(key=tuple(sorted(header_values)),
224 timeout=self.antistampede_timeout,
225 debug=self.debug)
226 if variant is not None:
227 self.tot_hist += 1
228 return variant
230 def put(self, variant, size):
231 """Store the current variant in the cache."""
232 request = cherrypy.serving.request
233 response = cherrypy.serving.response
235 uri = cherrypy.url(qs=request.query_string)
236 uricache = self.store.get(uri)
237 if uricache is None:
238 uricache = AntiStampedeCache()
239 uricache.selecting_headers = [
240 e.value for e in response.headers.elements('Vary')]
241 self.store[uri] = uricache
243 if len(self.store) < self.maxobjects:
244 total_size = self.cursize + size
246 # checks if there's space for the object
247 if (size < self.maxobj_size and total_size < self.maxsize):
248 # add to the expirations list
249 expiration_time = response.time + self.delay
250 bucket = self.expirations.setdefault(expiration_time, [])
251 bucket.append((size, uri, uricache.selecting_headers))
253 # add to the cache
254 header_values = [request.headers.get(h, '')
255 for h in uricache.selecting_headers]
256 uricache[tuple(sorted(header_values))] = variant
257 self.tot_puts += 1
258 self.cursize = total_size
260 def delete(self):
261 """Remove ALL cached variants of the current resource."""
262 uri = cherrypy.url(qs=cherrypy.serving.request.query_string)
263 self.store.pop(uri, None)
266 def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
267 """Try to obtain cached output. If fresh enough, raise HTTPError(304).
269 If POST, PUT, or DELETE:
270 * invalidates (deletes) any cached response for this resource
271 * sets request.cached = False
272 * sets request.cacheable = False
274 else if a cached copy exists:
275 * sets request.cached = True
276 * sets request.cacheable = False
277 * sets response.headers to the cached values
278 * checks the cached Last-Modified response header against the
279 current If-(Un)Modified-Since request headers; raises 304
280 if necessary.
281 * sets response.status and response.body to the cached values
282 * returns True
284 otherwise:
285 * sets request.cached = False
286 * sets request.cacheable = True
287 * returns False
289 request = cherrypy.serving.request
290 response = cherrypy.serving.response
292 if not hasattr(cherrypy, "_cache"):
293 # Make a process-wide Cache object.
294 cherrypy._cache = kwargs.pop("cache_class", MemoryCache)()
296 # Take all remaining kwargs and set them on the Cache object.
297 for k, v in kwargs.items():
298 setattr(cherrypy._cache, k, v)
299 cherrypy._cache.debug = debug
301 # POST, PUT, DELETE should invalidate (delete) the cached copy.
302 # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10.
303 if request.method in invalid_methods:
304 if debug:
305 cherrypy.log('request.method %r in invalid_methods %r' %
306 (request.method, invalid_methods), 'TOOLS.CACHING')
307 cherrypy._cache.delete()
308 request.cached = False
309 request.cacheable = False
310 return False
312 if 'no-cache' in [e.value for e in request.headers.elements('Pragma')]:
313 request.cached = False
314 request.cacheable = True
315 return False
317 cache_data = cherrypy._cache.get()
318 request.cached = bool(cache_data)
319 request.cacheable = not request.cached
320 if request.cached:
321 # Serve the cached copy.
322 max_age = cherrypy._cache.delay
323 for v in [e.value for e in request.headers.elements('Cache-Control')]:
324 atoms = v.split('=', 1)
325 directive = atoms.pop(0)
326 if directive == 'max-age':
327 if len(atoms) != 1 or not atoms[0].isdigit():
328 raise cherrypy.HTTPError(400, "Invalid Cache-Control header")
329 max_age = int(atoms[0])
330 break
331 elif directive == 'no-cache':
332 if debug:
333 cherrypy.log('Ignoring cache due to Cache-Control: no-cache',
334 'TOOLS.CACHING')
335 request.cached = False
336 request.cacheable = True
337 return False
339 if debug:
340 cherrypy.log('Reading response from cache', 'TOOLS.CACHING')
341 s, h, b, create_time = cache_data
342 age = int(response.time - create_time)
343 if (age > max_age):
344 if debug:
345 cherrypy.log('Ignoring cache due to age > %d' % max_age,
346 'TOOLS.CACHING')
347 request.cached = False
348 request.cacheable = True
349 return False
351 # Copy the response headers. See http://www.cherrypy.org/ticket/721.
352 response.headers = rh = httputil.HeaderMap()
353 for k in h:
354 dict.__setitem__(rh, k, dict.__getitem__(h, k))
356 # Add the required Age header
357 response.headers["Age"] = str(age)
359 try:
360 # Note that validate_since depends on a Last-Modified header;
361 # this was put into the cached copy, and should have been
362 # resurrected just above (response.headers = cache_data[1]).
363 cptools.validate_since()
364 except cherrypy.HTTPRedirect:
365 x = sys.exc_info()[1]
366 if x.status == 304:
367 cherrypy._cache.tot_non_modified += 1
368 raise
370 # serve it & get out from the request
371 response.status = s
372 response.body = b
373 else:
374 if debug:
375 cherrypy.log('request is not cached', 'TOOLS.CACHING')
376 return request.cached
379 def tee_output():
380 """Tee response output to cache storage. Internal."""
381 # Used by CachingTool by attaching to request.hooks
383 request = cherrypy.serving.request
384 if 'no-store' in request.headers.values('Cache-Control'):
385 return
387 def tee(body):
388 """Tee response.body into a list."""
389 if ('no-cache' in response.headers.values('Pragma') or
390 'no-store' in response.headers.values('Cache-Control')):
391 for chunk in body:
392 yield chunk
393 return
395 output = []
396 for chunk in body:
397 output.append(chunk)
398 yield chunk
400 # save the cache data
401 body = ntob('').join(output)
402 cherrypy._cache.put((response.status, response.headers or {},
403 body, response.time), len(body))
405 response = cherrypy.serving.response
406 response.body = tee(response.body)
409 def expires(secs=0, force=False, debug=False):
410 """Tool for influencing cache mechanisms using the 'Expires' header.
412 secs
413 Must be either an int or a datetime.timedelta, and indicates the
414 number of seconds between response.time and when the response should
415 expire. The 'Expires' header will be set to response.time + secs.
416 If secs is zero, the 'Expires' header is set one year in the past, and
417 the following "cache prevention" headers are also set:
419 * Pragma: no-cache
420 * Cache-Control': no-cache, must-revalidate
422 force
423 If False, the following headers are checked:
425 * Etag
426 * Last-Modified
427 * Age
428 * Expires
430 If any are already present, none of the above response headers are set.
434 response = cherrypy.serving.response
435 headers = response.headers
437 cacheable = False
438 if not force:
439 # some header names that indicate that the response can be cached
440 for indicator in ('Etag', 'Last-Modified', 'Age', 'Expires'):
441 if indicator in headers:
442 cacheable = True
443 break
445 if not cacheable and not force:
446 if debug:
447 cherrypy.log('request is not cacheable', 'TOOLS.EXPIRES')
448 else:
449 if debug:
450 cherrypy.log('request is cacheable', 'TOOLS.EXPIRES')
451 if isinstance(secs, datetime.timedelta):
452 secs = (86400 * secs.days) + secs.seconds
454 if secs == 0:
455 if force or ("Pragma" not in headers):
456 headers["Pragma"] = "no-cache"
457 if cherrypy.serving.request.protocol >= (1, 1):
458 if force or "Cache-Control" not in headers:
459 headers["Cache-Control"] = "no-cache, must-revalidate"
460 # Set an explicit Expires date in the past.
461 expiry = httputil.HTTPDate(1169942400.0)
462 else:
463 expiry = httputil.HTTPDate(response.time + secs)
464 if force or "Expires" not in headers:
465 headers["Expires"] = expiry