| 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 |
|
|---|
| 51 |
|
|---|
| 52 |
|
|---|
| 53 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 80 |
objSize = len(obj[2]) |
|---|
| 81 |
totalSize = self.cursize + objSize |
|---|
| 82 |
|
|---|
| 83 |
|
|---|
| 84 |
if ((objSize < self.maxobjsize) and |
|---|
| 85 |
(totalSize < self.maxsize) and |
|---|
| 86 |
(len(self.cache) < self.maxobjects)): |
|---|
| 87 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 123 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|---|