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

root/branches/cherrypy-2.x/cherrypy/filters/cachefilter.py

Revision 1577 (checked in by dowski, 2 years ago)

cachefilter tweak.

  • Property svn:eol-style set to native
Line 
1 import datetime
2 import Queue
3 import threading
4 import time
5
6 import cherrypy
7 from cherrypy.lib import httptools
8 import basefilter
9
10
11 class MemoryCache:
12    
13     def __init__(self):
14         self.clear()
15         self.expirationQueue = Queue.Queue()
16         t = self.expirationThread = threading.Thread(target=self.expireCache,
17                                                      name='expireCache')
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.totPuts = 0
25         self.totGets = 0
26         self.totHits = 0
27         self.totExpires = 0
28         self.totNonModified = 0
29         self.cursize = 0
30    
31     def _key(self):
32         return cherrypy.config.get("cache_filter.key", cherrypy.request.browser_url)
33     key = property(_key)
34    
35     def _maxobjsize(self):
36         return cherrypy.config.get("cache_filter.maxobjsize", 100000)
37     maxobjsize = property(_maxobjsize)
38    
39     def _maxsize(self):
40         return cherrypy.config.get("cache_filter.maxsize", 10000000)
41     maxsize = property(_maxsize)
42    
43     def _maxobjects(self):
44         return cherrypy.config.get("cache_filter.maxobjects", 1000)
45     maxobjects = property(_maxobjects)
46    
47     def expireCache(self):
48         while True:
49             expirationTime, objSize, objKey = self.expirationQueue.get(block=True, timeout=None)
50             # expireCache runs in a separate thread which the servers are
51             # not aware of. It's possible that "time" will be set to None
52             # arbitrarily, so we check "while time" to avoid exceptions.
53             # See tickets #99 and #180 for more information.
54             while time and (time.time() < expirationTime):
55                 time.sleep(0.1)
56             try:
57                 del self.cache[objKey]
58                 self.totExpires += 1
59                 self.cursize -= objSize
60             except KeyError:
61                 # the key may have been deleted elsewhere
62                 pass
63    
64     def get(self):
65         """
66         If the content is in the cache, returns a tuple containing the
67         expiration time, the lastModified response header and the object
68         (rendered as a string); returns None if the key is not found.
69         """
70         self.totGets += 1
71         cacheItem = self.cache.get(self.key, None)
72         if cacheItem:
73             self.totHits += 1
74             return cacheItem
75         else:
76             return None
77    
78     def put(self, lastModified, obj):
79         # Size check no longer includes header length
80         objSize = len(obj[2])
81         totalSize = self.cursize + objSize
82        
83         # checks if there's space for the object
84         if ((objSize < self.maxobjsize) and
85             (totalSize < self.maxsize) and
86             (len(self.cache) < self.maxobjects)):
87             # add to the expirationQueue & cache
88             try:
89                 expirationTime = cherrypy.response.time + cherrypy.config.get("cache_filter.delay", 600)
90                 objKey = self.key
91                 self.expirationQueue.put((expirationTime, objSize, objKey))
92                 self.cache[objKey] = (expirationTime, lastModified, obj)
93                 self.totPuts += 1
94                 self.cursize += objSize
95             except Queue.Full:
96                 # can't add because the queue is full
97                 return
98    
99     def delete(self):
100         self.cache.pop(self.key)
101
102
103 class CacheFilter(basefilter.BaseFilter):
104     """If the page is already stored in the cache, serves the contents.
105     If the page is not in the cache, caches the output.
106     """
107    
108     def __init__(self):
109         cache_class = cherrypy.config.get("cache_filter.cacheClass", MemoryCache)
110         cherrypy._cache = cache_class()
111    
112     def on_start_resource(self):
113         cherrypy.request.cacheable = False
114    
115     def before_main(self):
116         if not cherrypy.config.get('cache_filter.on', False):
117             return
118        
119         request = cherrypy.request
120         response = cherrypy.response
121        
122         # POST, PUT, DELETE should invalidate (delete) the cached copy.
123         # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10.
124         if request.method in cherrypy.config.get("cache_filter.invalid_methods",
125                                                  ("POST", "PUT", "DELETE")):
126             cherrypy._cache.delete()
127             return
128        
129         cacheData = cherrypy._cache.get()
130         if cacheData:
131             # found a hit! check the if-modified-since request header
132             expirationTime, lastModified, obj = cacheData
133             s, h, b, create_time = obj
134             modifiedSince = request.headers.get('If-Modified-Since', None)
135             if modifiedSince is not None and modifiedSince == lastModified:
136                 cherrypy._cache.totNonModified += 1
137                 response.status = "304 Not Modified"
138                 ct = h.get('Content-Type', None)
139                 if ct:
140                     response.headers['Content-Type'] = ct
141                 response.body = None
142             else:
143                 # serve it & get out from the request
144                 response = cherrypy.response
145                 response.status, response.headers, response.body = s, h, b
146             response.headers['Age'] = str(int(response.time - create_time))
147             request.execute_main = False
148         else:
149             request.cacheable = True
150    
151     def before_finalize(self):
152         if not cherrypy.request.cacheable:
153             return
154        
155         cherrypy.response._cachefilter_tee = []
156         def tee(body):
157             """Tee response.body into response._cachefilter_tee (a list)."""
158             for chunk in body:
159                 cherrypy.response._cachefilter_tee.append(chunk)
160                 yield chunk
161         cherrypy.response.body = tee(cherrypy.response.body)
162    
163     def on_end_request(self):
164         # Close & fix the cache entry after content was fully written
165         if not cherrypy.request.cacheable:
166             return
167        
168         response = cherrypy.response
169         if response.headers.get('Pragma', None) != 'no-cache':
170             lastModified = response.headers.get('Last-Modified', None)
171             # save the cache data
172             body = ''.join([chunk for chunk in response._cachefilter_tee])
173             create_time = time.time()
174             cherrypy._cache.put(lastModified, (response.status,
175                                                response.headers,
176                                                body,
177                                                create_time))
178
179
180 def percentual(n,d):
181     """calculates the percentual, dealing with div by zeros"""
182     if d == 0:
183         return 0
184     else:
185         return (float(n)/float(d))*100
186
187 def formatSize(n):
188     """formats a number as a memory size, in bytes, kbytes, MB, GB)"""
189     if n < 1024:
190         return "%4d bytes" % n
191     elif n < 1024*1024:
192         return "%4d kbytes" % (n / 1024)
193     elif n < 1024*1024*1024:
194         return "%4d MB" % (n / (1024*1024))
195     else:
196         return "%4d GB" % (n / (1024*1024*1024))
197
198
199 class CacheStats:
200    
201     def index(self):
202         cherrypy.response.headers['Content-Type'] = 'text/plain'
203         cherrypy.response.headers['Pragma'] = 'no-cache'
204         cache = cherrypy._cache
205         yield "Cache statistics\n"
206         yield "Maximum object size: %s\n" % formatSize(cache.maxobjsize)
207         yield "Maximum cache size: %s\n" % formatSize(cache.maxsize)
208         yield "Maximum number of objects: %d\n" % cache.maxobjects
209         yield "Current cache size: %s\n" % formatSize(cache.cursize)
210         yield "Approximated expiration queue size: %d\n" % cache.expirationQueue.qsize()
211         yield "Number of cache entries: %d\n" % len(cache.cache)
212         yield "Total cache writes: %d\n" % cache.totPuts
213         yield "Total cache read attempts: %d\n" % cache.totGets
214         yield "Total hits: %d (%1.2f%%)\n" % (cache.totHits, percentual(cache.totHits, cache.totGets))
215         yield "Total misses: %d (%1.2f%%)\n" % (cache.totGets-cache.totHits, percentual(cache.totGets-cache.totHits, cache.totGets))
216         yield "Total expires: %d\n" % cache.totExpires
217         yield "Total non-modified content: %d\n" % cache.totNonModified
218     index.exposed = True
219
220
221 def expires(secs=0, force=False):
222     """Tool for influencing cache mechanisms using the 'Expires' header.
223     
224     'secs' must be either an int or a datetime.timedelta, and indicates the
225     number of seconds between response.time and when the response should
226     expire. The 'Expires' header will be set to (response.time + secs).
227     
228     If 'secs' is zero, the following "cache prevention" headers are also set:
229        'Pragma': 'no-cache'
230        'Cache-Control': 'no-cache'
231     
232     If 'force' is False (the default), the following headers are checked:
233     'Etag', 'Last-Modified', 'Age', 'Expires'. If any are already present,
234     none of the above response headers are set.
235     """
236    
237     response = cherrypy.response
238    
239     cacheable = False
240     if not force:
241         # some header names that indicate that the response can be cached
242         for indicator in ('Etag', 'Last-Modified', 'Age', 'Expires'):
243             if indicator in response.headers:
244                 cacheable = True
245                 break
246    
247     if not cacheable:
248         if isinstance(secs, datetime.timedelta):
249             secs = (86400 * secs.days) + secs.seconds
250        
251         if secs == 0:
252             if force or ("Pragma" not in response.headers):
253                 response.headers["Pragma"] = "no-cache"
254             if cherrypy.response.version >= "1.1":
255                 if force or ("Cache-Control" not in response.headers):
256                     response.headers["Cache-Control"] = "no-cache"
257        
258         expiry = httptools.HTTPDate(time.gmtime(response.time + secs))
259         if force or ("Expires" not in response.headers):
260             response.headers["Expires"] = expiry
Note: See TracBrowser for help on using the browser.

Hosted by WebFaction

Log in as guest/cpguest to create tickets