Commit 16ee6128 authored by Paul Winkler's avatar Paul Winkler

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).
parent dc53bf39
...@@ -20,6 +20,8 @@ $Id$ ...@@ -20,6 +20,8 @@ $Id$
from OFS.Cache import Cache, CacheManager from OFS.Cache import Cache, CacheManager
from OFS.SimpleItem import SimpleItem from OFS.SimpleItem import SimpleItem
import logging
import socket
import time import time
from Globals import InitializeClass from Globals import InitializeClass
from Globals import DTMLFile from Globals import DTMLFile
...@@ -31,10 +33,15 @@ from urllib import quote ...@@ -31,10 +33,15 @@ from urllib import quote
from App.Common import rfc1123_date from App.Common import rfc1123_date
logger = logging.getLogger('Zope.AcceleratedHTTPCacheManager')
class AcceleratedHTTPCache (Cache): class AcceleratedHTTPCache (Cache):
# Note the need to take thread safety into account. # Note the need to take thread safety into account.
# Also note that objects of this class are not persistent, # Also note that objects of this class are not persistent,
# nor do they use acquisition. # nor do they use acquisition.
connection_factory = httplib.HTTPConnection
def __init__(self): def __init__(self):
self.hit_counts = {} self.hit_counts = {}
...@@ -44,14 +51,30 @@ class AcceleratedHTTPCache (Cache): ...@@ -44,14 +51,30 @@ class AcceleratedHTTPCache (Cache):
self.__dict__.update(kw) self.__dict__.update(kw)
def ZCache_invalidate(self, ob): 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() phys_path = ob.getPhysicalPath()
if self.hit_counts.has_key(phys_path): if self.hit_counts.has_key(phys_path):
del self.hit_counts[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 = [] results = []
for url in self.notify_urls: for url in self.notify_urls:
if not url: if not url.strip():
continue continue
# Send the PURGE request to each HTTP accelerator. # Send the PURGE request to each HTTP accelerator.
if url[:7].lower() == 'http://': if url[:7].lower() == 'http://':
...@@ -60,23 +83,37 @@ class AcceleratedHTTPCache (Cache): ...@@ -60,23 +83,37 @@ class AcceleratedHTTPCache (Cache):
u = 'http://' + url u = 'http://' + url
(scheme, host, path, params, query, fragment (scheme, host, path, params, query, fragment
) = urlparse.urlparse(u) ) = urlparse.urlparse(u)
if path[-1:] == '/': if path.lower().startswith('/http://'):
p = path[:-1] + ob_path path = path.lstrip('/')
else: for ob_path in purge_paths:
p = path + ob_path p = path.rstrip('/') + ob_path
h = httplib.HTTPConnection(host) h = self.connection_factory(host)
h.request('PURGE', p) logger.debug('PURGING host %s, path %s' % (host, p))
r = h.getresponse() # An exception on one purge should not prevent the others.
results.append('%s %s' % (r.status, r.reason)) 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) return 'Server response(s): ' + ';'.join(results)
def ZCache_get(self, ob, view_name, keywords, mtime_func, default): def ZCache_get(self, ob, view_name, keywords, mtime_func, default):
return default return default
def ZCache_set(self, ob, data, view_name, keywords, mtime_func): def ZCache_set(self, ob, data, view_name, keywords, mtime_func):
# Note the blatant ignorance of view_name, keywords, and # Note the blatant ignorance of view_name and keywords.
# mtime_func. Standard HTTP accelerators are not able to make # Standard HTTP accelerators are not able to make use of this
# use of this data. # 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 REQUEST = ob.REQUEST
RESPONSE = REQUEST.RESPONSE RESPONSE = REQUEST.RESPONSE
anon = 1 anon = 1
...@@ -148,7 +185,7 @@ class AcceleratedHTTPCacheManager (CacheManager, SimpleItem): ...@@ -148,7 +185,7 @@ class AcceleratedHTTPCacheManager (CacheManager, SimpleItem):
security.declareProtected(view_management_screens, 'getSettings') security.declareProtected(view_management_screens, 'getSettings')
def getSettings(self): 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') security.declareProtected(view_management_screens, 'manage_main')
manage_main = DTMLFile('dtml/propsAccel', globals()) manage_main = DTMLFile('dtml/propsAccel', globals())
......
...@@ -15,87 +15,139 @@ ...@@ -15,87 +15,139 @@
$Id$ $Id$
""" """
import unittest import unittest
import threading from Products.StandardCacheManagers.AcceleratedHTTPCacheManager \
import time import AcceleratedHTTPCache, AcceleratedHTTPCacheManager
from SimpleHTTPServer import SimpleHTTPRequestHandler
from BaseHTTPServer import HTTPServer
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.""" def getPhysicalPath(self):
self.server.test_case.purged_host = self.headers.get('Host','xxx') return tuple(self.path.split('/'))
self.server.test_case.purged_path = self.path
self.send_response(200)
self.end_headers()
def log_request(self, code='ignored', size='ignored'): def absolute_url_path(self):
pass 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): def request(self, method, path):
return tuple(self._PATH.split('/')) 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): class AcceleratedHTTPCacheTests(unittest.TestCase):
if self.thread:
self.httpd.server_close()
self.thread.join(2)
def _getTargetClass(self): def _getTargetClass(self):
from Products.StandardCacheManagers.AcceleratedHTTPCacheManager \
import AcceleratedHTTPCache
return AcceleratedHTTPCache return AcceleratedHTTPCache
def _makeOne(self, *args, **kw): def _makeOne(self, *args, **kw):
return self._getTargetClass()(*args, **kw) return self._getTargetClass()(*args, **kw)
def _handleServerRequest(self):
server_address = ('', self._SERVER_PORT)
self.httpd = HTTPServer(server_address, PurgingHTTPRequestHandler)
self.httpd.test_case = self
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): def test_PURGE_passes_Host_header(self):
_TO_NOTIFY = 'localhost:1888'
_TO_NOTIFY = 'localhost:%d' % self._SERVER_PORT
cache = self._makeOne() cache = self._makeOne()
cache.notify_urls = ['http://%s' % _TO_NOTIFY] cache.notify_urls = ['http://%s' % _TO_NOTIFY]
object = DummyObject() 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)
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)
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)
# Run the HTTP server for this test.
self._handleServerRequest()
cache.ZCache_invalidate(object) class CacheManagerTests(unittest.TestCase):
def _getTargetClass(self):
return AcceleratedHTTPCacheManager
def _makeOne(self, *args, **kw):
return self._getTargetClass()(*args, **kw)
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
def test_add(self):
# ensure __init__ doesn't raise errors.
root, cachemanager = self._makeContext()
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())
self.assertEqual(self.purged_host, _TO_NOTIFY)
self.assertEqual(self.purged_path, DummyObject._PATH)
def test_suite(): 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__': if __name__ == '__main__':
unittest.main(defaultTest='test_suite') unittest.main(defaultTest='test_suite')
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment