Package cherrypy :: Package lib :: Module caching
[hide private]
[frames] | no frames]

Source Code for Module cherrypy.lib.caching

  1  import datetime 
  2  import threading 
  3  import time 
  4   
  5  import cherrypy 
  6  from cherrypy.lib import cptools, http 
  7   
  8   
  9  _missing = object() 
 10   
 11   
12 -class MemoryCache:
13
14 - def __init__(self):
15 self.clear() 16 t = threading.Thread(target=self.expire_cache, name='expire_cache') 17 self.expiration_thread = t 18 t.setDaemon(True) 19 t.start()
20
21 - def clear(self):
22 """Reset the cache to its initial, empty state.""" 23 self.cache = {} 24 self.expirations = {} 25 self.tot_puts = 0 26 self.tot_gets = 0 27 self.tot_hist = 0 28 self.tot_expires = 0 29 self.tot_non_modified = 0 30 self.cursize = 0
31
32 - def _key(self):
33 request = cherrypy.request 34 key = request.config.get("tools.caching.key", _missing) 35 if key is _missing: 36 key = cherrypy.url(qs=request.query_string) 37 return key
38 key = property(_key) 39
40 - def expire_cache(self):
41 # expire_cache runs in a separate thread which the servers are 42 # not aware of. It's possible that "time" will be set to None 43 # arbitrarily, so we check "while time" to avoid exceptions. 44 # See tickets #99 and #180 for more information. 45 while time: 46 now = time.time() 47 for expiration_time, objects in self.expirations.items(): 48 if expiration_time <= now: 49 for obj_size, obj_key in objects: 50 try: 51 del self.cache[obj_key] 52 self.tot_expires += 1 53 self.cursize -= obj_size 54 except KeyError: 55 # the key may have been deleted elsewhere 56 pass 57 del self.expirations[expiration_time] 58 time.sleep(0.1)
59
60 - def get(self):
61 """Return the object if in the cache, else None.""" 62 self.tot_gets += 1 63 cache_item = self.cache.get(self.key, None) 64 if cache_item: 65 self.tot_hist += 1 66 return cache_item 67 else: 68 return None
69
70 - def put(self, obj):
71 conf = cherrypy.request.config.get 72 73 if len(self.cache) < conf("tools.caching.maxobjects", 1000): 74 # Size check no longer includes header length 75 obj_size = len(obj[2]) 76 maxobj_size = conf("tools.caching.maxobj_size", 100000) 77 78 total_size = self.cursize + obj_size 79 maxsize = conf("tools.caching.maxsize", 10000000) 80 81 # checks if there's space for the object 82 if (obj_size < maxobj_size and total_size < maxsize): 83 # add to the expirations list and cache 84 expiration_time = cherrypy.response.time + conf("tools.caching.delay", 600) 85 obj_key = self.key 86 bucket = self.expirations.setdefault(expiration_time, []) 87 bucket.append((obj_size, obj_key)) 88 self.cache[obj_key] = obj 89 self.tot_puts += 1 90 self.cursize = total_size
91
92 - def delete(self):
93 self.cache.pop(self.key, None)
94 95
96 -def get(invalid_methods=("POST", "PUT", "DELETE"), cache_class=MemoryCache, **kwargs):
97 """Try to obtain cached output. If fresh enough, raise HTTPError(304). 98 99 If POST, PUT, or DELETE: 100 * invalidates (deletes) any cached response for this resource 101 * sets request.cached = False 102 * sets request.cacheable = False 103 104 else if a cached copy exists: 105 * sets request.cached = True 106 * sets request.cacheable = False 107 * sets response.headers to the cached values 108 * checks the cached Last-Modified response header against the 109 current If-(Un)Modified-Since request headers; raises 304 110 if necessary. 111 * sets response.status and response.body to the cached values 112 * returns True 113 114 otherwise: 115 * sets request.cached = False 116 * sets request.cacheable = True 117 * returns False 118 """ 119 if not hasattr(cherrypy, "_cache"): 120 cherrypy._cache = cache_class() 121 122 request = cherrypy.request 123 124 # POST, PUT, DELETE should invalidate (delete) the cached copy. 125 # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10. 126 if request.method in invalid_methods: 127 cherrypy._cache.delete() 128 request.cached = False 129 request.cacheable = False 130 return False 131 132 cache_data = cherrypy._cache.get() 133 request.cached = c = bool(cache_data) 134 request.cacheable = not c 135 if c: 136 response = cherrypy.response 137 s, h, b, create_time, original_req_headers = cache_data 138 139 # Check 'Vary' selecting headers. If any headers mentioned in "Vary" 140 # differ between the cached and current request, bail out and 141 # let the rest of CP handle the request. This should properly 142 # mimic the behavior of isolated caches as RFC 2616 assumes: 143 # "If the selecting request header fields for the cached entry 144 # do not match the selecting request header fields of the new 145 # request, then the cache MUST NOT use a cached entry to satisfy 146 # the request unless it first relays the new request to the origin 147 # server in a conditional request and the server responds with 148 # 304 (Not Modified), including an entity tag or Content-Location 149 # that indicates the entity to be used. 150 # TODO: can we store multiple variants based on Vary'd headers? 151 for header_element in h.elements('Vary'): 152 key = header_element.value 153 if original_req_headers[key] != request.headers.get(key, 'missing'): 154 request.cached = False 155 request.cacheable = True 156 return False 157 158 # Add the required Age header 159 response.headers = h 160 response.headers["Age"] = str(int(response.time - create_time)) 161 162 try: 163 # Note that validate_since depends on a Last-Modified header; 164 # this was put into the cached copy, and should have been 165 # resurrected just above (response.headers = cache_data[1]). 166 cptools.validate_since() 167 except cherrypy.HTTPError, x: 168 if x.status == 304: 169 cherrypy._cache.tot_non_modified += 1 170 raise 171 172 # serve it & get out from the request 173 response.status = s 174 response.body = b 175 return c
176 177
178 -def tee_output():
179 response = cherrypy.response 180 output = [] 181 def tee(body): 182 """Tee response.body into a list.""" 183 for chunk in body: 184 output.append(chunk) 185 yield chunk 186 187 # Might as well do this here; why cache if the body isn't consumed? 188 if response.headers.get('Pragma', None) != 'no-cache': 189 # save the cache data 190 body = ''.join([chunk for chunk in output]) 191 vary = [he.value for he in 192 cherrypy.response.headers.elements('Vary')] 193 if vary: 194 sel_headers = dict([(k, v) for k, v 195 in cherrypy.request.headers.iteritems() 196 if k in vary]) 197 else: 198 sel_headers = {} 199 cherrypy._cache.put((response.status, response.headers or {}, 200 body, response.time, sel_headers))
201 response.body = tee(response.body) 202 203
204 -def expires(secs=0, force=False):
205 """Tool for influencing cache mechanisms using the 'Expires' header. 206 207 'secs' must be either an int or a datetime.timedelta, and indicates the 208 number of seconds between response.time and when the response should 209 expire. The 'Expires' header will be set to (response.time + secs). 210 211 If 'secs' is zero, the following "cache prevention" headers are also set: 212 'Pragma': 'no-cache' 213 'Cache-Control': 'no-cache, must-revalidate' 214 215 If 'force' is False (the default), the following headers are checked: 216 'Etag', 'Last-Modified', 'Age', 'Expires'. If any are already present, 217 none of the above response headers are set. 218 """ 219 220 response = cherrypy.response 221 headers = response.headers 222 223 cacheable = False 224 if not force: 225 # some header names that indicate that the response can be cached 226 for indicator in ('Etag', 'Last-Modified', 'Age', 'Expires'): 227 if indicator in headers: 228 cacheable = True 229 break 230 231 if not cacheable: 232 if isinstance(secs, datetime.timedelta): 233 secs = (86400 * secs.days) + secs.seconds 234 235 if secs == 0: 236 if force or "Pragma" not in headers: 237 headers["Pragma"] = "no-cache" 238 if cherrypy.request.protocol >= (1, 1): 239 if force or "Cache-Control" not in headers: 240 headers["Cache-Control"] = "no-cache, must-revalidate" 241 242 expiry = http.HTTPDate(response.time + secs) 243 if force or "Expires" not in headers: 244 headers["Expires"] = expiry
245