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

root/trunk/cherrypy/lib/caching.py

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

Trunk fix for #714 (bug in tools.caching).

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

Hosted by WebFaction

Log in as guest/cpguest to create tickets