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