Commit b5de7fac authored by Sebastien Robin's avatar Sebastien Robin

new version wich use ERP5Cache

git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@11020 20353a03-c40f-0410-a6d1-a30d3c3de9de
parent c56366ee
...@@ -26,159 +26,208 @@ ...@@ -26,159 +26,208 @@
# #
############################################################################## ##############################################################################
from Globals import PersistentMapping import string
from AccessControl.SecurityInfo import allow_class
from time import time from time import time
from AccessControl.SecurityInfo import allow_class
from AccessControl import getSecurityManager
#from Products import ERP5Cache
from zLOG import LOG from zLOG import LOG
# XXX need to expire old objects in a way. is_cache_initialized = 0
cache_check_time = time()
CACHE_CHECK_TIMEOUT = 3600 def initializePortalCachingProperties(self):
""" Init CachingMethod properties."""
# Special Exception for this code. ## check if global CachingMethod is initialized in RAM for this ERP5 site. If not init it
class CachedMethodError(Exception): pass global is_cache_initialized
if not is_cache_initialized:
# This is just a storage. is_cache_initialized = 1
class CachedObject: erp5_site_id = self.getPortalObject().getId()
pass ## update cache structure from portal_caches
self.getPortalObject().portal_caches.updateCache()
class CacheFactory:
"""
CacheFactory is a RAM based object which contains different cache plugin objects ordered in a list.
"""
cache_plugins = []
cache_duration = 180
def __init__(self, cache_plugins, cache_params):
self.cache_plugins = cache_plugins
self.cache_duration = cache_params.get('cache_duration')
## separete local and shared cache plugins
self.quick_cache = self.cache_plugins[0]
try:
self.shared_caches =self.cache_plugins[1:]
except IndexError:
self.shared_caches = []
## set 'check_expire_cache_interval' to the minimal value between
## individual 'check_expire_cache_interval' for each cache plugin contained
l = []
self._last_cache_expire_check_at = time()
for cp in self.cache_plugins:
l.append(cp.cache_expire_check_interval)
l = filter(lambda x: x!=None and x!=0, l)
self.cache_expire_check_interval = min(l)
def __call__(self, callable_object, cache_id, scope, cache_duration=None, *args, **kwd):
"""
When CacheFactory is called it will try to return cached value using appropriate cache plugin.
"""
cache_duration = self.cache_duration
## Expired Cache (if needed)
self.expire()
quick_cached = self.quick_cache.get(cache_id, scope)
if quick_cached:
#print "HIT RAM", self.quick_cache
return quick_cached.getValue()
else:
## not in local, check if it's in shared
for shared_cache in self.shared_caches:
if shared_cache.has_key(cache_id, scope):
cache_entry = shared_cache.get(cache_id, scope)
value = cache_entry.getValue()
## update local cache
self.quick_cache.set(cache_id, scope, value, cache_entry.cache_duration, cache_entry.calculation_time)
return value
## not in any available cache plugins calculate and set to local ..
start = time()
value = callable_object(*args, **kwd)
end = time()
calculation_time = end - start
self.quick_cache.set(cache_id, scope, value, cache_duration, calculation_time)
## .. and update rest of caches in chain except already updated local one
for shared_cache in self.shared_caches:
shared_cache.set(cache_id, scope, value, cache_duration, calculation_time)
return value
def expire(self):
""" Expire (if needed) cache plugins """
now = time()
if now > (self._last_cache_expire_check_at + self.cache_expire_check_interval):
self._last_cache_expire_check_at = now
for cache_plugin in self.getCachePluginList():
cache_plugin.expireOldCacheEntries()
def getCachePluginList(self, omit_cache_plugin_name=None):
""" get list of all cache plugins except specified by name in omit """
rl = []
for cp in self.cache_plugins:
if omit_cache_plugin_name != cp.__class__.__name__:
rl.append(cp)
return rl
def getCachePluginByClassName(self, cache_plugin_name):
""" get cache plugin by its class name """
for cp in self.cache_plugins:
if cache_plugin_name == cp.__class__.__name__:
return cp
return None
def clearCache(self):
""" clear cache for this cache factory """
for cp in self.cache_plugins:
cp.clearCache()
class CachingMethod: class CachingMethod:
""" """
CachingMethod wraps a callable object to cache the result. CachingMethod is a RAM based global Zope class which contains different CacheFactory objects
for every available ERP5 site instance.
Example:
def toto(arg=None):
# heavy operations...
method = CachingMethod(toto, id='toto')
return method(arg='titi')
Some caveats:
- You must make sure that the method call takes all parameters which can make
the result vary. Otherwise, you will get inconsistent results.
- You should make sure that the method call does not take any parameter which
never make the result vary. Otherwise, the cache ratio will be worse.
- Choose the id carefully. If you use the same id in different methods, this may
lead to inconsistent results.
- This system can be sometimes quite slow if there are many entries, because
all entries are checked to expire old ones. This should not be significant,
since this is done once per 100 calls.
""" """
# Use this global variable to store cached objects.
cached_object_dict = {} ## cache factories will be initialized for every ERP5 site
factories = {}
def __init__(self, callable_object, id = None, cache_duration = 180):
## replace string table for some control characters not allowed in cache id
_cache_id_translate_table = string.maketrans("""[]()<>'", """,'__________')
def __init__(self, callable_object, id, cache_duration = 180, cache_factory = 'erp5_user_interface'):
""" """
callable_object must be callable. callable_object must be callable.
id is used to identify what call should be treated as the same call. id is used to identify what call should be treated as the same call.
cache_duration is specified in seconds, None to last untill zope process cache_duration is an old argument kept for backwards compatibility.
is stopped. cache_duration is specified per cache factory.
cache_factory is the id of the cache_factory to use.
""" """
if not callable(callable_object): if not callable(callable_object):
raise CachedMethodError, "callable_object %s is not callable" % str(callable_object) raise CachedMethodError, "callable_object %s is not callable" % str(callable_object)
if not id: if not id:
raise CachedMethodError, "id must be specified" raise CachedMethodError, "id must be specified"
self.method = callable_object
self.id = id self.id = id
self.duration = cache_duration self.callable_object = callable_object
self.cache_duration = cache_duration
self.cache_factory = cache_factory
def __call__(self, *args, **kwd): def __call__(self, *args, **kwd):
""" """ Call the method or return cached value using appropriate cache plugin """
Call the method only if the result is not cached. ## CachingMethod is global Zope class and thus we must make sure
#erp5_site_id = kwd.get('portal_path', ('','erp5'))[1]
This code looks not aware of multi-threading, but there should no bad effect in reality,
since the worst case is that multiple threads compute the same call at a time. ## cache scope is based on user which is a kwd argument
""" scope = kwd.get('user', 'GLOBAL')
global cache_check_time
## generate unique cache id
# Store the current time in the REQUEST object, and cache_id = self.generateCacheId(self.id, *args, **kwd)
# check the expiration only at the first time.
from Products.ERP5Type.Utils import get_request try:
request = get_request() ## try to get value from cache in a try block
now = request.get('_erp5_cache_time', None) ## which is faster than checking for keys
if now is None: value = self.factories[self.cache_factory](self.callable_object,
now = time() cache_id,
request.set('_erp5_cache_time', now) scope,
self.cache_duration,
if cache_check_time + CACHE_CHECK_TIMEOUT < now: *args,
# If the time reachs the timeout, expire all old entries. **kwd)
# XXX this can be quite slow, if many results are cached. except KeyError:
# LOG('CachingMethod', 0, 'checking all entries to expire') ## no caching enabled for this site or no such cache factory
cache_check_time = now value = self.callable_object(*args, **kwd)
try: return value
for index in CachingMethod.cached_object_dict.keys():
obj = CachingMethod.cached_object_dict[index] def generateCacheId(self, method_id, *args, **kwd):
if obj.duration is not None and obj.time + obj.duration < now: """ Generate proper cache id based on *args and **kwd """
# LOG('CachingMethod', 0, 'expire %s' % index) cache_id = [method_id]
del CachingMethod.cached_object_dict[index]
except KeyError:
# This is necessary for multi-threading, because two threads can
# delete the same entry at a time.
pass
key_list = kwd.keys() key_list = kwd.keys()
key_list.sort() key_list.sort()
index = [self.id]
for arg in args: for arg in args:
index.append((None, arg)) cache_id.append((None, arg))
for key in key_list: for key in key_list:
index.append((key, str(kwd[key]))) cache_id.append((key, str(kwd[key])))
index = str(index) cache_id = str(cache_id)
## because some cache backends don't allow some chars in cached id we make sure to replace them
obj = CachingMethod.cached_object_dict.get(index) cache_id = cache_id.translate(self._cache_id_translate_table)
if obj is None or (obj.duration is not None and obj.time + obj.duration < now): return cache_id
#LOG('CachingMethod', 0, 'cache miss: id = %s, duration = %s, method = %s, args = %s, kwd = %s' % (str(self.id), str(self.duration), str(self.method), str(args), str(kwd)))
obj = CachedObject()
obj.time = now
obj.duration = self.duration
obj.result = self.method(*args, **kwd)
CachingMethod.cached_object_dict[index] = obj
else:
#LOG('CachingMethod', 0, 'cache hit: id = %s, duration = %s, method = %s, args = %s, kwd = %s' % (str(self.id), str(self.duration), str(self.method), str(args), str(kwd)))
pass
return obj.result
allow_class(CachingMethod) allow_class(CachingMethod)
########################################################
## Old global cache functions ##
## TODO: Check if it make sense to keep them any more ##
########################################################
def clearCache(method_id=None): def clearCache(method_id=None):
"""Clear the cache. """
Clear the cache.
If method_id is specified, it clears the cache only for this method, If method_id is specified, it clears the cache only for this method,
otherwise, it clears the whole cache.""" otherwise, it clears the whole cache.
if method_id is None: """
CachingMethod.cached_object_dict.clear() pass
else:
caching_method_keys = CachingMethod.cached_object_dict.keys()
for key in caching_method_keys :
# CachingMethod dict contains a string representation of a list
# of tuples keys.
if method_id in key :
del CachingMethod.cached_object_dict[key]
# TransactionCache is a cache per transaction. The purpose of this cache is
# to accelerate some heavy read-only operations. Note that this must not be
# enabled when a trasaction may modify ZODB objects.
def getReadOnlyTransactionCache(context): def getReadOnlyTransactionCache(context):
"""Get the transaction cache. """ Get the transaction cache. """
""" pass
try:
return context.REQUEST['_erp5_read_only_transaction_cache']
except KeyError:
return None
def enableReadOnlyTransactionCache(context): def enableReadOnlyTransactionCache(context):
"""Enable the transaction cache. """ Enable the transaction cache. """
""" pass
context.REQUEST.set('_erp5_read_only_transaction_cache', {})
def disableReadOnlyTransactionCache(context): def disableReadOnlyTransactionCache(context):
"""Disable the transaction cache. """ Disable the transaction cache. """
""" pass
context.REQUEST.set('_erp5_read_only_transaction_cache', None)
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