1. cherrypy
  2. CherryPy

Commits

Carlos Ribeiro  committed 2098edb

Cache filter, initial implementation. Check ticket #21 for comments & the CacheFilter documentation on the Wiki

  • Participants
  • Parent commits 60661ef
  • Branches default

Comments (0)

Files changed (4)

File cherrypy/_cphttptools.py

View file
  • Ignore whitespace
     # inits the cpg.responsed.wfile so filters can access it
     cpg.response.wfile = wfile
     cpg.response.sendResponse = 1
-    initRequest(clientAddress, remoteHost, requestLine, headers, rfile, wfile)
-    # reads back wfile; if may be redirected by a filter
-    wfile = cpg.response.wfile
+    try:
+        initRequest(clientAddress, remoteHost, requestLine, headers, rfile, wfile)
+    except basefilter.RequestHandled:
+        # cache hit!
+        return
 
     # Prepare response variables
     now = time.time()
     cpg.response.simpleCookie = Cookie.SimpleCookie()
 
     try:
-        handleRequest(wfile)
+        handleRequest(cpg.response.wfile)
     except:
+        # TODO: in some cases exceptions and filters are conflicting; 
+        # error reporting seems to be broken in some cases. This code is 
+        # a helper to check it
+        print "%"*80
+        traceback.print_exc()
+        print "%"*80
         err = ""
         exc_info_1 = sys.exc_info()[1]
         if hasattr(exc_info_1, 'args') and len(exc_info_1.args) >= 1:
             for line in cpg.response.body:
                 wfile.write(line)
         except:
+            # TODO: in some cases exceptions and filters are conflicting; 
+            # error reporting seems to be broken in some cases. This code is 
+            # a helper to check it
+            #print "%"*80
+            #traceback.print_exc()
+            #print "%"*80
             bodyFile = StringIO.StringIO()
             traceback.print_exc(file = bodyFile)
             body = bodyFile.getvalue()
         if key not in ('Status', 'protocolVersion'):
             if type(valueList) != type([]): valueList = [valueList]
             for value in valueList:
-                wfile.write('%s: %s\r\n'%(key, value))
+                wfile.write('%s: %s\r\n' % (key, value))
 
     # Send response cookies
     cookie = cpg.response.simpleCookie.output()
             if cpg.request.headerMap.has_key('If-Modified-Since'):
                 # Check if if-modified-since date is the same as strModifTime
                 if cpg.request.headerMap['If-Modified-Since'] == strModifTime:
-                    cpg.response.headerMap = {'Status': 304, 'protocolVersion': cpg.configOption.protocolVersion, 'Date': cpg.response.headerMap['Date']}
-                    cpg.response.body = ''
+                    cpg.response.headerMap = {
+                        'Status': 304, 
+                        'protocolVersion': cpg.configOption.protocolVersion, 
+                        'Date': cpg.response.headerMap['Date']}
+                    cpg.response.body = []
                     sendResponse(wfile)
                     return
 

File cherrypy/lib/filter/basefilter.py

View file
  • Ignore whitespace
 """
 
 class InternalRedirect(Exception): pass
+class RequestHandled(Exception): pass
 
 class BaseInputFilter(object):
     """

File cherrypy/lib/filter/cachefilter.py

View file
  • Ignore whitespace
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, 
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice, 
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice, 
+      this list of conditions and the following disclaimer in the documentation 
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors 
+      may be used to endorse or promote products derived from this software 
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+import threading
+import Queue
+import time
+import cStringIO
+
+from basefilter import BaseInputFilter, RequestHandled
+from cherrypy import cpg
+
+def defaultCacheKey():
+    return cpg.request.browserUrl
+    
+class Tee:
+    """
+    Wraps a stream object; chains the content that is written and keep a 
+    copy in a StringIO for caching purposes.
+    """
+    
+    def __init__(self, wfile, maxobjsize):
+        self.wfile = wfile
+        self.cache = cStringIO.StringIO()
+        self.maxobjsize = maxobjsize
+        self.caching = True
+        self.size = 0
+        
+    def write(self, s):
+        self.wfile.write(s)
+        if self.caching:
+            self.size += len(s)
+            if self.size < self.maxobjsize:
+                self.cache.write(s)
+            else:
+                # exceeded the limit, aborts caching
+                self.stopCaching()
+        
+    def flush(self):
+        self.wfile.flush()
+            
+    def close(self):
+        self.wfile.close()
+        if self.caching:
+            self.stopCaching()
+
+    def stopCaching(self):
+        self.caching = False
+        self.cache.close()
+
+class MemoryCache:
+
+    def __init__(self, key, delay, maxobjsize, maxsize, maxobjects):
+        self.key = key
+        self.delay = delay
+        self.maxobjsize = maxobjsize
+        self.maxsize = maxsize
+        self.maxobjects = maxobjects
+        self.cursize = 0
+        self.cache = {}
+        self.expirationQueue = Queue.Queue()
+        self.expirationThread = threading.Thread(target=self.expireCache, name='expireCache')
+        self.expirationThread.setDaemon(True)
+        self.expirationThread.start()
+        self.totPuts = 0
+        self.totGets = 0
+        self.totHits = 0
+        self.totExpires = 0
+
+    def expireCache(self):
+        while True:
+            expirationTime, itemKey = self.expirationQueue.get(block=True, timeout=None)
+            while (time.time() < expirationTime):
+                time.sleep(0.1)
+            try:
+                del self.cache[itemKey]
+                self.totExpires += 1
+            except KeyError:
+                # the key may have been deleted elsewhere
+                pass
+
+    def get(self):
+        """
+        If the content is in the cache, returns a string; returns None if the
+        content is not there.
+        """
+        self.totGets += 1
+        cacheItem = self.cache.get(self.key(), None)
+        if cacheItem:
+            self.totHits += 1
+            expirationTime, obj = cacheItem
+            return obj
+        else:
+            return None
+        
+    def put(self, obj):
+        objsize = len(obj)
+        totalsize = self.cursize + objsize
+        # checks if there's space for the object
+        if ((objsize < self.maxobjsize) and 
+            (totalsize < self.maxsize) and 
+            (len(self.cache) < self.maxobjects)):
+            # add to the expirationQueue & cache
+            try:
+                expirationTime = time.time() + self.delay
+                itemKey = self.key()
+                self.expirationQueue.put((expirationTime, itemKey))
+                self.totPuts += 1
+            except Queue.Full:
+                # can't add because the queue is full
+                return
+            self.cache[itemKey] = (expirationTime, obj)
+
+class CacheInputFilter(BaseInputFilter):
+    """
+    Works on the input chain. If the page is already stored in the cache
+    serves the contents. If the page is not in the cache, it wraps the 
+    cpg.response.wfile object; in this way, everything that is written is 
+    recorded, independent if it was sent directly or not.
+    """
+
+    def __init__(
+            self, 
+            CacheClass=MemoryCache,
+            key=defaultCacheKey, 
+            delay=600,         # 10 minutes
+            maxobjsize=100000, # 100 KB
+            maxsize=10000000,  # 10 MB
+            maxobjects=1000    # 1000 objects
+            ):
+        cpg._cache = CacheClass(key, delay, maxobjsize, maxsize, maxobjects)
+    
+    def afterRequestBody(self):
+        """ Checks if the page is already in the cache """
+        cacheData = cpg._cache.get()
+        if cacheData:
+            # found a hit! serve it & get out from the request
+            cpg.response.wfile.write(cacheData)
+            raise RequestHandled
+        else:
+            # sets a wrapper to cache the contents
+            cpg.response.wfile = Tee(cpg.response.wfile, cpg._cache.maxobjsize)
+            cpg.threadData.cacheable = True
+
+class CacheOutputFilter(object):
+    """
+    Works on the output chain. Stores the content of the page in the cache.
+    """
+    
+    def beforeResponse(self):
+        """
+        Checks if the page is cacheable; if not so disables the cache. 
+        Uses a flag that may be reset by intermediate filters. Note that 
+        the output filter is usually the last filter in the chain, so
+        this method is probably the last one called before the response
+        is written.
+        """
+        if isinstance(cpg.response.wfile, Tee):
+            if cpg.threadData.cacheable:
+                return
+            # cancel caching
+            wrapper = cpg.response.wfile
+            wrapper.stopCaching()
+            cpg.response.wfile = wrapper.wfile
+
+    def afterResponse(self):
+        """
+        Close & fix the cache entry after content was fully written
+        """
+        if isinstance(cpg.response.wfile, Tee):
+            wrapper = cpg.response.wfile
+            if wrapper.caching:
+                if cpg.response.headerMap.get('Pragma', None) != 'no-cache':
+                    # saves the cache data
+                    cpg._cache.put(wrapper.cache.getvalue())
+                # closes the wrapper
+                wrapper.stopCaching()
+                cpg.response.wfile = wrapper.wfile
+
+def percentual(n,d):
+    """calculates the percentual, dealing with div by zeros"""
+    if d == 0:
+        return 0
+    else:
+        return (float(n)/float(d))*100
+
+def formatSize(n):
+    """formats a number as a memory size, in bytes, kbytes, MB, GB)"""
+    if n < 1024:
+        return "%4d bytes" % n
+    elif n < 1024*1024:
+        return "%4d kbytes" % (n / 1024)
+    elif n < 1024*1024*1024:
+        return "%4d MB" % (n / (1024*1024))
+    else:
+        return "%4d GB" % (n / (1024*1024*1024))
+        
+class CacheStats:
+
+    def index(self):
+        cpg.response.headerMap['Content-Type'] = 'text/plain'
+        cpg.response.headerMap['Pragma'] = 'no-cache'
+        cache = cpg._cache
+        yield "Cache statistics\n"
+        yield "Maximum object size: %s\n" % formatSize(cache.maxobjsize)
+        yield "Maximum cache size: %s\n" % formatSize(cache.maxsize)
+        yield "Maximum number of objects: %d\n" % cache.maxobjects
+        yield "Current cache size: %s\n" % formatSize(cache.cursize)
+        yield "Approximated expiration queue size: %d\n" % cache.expirationQueue.qsize()
+        yield "Number of cache entries: %d\n" % len(cache.cache)
+        yield "Total cache writes: %d\n" % cache.totPuts
+        yield "Total cache reads: %d\n" % cache.totGets
+        yield "Total hits: %d (%1.2f%%)\n" % (cache.totHits, percentual(cache.totHits, cache.totGets))
+        yield "Total expires: %d\n" % cache.totExpires
+    index.exposed = True

File cherrypy/lib/filter/encodingfilter.py

View file
  • Ignore whitespace
         self.mimeTypeList = mimeTypeList
 
     def beforeResponse(self):
-        ct = cpg.response.headerMap.get('Content-Type').split(';')[0]
-        if (ct in self.mimeTypeList):
-            # Add "charset=..." to response Content-Type header
-            contentType = cpg.response.headerMap.get("Content-Type")
-            if contentType and 'charset' not in contentType:
-                cpg.response.headerMap["Content-Type"] += ";charset=%s" % self.encoding
-            # Return a generator that encodes the sequence
-            cpg.response.body = self.encode_body(cpg.response.body)
+        contentType = cpg.response.headerMap.get("Content-Type")
+        if contentType:
+            ctlist = contentType.split(';')[0]
+            if (ctlist in self.mimeTypeList):
+                # Add "charset=..." to response Content-Type header
+                if contentType and 'charset' not in contentType:
+                    cpg.response.headerMap["Content-Type"] += ";charset=%s" % self.encoding
+                # Return a generator that encodes the sequence
+                cpg.response.body = self.encode_body(cpg.response.body)
 
     def encode_body(self, body):
         for line in body: