[Zope-Checkins]
SVN: Zope/branches/slinkp-1447-httpcache-fix-branch/lib/python/Products/StandardCacheManagers/
Fixes and unit tests for http://www.zope.org/Collectors/Zope/1447:
Paul Winkler
pw_lists at slinkp.com
Thu May 4 22:22:12 EDT 2006
Log message for revision 67976:
Fixes and unit tests for http://www.zope.org/Collectors/Zope/1447:
when editing content on a virtual-hosted zope, changes will purge
correctly.
Also simplified the test framework (no more need to launch an
HTTP server).
Changed:
U Zope/branches/slinkp-1447-httpcache-fix-branch/lib/python/Products/StandardCacheManagers/AcceleratedHTTPCacheManager.py
U Zope/branches/slinkp-1447-httpcache-fix-branch/lib/python/Products/StandardCacheManagers/tests/test_AcceleratedHTTPCacheManager.py
-=-
Modified: Zope/branches/slinkp-1447-httpcache-fix-branch/lib/python/Products/StandardCacheManagers/AcceleratedHTTPCacheManager.py
===================================================================
--- Zope/branches/slinkp-1447-httpcache-fix-branch/lib/python/Products/StandardCacheManagers/AcceleratedHTTPCacheManager.py 2006-05-05 01:41:05 UTC (rev 67975)
+++ Zope/branches/slinkp-1447-httpcache-fix-branch/lib/python/Products/StandardCacheManagers/AcceleratedHTTPCacheManager.py 2006-05-05 02:22:12 UTC (rev 67976)
@@ -20,6 +20,8 @@
from OFS.Cache import Cache, CacheManager
from OFS.SimpleItem import SimpleItem
+import logging
+import socket
import time
from Globals import InitializeClass
from Globals import DTMLFile
@@ -31,10 +33,15 @@
from App.Common import rfc1123_date
+logger = logging.getLogger('Zope.AcceleratedHTTPCacheManager')
+
class AcceleratedHTTPCache (Cache):
# Note the need to take thread safety into account.
# Also note that objects of this class are not persistent,
# nor do they use acquisition.
+
+ connection_factory = httplib.HTTPConnection
+
def __init__(self):
self.hit_counts = {}
@@ -44,14 +51,30 @@
self.__dict__.update(kw)
def ZCache_invalidate(self, ob):
- # Note that this only works for default views of objects.
+ # Note that this only works for default views of objects at
+ # their canonical path. If an object is viewed and cached at
+ # any other path via acquisition or virtual hosting, that
+ # cache entry cannot be purged because there is an infinite
+ # number of such possible paths, and Squid does not support
+ # any kind of fuzzy purging; we have to specify exactly the
+ # URL to purge. So we try to purge the known paths most
+ # likely to turn up in practice: the physical path and the
+ # current absolute_url_path. Any of those can be
+ # wrong in some circumstances, but it may be the best we can
+ # do :-(
+ # It would be nice if Squid's purge feature was better
+ # documented. (pot! kettle! black!)
+
phys_path = ob.getPhysicalPath()
if self.hit_counts.has_key(phys_path):
del self.hit_counts[phys_path]
- ob_path = quote('/'.join(phys_path))
+ purge_paths = (ob.absolute_url_path(), quote('/'.join(phys_path)))
+ # Don't purge the same path twice.
+ if purge_paths[0] == purge_paths[1]:
+ purge_paths = purge_paths[:1]
results = []
for url in self.notify_urls:
- if not url:
+ if not url.strip():
continue
# Send the PURGE request to each HTTP accelerator.
if url[:7].lower() == 'http://':
@@ -60,23 +83,37 @@
u = 'http://' + url
(scheme, host, path, params, query, fragment
) = urlparse.urlparse(u)
- if path[-1:] == '/':
- p = path[:-1] + ob_path
- else:
- p = path + ob_path
- h = httplib.HTTPConnection(host)
- h.request('PURGE', p)
- r = h.getresponse()
- results.append('%s %s' % (r.status, r.reason))
+ if path.lower().startswith('/http://'):
+ path = path.lstrip('/')
+ for ob_path in purge_paths:
+ p = path.rstrip('/') + ob_path
+ h = self.connection_factory(host)
+ logger.debug('PURGING host %s, path %s' % (host, p))
+ # An exception on one purge should not prevent the others.
+ try:
+ h.request('PURGE', p)
+ # This better not hang. I wish httplib gave us
+ # control of timeouts.
+ except socket.gaierror:
+ msg = 'socket.gaierror: maybe the server ' + \
+ 'at %s is down, or the cache manager ' + \
+ 'is misconfigured?'
+ logger.error(msg % url)
+ continue
+ r = h.getresponse()
+ status = '%s %s' % (r.status, r.reason)
+ results.append(status)
+ logger.debug('purge response: %s' % status)
return 'Server response(s): ' + ';'.join(results)
def ZCache_get(self, ob, view_name, keywords, mtime_func, default):
return default
def ZCache_set(self, ob, data, view_name, keywords, mtime_func):
- # Note the blatant ignorance of view_name, keywords, and
- # mtime_func. Standard HTTP accelerators are not able to make
- # use of this data.
+ # Note the blatant ignorance of view_name and keywords.
+ # Standard HTTP accelerators are not able to make use of this
+ # data. mtime_func is also ignored because using "now" for
+ # Last-Modified is as good as using any time in the past.
REQUEST = ob.REQUEST
RESPONSE = REQUEST.RESPONSE
anon = 1
@@ -148,7 +185,7 @@
security.declareProtected(view_management_screens, 'getSettings')
def getSettings(self):
' '
- return self._settings.copy() # Don't let DTML modify it.
+ return self._settings.copy() # Don't let UI modify it.
security.declareProtected(view_management_screens, 'manage_main')
manage_main = DTMLFile('dtml/propsAccel', globals())
Modified: Zope/branches/slinkp-1447-httpcache-fix-branch/lib/python/Products/StandardCacheManagers/tests/test_AcceleratedHTTPCacheManager.py
===================================================================
--- Zope/branches/slinkp-1447-httpcache-fix-branch/lib/python/Products/StandardCacheManagers/tests/test_AcceleratedHTTPCacheManager.py 2006-05-05 01:41:05 UTC (rev 67975)
+++ Zope/branches/slinkp-1447-httpcache-fix-branch/lib/python/Products/StandardCacheManagers/tests/test_AcceleratedHTTPCacheManager.py 2006-05-05 02:22:12 UTC (rev 67976)
@@ -15,87 +15,139 @@
$Id$
"""
+
import unittest
-import threading
-import time
-from SimpleHTTPServer import SimpleHTTPRequestHandler
-from BaseHTTPServer import HTTPServer
+from Products.StandardCacheManagers.AcceleratedHTTPCacheManager \
+ import AcceleratedHTTPCache, AcceleratedHTTPCacheManager
-class PurgingHTTPRequestHandler(SimpleHTTPRequestHandler):
- protocol_version = 'HTTP/1.0'
+class DummyObject:
- def do_PURGE(self):
+ def __init__(self, path='/path/to/object', urlpath=None):
+ self.path = path
+ if urlpath is None:
+ self.urlpath = path
+ else:
+ self.urlpath = urlpath
- """Serve a PURGE request."""
- self.server.test_case.purged_host = self.headers.get('Host','xxx')
- self.server.test_case.purged_path = self.path
- self.send_response(200)
- self.end_headers()
+ def getPhysicalPath(self):
+ return tuple(self.path.split('/'))
- def log_request(self, code='ignored', size='ignored'):
- pass
+ def absolute_url_path(self):
+ return self.urlpath
+class MockResponse:
+ status = '200'
+ reason = "who knows, I'm just a mock"
-class DummyObject:
+def MockConnectionClassFactory():
+ # Returns both a class that mocks an HTTPConnection,
+ # and a reference to a data structure where it logs requests.
+ request_log = []
- _PATH = '/path/to/object'
+ class MockConnection:
+ # Minimal replacement for httplib.HTTPConnection.
+ def __init__(self, host):
+ self.host = host
+ self.request_log = request_log
- def getPhysicalPath(self):
- return tuple(self._PATH.split('/'))
+ def request(self, method, path):
+ self.request_log.append({'method':method,
+ 'host':self.host,
+ 'path':path,})
+ def getresponse(self):
+ return MockResponse()
-class AcceleratedHTTPCacheTests(unittest.TestCase):
+ return MockConnection, request_log
- _SERVER_PORT = 1888
- thread = purged_host = purged_path = None
- def tearDown(self):
- if self.thread:
- self.httpd.server_close()
- self.thread.join(2)
+class AcceleratedHTTPCacheTests(unittest.TestCase):
def _getTargetClass(self):
-
- from Products.StandardCacheManagers.AcceleratedHTTPCacheManager \
- import AcceleratedHTTPCache
-
return AcceleratedHTTPCache
def _makeOne(self, *args, **kw):
-
return self._getTargetClass()(*args, **kw)
- def _handleServerRequest(self):
+ def test_PURGE_passes_Host_header(self):
+ _TO_NOTIFY = 'localhost:1888'
+ cache = self._makeOne()
+ cache.notify_urls = ['http://%s' % _TO_NOTIFY]
+ cache.connection_factory, requests = MockConnectionClassFactory()
+ dummy = DummyObject()
+ cache.ZCache_invalidate(dummy)
+ self.assertEqual(len(requests), 1)
+ result = requests[-1]
+ self.assertEqual(result['method'], 'PURGE')
+ self.assertEqual(result['host'], _TO_NOTIFY)
+ self.assertEqual(result['path'], dummy.path)
- server_address = ('', self._SERVER_PORT)
+ def test_multiple_notify(self):
+ cache = self._makeOne()
+ cache.notify_urls = ['http://foo', 'bar', 'http://baz/bat']
+ cache.connection_factory, requests = MockConnectionClassFactory()
+ cache.ZCache_invalidate(DummyObject())
+ self.assertEqual(len(requests), 3)
+ self.assertEqual(requests[0]['host'], 'foo')
+ self.assertEqual(requests[1]['host'], 'bar')
+ self.assertEqual(requests[2]['host'], 'baz')
+ cache.ZCache_invalidate(DummyObject())
+ self.assertEqual(len(requests), 6)
- self.httpd = HTTPServer(server_address, PurgingHTTPRequestHandler)
- self.httpd.test_case = self
+ def test_vhost_purging_1447(self):
+ # Test for http://www.zope.org/Collectors/Zope/1447
+ cache = self._makeOne()
+ cache.notify_urls = ['http://foo.com']
+ cache.connection_factory, requests = MockConnectionClassFactory()
+ dummy = DummyObject(urlpath='/published/elsewhere')
+ cache.ZCache_invalidate(dummy)
+ # That should fire off two invalidations,
+ # one for the physical path and one for the abs. url path.
+ self.assertEqual(len(requests), 2)
+ self.assertEqual(requests[0]['path'], dummy.absolute_url_path())
+ self.assertEqual(requests[1]['path'], dummy.path)
- sa = self.httpd.socket.getsockname()
- self.thread = threading.Thread(target=self.httpd.handle_request)
- self.thread.setDaemon(True)
- self.thread.start()
- time.sleep(0.2) # Allow time for server startup
- def test_PURGE_passes_Host_header(self):
+class CacheManagerTests(unittest.TestCase):
- _TO_NOTIFY = 'localhost:%d' % self._SERVER_PORT
+ def _getTargetClass(self):
+ return AcceleratedHTTPCacheManager
- cache = self._makeOne()
- cache.notify_urls = ['http://%s' % _TO_NOTIFY]
- object = DummyObject()
+ def _makeOne(self, *args, **kw):
+ return self._getTargetClass()(*args, **kw)
- # Run the HTTP server for this test.
- self._handleServerRequest()
+ def _makeContext(self):
+ from OFS.Folder import Folder
+ root = Folder()
+ root.getPhysicalPath = lambda: ('', 'some_path',)
+ cm_id = 'http_cache'
+ manager = self._makeOne(cm_id)
+ root._setObject(cm_id, manager)
+ manager = root[cm_id]
+ return root, manager
- cache.ZCache_invalidate(object)
+ def test_add(self):
+ # ensure __init__ doesn't raise errors.
+ root, cachemanager = self._makeContext()
- self.assertEqual(self.purged_host, _TO_NOTIFY)
- self.assertEqual(self.purged_path, DummyObject._PATH)
+ def test_ZCacheManager_getCache(self):
+ root, cachemanager = self._makeContext()
+ cache = cachemanager.ZCacheManager_getCache()
+ self.assert_(isinstance(cache, AcceleratedHTTPCache))
+ def test_getSettings(self):
+ root, cachemanager = self._makeContext()
+ settings = cachemanager.getSettings()
+ self.assert_('anonymous_only' in settings.keys())
+ self.assert_('interval' in settings.keys())
+ self.assert_('notify_urls' in settings.keys())
+
+
def test_suite():
- return unittest.makeSuite(AcceleratedHTTPCacheTests)
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(AcceleratedHTTPCacheTests))
+ suite.addTest(unittest.makeSuite(CacheManagerTests))
+ return suite
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')
More information about the Zope-Checkins
mailing list