diff --git a/CHANGELOG b/CHANGELOG index 594c0ba54f..e3cb53a6f9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Add duration on Cache * Add strip wildcards helpers * Add list-form view * Do not set id for on_change calls if cached diff --git a/doc/ref/cache.rst b/doc/ref/cache.rst index f17ee2d77b..84b9f7b14c 100644 --- a/doc/ref/cache.rst +++ b/doc/ref/cache.rst @@ -5,16 +5,17 @@ Cache ===== -.. class:: Cache(name[, size_limit[, context]]) +.. class:: Cache(name[, size_limit[, duration[, context]]]) -The class is used to cache values between server requests. The `name` should -be unique and it's used to identify the cache. We usually use -`.` to make it unique. The `size_limit` field can -be used to limit the number of values cached and the `context` parameter -is used to indicate if the cache depends on the user context and is true -by default. -The cache is cleaned on :class:`Transaction` starts and resets on -:class:`Transaction` commit or rollback. +The class is used to cache values between server requests. The `name` should be +unique and it's used to identify the cache. We usually use +`.` to make it unique. The `size_limit` parameter can +be used to limit the number of values cached and it has 1024 as the default +value. The `duration` parameter defines how long a cached value stays valid +but if it is not set the value remains valid until it is cleared. And the +`context` parameter is used to indicate if the cache depends on the user +context and is true by default. The cache is cleaned on :class:`Transaction` +starts and resets on :class:`Transaction` commit or rollback. .. warning:: As there is no deepcopy of the values cached, they must never be mutated diff --git a/trytond/cache.py b/trytond/cache.py index 4c477464ae..df428cb991 100644 --- a/trytond/cache.py +++ b/trytond/cache.py @@ -1,5 +1,6 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. +import datetime as dt import json import logging import select @@ -44,10 +45,18 @@ def freeze(o): class BaseCache(object): _instances = {} - def __init__(self, name, size_limit=1024, context=True): + def __init__(self, name, size_limit=1024, duration=None, context=True): self._name = name self.size_limit = size_limit self.context = context + if isinstance(duration, dt.timedelta): + self.duration = duration + elif isinstance(duration, (int, float)): + self.duration = dt.timedelta(seconds=duration) + elif duration: + self.duration = dt.timedelta(**duration) + else: + self.duration = None assert self._name not in self._instances self._instances[self._name] = self @@ -94,9 +103,9 @@ class MemoryCache(BaseCache): _table = 'ir_cache' _channel = _table - def __init__(self, name, size_limit=1024, context=True): - super(MemoryCache, self).__init__(name, size_limit, context) - self._database_cache = defaultdict(lambda: LRUDict(size_limit)) + def __init__(self, *args, **kwargs): + super(MemoryCache, self).__init__(*args, **kwargs) + self._database_cache = defaultdict(lambda: LRUDict(self.size_limit)) self._transaction_cache = WeakKeyDictionary() self._transaction_lower = {} self._timestamp = {} @@ -120,7 +129,10 @@ class MemoryCache(BaseCache): key = self._key(key) cache = self._get_cache() try: - result = cache[key] = cache.pop(key) + (expire, result) = cache.pop(key) + if expire and expire < dt.datetime.now(): + return default + cache[key] = (expire, result) return result except (KeyError, TypeError): return default @@ -128,8 +140,12 @@ class MemoryCache(BaseCache): def set(self, key, value): key = self._key(key) cache = self._get_cache() + if self.duration: + expire = dt.datetime.now() + self.duration + else: + expire = None try: - cache[key] = value + cache[key] = (expire, value) except TypeError: pass return value diff --git a/trytond/tests/test_cache.py b/trytond/tests/test_cache.py index 7b6751f685..de7cb532e4 100644 --- a/trytond/tests/test_cache.py +++ b/trytond/tests/test_cache.py @@ -12,6 +12,7 @@ from trytond.transaction import Transaction cache = MemoryCache('test.cache') +cache_expire = MemoryCache('test.cache_expire', duration=1) class CacheTestCase(unittest.TestCase): @@ -134,6 +135,14 @@ class MemoryCacheTestCase(unittest.TestCase): self.addCleanup(transaction3.stop) self.assertEqual(cache.get('foo'), None) + @with_transaction() + def test_memory_cache_expire(self): + "Test expired cache" + cache_expire.set('foo', "bar") + time.sleep(cache_expire.duration.total_seconds()) + + self.assertEqual(cache_expire.get('foo'), None) + @unittest.skipIf(backend.name() == 'sqlite', "SQLite has not channel") class MemoryCacheChannelTestCase(MemoryCacheTestCase):