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.
18 Configuration file example::
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.
41 from cherrypy
.lib
import cptools
, httputil
42 from cherrypy
._cpcompat
import copyitems
, ntob
, set_daemon
, sorted
46 """Base class for Cache implementations."""
49 """Return the current variant if in the cache, else None."""
52 def put(self
, obj
, size
):
53 """Store the current variant in the cache."""
57 """Remove ALL cached variants of the current resource."""
61 """Reset the cache to its initial, empty state."""
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.
84 if isinstance(value
, threading
._Event
):
86 # Ignore the other thread and recalc it ourselves.
88 cherrypy
.log('No timeout', 'TOOLS.CACHING')
91 # Wait until it's done or times out.
93 cherrypy
.log('Waiting up to %s seconds' % timeout
, 'TOOLS.CACHING')
95 if value
.result
is not None:
96 # The other thread finished its calculation. Use it.
98 cherrypy
.log('Result!', 'TOOLS.CACHING')
100 # Timed out. Stick an Event in the slot so other threads wait
101 # on this one to finish calculating the value.
103 cherrypy
.log('Timed out', 'TOOLS.CACHING')
104 e
= threading
.Event()
106 dict.__setitem
__(self
, key
, e
)
110 # Stick an Event in the slot so other threads wait
111 # on this one to finish calculating the value.
113 cherrypy
.log('Timed out', 'TOOLS.CACHING')
114 e
= threading
.Event()
116 dict.__setitem
__(self
, key
, e
)
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
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.
146 """The maximum number of cached objects; defaults to 1000."""
149 """The maximum size of each cached object in bytes; defaults to 100 KB."""
152 """The maximum size of the entire cache in bytes; defaults to 10 MB."""
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."""
161 """Seconds to sleep between cache expiration sweeps."""
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
175 """Reset the cache to its initial, empty state."""
177 self
.expirations
= {}
182 self
.tot_non_modified
= 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.
196 # Must make a copy of expirations so it doesn't change size
198 for expiration_time
, objects
in copyitems(self
.expirations
):
199 if expiration_time
<= now
:
200 for obj_size
, uri
, sel_header_values
in objects
:
202 del self
.store
[uri
][tuple(sel_header_values
)]
203 self
.tot_expires
+= 1
204 self
.cursize
-= obj_size
206 # the key may have been deleted elsewhere
208 del self
.expirations
[expiration_time
]
209 time
.sleep(self
.expire_freq
)
212 """Return the current variant if in the cache, else None."""
213 request
= cherrypy
.serving
.request
216 uri
= cherrypy
.url(qs
=request
.query_string
)
217 uricache
= self
.store
.get(uri
)
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
,
226 if variant
is not None:
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
)
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
))
254 header_values
= [request
.headers
.get(h
, '')
255 for h
in uricache
.selecting_headers
]
256 uricache
[tuple(sorted(header_values
))] = variant
258 self
.cursize
= total_size
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
281 * sets response.status and response.body to the cached values
285 * sets request.cached = False
286 * sets request.cacheable = True
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
:
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
312 if 'no-cache' in [e
.value
for e
in request
.headers
.elements('Pragma')]:
313 request
.cached
= False
314 request
.cacheable
= True
317 cache_data
= cherrypy
._cache
.get()
318 request
.cached
= bool(cache_data
)
319 request
.cacheable
= not 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])
331 elif directive
== 'no-cache':
333 cherrypy
.log('Ignoring cache due to Cache-Control: no-cache',
335 request
.cached
= False
336 request
.cacheable
= True
340 cherrypy
.log('Reading response from cache', 'TOOLS.CACHING')
341 s
, h
, b
, create_time
= cache_data
342 age
= int(response
.time
- create_time
)
345 cherrypy
.log('Ignoring cache due to age > %d' % max_age
,
347 request
.cached
= False
348 request
.cacheable
= True
351 # Copy the response headers. See http://www.cherrypy.org/ticket/721.
352 response
.headers
= rh
= httputil
.HeaderMap()
354 dict.__setitem
__(rh
, k
, dict.__getitem
__(h
, k
))
356 # Add the required Age header
357 response
.headers
["Age"] = str(age
)
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]
367 cherrypy
._cache
.tot_non_modified
+= 1
370 # serve it & get out from the request
375 cherrypy
.log('request is not cached', 'TOOLS.CACHING')
376 return request
.cached
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'):
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')):
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.
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:
420 * Cache-Control': no-cache, must-revalidate
423 If False, the following headers are checked:
430 If any are already present, none of the above response headers are set.
434 response
= cherrypy
.serving
.response
435 headers
= response
.headers
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
:
445 if not cacheable
and not force
:
447 cherrypy
.log('request is not cacheable', 'TOOLS.EXPIRES')
450 cherrypy
.log('request is cacheable', 'TOOLS.EXPIRES')
451 if isinstance(secs
, datetime
.timedelta
):
452 secs
= (86400 * secs
.days
) + secs
.seconds
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)
463 expiry
= httputil
.HTTPDate(response
.time
+ secs
)
464 if force
or "Expires" not in headers
:
465 headers
["Expires"] = expiry