From e9d5247d1f7834ce8e1a32e9e0e9c0477c474cca Mon Sep 17 00:00:00 2001 From: Yoshinori Okuji <yo@nexedi.com> Date: Fri, 7 May 2004 15:39:31 +0000 Subject: [PATCH] Initial import. This is a general implementation of a cache system. git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@858 20353a03-c40f-0410-a6d1-a30d3c3de9de --- product/ERP5Type/Cache.py | 140 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100755 product/ERP5Type/Cache.py diff --git a/product/ERP5Type/Cache.py b/product/ERP5Type/Cache.py new file mode 100755 index 0000000000..b6dff36161 --- /dev/null +++ b/product/ERP5Type/Cache.py @@ -0,0 +1,140 @@ +############################################################################## +# +# Copyright (c) 2004 Nexedi SARL and Contributors. All Rights Reserved. +# Yoshinori Okuji <yo@nexedi.com> +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# garantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +############################################################################## + +from AccessControl.SecurityInfo import allow_class +from time import time + +from zLOG import LOG + +# XXX need to expire old objects in a way. +cache_check_count = 0 +CACHE_CHECK_MAX = 100 + +# Use this global variable to store cached objects. +cached_object_dict = {} + +# Special Exception for this code. +class CachedMethodError(Exception): pass + +# This is just a storage. +class CachedObject: + pass + +class CachingMethod: + """ + CachingMethod wraps a callable object to cache the result. + + 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. + """ + def __init__(self, callable_object, id = None, cache_duration = 180): + """ + callable_object must be callable. + id is used to identify what call should be treated as the same call. + cache_duration is specified in seconds. + """ + if not callable(callable_object): + raise CachedMethodError, "callable_object %s is not callable" % str(callable_object) + if not id: + raise CachedMethodError, "id must be specified" + self.method = callable_object + self.id = id + self.duration = cache_duration + + def __call__(self, *args, **kwd): + """ + Call the method only if the result is not cached. + + 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. + """ + global cache_check_count + + now = time() + + cache_check_count += 1 + if cache_check_count >= CACHE_CHECK_MAX: + # If the count reachs the max, expire all old entries. + # XXX this can be quite slow, if many results are cached. + LOG('CachingMethod', 0, 'checking all entries to expire') + cache_check_count = 0 + try: + for index,obj in cached_object_dict.items(): + if obj.time + obj.duration < now: + LOG('CachingMethod', 0, 'expire %s' % index) + del cached_object_dict[index] + except: + # This is necessary for multi-threading, because two threads can + # delete the same entry at a time. + pass + + key_list = kwd.keys() + key_list.sort() + index = [self.id] + for arg in args: + index.append((None, arg)) + for key in key_list: + index.append((key, str(kwd[key]))) + index = str(index) + + obj = cached_object_dict.get(index) + if obj is None or obj.time + obj.duration < now: + 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))) + if obj is None: + obj = CachedObject() + obj.time = now + obj.duration = self.duration + obj.result = self.method(*args, **kwd) + + 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))) + + return obj.result + +allow_class(CachingMethod) \ No newline at end of file -- 2.30.9