Commit 996ebaa9 authored by Hanno Schlichting's avatar Hanno Schlichting

Factored out Products.StandardCacheManagers

parent 3af382d3
......@@ -52,6 +52,7 @@ eggs =
Products.BTreeFolder2
Products.ExternalMethod
Products.PythonScripts
Products.StandardCacheManagers
Products.ZCTextIndex
Record
RestrictedPython
......
......@@ -37,9 +37,10 @@ Restructuring
database manager ZMI.
- Factored out the `Products.BTreeFolder2`, `Products.ExternalMethod`,
`Products.MIMETools`, `Products.OFSP` and `Products.PythonScripts` packages
into their own distributions. They will no longer be included by default in
Zope 2.14 but live on as independent add-ons.
`Products.MIMETools`, `Products.OFSP`, `Products.PythonScripts` and
`Products.StandardCacheManagers` packages into their own distributions. They
will no longer be included by default in Zope 2.14 but live on as independent
add-ons.
- Factored out the `Products.ZSQLMethods` into its own distribution. The
distribution also includes the `Shared.DC.ZRDB` code. The Zope2 distribution
......
......@@ -104,6 +104,7 @@ setup(name='Zope2',
'Products.MIMETools',
'Products.OFSP',
'Products.PythonScripts',
'Products.StandardCacheManagers',
],
include_package_data=True,
......
......@@ -14,6 +14,7 @@ Products.ExternalMethod = svn ^/Products.ExternalMethod/trunk
Products.MIMETools = svn ^/Products.MIMETools/trunk
Products.OFSP = svn ^/Products.OFSP/trunk
Products.PythonScripts = svn ^/Products.PythonScripts/trunk
Products.StandardCacheManagers = svn ^/Products.StandardCacheManagers/trunk
Products.ZCTextIndex = svn ^/Products.ZCTextIndex/trunk
Record = svn ^/Record/trunk
tempstorage = svn ^/tempstorage/trunk
......
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
'''
Accelerated HTTP cache manager --
Adds caching headers to the response so that downstream caches will
cache according to a common policy.
$Id$
'''
from cgi import escape
import httplib
import logging
import socket
import time
from urllib import quote
import urlparse
from AccessControl.class_init import InitializeClass
from AccessControl.Permissions import view_management_screens
from AccessControl.SecurityInfo import ClassSecurityInfo
from App.Common import rfc1123_date
from App.special_dtml import DTMLFile
from OFS.Cache import Cache
from OFS.Cache import CacheManager
from OFS.SimpleItem import SimpleItem
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 = {}
def initSettings(self, kw):
# Note that we lazily allow AcceleratedHTTPCacheManager
# to verify the correctness of the internal settings.
self.__dict__.update(kw)
def ZCache_invalidate(self, ob):
# 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]
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.strip():
continue
# Send the PURGE request to each HTTP accelerator.
if url[:7].lower() == 'http://':
u = url
else:
u = 'http://' + url
(scheme, host, path, params, query, fragment
) = urlparse.urlparse(u)
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 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
u = REQUEST.get('AUTHENTICATED_USER', None)
if u is not None:
if u.getUserName() != 'Anonymous User':
anon = 0
phys_path = ob.getPhysicalPath()
if self.hit_counts.has_key(phys_path):
hits = self.hit_counts[phys_path]
else:
self.hit_counts[phys_path] = hits = [0,0]
if anon:
hits[0] = hits[0] + 1
else:
hits[1] = hits[1] + 1
if not anon and self.anonymous_only:
return
# Set HTTP Expires and Cache-Control headers
seconds=self.interval
expires=rfc1123_date(time.time() + seconds)
RESPONSE.setHeader('Last-Modified',rfc1123_date(time.time()))
RESPONSE.setHeader('Cache-Control', 'max-age=%d' % seconds)
RESPONSE.setHeader('Expires', expires)
caches = {}
PRODUCT_DIR = __name__.split('.')[-2]
class AcceleratedHTTPCacheManager (CacheManager, SimpleItem):
' '
security = ClassSecurityInfo()
security.setPermissionDefault('Change cache managers', ('Manager',))
manage_options = (
{'label':'Properties', 'action':'manage_main',
'help':(PRODUCT_DIR, 'Accel.stx'),},
{'label':'Statistics', 'action':'manage_stats',
'help':(PRODUCT_DIR, 'Accel.stx'),},
) + CacheManager.manage_options + SimpleItem.manage_options
meta_type = 'Accelerated HTTP Cache Manager'
def __init__(self, ob_id):
self.id = ob_id
self.title = ''
self._settings = {'anonymous_only':1,
'interval':3600,
'notify_urls':()}
self._resetCacheId()
def getId(self):
' '
return self.id
security.declarePrivate('_remove_data')
def _remove_data(self):
caches.pop(self.__cacheid, None)
security.declarePrivate('_resetCacheId')
def _resetCacheId(self):
self.__cacheid = '%s_%f' % (id(self), time.time())
security.declarePrivate('ZCacheManager_getCache')
def ZCacheManager_getCache(self):
cacheid = self.__cacheid
try:
return caches[cacheid]
except KeyError:
cache = AcceleratedHTTPCache()
cache.initSettings(self._settings)
caches[cacheid] = cache
return cache
security.declareProtected(view_management_screens, 'getSettings')
def getSettings(self):
' '
return self._settings.copy() # Don't let UI modify it.
security.declareProtected(view_management_screens, 'manage_main')
manage_main = DTMLFile('dtml/propsAccel', globals())
security.declareProtected('Change cache managers', 'manage_editProps')
def manage_editProps(self, title, settings=None, REQUEST=None):
' '
if settings is None:
settings = REQUEST
self.title = str(title)
self._settings = {
'anonymous_only':settings.get('anonymous_only') and 1 or 0,
'interval':int(settings['interval']),
'notify_urls':tuple(settings['notify_urls']),}
cache = self.ZCacheManager_getCache()
cache.initSettings(self._settings)
if REQUEST is not None:
return self.manage_main(
self, REQUEST, manage_tabs_message='Properties changed.')
security.declareProtected(view_management_screens, 'manage_stats')
manage_stats = DTMLFile('dtml/statsAccel', globals())
def _getSortInfo(self):
"""
Returns the value of sort_by and sort_reverse.
If not found, returns default values.
"""
req = self.REQUEST
sort_by = req.get('sort_by', 'anon')
sort_reverse = int(req.get('sort_reverse', 1))
return sort_by, sort_reverse
security.declareProtected(view_management_screens, 'getCacheReport')
def getCacheReport(self):
"""
Returns the list of objects in the cache, sorted according to
the user's preferences.
"""
sort_by, sort_reverse = self._getSortInfo()
c = self.ZCacheManager_getCache()
rval = []
for path, (anon, auth) in c.hit_counts.items():
rval.append({'path': '/'.join(path),
'anon': anon,
'auth': auth})
if sort_by:
rval.sort(lambda e1, e2, sort_by=sort_by:
cmp(e1[sort_by], e2[sort_by]))
if sort_reverse:
rval.reverse()
return rval
security.declareProtected(view_management_screens, 'sort_link')
def sort_link(self, name, id):
"""
Utility for generating a sort link.
"""
# XXX This ought to be in a library or something.
sort_by, sort_reverse = self._getSortInfo()
url = self.absolute_url() + '/manage_stats?sort_by=' + id
newsr = 0
if sort_by == id:
newsr = not sort_reverse
url = url + '&sort_reverse=' + (newsr and '1' or '0')
return '<a href="%s">%s</a>' % (escape(url, 1), escape(name))
InitializeClass(AcceleratedHTTPCacheManager)
manage_addAcceleratedHTTPCacheManagerForm = DTMLFile('dtml/addAccel',
globals())
def manage_addAcceleratedHTTPCacheManager(self, id, REQUEST=None):
' '
self._setObject(id, AcceleratedHTTPCacheManager(id))
if REQUEST is not None:
return self.manage_main(self, REQUEST)
# FYI good resource: http://www.web-caching.com/proxy-caches.html
Preface
=======
This document is intended for people interested in the internals of
RAMCacheManager, such as maintainers. It should be updated when any
significant changes are made to the RAMCacheManager.
$Id$
Introduction
===============
The caching framework does not interpret the data in any way, it acts
just as a general storage for data passed to it. It tries to check if
the data is pickleable though. IOW, only pickleable data is
cacheable.
The idea behind the RAMCacheManager is that it should be shared between
threads, so that the same objects are not cached in each thread. This
is achieved by storing the cache data structure itself as a module
level variable (RAMCacheManager.caches). This, of course, requires
locking on modifications of that data structure.
Each RAMCacheManager instance has one cache in RAMCacheManager.caches
dictionary. A unique __cacheid is generated when creating a cache
manager and it's used as a key for caches.
Object Hierarchy
================
RAMCacheManager
RAMCache
ObjectCacheEntries
CacheEntry
RAMCacheManager is a persistent placeful object. It is assigned a
unique __cacheid on its creation. It is then used as a key to look up
the corresponding RAMCache object in the global caches dictionary.
So, each RAMCacheManager has a single RAMCache related to it.
RAMCache is a volatile cache, unique for each RAMCacheManager. It is
shared among threads and does all the locking. It has a writelock.
No locking is done on reading though. RAMCache keeps a dictionary of
ObjectCacheEntries indexed by the physical path of a cached object.
ObjectCacheEntries is a container for cached values for a single object.
The values in it are indexed by a tuple of a view_name, interesting
request variables, and extra keywords passed to Cache.ZCache_set().
CacheEntry is a wrapper around a single cached value. It stores the
data itself, creation time, view_name and keeps the access count.
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
'''
RAM cache manager --
Caches the results of method calls in RAM.
$Id$
'''
from cgi import escape
from thread import allocate_lock
import time
from AccessControl.class_init import InitializeClass
from AccessControl.Permissions import view_management_screens
from AccessControl.SecurityInfo import ClassSecurityInfo
from App.special_dtml import DTMLFile
from OFS.Cache import Cache
from OFS.Cache import CacheManager
from OFS.SimpleItem import SimpleItem
try:
from cPickle import Pickler
from cPickle import HIGHEST_PROTOCOL
except ImportError:
from pickle import Pickler
from pickle import HIGHEST_PROTOCOL
_marker = [] # Create a new marker object.
class CacheException (Exception):
'''
A cache-related exception.
'''
class CacheEntry:
'''
Represents a cached value.
'''
def __init__(self, index, data, view_name):
try:
# This is a protective barrier that hopefully prevents
# us from caching something that might result in memory
# leaks. It's also convenient for determining the
# approximate memory usage of the cache entry.
# DM 2004-11-29: this code causes excessive time.
# Note also that it does not prevent us from
# caching objects with references to persistent objects
# When we do, nasty persistency errors are likely
# to occur ("shouldn't load data while connection is closed").
#self.size = len(dumps(index)) + len(dumps(data))
sizer = _ByteCounter()
pickler = Pickler(sizer, HIGHEST_PROTOCOL)
pickler.dump(index)
pickler.dump(data)
self.size = sizer.getCount()
except:
raise CacheException('The data for the cache is not pickleable.')
self.created = time.time()
self.data = data
self.view_name = view_name
self.access_count = 0
class ObjectCacheEntries:
'''
Represents the cache for one Zope object.
'''
hits = 0
misses = 0
def __init__(self, path):
self.physical_path = path
self.lastmod = 0 # Mod time of the object, class, etc.
self.entries = {}
def aggregateIndex(self, view_name, req, req_names, local_keys):
'''
Returns the index to be used when looking for or inserting
a cache entry.
view_name is a string.
local_keys is a mapping or None.
'''
req_index = []
# Note: req_names is already sorted.
for key in req_names:
if req is None:
val = ''
else:
val = req.get(key, '')
req_index.append((str(key), str(val)))
if local_keys:
local_index = []
for key, val in local_keys.items():
local_index.append((str(key), str(val)))
local_index.sort()
else:
local_index = ()
return (str(view_name), tuple(req_index), tuple(local_index))
def getEntry(self, lastmod, index):
if self.lastmod < lastmod:
# Expired.
self.entries = {}
self.lastmod = lastmod
return _marker
return self.entries.get(index, _marker)
def setEntry(self, lastmod, index, data, view_name):
self.lastmod = lastmod
self.entries[index] = CacheEntry(index, data, view_name)
def delEntry(self, index):
try: del self.entries[index]
except KeyError: pass
class RAMCache (Cache):
# Note the need to take thread safety into account.
# Also note that objects of this class are not persistent,
# nor do they make use of acquisition.
max_age = 0
def __init__(self):
# cache maps physical paths to ObjectCacheEntries.
self.cache = {}
self.writelock = allocate_lock()
self.next_cleanup = 0
def initSettings(self, kw):
# Note that we lazily allow RAMCacheManager
# to verify the correctness of the internal settings.
self.__dict__.update(kw)
def getObjectCacheEntries(self, ob, create=0):
"""
Finds or creates the associated ObjectCacheEntries object.
Remember to lock writelock when calling with the 'create' flag.
"""
cache = self.cache
path = ob.getPhysicalPath()
oc = cache.get(path, None)
if oc is None:
if create:
cache[path] = oc = ObjectCacheEntries(path)
else:
return None
return oc
def countAllEntries(self):
'''
Returns the count of all cache entries.
'''
count = 0
for oc in self.cache.values():
count = count + len(oc.entries)
return count
def countAccesses(self):
'''
Returns a mapping of
(n) -> number of entries accessed (n) times
'''
counters = {}
for oc in self.cache.values():
for entry in oc.entries.values():
access_count = entry.access_count
counters[access_count] = counters.get(
access_count, 0) + 1
return counters
def clearAccessCounters(self):
'''
Clears access_count for each cache entry.
'''
for oc in self.cache.values():
for entry in oc.entries.values():
entry.access_count = 0
def deleteEntriesAtOrBelowThreshold(self, threshold_access_count):
"""
Deletes entries that haven't been accessed recently.
"""
self.writelock.acquire()
try:
for p, oc in self.cache.items():
for agindex, entry in oc.entries.items():
if entry.access_count <= threshold_access_count:
del oc.entries[agindex]
if len(oc.entries) < 1:
del self.cache[p]
finally:
self.writelock.release()
def deleteStaleEntries(self):
"""
Deletes entries that have expired.
"""
if self.max_age > 0:
self.writelock.acquire()
try:
min_created = time.time() - self.max_age
for p, oc in self.cache.items():
for agindex, entry in oc.entries.items():
if entry.created < min_created:
del oc.entries[agindex]
if len(oc.entries) < 1:
del self.cache[p]
finally:
self.writelock.release()
def cleanup(self):
'''
Removes cache entries.
'''
self.deleteStaleEntries()
new_count = self.countAllEntries()
if new_count > self.threshold:
counters = self.countAccesses()
priorities = counters.items()
# Remove the least accessed entries until we've reached
# our target count.
if len(priorities) > 0:
priorities.sort()
access_count = 0
for access_count, effect in priorities:
new_count = new_count - effect
if new_count <= self.threshold:
break
self.deleteEntriesAtOrBelowThreshold(access_count)
self.clearAccessCounters()
def getCacheReport(self):
"""
Reports on the contents of the cache.
"""
rval = []
for oc in self.cache.values():
size = 0
ac = 0
views = []
for entry in oc.entries.values():
size = size + entry.size
ac = ac + entry.access_count
view = entry.view_name or '<default>'
if view not in views:
views.append(view)
views.sort()
info = {'path': '/'.join(oc.physical_path),
'hits': oc.hits,
'misses': oc.misses,
'size': size,
'counter': ac,
'views': views,
'entries': len(oc.entries)
}
rval.append(info)
return rval
def ZCache_invalidate(self, ob):
'''
Invalidates the cache entries that apply to ob.
'''
path = ob.getPhysicalPath()
# Invalidates all subobjects as well.
self.writelock.acquire()
try:
for p, oc in self.cache.items():
pp = oc.physical_path
if pp[:len(path)] == path:
del self.cache[p]
finally:
self.writelock.release()
def ZCache_get(self, ob, view_name='', keywords=None,
mtime_func=None, default=None):
'''
Gets a cache entry or returns default.
'''
oc = self.getObjectCacheEntries(ob)
if oc is None:
return default
lastmod = ob.ZCacheable_getModTime(mtime_func)
index = oc.aggregateIndex(view_name, ob.REQUEST,
self.request_vars, keywords)
entry = oc.getEntry(lastmod, index)
if entry is _marker:
return default
if self.max_age > 0 and entry.created < time.time() - self.max_age:
# Expired.
self.writelock.acquire()
try:
oc.delEntry(index)
finally:
self.writelock.release()
return default
oc.hits = oc.hits + 1
entry.access_count = entry.access_count + 1
return entry.data
def ZCache_set(self, ob, data, view_name='', keywords=None,
mtime_func=None):
'''
Sets a cache entry.
'''
now = time.time()
if self.next_cleanup <= now:
self.cleanup()
self.next_cleanup = now + self.cleanup_interval
lastmod = ob.ZCacheable_getModTime(mtime_func)
self.writelock.acquire()
try:
oc = self.getObjectCacheEntries(ob, create=1)
index = oc.aggregateIndex(view_name, ob.REQUEST,
self.request_vars, keywords)
oc.setEntry(lastmod, index, data, view_name)
oc.misses = oc.misses + 1
finally:
self.writelock.release()
caches = {}
PRODUCT_DIR = __name__.split('.')[-2]
class RAMCacheManager (CacheManager, SimpleItem):
"""Manage a RAMCache, which stores rendered data in RAM.
This is intended to be used as a low-level cache for
expensive Python code, not for objects published
under their own URLs such as web pages.
RAMCacheManager *can* be used to cache complete publishable
pages, such as DTMLMethods/Documents and Page Templates,
but this is not advised: such objects typically do not attempt
to cache important out-of-band data such as 3xx HTTP responses,
and the client would get an erroneous 200 response.
Such objects should instead be cached with an
AcceleratedHTTPCacheManager and/or downstream
caching.
"""
security = ClassSecurityInfo()
security.setPermissionDefault('Change cache managers', ('Manager',))
manage_options = (
{'label':'Properties', 'action':'manage_main',
'help':(PRODUCT_DIR, 'RAM.stx'),},
{'label':'Statistics', 'action':'manage_stats',
'help':(PRODUCT_DIR, 'RAM.stx'),},
) + CacheManager.manage_options + SimpleItem.manage_options
meta_type = 'RAM Cache Manager'
def __init__(self, ob_id):
self.id = ob_id
self.title = ''
self._settings = {
'threshold': 1000,
'cleanup_interval': 300,
'request_vars': ('AUTHENTICATED_USER',),
'max_age': 3600,
}
self._resetCacheId()
def getId(self):
' '
return self.id
security.declarePrivate('_remove_data')
def _remove_data(self):
caches.pop(self.__cacheid, None)
security.declarePrivate('_resetCacheId')
def _resetCacheId(self):
self.__cacheid = '%s_%f' % (id(self), time.time())
ZCacheManager_getCache__roles__ = ()
def ZCacheManager_getCache(self):
cacheid = self.__cacheid
try:
return caches[cacheid]
except KeyError:
cache = RAMCache()
cache.initSettings(self._settings)
caches[cacheid] = cache
return cache
security.declareProtected(view_management_screens, 'getSettings')
def getSettings(self):
'Returns the current cache settings.'
res = self._settings.copy()
if not res.has_key('max_age'):
res['max_age'] = 0
return res
security.declareProtected(view_management_screens, 'manage_main')
manage_main = DTMLFile('dtml/propsRCM', globals())
security.declareProtected('Change cache managers', 'manage_editProps')
def manage_editProps(self, title, settings=None, REQUEST=None):
'Changes the cache settings.'
if settings is None:
settings = REQUEST
self.title = str(title)
request_vars = list(settings['request_vars'])
request_vars.sort()
self._settings = {
'threshold': int(settings['threshold']),
'cleanup_interval': int(settings['cleanup_interval']),
'request_vars': tuple(request_vars),
'max_age': int(settings['max_age']),
}
cache = self.ZCacheManager_getCache()
cache.initSettings(self._settings)
if REQUEST is not None:
return self.manage_main(
self, REQUEST, manage_tabs_message='Properties changed.')
security.declareProtected(view_management_screens, 'manage_stats')
manage_stats = DTMLFile('dtml/statsRCM', globals())
def _getSortInfo(self):
"""
Returns the value of sort_by and sort_reverse.
If not found, returns default values.
"""
req = self.REQUEST
sort_by = req.get('sort_by', 'hits')
sort_reverse = int(req.get('sort_reverse', 1))
return sort_by, sort_reverse
security.declareProtected(view_management_screens, 'getCacheReport')
def getCacheReport(self):
"""
Returns the list of objects in the cache, sorted according to
the user's preferences.
"""
sort_by, sort_reverse = self._getSortInfo()
c = self.ZCacheManager_getCache()
rval = c.getCacheReport()
if sort_by:
rval.sort(lambda e1, e2, sort_by=sort_by:
cmp(e1[sort_by], e2[sort_by]))
if sort_reverse:
rval.reverse()
return rval
security.declareProtected(view_management_screens, 'sort_link')
def sort_link(self, name, id):
"""
Utility for generating a sort link.
"""
sort_by, sort_reverse = self._getSortInfo()
url = self.absolute_url() + '/manage_stats?sort_by=' + id
newsr = 0
if sort_by == id:
newsr = not sort_reverse
url = url + '&sort_reverse=' + (newsr and '1' or '0')
return '<a href="%s">%s</a>' % (escape(url, 1), escape(name))
security.declareProtected('Change cache managers', 'manage_invalidate')
def manage_invalidate(self, paths, REQUEST=None):
""" ZMI helper to invalidate an entry """
for path in paths:
try:
ob = self.unrestrictedTraverse(path)
except (AttributeError, KeyError):
pass
ob.ZCacheable_invalidate()
if REQUEST is not None:
msg = 'Cache entries invalidated'
return self.manage_stats(manage_tabs_message=msg)
InitializeClass(RAMCacheManager)
class _ByteCounter:
'''auxiliary file like class which just counts the bytes written.'''
_count = 0
def write(self, bytes):
self._count += len(bytes)
def getCount(self):
return self._count
manage_addRAMCacheManagerForm = DTMLFile('dtml/addRCM', globals())
def manage_addRAMCacheManager(self, id, REQUEST=None):
'Adds a RAM cache manager to the folder.'
self._setObject(id, RAMCacheManager(id))
if REQUEST is not None:
return self.manage_main(self, REQUEST)
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
'''
Some standard Zope cache managers from Digital Creations.
$Id$
'''
import RAMCacheManager
import AcceleratedHTTPCacheManager
def initialize(context):
context.registerClass(
RAMCacheManager.RAMCacheManager,
constructors = (RAMCacheManager.manage_addRAMCacheManagerForm,
RAMCacheManager.manage_addRAMCacheManager),
icon="cache.gif"
)
context.registerClass(
AcceleratedHTTPCacheManager.AcceleratedHTTPCacheManager,
constructors = (
AcceleratedHTTPCacheManager.manage_addAcceleratedHTTPCacheManagerForm,
AcceleratedHTTPCacheManager.manage_addAcceleratedHTTPCacheManager),
icon="cache.gif"
)
context.registerHelp()
<configure xmlns="http://namespaces.zope.org/zope">
<subscriber
for="Products.StandardCacheManagers.RAMCacheManager.RAMCacheManager
OFS.interfaces.IObjectClonedEvent"
handler="Products.StandardCacheManagers.subscribers.cloned" />
<subscriber
for="Products.StandardCacheManagers.RAMCacheManager.RAMCacheManager
zope.lifecycleevent.ObjectRemovedEvent"
handler="Products.StandardCacheManagers.subscribers.removed" />
<subscriber
for="Products.StandardCacheManagers.AcceleratedHTTPCacheManager.AcceleratedHTTPCacheManager
OFS.interfaces.IObjectClonedEvent"
handler="Products.StandardCacheManagers.subscribers.cloned" />
<subscriber
for="Products.StandardCacheManagers.AcceleratedHTTPCacheManager.AcceleratedHTTPCacheManager
zope.lifecycleevent.ObjectRemovedEvent"
handler="Products.StandardCacheManagers.subscribers.removed" />
</configure>
<dtml-var manage_page_header>
<dtml-var "manage_form_title(this(), _,
form_title='Add Accelerated HTTP Cache Manager',
)">
<form action="manage_addAcceleratedHTTPCacheManager" method="POST">
<table cellspacing="0" cellpadding="2" border="0">
<tr>
<td align="left" valign="top">
<div class="form-label">
Id
</div>
</td>
<td align="left" valign="top">
<input type="text" name="id" size="40" />
</td>
</tr>
<tr>
<td align="left" valign="top">
</td>
<td align="left" valign="top">
<div class="form-element">
<input class="form-element" type="submit" name="submit"
value=" Add " />
</div>
</td>
</tr>
</table>
</form>
<dtml-var manage_page_footer>
<dtml-var manage_page_header>
<dtml-var "manage_form_title(this(), _,
form_title='Add RAM Cache Manager',
)">
<form action="manage_addRAMCacheManager" method="POST">
<table cellspacing="0" cellpadding="2" border="0">
<tr>
<td align="left" valign="top">
<div class="form-label">
Id
</div>
</td>
<td align="left" valign="top">
<input type="text" name="id" size="40" />
</td>
</tr>
<tr>
<td align="left" valign="top">
</td>
<td align="left" valign="top">
<div class="form-element">
<input class="form-element" type="submit" name="submit"
value=" Add " />
</div>
</td>
</tr>
</table>
</form>
<dtml-var manage_page_footer>
<dtml-var manage_page_header>
<dtml-var manage_tabs>
<form action="manage_editProps" method="POST">
<dtml-with getSettings mapping>
<table cellspacing="0" cellpadding="2" border="0">
<tr>
<td align="left" valign="top">
<div class="form-optional">
Title
</div>
</td>
<td align="left" valign="top">
<input type="text" name="title" size="40"
value="&dtml-title;" />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Interval (seconds)
</div>
</td>
<td align="left" valign="top">
<input type="text" name="interval" size="40"
value="&dtml-interval;" />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Cache anonymous <br />connections only?
</div>
</td>
<td align="left" valign="top">
<input type="checkbox" name="anonymous_only" value="1"<dtml-if
anonymous_only> checked="checked"</dtml-if> />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Notify URLs (via PURGE)
</div>
</td>
<td align="left" valign="top">
<textarea name="notify_urls:lines" rows="5" cols="30"><dtml-in
notify_urls>&dtml-sequence-item;</dtml-in></textarea>
</td>
</tr>
<tr>
<td align="left" valign="top">
</td>
<td align="left" valign="top">
<div class="form-element">
<input class="form-element" type="submit" name="submit"
value="Save Changes" />
</div>
</td>
</tr>
</table>
</dtml-with>
</form>
<dtml-var manage_page_footer>
<dtml-var manage_page_header>
<dtml-var manage_tabs>
<p class="form-help">
The <em>RAM Cache Manager</em> allows you to cache the result of
calling expensive objects, such as Python Scripts and External
Methods, in memory. Because it does <em>not</em> cache HTTP headers,
caching full web pages is generally not advised.
</p>
<form action="manage_editProps" method="POST">
<dtml-with getSettings mapping>
<table cellspacing="0" cellpadding="2" border="0">
<tr>
<td align="left" valign="top">
<div class="form-optional">
Title
</div>
</td>
<td align="left" valign="top">
<input type="text" name="title" size="40"
value="&dtml-title;" />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
REQUEST variables
</div>
</td>
<td align="left" valign="top">
<textarea name="request_vars:lines" rows="5" cols="30"><dtml-in
request_vars>&dtml-sequence-item;
</dtml-in></textarea>
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Threshold entries
</div>
</td>
<td align="left" valign="top">
<input type="text" name="threshold" size="40"
value="&dtml-threshold;" />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Maximum age of a cache entry (seconds)
</div>
</td>
<td align="left" valign="top">
<input type="text" name="max_age" size="40"
value="&dtml-max_age;" />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Cleanup interval (seconds)
</div>
</td>
<td align="left" valign="top">
<input type="text" name="cleanup_interval" size="40"
value="&dtml-cleanup_interval;" />
</td>
</tr>
<tr>
<td align="left" valign="top">
</td>
<td align="left" valign="top">
<div class="form-element">
<input class="form-element" type="submit" name="submit"
value="Save Changes" />
</div>
</td>
</tr>
</table>
</dtml-with>
</form>
<dtml-var manage_page_footer>
<dtml-var manage_page_header>
<dtml-var manage_tabs>
<p class="form-help">
Cache manager hits generally correspond to HTTP accelerator misses.
A hit is counted in the "authenticated hits" column even if headers
are only set for anonymous requests.
</p>
<dtml-if getCacheReport>
<table width="100%" cellspacing="0" cellpadding="2" border="0">
<tr class="list-header">
<td align="left" valign="top">
<div class="list-nav">
<dtml-var expr="sort_link('Path', 'path')">
</div>
</td>
<td align="left" valign="top">
<div class="list-nav">
<dtml-var expr="sort_link('Anonymous hits', 'anon')">
</div>
</td>
<td align="left" valign="top">
<div class="list-nav">
<dtml-var expr="sort_link('Authenticated hits', 'auth')">
</div>
</td>
</tr>
<dtml-in getCacheReport mapping>
<dtml-if sequence-odd>
<tr class="row-normal">
<dtml-else>
<tr class="row-hilite">
</dtml-if>
<td align="left" valign="top">
<div class="list-item">
<a href="&dtml-path;/ZCacheable_manage">&dtml-path;</a>
</div>
</td>
<td align="left" valign="top">
<div class="list-item">
&dtml-anon;
</div>
</td>
<td align="left" valign="top">
<div class="list-item">
&dtml-auth;
</div>
</td>
</tr>
</dtml-in>
</table>
<dtml-else>
<p class="form-text">
<strong>Nothing is in the cache.</strong>
</p>
</dtml-if>
<dtml-var manage_page_footer>
<dtml-var manage_page_header>
<dtml-var manage_tabs>
<p class="form-help">
Memory usage is approximate. It is based on the pickled value of the
cached data. The cache is cleaned up by removing the least frequently
accessed entries since the last cleanup operation. The determination
is made using the <em>recent hits</em> counter.
</p>
<dtml-if getCacheReport>
<form method="post" action="manage_invalidate">
<table width="100%" cellspacing="0" cellpadding="2" border="0">
<tr class="list-header">
<td align="left" valign="top" class="list-nav" width="16">
</td>
<td align="left" valign="top">
<div class="list-nav">
<dtml-var expr="sort_link('Path', 'path')">
</div>
</td>
<td align="left" valign="top">
<div class="list-nav">
<dtml-var expr="sort_link('Hits', 'hits')">
</div>
</td>
<td align="left" valign="top">
<div class="list-nav">
<dtml-var expr="sort_link('Recent Hits', 'counter')">
</div>
</td>
<td align="left" valign="top">
<div class="list-nav">
<dtml-var expr="sort_link('Misses', 'misses')">
</div>
</td>
<td align="left" valign="top">
<div class="list-nav">
<dtml-var expr="sort_link('Memory', 'size')">
</div>
</td>
<td align="left" valign="top">
<div class="list-nav">
<dtml-var expr="sort_link('Views', 'views')">
</div>
</td>
<td align="left" valign="top">
<div class="list-nav">
<dtml-var expr="sort_link('Entries', 'entries')">
</div>
</td>
</tr>
<dtml-in getCacheReport mapping>
<dtml-if sequence-odd>
<tr class="row-normal">
<dtml-else>
<tr class="row-hilite">
</dtml-if>
<td align="left" valign="top" width="16">
<input type="checkbox" name="paths:list" value="&dtml-path;" />
</td>
<td align="left" valign="top">
<div class="list-item">
<a href="&dtml-path;/ZCacheable_manage">&dtml-path;</a>
</div>
</td>
<td align="left" valign="top">
<div class="list-item">
&dtml-hits;
</div>
</td>
<td align="left" valign="top">
<div class="list-item">
&dtml-counter;
</div>
</td>
<td align="left" valign="top">
<div class="list-item">
&dtml-misses;
</div>
</td>
<td align="left" valign="top">
<div class="list-item">
&dtml-size;
</div>
</td>
<td align="left" valign="top">
<div class="list-item">
<dtml-var expr="_.string.join(views, ', ')" html_quote>
</div>
</td>
<td align="left" valign="top">
<div class="list-item">
&dtml-entries;
</div>
</td>
</tr>
</dtml-in>
<tr>
<td width="16"> </td>
<td colspan="7">
<input type="submit" value=" Remove " />
</td>
</tr>
</table>
</form>
<dtml-else>
<p class="form-text">
<strong>Nothing is in the cache.</strong>
</p>
</dtml-if>
<dtml-var manage_page_footer>
Accelerated HTTP Cache Managers
The HTTP protocol provides for headers that can indicate to
downstream proxy caches, browser caches, and dedicated caches that
certain documents and images are cacheable. Most images, for example,
can safely be cached for a long time. Anonymous visits to most
primary pages can be cached as well.
An accelerated HTTP cache manager lets you control the headers that
get sent with the responses to requests so that downstream caches
will know what to cache and for how long. This allows you to reduce
the traffic to your site and handle larger loads than otherwise
possible. You can associate accelerated HTTP cache managers with
any kind of cacheable object that can be viewed through the web.
The main risk in using an accelerated HTTP cache manager involves
a part of a page setting headers that apply to the whole response.
If, for example, your home page contains three parts that are
cacheable and one of those parts is associated with an accelerated
HTTP cache manager, Zope will return the headers set by the part of
the page, making downstream caches think that the whole page should
be cached.
The workaround is simple: don't use an accelerated HTTP cache manager
with objects that make up parts of a page unless you really know
what you're doing.
There are some parameters available for accelerated HTTP cache managers.
The interval is the number of seconds the downstream caches should
cache the object. 3600 seconds, or one hour, is a good default.
If you find that some objects need one interval and other objects
should be set to another interval, use multiple cache managers.
If you set the *cache anonymous connections only* checkbox, you
will reduce the possibility of caching private data.
The *notify URLs* parameter allows you to specify the URLs of
specific downstream caches so they can receive invalidation messages
as 'PURGE' directives. Dedicated HTTP cache software such
as Squid will clear cached data for a given URL when receiving the
'PURGE' directive. (More details below.)
Simple statistics are provided. Remember that the only time Zope
receives a request that goes through an HTTP cache is when the
HTTP cache had a *miss*. So the hits seen by Zope correspond to
misses seen by the HTTP cache. To do traffic analysis, you should
consult the downstream HTTP caches.
When testing the accelerated HTTP cache manager, keep in mind that
the *reload* button on most browsers causes the 'Pragma: no-cache'
header to be sent, forcing HTTP caches to reload the page as well.
Try using telnet, netcat, or tcpwatch to observe the headers.
To allow Zope to execute the Squid PURGE directive, make sure the
following lines or the equivalent are in squid.conf (changing
'localhost' to the correct host name if Squid is on a different
machine)::
acl PURGE method purge
http_access allow localhost
http_access allow purge localhost
http_access deny purge
http_access deny all
RAM Cache Managers
The RAM cache manager allows you to cache the result of calling
expensive objects, such as Python Scripts and External Methods,
in memory. It provides access statistics and simple configuration
options.
Not all objects are appropriate for use with a RAM Cache Manager.
See the **caveats** section below.
Storing the result in memory results in the fastest possible cache
retrieval, but carries some risks. Unconstrained, it can consume too
much RAM. And it doesn't reduce network traffic, it only helps
Zope return a result more quickly.
Fortunately, RAM cache managers have tunable parameters. You can
configure the threshold on the number of entries that should be in
the cache, which defaults to 1000. Reduce it if the cache is taking
up too much memory or increase it if entries are being cleared too
often.
You can also configure the cleanup interval. If the RAM cache is
fluctuating too much in memory usage, reduce the cleanup interval.
Finally, you can configure the list of REQUEST variables that will
be used in the cache key. This can be a simple and effective way
to distinguish requests from authenticated versus anonymous users
or those with session cookies.
If you find that some of your objects need certain cache parameters
while others need somewhat different parameters, create multiple
RAM cache managers.
The 'Statistics' tab allows you to view a summary of the contents
of the cache. Click the column headers to re-sort the list, twice
to sort backwards. You can use the statistics to gauge the
benefit of caching each of your objects. For a given object, if
the number of hits is less than or not much greater than the number
of misses, you probably need to re-evaluate how that object is
cached.
Caveats
You should generally not cache the following with RAM Cache Manager:
* Images
* Files
* Complete web pages
Although Zope does not prevent you from doing so,
it generally does not make sense to associate any of these objects
with a RAM cache manager. The cache will simply not cache image or
file data, since the data is already available in RAM.
In addition, be careful with complete web pages.
The problem is that most cacheable objects will cache only their
return value; important out-of-band information such as the HTTP
response code is typically not cached. For example, if you cache
a page which calls RESPONSE.redirect(), a client that gets
a cache hit will see an HTTP 200 response code instead
of the redirect.
For all of the above objects, another kind of cache manager, an
*accelerated HTTP cache manager*, is available and more suitable.
##############################################################################
#
# Copyright (c) 2010 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
""" subscribers to events affecting StandardCacheManagers
"""
def cloned(obj, event):
"""
Reset the Id of the module level cache so the clone gets a different cache
than its source object
"""
obj._resetCacheId()
def removed(obj, event):
obj._remove_data()
##############################################################################
#
# Copyright (c) 2005 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
""" Unit tests for StandardCacheManagers product.
$Id$
"""
##############################################################################
#
# Copyright (c) 2005 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
""" Unit tests for AcceleratedCacheManager module.
$Id$
"""
import unittest
from Products.StandardCacheManagers.AcceleratedHTTPCacheManager \
import AcceleratedHTTPCache, AcceleratedHTTPCacheManager
class DummyObject:
def __init__(self, path='/path/to/object', urlpath=None):
self.path = path
if urlpath is None:
self.urlpath = path
else:
self.urlpath = urlpath
def getPhysicalPath(self):
return tuple(self.path.split('/'))
def absolute_url_path(self):
return self.urlpath
class MockResponse:
status = '200'
reason = "who knows, I'm just a mock"
def MockConnectionClassFactory():
# Returns both a class that mocks an HTTPConnection,
# and a reference to a data structure where it logs requests.
request_log = []
class MockConnection:
# Minimal replacement for httplib.HTTPConnection.
def __init__(self, host):
self.host = host
self.request_log = request_log
def request(self, method, path):
self.request_log.append({'method':method,
'host':self.host,
'path':path,})
def getresponse(self):
return MockResponse()
return MockConnection, request_log
class AcceleratedHTTPCacheTests(unittest.TestCase):
def _getTargetClass(self):
return AcceleratedHTTPCache
def _makeOne(self, *args, **kw):
return self._getTargetClass()(*args, **kw)
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)
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)
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())
def test_suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(AcceleratedHTTPCacheTests))
suite.addTest(unittest.makeSuite(CacheManagerTests))
return suite
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')
##############################################################################
#
# Copyright (c) 2010 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
""" Unit tests for AcceleratedCacheManager module.
$Id$
"""
import unittest
import transaction
import zope.component
from zope.component import testing as componenttesting
from zope.component import eventtesting
from AccessControl import SecurityManager
from AccessControl.SecurityManagement import newSecurityManager
from AccessControl.SecurityManagement import noSecurityManager
from OFS.Folder import Folder
from OFS.tests.testCopySupport import CopySupportTestBase
from OFS.tests.testCopySupport import UnitTestSecurityPolicy
from OFS.tests.testCopySupport import UnitTestUser
from Zope2.App import zcml
from Products.StandardCacheManagers.RAMCacheManager import RAMCacheManager
from Products.StandardCacheManagers.AcceleratedHTTPCacheManager \
import AcceleratedHTTPCacheManager
import Products.StandardCacheManagers
CACHE_META_TYPES = tuple(dict(name=instance_class.meta_type,
action='unused_constructor_name',
permission="Add %ss" % instance_class.meta_type)
for instance_class in (RAMCacheManager,
AcceleratedHTTPCacheManager)
)
class CacheManagerLocationTests(CopySupportTestBase):
_targetClass = None
def _makeOne(self, *args, **kw):
return self._targetClass(*args, **kw)
def setUp( self ):
componenttesting.setUp()
eventtesting.setUp()
zcml.load_config('meta.zcml', zope.component)
zcml.load_config('configure.zcml', Products.StandardCacheManagers)
folder1, folder2 = self._initFolders()
folder1.all_meta_types = folder2.all_meta_types = CACHE_META_TYPES
self.folder1 = folder1
self.folder2 = folder2
self.policy = UnitTestSecurityPolicy()
self.oldPolicy = SecurityManager.setSecurityPolicy( self.policy )
cm_id = 'cache'
manager = self._makeOne(cm_id)
self.folder1._setObject(cm_id, manager)
self.cachemanager = self.folder1[cm_id]
transaction.savepoint(optimistic=True)
newSecurityManager( None, UnitTestUser().__of__( self.root ) )
CopySupportTestBase.setUp(self)
def tearDown( self ):
noSecurityManager()
SecurityManager.setSecurityPolicy( self.oldPolicy )
del self.oldPolicy
del self.policy
del self.folder2
del self.folder1
self._cleanApp()
componenttesting.tearDown()
CopySupportTestBase.tearDown(self)
def test_cache_differs_on_copy(self):
# ensure copies don't hit the same cache
cache = self.cachemanager.ZCacheManager_getCache()
cachemanager_copy = self.folder2.manage_clone(self.cachemanager,
'cache_copy')
cache_copy = cachemanager_copy.ZCacheManager_getCache()
self.assertNotEqual(cache, cache_copy)
def test_cache_remains_on_move(self):
# test behaviour of cache on move.
# NOTE: This test verifies current behaviour, but there is no actual
# need for cache managers to maintain the same cache on move.
# if physical path starts being used as a cache key, this test might
# need to be fixed.
cache = self.cachemanager.ZCacheManager_getCache()
cut = self.folder1.manage_cutObjects(['cache'])
self.folder2.manage_pasteObjects(cut)
cachemanager_moved = self.folder2['cache']
cache_moved = cachemanager_moved.ZCacheManager_getCache()
self.assertEqual(cache, cache_moved)
def test_cache_deleted_on_remove(self):
old_cache = self.cachemanager.ZCacheManager_getCache()
self.folder1.manage_delObjects(['cache'])
new_cache = self.cachemanager.ZCacheManager_getCache()
self.assertNotEqual(old_cache, new_cache)
class AcceleratedHTTPCacheManagerLocationTests(CacheManagerLocationTests):
_targetClass = AcceleratedHTTPCacheManager
class RamCacheManagerLocationTests(CacheManagerLocationTests):
_targetClass = RAMCacheManager
def test_suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(AcceleratedHTTPCacheManagerLocationTests))
suite.addTest(unittest.makeSuite(RamCacheManagerLocationTests))
return suite
if __name__ == '__main__':
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