Ticket #5126: caching_agent.patch

File caching_agent.patch, 13.4 KB (added by chris-, 6 years ago)
  • web/test/test_webclient.py

     
    18341834
    18351835        return deferred.addCallback(checkFailure)
    18361836
     1837class CachingAgentTests(unittest.TestCase,
     1838                                       FakeReactorAndConnectMixin):
     1839
     1840    def setUp(self):
     1841        """
     1842        Create an L{Agent} wrapped around a fake reactor with a memory cache as a backend.
     1843        """
     1844        self.reactor = self.Reactor()
     1845        agent = client.Agent(self.reactor)
     1846        agent._connect = self._dummyConnect
     1847        self.cache = client.MemoryCache()
     1848        self.agent = client.CachingAgent(
     1849                     agent,cache=self.cache)
     1850
     1851    def test_requestHeaders(self):
     1852
     1853        e = {"etag":"qwertz",
     1854             "last-modified":"Sun, 06 Nov 1994 08:49:37 GMT",
     1855             "content": "0123456789"}
     1856        self.cache.put("http://example.com/foo",e)
     1857
     1858        self.agent.request('GET','http://example.com/foo')
     1859
     1860        protocol = self.protocol
     1861
     1862        self.assertEquals(len(protocol.requests),1)
     1863        req,res = protocol.requests.pop()
     1864
     1865        self.assertEquals(req.headers.getRawHeaders("if-none-match"),
     1866                          [e["etag"]])
     1867        self.assertEquals(req.headers.getRawHeaders("if-modified-since"),
     1868                          [e["last-modified"]])
     1869
     1870    def test_freshContent(self):
     1871
     1872        d = self.agent.request('GET','http://example.com/foo')
     1873
     1874        req,res = self.protocol.requests.pop()
     1875
     1876        headers = http_headers.Headers({'etag': ['qwertz'],
     1877                                        "last-modified": ["Sun, 06 Nov 1994 08:49:37 GMT"]})
     1878
     1879        data = "0123456789"
     1880        transport = StringTransport()
     1881        response = Response(('HTTP',1,1),200,'OK',headers,transport)
     1882        response.length = 10
     1883        res.callback(response)
     1884
     1885        def checkResponse(result):
     1886            self.assertNotIdentical(result,response)
     1887            self.assertEquals(result.version,('HTTP',1,1))
     1888            self.assertEquals(result.code,200)
     1889            self.assertEquals(result.phrase,'OK')
     1890            self.assertEquals(result.headers.getRawHeaders("etag"),["qwertz"])
     1891            self.assertEquals(result.headers.getRawHeaders("last-modified"),
     1892                                        ["Sun, 06 Nov 1994 08:49:37 GMT"])
     1893
     1894
     1895            response._bodyDataReceived(data)
     1896            response._bodyDataFinished()
     1897
     1898            protocol = SimpleAgentProtocol()
     1899            result.deliverBody(protocol)
     1900
     1901            self.assertEquals(protocol.received,[data])
     1902
     1903            c = self.cache.get('http://example.com/foo')
     1904            self.assertEquals(c["content"],data)
     1905            self.assertEquals(c["etag"],"qwertz")
     1906            self.assertEquals(c["last-modified"],"Sun, 06 Nov 1994 08:49:37 GMT")
     1907
     1908
     1909            return defer.gatherResults([protocol.made,protocol.finished])
     1910
     1911        d.addCallback(checkResponse)
     1912
     1913        return d
     1914
     1915
     1916    def test_cachedContent(self):
     1917
     1918        data = "0123456789"
     1919
     1920        e = {"etag":"qwertz",
     1921             "last-modified":"Sun, 06 Nov 1994 08:49:37 GMT",
     1922             "content": data}
     1923
     1924        self.cache.put("http://example.com/foo",e)
     1925
     1926        d = self.agent.request('GET','http://example.com/foo')
     1927
     1928        req,res = self.protocol.requests.pop()
     1929
     1930        headers = http_headers.Headers({'etag': ['qwertz'],
     1931                                        "last-modified": ["Sun, 06 Nov 1994 08:49:37 GMT"]})
     1932
     1933        transport = StringTransport()
     1934        response = Response(('HTTP',1,1),304,'OK',headers,transport)
     1935        response.length = 10
     1936        res.callback(response)
     1937
     1938        def checkResponse(result):
     1939
     1940            self.assertNotIdentical(result,response)
     1941            self.assertEquals(result.version,('HTTP',1,1))
     1942            self.assertEquals(result.code,200)
     1943            self.assertEquals(result.phrase,'OK')
     1944            self.assertEquals(result.headers.getRawHeaders("etag"),["qwertz"])
     1945            self.assertEquals(result.headers.getRawHeaders("last-modified"),
     1946                                        ["Sun, 06 Nov 1994 08:49:37 GMT"])
     1947
     1948            response._bodyDataReceived("")
     1949            response._bodyDataFinished()
     1950
     1951            protocol = SimpleAgentProtocol()
     1952            result.deliverBody(protocol)
     1953
     1954            self.assertEquals(protocol.received,[data])
     1955
     1956            c = self.cache.get('http://example.com/foo')
     1957            self.assertEquals(c["content"],data)
     1958            self.assertEquals(c["etag"],"qwertz")
     1959            self.assertEquals(c["last-modified"],"Sun, 06 Nov 1994 08:49:37 GMT")
     1960
     1961            return defer.gatherResults([protocol.made,protocol.finished])
     1962
     1963        d.addCallback(checkResponse)
    18371964
     1965        return d
    18381966
    18391967if ssl is None or not hasattr(ssl, 'DefaultOpenSSLContextFactory'):
    18401968    for case in [WebClientSSLTestCase, WebClientRedirectBetweenSSLandPlainText]:
  • web/client.py

     
    1010from urlparse import urlunparse
    1111import zlib
    1212
     13from zope.interface import implements
     14
    1315from twisted.python import log
    1416from twisted.web import http
    1517from twisted.internet import defer, protocol, reactor
     
    1820from twisted.python.util import InsensitiveDict
    1921from twisted.python.components import proxyForInterface
    2022from twisted.web import error
    21 from twisted.web.iweb import UNKNOWN_LENGTH, IResponse
     23from twisted.web.iweb import UNKNOWN_LENGTH, IResponse, IHTTPCache
    2224from twisted.web.http_headers import Headers
    2325from twisted.python.compat import set
    2426
     
    10031005
    10041006
    10051007
     1008class CacheBodyProducer(proxyForInterface(IResponse)):
     1009    """
     1010    A wrapper for a L{Response} instance which handles cached response bodies.
     1011    This type of response will be used if a cache hit occurs.
     1012   
     1013    @ivar original: The original L{Response} object.
     1014
     1015    @since: 11.1
     1016    """
     1017
     1018    def __init__(self,response,cache):
     1019        self.original = response
     1020        self.cache = cache
     1021        self.length = len(cache["content"])
     1022
     1023
     1024    def deliverBody(self,protocol):
     1025        """
     1026        Override C{deliverBody} to deliver the cached content
     1027        to the given protocol
     1028        """
     1029        try:
     1030            protocol.connectionMade()
     1031            protocol.dataReceived(self.cache["content"])
     1032            protocol.connectionLost(failure.Failure(ResponseDone("Body delivered from cache.")))
     1033        except:
     1034            protocol.connectionLost(failure.Failure())
     1035
     1036
     1037
     1038class CacheBodyUpdater(proxyForInterface(IResponse)):
     1039    """
     1040    A wrapper for a L{Response} instance which transparently generates a cache entry
     1041    for new content.
     1042    This type of response will be used if a cache muss occurs.
     1043   
     1044    @ivar original: The original L{Response} object.
     1045
     1046    @since: 11.1
     1047    """
     1048    def __init__(self,response,cache,cacheKey):
     1049        self.original = response
     1050        self.cache = cache
     1051        self.cacheKey = cacheKey
     1052
     1053
     1054    def deliverBody(self,protocol):
     1055        self.original.deliverBody(_CachingProtocol(protocol,self.cache,self.cacheKey))
     1056
     1057
     1058
     1059class _CachingProtocol(proxyForInterface(IProtocol)):
     1060    """
     1061    A L{Protocol} implementation which wraps another one, transparently
     1062    cacheing the content as data is received.
     1063
     1064    @ivar cache: The cache object used for storing cached responses.
     1065   
     1066    @ivar cacheKey: The key to identify the current response by.
     1067   
     1068    @since: 11.1
     1069    """
     1070
     1071    def __init__(self,protocol,cache,cacheKey):
     1072        self.original = protocol
     1073        self.cache = cache
     1074        self.cacheKey = cacheKey
     1075        self.buffer = ""
     1076
     1077
     1078    def dataReceived(self,data):
     1079        """
     1080        Buffer all incoming C{data} before writing it to the receiving protocol
     1081        """
     1082        self.buffer += data
     1083        self.original.dataReceived(data)
     1084
     1085
     1086    def connectionLost(self,reason):
     1087        """
     1088        Forward the connection lost event, placing the buffered content into
     1089        the cache beforehand.
     1090        """
     1091        entry = self.cache.get(self.cacheKey)
     1092        if entry is None:
     1093            entry = {}
     1094        entry["content"] = self.buffer
     1095        self.cache.put(self.cacheKey,entry)
     1096        self.original.connectionLost(reason)
     1097
     1098
     1099
     1100class MemoryCache(object):
     1101    """
     1102    An L{IHTTPCache} storing all data in system memory.
     1103    A cache entry for this data store musst be a C{dict} object the contains all
     1104    nessecary http header fileds as keys plus an extra 'content' key to map the
     1105    request body.
     1106   
     1107    @ivar _storage: The C{dict} storing the cache entries.
     1108    """
     1109    implements(IHTTPCache)
     1110
     1111    def __init__(self):
     1112        self._storage = {}
     1113
     1114
     1115    def get(self,key,default=None):
     1116        """
     1117        Returns a cache entry from the cache if one exists for a specific C{key}.
     1118        If none exists, C{default} is returned.
     1119        """
     1120        return self._storage.get(key,default)
     1121
     1122
     1123    def put(self,key,entry):
     1124        """
     1125        Place a cache C{entry} into the store referenced by a unique C{key}.
     1126        If an entry already exists for a key, this entry will be overwritten.
     1127        """
     1128        self._storage[key] = entry
     1129
     1130
     1131    def delete(self,key):
     1132        """
     1133        Delete all entries from the cache that are referenced by C{key}.
     1134        """
     1135        if key in self._storage:
     1136            del self._storage[key]
     1137
     1138
     1139
     1140class CachingAgent(object):
     1141    """
     1142    An L{Agent} wrapper to handle cachable content.
     1143
     1144    I manages a cache system by looking at certain http headers and determains
     1145    if it sould satisfy a request with localy cached content or if a fresh copy
     1146    should be used.
     1147    Currently, the following caching-related headers are supported:
     1148    etag, last-modified, if-match, if-not-match
     1149     
     1150    @param cache: An instance of a cache to store data and to satisfy responses from.
     1151   
     1152    @since: 11.1
     1153    """
     1154
     1155    def __init__(self,agent,cache=MemoryCache()):
     1156        self._agent = agent
     1157        self._cache = cache
     1158
     1159
     1160    def request(self,method,uri,headers=None,bodyProducer=None):
     1161        """
     1162        Send a client request which will be checked against the cache.
     1163
     1164        @see: L{Agent.request}.
     1165        """
     1166        if headers is None:
     1167            headers = Headers()
     1168        else:
     1169            headers = headers.copy()
     1170
     1171        cacheKey = uri
     1172        entry = self._cache.get(cacheKey)
     1173        if entry is not None:
     1174            if method in ("GET","HEAD"):
     1175                if entry.has_key("etag"):
     1176                    headers.addRawHeader("if-none-match",entry["etag"])
     1177                if entry.has_key("last-modified"):
     1178                    headers.addRawHeader("if-modified-since",entry["last-modified"])
     1179            if method in ("PUT",):
     1180                if entry.has_key("etag"):
     1181                    headers.addRawHeader("if-match",entry["etag"])
     1182        deferred = self._agent.request(method,uri,headers,bodyProducer)
     1183        return deferred.addCallback(self._handleResponse,method=method,cacheKey=cacheKey)
     1184
     1185
     1186    def _handleResponse(self,response,method,cacheKey):
     1187        """
     1188        Check if the server response with a cache hit and read or write to the cache if nessecary.
     1189        """
     1190        cache = self._cache.get(cacheKey,{})
     1191        if cache:
     1192            self._cache.delete(cacheKey)
     1193        if response.headers.hasHeader("etag"):
     1194            cache["etag"] = response.headers.getRawHeaders("etag")[0]
     1195        if response.headers.hasHeader("last-modified"):
     1196            cache["last-modified"] = response.headers.getRawHeaders("last-modified")[0]
     1197        self._cache.put(cacheKey,cache)
     1198
     1199        if response.code == 304 and method == "GET":
     1200            response.code = 200
     1201            response = CacheBodyProducer(response,cache)
     1202        elif cache:
     1203            response = CacheBodyUpdater(response,cache=self._cache,cacheKey=cacheKey)
     1204        return response
     1205
    10061206__all__ = [
    10071207    'PartialDownloadError', 'HTTPPageGetter', 'HTTPPageDownloader',
    10081208    'HTTPClientFactory', 'HTTPDownloader', 'getPage', 'downloadPage',
    10091209    'ResponseDone', 'Response', 'ResponseFailed', 'Agent', 'CookieAgent',
    1010     'ContentDecoderAgent', 'GzipDecoder']
     1210    'ContentDecoderAgent', 'GzipDecoder', "CachingAgent", "MemoryCache"]
  • web/iweb.py

     
    519519
    520520UNKNOWN_LENGTH = u"twisted.web.iweb.UNKNOWN_LENGTH"
    521521
     522class IHTTPCache(Interface):
     523    """
     524    An object representing a cache to store and satisfy http content requests.
     525    To accomplish that, it stores cache entries which are in themselves C{dict}
     526    objects containing the keys and values as produced by the L{Response} plus
     527    a special 'content' key holding the message body, if any.
     528    """
     529   
     530    def put(self, key, entry):
     531        """
     532        Place a cache entry into the cache.
     533        """
     534
     535
     536    def get(self, key, default=None):
     537        """
     538        Reteive an entry from the cache referenced by L{key}.
     539        If no such entry exists, return L{default}.
     540        """
     541
     542    def delete(self, key):
     543        """
     544        Delete an entry L{key} from the cache.
     545        """
     546
    522547__all__ = [
    523548    "IUsernameDigestHash", "ICredentialFactory", "IRequest",
    524     "IBodyProducer", "IRenderable", "IResponse",
     549    "IBodyProducer", "IRenderable", "IResponse", "IHTTPCache,"
    525550
    526551    "UNKNOWN_LENGTH"]