| Index: third_party/cherrypy/lib/caching.py
|
| ===================================================================
|
| --- third_party/cherrypy/lib/caching.py (revision 0)
|
| +++ third_party/cherrypy/lib/caching.py (revision 0)
|
| @@ -0,0 +1,465 @@
|
| +"""
|
| +CherryPy implements a simple caching system as a pluggable Tool. This tool tries
|
| +to be an (in-process) HTTP/1.1-compliant cache. It's not quite there yet, but
|
| +it's probably good enough for most sites.
|
| +
|
| +In general, GET responses are cached (along with selecting headers) and, if
|
| +another request arrives for the same resource, the caching Tool will return 304
|
| +Not Modified if possible, or serve the cached response otherwise. It also sets
|
| +request.cached to True if serving a cached representation, and sets
|
| +request.cacheable to False (so it doesn't get cached again).
|
| +
|
| +If POST, PUT, or DELETE requests are made for a cached resource, they invalidate
|
| +(delete) any cached response.
|
| +
|
| +Usage
|
| +=====
|
| +
|
| +Configuration file example::
|
| +
|
| + [/]
|
| + tools.caching.on = True
|
| + tools.caching.delay = 3600
|
| +
|
| +You may use a class other than the default
|
| +:class:`MemoryCache<cherrypy.lib.caching.MemoryCache>` by supplying the config
|
| +entry ``cache_class``; supply the full dotted name of the replacement class
|
| +as the config value. It must implement the basic methods ``get``, ``put``,
|
| +``delete``, and ``clear``.
|
| +
|
| +You may set any attribute, including overriding methods, on the cache
|
| +instance by providing them in config. The above sets the
|
| +:attr:`delay<cherrypy.lib.caching.MemoryCache.delay>` attribute, for example.
|
| +"""
|
| +
|
| +import datetime
|
| +import sys
|
| +import threading
|
| +import time
|
| +
|
| +import cherrypy
|
| +from cherrypy.lib import cptools, httputil
|
| +from cherrypy._cpcompat import copyitems, ntob, set_daemon, sorted
|
| +
|
| +
|
| +class Cache(object):
|
| + """Base class for Cache implementations."""
|
| +
|
| + def get(self):
|
| + """Return the current variant if in the cache, else None."""
|
| + raise NotImplemented
|
| +
|
| + def put(self, obj, size):
|
| + """Store the current variant in the cache."""
|
| + raise NotImplemented
|
| +
|
| + def delete(self):
|
| + """Remove ALL cached variants of the current resource."""
|
| + raise NotImplemented
|
| +
|
| + def clear(self):
|
| + """Reset the cache to its initial, empty state."""
|
| + raise NotImplemented
|
| +
|
| +
|
| +
|
| +# ------------------------------- Memory Cache ------------------------------- #
|
| +
|
| +
|
| +class AntiStampedeCache(dict):
|
| + """A storage system for cached items which reduces stampede collisions."""
|
| +
|
| + def wait(self, key, timeout=5, debug=False):
|
| + """Return the cached value for the given key, or None.
|
| +
|
| + If timeout is not None, and the value is already
|
| + being calculated by another thread, wait until the given timeout has
|
| + elapsed. If the value is available before the timeout expires, it is
|
| + returned. If not, None is returned, and a sentinel placed in the cache
|
| + to signal other threads to wait.
|
| +
|
| + If timeout is None, no waiting is performed nor sentinels used.
|
| + """
|
| + value = self.get(key)
|
| + if isinstance(value, threading._Event):
|
| + if timeout is None:
|
| + # Ignore the other thread and recalc it ourselves.
|
| + if debug:
|
| + cherrypy.log('No timeout', 'TOOLS.CACHING')
|
| + return None
|
| +
|
| + # Wait until it's done or times out.
|
| + if debug:
|
| + cherrypy.log('Waiting up to %s seconds' % timeout, 'TOOLS.CACHING')
|
| + value.wait(timeout)
|
| + if value.result is not None:
|
| + # The other thread finished its calculation. Use it.
|
| + if debug:
|
| + cherrypy.log('Result!', 'TOOLS.CACHING')
|
| + return value.result
|
| + # Timed out. Stick an Event in the slot so other threads wait
|
| + # on this one to finish calculating the value.
|
| + if debug:
|
| + cherrypy.log('Timed out', 'TOOLS.CACHING')
|
| + e = threading.Event()
|
| + e.result = None
|
| + dict.__setitem__(self, key, e)
|
| +
|
| + return None
|
| + elif value is None:
|
| + # Stick an Event in the slot so other threads wait
|
| + # on this one to finish calculating the value.
|
| + if debug:
|
| + cherrypy.log('Timed out', 'TOOLS.CACHING')
|
| + e = threading.Event()
|
| + e.result = None
|
| + dict.__setitem__(self, key, e)
|
| + return value
|
| +
|
| + def __setitem__(self, key, value):
|
| + """Set the cached value for the given key."""
|
| + existing = self.get(key)
|
| + dict.__setitem__(self, key, value)
|
| + if isinstance(existing, threading._Event):
|
| + # Set Event.result so other threads waiting on it have
|
| + # immediate access without needing to poll the cache again.
|
| + existing.result = value
|
| + existing.set()
|
| +
|
| +
|
| +class MemoryCache(Cache):
|
| + """An in-memory cache for varying response content.
|
| +
|
| + Each key in self.store is a URI, and each value is an AntiStampedeCache.
|
| + The response for any given URI may vary based on the values of
|
| + "selecting request headers"; that is, those named in the Vary
|
| + response header. We assume the list of header names to be constant
|
| + for each URI throughout the lifetime of the application, and store
|
| + that list in ``self.store[uri].selecting_headers``.
|
| +
|
| + The items contained in ``self.store[uri]`` have keys which are tuples of
|
| + request header values (in the same order as the names in its
|
| + selecting_headers), and values which are the actual responses.
|
| + """
|
| +
|
| + maxobjects = 1000
|
| + """The maximum number of cached objects; defaults to 1000."""
|
| +
|
| + maxobj_size = 100000
|
| + """The maximum size of each cached object in bytes; defaults to 100 KB."""
|
| +
|
| + maxsize = 10000000
|
| + """The maximum size of the entire cache in bytes; defaults to 10 MB."""
|
| +
|
| + delay = 600
|
| + """Seconds until the cached content expires; defaults to 600 (10 minutes)."""
|
| +
|
| + antistampede_timeout = 5
|
| + """Seconds to wait for other threads to release a cache lock."""
|
| +
|
| + expire_freq = 0.1
|
| + """Seconds to sleep between cache expiration sweeps."""
|
| +
|
| + debug = False
|
| +
|
| + def __init__(self):
|
| + self.clear()
|
| +
|
| + # Run self.expire_cache in a separate daemon thread.
|
| + t = threading.Thread(target=self.expire_cache, name='expire_cache')
|
| + self.expiration_thread = t
|
| + set_daemon(t, True)
|
| + t.start()
|
| +
|
| + def clear(self):
|
| + """Reset the cache to its initial, empty state."""
|
| + self.store = {}
|
| + self.expirations = {}
|
| + self.tot_puts = 0
|
| + self.tot_gets = 0
|
| + self.tot_hist = 0
|
| + self.tot_expires = 0
|
| + self.tot_non_modified = 0
|
| + self.cursize = 0
|
| +
|
| + def expire_cache(self):
|
| + """Continuously examine cached objects, expiring stale ones.
|
| +
|
| + This function is designed to be run in its own daemon thread,
|
| + referenced at ``self.expiration_thread``.
|
| + """
|
| + # It's possible that "time" will be set to None
|
| + # arbitrarily, so we check "while time" to avoid exceptions.
|
| + # See tickets #99 and #180 for more information.
|
| + while time:
|
| + now = time.time()
|
| + # Must make a copy of expirations so it doesn't change size
|
| + # during iteration
|
| + for expiration_time, objects in copyitems(self.expirations):
|
| + if expiration_time <= now:
|
| + for obj_size, uri, sel_header_values in objects:
|
| + try:
|
| + del self.store[uri][tuple(sel_header_values)]
|
| + self.tot_expires += 1
|
| + self.cursize -= obj_size
|
| + except KeyError:
|
| + # the key may have been deleted elsewhere
|
| + pass
|
| + del self.expirations[expiration_time]
|
| + time.sleep(self.expire_freq)
|
| +
|
| + def get(self):
|
| + """Return the current variant if in the cache, else None."""
|
| + request = cherrypy.serving.request
|
| + self.tot_gets += 1
|
| +
|
| + uri = cherrypy.url(qs=request.query_string)
|
| + uricache = self.store.get(uri)
|
| + if uricache is None:
|
| + return None
|
| +
|
| + header_values = [request.headers.get(h, '')
|
| + for h in uricache.selecting_headers]
|
| + variant = uricache.wait(key=tuple(sorted(header_values)),
|
| + timeout=self.antistampede_timeout,
|
| + debug=self.debug)
|
| + if variant is not None:
|
| + self.tot_hist += 1
|
| + return variant
|
| +
|
| + def put(self, variant, size):
|
| + """Store the current variant in the cache."""
|
| + request = cherrypy.serving.request
|
| + response = cherrypy.serving.response
|
| +
|
| + uri = cherrypy.url(qs=request.query_string)
|
| + uricache = self.store.get(uri)
|
| + if uricache is None:
|
| + uricache = AntiStampedeCache()
|
| + uricache.selecting_headers = [
|
| + e.value for e in response.headers.elements('Vary')]
|
| + self.store[uri] = uricache
|
| +
|
| + if len(self.store) < self.maxobjects:
|
| + total_size = self.cursize + size
|
| +
|
| + # checks if there's space for the object
|
| + if (size < self.maxobj_size and total_size < self.maxsize):
|
| + # add to the expirations list
|
| + expiration_time = response.time + self.delay
|
| + bucket = self.expirations.setdefault(expiration_time, [])
|
| + bucket.append((size, uri, uricache.selecting_headers))
|
| +
|
| + # add to the cache
|
| + header_values = [request.headers.get(h, '')
|
| + for h in uricache.selecting_headers]
|
| + uricache[tuple(sorted(header_values))] = variant
|
| + self.tot_puts += 1
|
| + self.cursize = total_size
|
| +
|
| + def delete(self):
|
| + """Remove ALL cached variants of the current resource."""
|
| + uri = cherrypy.url(qs=cherrypy.serving.request.query_string)
|
| + self.store.pop(uri, None)
|
| +
|
| +
|
| +def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
|
| + """Try to obtain cached output. If fresh enough, raise HTTPError(304).
|
| +
|
| + If POST, PUT, or DELETE:
|
| + * invalidates (deletes) any cached response for this resource
|
| + * sets request.cached = False
|
| + * sets request.cacheable = False
|
| +
|
| + else if a cached copy exists:
|
| + * sets request.cached = True
|
| + * sets request.cacheable = False
|
| + * sets response.headers to the cached values
|
| + * checks the cached Last-Modified response header against the
|
| + current If-(Un)Modified-Since request headers; raises 304
|
| + if necessary.
|
| + * sets response.status and response.body to the cached values
|
| + * returns True
|
| +
|
| + otherwise:
|
| + * sets request.cached = False
|
| + * sets request.cacheable = True
|
| + * returns False
|
| + """
|
| + request = cherrypy.serving.request
|
| + response = cherrypy.serving.response
|
| +
|
| + if not hasattr(cherrypy, "_cache"):
|
| + # Make a process-wide Cache object.
|
| + cherrypy._cache = kwargs.pop("cache_class", MemoryCache)()
|
| +
|
| + # Take all remaining kwargs and set them on the Cache object.
|
| + for k, v in kwargs.items():
|
| + setattr(cherrypy._cache, k, v)
|
| + cherrypy._cache.debug = debug
|
| +
|
| + # POST, PUT, DELETE should invalidate (delete) the cached copy.
|
| + # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10.
|
| + if request.method in invalid_methods:
|
| + if debug:
|
| + cherrypy.log('request.method %r in invalid_methods %r' %
|
| + (request.method, invalid_methods), 'TOOLS.CACHING')
|
| + cherrypy._cache.delete()
|
| + request.cached = False
|
| + request.cacheable = False
|
| + return False
|
| +
|
| + if 'no-cache' in [e.value for e in request.headers.elements('Pragma')]:
|
| + request.cached = False
|
| + request.cacheable = True
|
| + return False
|
| +
|
| + cache_data = cherrypy._cache.get()
|
| + request.cached = bool(cache_data)
|
| + request.cacheable = not request.cached
|
| + if request.cached:
|
| + # Serve the cached copy.
|
| + max_age = cherrypy._cache.delay
|
| + for v in [e.value for e in request.headers.elements('Cache-Control')]:
|
| + atoms = v.split('=', 1)
|
| + directive = atoms.pop(0)
|
| + if directive == 'max-age':
|
| + if len(atoms) != 1 or not atoms[0].isdigit():
|
| + raise cherrypy.HTTPError(400, "Invalid Cache-Control header")
|
| + max_age = int(atoms[0])
|
| + break
|
| + elif directive == 'no-cache':
|
| + if debug:
|
| + cherrypy.log('Ignoring cache due to Cache-Control: no-cache',
|
| + 'TOOLS.CACHING')
|
| + request.cached = False
|
| + request.cacheable = True
|
| + return False
|
| +
|
| + if debug:
|
| + cherrypy.log('Reading response from cache', 'TOOLS.CACHING')
|
| + s, h, b, create_time = cache_data
|
| + age = int(response.time - create_time)
|
| + if (age > max_age):
|
| + if debug:
|
| + cherrypy.log('Ignoring cache due to age > %d' % max_age,
|
| + 'TOOLS.CACHING')
|
| + request.cached = False
|
| + request.cacheable = True
|
| + return False
|
| +
|
| + # Copy the response headers. See http://www.cherrypy.org/ticket/721.
|
| + response.headers = rh = httputil.HeaderMap()
|
| + for k in h:
|
| + dict.__setitem__(rh, k, dict.__getitem__(h, k))
|
| +
|
| + # Add the required Age header
|
| + response.headers["Age"] = str(age)
|
| +
|
| + try:
|
| + # Note that validate_since depends on a Last-Modified header;
|
| + # this was put into the cached copy, and should have been
|
| + # resurrected just above (response.headers = cache_data[1]).
|
| + cptools.validate_since()
|
| + except cherrypy.HTTPRedirect:
|
| + x = sys.exc_info()[1]
|
| + if x.status == 304:
|
| + cherrypy._cache.tot_non_modified += 1
|
| + raise
|
| +
|
| + # serve it & get out from the request
|
| + response.status = s
|
| + response.body = b
|
| + else:
|
| + if debug:
|
| + cherrypy.log('request is not cached', 'TOOLS.CACHING')
|
| + return request.cached
|
| +
|
| +
|
| +def tee_output():
|
| + """Tee response output to cache storage. Internal."""
|
| + # Used by CachingTool by attaching to request.hooks
|
| +
|
| + request = cherrypy.serving.request
|
| + if 'no-store' in request.headers.values('Cache-Control'):
|
| + return
|
| +
|
| + def tee(body):
|
| + """Tee response.body into a list."""
|
| + if ('no-cache' in response.headers.values('Pragma') or
|
| + 'no-store' in response.headers.values('Cache-Control')):
|
| + for chunk in body:
|
| + yield chunk
|
| + return
|
| +
|
| + output = []
|
| + for chunk in body:
|
| + output.append(chunk)
|
| + yield chunk
|
| +
|
| + # save the cache data
|
| + body = ntob('').join(output)
|
| + cherrypy._cache.put((response.status, response.headers or {},
|
| + body, response.time), len(body))
|
| +
|
| + response = cherrypy.serving.response
|
| + response.body = tee(response.body)
|
| +
|
| +
|
| +def expires(secs=0, force=False, debug=False):
|
| + """Tool for influencing cache mechanisms using the 'Expires' header.
|
| +
|
| + secs
|
| + Must be either an int or a datetime.timedelta, and indicates the
|
| + number of seconds between response.time and when the response should
|
| + expire. The 'Expires' header will be set to response.time + secs.
|
| + If secs is zero, the 'Expires' header is set one year in the past, and
|
| + the following "cache prevention" headers are also set:
|
| +
|
| + * Pragma: no-cache
|
| + * Cache-Control': no-cache, must-revalidate
|
| +
|
| + force
|
| + If False, the following headers are checked:
|
| +
|
| + * Etag
|
| + * Last-Modified
|
| + * Age
|
| + * Expires
|
| +
|
| + If any are already present, none of the above response headers are set.
|
| +
|
| + """
|
| +
|
| + response = cherrypy.serving.response
|
| + headers = response.headers
|
| +
|
| + cacheable = False
|
| + if not force:
|
| + # some header names that indicate that the response can be cached
|
| + for indicator in ('Etag', 'Last-Modified', 'Age', 'Expires'):
|
| + if indicator in headers:
|
| + cacheable = True
|
| + break
|
| +
|
| + if not cacheable and not force:
|
| + if debug:
|
| + cherrypy.log('request is not cacheable', 'TOOLS.EXPIRES')
|
| + else:
|
| + if debug:
|
| + cherrypy.log('request is cacheable', 'TOOLS.EXPIRES')
|
| + if isinstance(secs, datetime.timedelta):
|
| + secs = (86400 * secs.days) + secs.seconds
|
| +
|
| + if secs == 0:
|
| + if force or ("Pragma" not in headers):
|
| + headers["Pragma"] = "no-cache"
|
| + if cherrypy.serving.request.protocol >= (1, 1):
|
| + if force or "Cache-Control" not in headers:
|
| + headers["Cache-Control"] = "no-cache, must-revalidate"
|
| + # Set an explicit Expires date in the past.
|
| + expiry = httputil.HTTPDate(1169942400.0)
|
| + else:
|
| + expiry = httputil.HTTPDate(response.time + secs)
|
| + if force or "Expires" not in headers:
|
| + headers["Expires"] = expiry
|
|
|
| Property changes on: third_party/cherrypy/lib/caching.py
|
| ___________________________________________________________________
|
| Added: svn:eol-style
|
| + LF
|
|
|
|
|