Files
tryton/trytond/cache.py
T

220 lines
6.7 KiB
Python
Raw Normal View History

2014-12-02 12:54:01 +01:00
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from threading import Lock
2014-01-31 00:02:03 +01:00
from collections import OrderedDict
from datetime import datetime
2014-01-31 00:02:03 +01:00
from sql import Table
2015-07-01 19:41:49 +02:00
from sql.functions import CurrentTimestamp
2016-10-26 15:55:05 +02:00
from trytond.config import config
from trytond.transaction import Transaction
2016-10-26 15:55:05 +02:00
from trytond.tools import resolve
2016-10-26 15:55:05 +02:00
__all__ = ['BaseCache', 'Cache', 'LRUDict']
_clear_timeout = config.getint('cache', 'clean_timeout', default=5 * 60)
2013-02-04 19:59:05 +01:00
def freeze(o):
if isinstance(o, (set, tuple, list)):
return tuple(freeze(x) for x in o)
elif isinstance(o, dict):
2018-06-26 19:11:19 +02:00
return frozenset((x, freeze(y)) for x, y in o.items())
2013-02-04 19:59:05 +01:00
else:
return o
2016-10-26 15:55:05 +02:00
class BaseCache(object):
_cache_instance = []
2012-09-24 22:16:53 +02:00
def __init__(self, name, size_limit=1024, context=True):
2016-10-26 15:55:05 +02:00
self._name = name
2012-09-24 22:16:53 +02:00
self.size_limit = size_limit
self.context = context
self._cache_instance.append(self)
2012-09-24 22:16:53 +02:00
def _key(self, key):
if self.context:
2013-02-04 19:59:05 +01:00
return (key, Transaction().user, freeze(Transaction().context))
2012-09-24 22:16:53 +02:00
return key
2016-10-26 15:55:05 +02:00
def get(self, key, default=None):
raise NotImplemented
def set(self, key, value):
raise NotImplemented
def clear(self):
raise NotImplemented
@staticmethod
def clean(dbname):
raise NotImplemented
@staticmethod
def reset(dbname, name):
raise NotImplemented
@staticmethod
def resets(dbname):
raise NotImplemented
@classmethod
def drop(cls, dbname):
raise NotImplemented
class MemoryCache(BaseCache):
"""
A key value LRU cache with size limit.
"""
_resets = {}
_resets_lock = Lock()
_clean_last = datetime.now()
2016-10-26 15:55:05 +02:00
def __init__(self, name, size_limit=1024, context=True):
super(MemoryCache, self).__init__(name, size_limit, context)
self._cache = {}
self._timestamp = None
self._lock = Lock()
2012-09-24 22:16:53 +02:00
def get(self, key, default=None):
dbname = Transaction().database.name
2012-09-24 22:16:53 +02:00
key = self._key(key)
with self._lock:
cache = self._cache.setdefault(dbname, LRUDict(self.size_limit))
try:
2012-09-24 22:16:53 +02:00
result = cache[key] = cache.pop(key)
return result
2013-02-04 19:59:05 +01:00
except (KeyError, TypeError):
2012-09-24 22:16:53 +02:00
return default
2012-09-24 22:16:53 +02:00
def set(self, key, value):
dbname = Transaction().database.name
2012-09-24 22:16:53 +02:00
key = self._key(key)
with self._lock:
cache = self._cache.setdefault(dbname, LRUDict(self.size_limit))
2013-02-04 19:59:05 +01:00
try:
cache[key] = value
except TypeError:
pass
return value
2012-09-24 22:16:53 +02:00
def clear(self):
dbname = Transaction().database.name
Cache.reset(dbname, self._name)
2012-09-24 22:16:53 +02:00
with self._lock:
self._cache[dbname] = LRUDict(self.size_limit)
@classmethod
def clean(cls, dbname):
if (datetime.now() - cls._clean_last).total_seconds() < _clear_timeout:
return
2017-02-02 12:04:27 +01:00
with Transaction().new_transaction(_nocache=True) as transaction,\
transaction.connection.cursor() as cursor:
table = Table('ir_cache')
cursor.execute(*table.select(table.timestamp, table.name))
timestamps = {}
for timestamp, name in cursor.fetchall():
timestamps[name] = timestamp
for inst in cls._cache_instance:
2012-09-24 22:16:53 +02:00
if inst._name in timestamps:
with inst._lock:
if (not inst._timestamp
or timestamps[inst._name] > inst._timestamp):
inst._timestamp = timestamps[inst._name]
inst._cache[dbname] = LRUDict(inst.size_limit)
cls._clean_last = datetime.now()
@classmethod
def reset(cls, dbname, name):
with cls._resets_lock:
cls._resets.setdefault(dbname, set())
cls._resets[dbname].add(name)
@classmethod
def resets(cls, dbname):
table = Table('ir_cache')
resets = cls._resets.setdefault(dbname, set())
if not resets:
return
2017-02-02 12:04:27 +01:00
with Transaction().new_transaction(_nocache=True) as transaction,\
transaction.connection.cursor() as cursor,\
cls._resets_lock:
for name in resets:
cursor.execute(*table.select(table.name,
where=table.name == name,
limit=1))
if cursor.fetchone():
# It would be better to insert only
cursor.execute(*table.update([table.timestamp],
[CurrentTimestamp()],
where=table.name == name))
else:
cursor.execute(*table.insert(
[table.timestamp, table.name],
[[CurrentTimestamp(), name]]))
resets.clear()
2011-06-14 11:44:26 +02:00
@classmethod
def drop(cls, dbname):
for inst in cls._cache_instance:
inst._cache.pop(dbname, None)
2016-10-26 15:55:05 +02:00
if config.get('cache', 'class'):
Cache = resolve(config.get('cache', 'class'))
else:
Cache = MemoryCache
2011-06-14 11:44:26 +02:00
class LRUDict(OrderedDict):
"""
Dictionary with a size limit.
If size limit is reached, it will remove the first added items.
"""
2016-03-03 23:27:38 +01:00
__slots__ = ('size_limit',)
2011-06-14 11:44:26 +02:00
def __init__(self, size_limit, *args, **kwargs):
assert size_limit > 0
self.size_limit = size_limit
super(LRUDict, self).__init__(*args, **kwargs)
self._check_size_limit()
def __setitem__(self, key, value):
super(LRUDict, self).__setitem__(key, value)
self._check_size_limit()
2011-07-09 18:30:30 -07:00
def update(self, *args, **kwargs):
2011-06-14 11:44:26 +02:00
super(LRUDict, self).update(*args, **kwargs)
self._check_size_limit()
def setdefault(self, key, default=None):
default = super(LRUDict, self).setdefault(key, default=default)
self._check_size_limit()
return default
def _check_size_limit(self):
while len(self) > self.size_limit:
self.popitem(last=False)
2016-03-03 23:27:38 +01:00
class LRUDictTransaction(LRUDict):
"""
Dictionary with a size limit. (see LRUDict)
It is refreshed when transaction counter is changed.
"""
__slots__ = ('transaction', 'counter')
def __init__(self, *args, **kwargs):
super(LRUDictTransaction, self).__init__(*args, **kwargs)
self.transaction = Transaction()
self.counter = self.transaction.counter
def clear(self):
super(LRUDictTransaction, self).clear()
self.counter = self.transaction.counter
def refresh(self):
if self.counter != self.transaction.counter:
self.clear()