Download Install Tutorial Docs FAQ Tools WikiLicense Team IRC Planet Involvement Shop Book

root/branches/cherrypy-3.0.x/cherrypy/lib/caching.py

Revision 1705 (checked in by fumanchu, 1 year ago)

Fix for #714 (bug in tools.caching). Also tweaked MemoryCache?.key to be faster when tools.caching.key is set.

  • Property svn:eol-style set to native
Line 
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
Note: See TracBrowser for help on using the browser.

Hosted by WebFaction

Log in as guest/cpguest to create tickets