Files
tryton/trytond/cache.py
T

426 lines
14 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.
2019-04-11 18:35:25 +02:00
import datetime as dt
2019-03-09 17:03:38 +01:00
import json
import logging
2020-11-27 22:19:56 +01:00
import os
2019-03-09 17:03:38 +01:00
import select
import threading
2019-01-18 11:57:11 +01:00
from collections import OrderedDict, defaultdict
from datetime import datetime
2019-01-18 11:57:11 +01:00
from weakref import WeakKeyDictionary
2014-01-31 00:02:03 +01:00
from sql import Table
2019-01-18 11:57:11 +01:00
from sql.aggregate import Max
from sql.functions import CurrentTimestamp, Function
from trytond import backend
2016-10-26 15:55:05 +02:00
from trytond.config import config
2021-01-20 00:52:07 +01:00
from trytond.pool import Pool
from trytond.transaction import Transaction
from trytond.tools import resolve, grouped_slice
2020-06-13 09:48:03 +02:00
__all__ = ['BaseCache', 'Cache', 'LRUDict', 'LRUDictTransaction']
_clear_timeout = config.getint('cache', 'clean_timeout', default=5 * 60)
2019-03-09 17:03:38 +01:00
logger = logging.getLogger(__name__)
2013-02-04 19:59:05 +01:00
def _cast(column):
class SQLite_DateTime(Function):
__slots__ = ()
_function = 'DATETIME'
2019-12-06 17:33:47 +01:00
if backend.name == 'sqlite':
column = SQLite_DateTime(column)
return column
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
2021-01-20 00:52:07 +01:00
def _get_modules(cursor):
ir_module = Table('ir_module')
cursor.execute(*ir_module.select(
ir_module.name,
where=ir_module.state.in_(
['activated', 'to upgrade', 'to remove'])))
return {m for m, in cursor}
2016-10-26 15:55:05 +02:00
class BaseCache(object):
2019-01-18 11:57:11 +01:00
_instances = {}
2019-04-11 18:35:25 +02:00
def __init__(self, name, size_limit=1024, duration=None, 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
2019-04-11 18:35:25 +02:00
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
2019-01-18 11:57:11 +01:00
assert self._name not in self._instances
self._instances[self._name] = 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 NotImplementedError
2016-10-26 15:55:05 +02:00
def set(self, key, value):
raise NotImplementedError
2016-10-26 15:55:05 +02:00
def clear(self):
raise NotImplementedError
2016-10-26 15:55:05 +02:00
2019-01-18 11:57:11 +01:00
@classmethod
2021-01-13 00:26:53 +01:00
def clear_all(cls):
for inst in cls._instances.values():
inst.clear()
@classmethod
2019-01-18 11:57:11 +01:00
def sync(cls, transaction):
raise NotImplementedError
2016-10-26 15:55:05 +02:00
def sync_since(self, value):
raise NotImplementedError
2019-01-18 11:57:11 +01:00
@classmethod
def commit(cls, transaction):
raise NotImplementedError
2016-10-26 15:55:05 +02:00
2019-01-18 11:57:11 +01:00
@classmethod
def rollback(cls, transaction):
raise NotImplementedError
2016-10-26 15:55:05 +02:00
@classmethod
def drop(cls, dbname):
raise NotImplementedError
2016-10-26 15:55:05 +02:00
class MemoryCache(BaseCache):
"""
A key value LRU cache with size limit.
"""
2019-01-18 11:57:11 +01:00
_reset = WeakKeyDictionary()
_clean_last = datetime.now()
_default_lower = Transaction.monotonic_time()
2019-03-09 17:03:38 +01:00
_listener = {}
2020-11-27 22:19:56 +01:00
_listener_lock = defaultdict(threading.Lock)
2019-03-09 17:03:38 +01:00
_table = 'ir_cache'
_channel = _table
2016-10-26 15:55:05 +02:00
2019-04-11 18:35:25 +02:00
def __init__(self, *args, **kwargs):
super(MemoryCache, self).__init__(*args, **kwargs)
self._database_cache = defaultdict(lambda: LRUDict(self.size_limit))
2019-01-18 11:57:11 +01:00
self._transaction_cache = WeakKeyDictionary()
self._transaction_lower = {}
2019-01-30 10:05:55 +01:00
self._timestamp = {}
2019-01-18 11:57:11 +01:00
def _get_cache(self):
transaction = Transaction()
dbname = transaction.database.name
lower = self._transaction_lower.get(dbname, self._default_lower)
if (transaction in self._reset
or transaction.started_at < lower):
2019-01-18 11:57:11 +01:00
try:
return self._transaction_cache[transaction]
except KeyError:
2019-03-09 17:03:38 +01:00
cache = self._database_cache.default_factory()
self._transaction_cache[transaction] = cache
return cache
2019-01-18 11:57:11 +01:00
else:
return self._database_cache[dbname]
2016-10-26 15:55:05 +02:00
2012-09-24 22:16:53 +02:00
def get(self, key, default=None):
key = self._key(key)
2019-01-18 11:57:11 +01:00
cache = self._get_cache()
try:
2019-04-11 18:35:25 +02:00
(expire, result) = cache.pop(key)
if expire and expire < dt.datetime.now():
return default
cache[key] = (expire, result)
2019-01-18 11:57:11 +01:00
return result
except (KeyError, TypeError):
return default
2012-09-24 22:16:53 +02:00
def set(self, key, value):
key = self._key(key)
2019-01-18 11:57:11 +01:00
cache = self._get_cache()
2019-04-11 18:35:25 +02:00
if self.duration:
expire = dt.datetime.now() + self.duration
else:
expire = None
2019-01-18 11:57:11 +01:00
try:
2019-04-11 18:35:25 +02:00
cache[key] = (expire, value)
2019-01-18 11:57:11 +01:00
except TypeError:
pass
return value
2012-09-24 22:16:53 +02:00
def clear(self):
2019-01-18 11:57:11 +01:00
transaction = Transaction()
self._reset.setdefault(transaction, set()).add(self._name)
self._transaction_cache.pop(transaction, None)
2019-03-09 17:03:38 +01:00
def _clear(self, dbname, timestamp=None):
logger.debug("clearing cache '%s' of '%s'", self._name, dbname)
self._timestamp[dbname] = timestamp
self._database_cache[dbname] = self._database_cache.default_factory()
self._transaction_lower[dbname] = max(
Transaction.monotonic_time(),
self._transaction_lower.get(dbname, self._default_lower))
@classmethod
2019-01-18 11:57:11 +01:00
def sync(cls, transaction):
database = transaction.database
dbname = database.name
if not _clear_timeout and database.has_channel():
2020-11-27 22:19:56 +01:00
pid = os.getpid()
with cls._listener_lock[pid]:
if (pid, dbname) not in cls._listener:
cls._listener[pid, dbname] = listener = threading.Thread(
2019-03-09 17:03:38 +01:00
target=cls._listen, args=(dbname,), daemon=True)
listener.start()
return
if (datetime.now() - cls._clean_last).total_seconds() < _clear_timeout:
return
connection = database.get_connection(readonly=True, autocommit=True)
try:
with connection.cursor() as cursor:
table = Table(cls._table)
cursor.execute(*table.select(
_cast(table.timestamp), table.name))
timestamps = {}
for timestamp, name in cursor.fetchall():
timestamps[name] = timestamp
2021-01-20 00:52:07 +01:00
modules = _get_modules(cursor)
finally:
database.put_connection(connection)
2019-01-18 11:57:11 +01:00
for name, timestamp in timestamps.items():
try:
inst = cls._instances[name]
except KeyError:
continue
2019-01-30 10:05:55 +01:00
inst_timestamp = inst._timestamp.get(dbname)
if not inst_timestamp or timestamp > inst_timestamp:
2019-03-09 17:03:38 +01:00
inst._clear(dbname, timestamp)
2021-01-20 00:52:07 +01:00
Pool(dbname).refresh(modules)
cls._clean_last = datetime.now()
def sync_since(self, value):
return self._clean_last > value
@classmethod
2019-01-18 11:57:11 +01:00
def commit(cls, transaction):
2019-03-09 17:03:38 +01:00
table = Table(cls._table)
2019-01-18 11:57:11 +01:00
reset = cls._reset.setdefault(transaction, set())
if not reset:
return
database = transaction.database
dbname = database.name
if not _clear_timeout and transaction.database.has_channel():
with transaction.connection.cursor() as cursor:
# The count computed as
# 8000 (max notify size) / 64 (max name data len)
for sub_reset in grouped_slice(reset, 125):
cursor.execute(
'NOTIFY "%s", %%s' % cls._channel,
(json.dumps(list(sub_reset), separators=(',', ':')),))
else:
connection = database.get_connection(
readonly=False, autocommit=True)
try:
with connection.cursor() as cursor:
for name in reset:
cursor.execute(*table.select(table.name, table.id,
table.timestamp,
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]]))
cursor.execute(*table.select(
Max(table.timestamp),
2019-03-09 17:03:38 +01:00
where=table.name == name))
timestamp, = cursor.fetchone()
2019-03-09 17:03:38 +01:00
cursor.execute(*table.select(
_cast(Max(table.timestamp)),
where=table.name == name))
timestamp, = cursor.fetchone()
2019-01-18 11:57:11 +01:00
inst = cls._instances[name]
inst._clear(dbname, timestamp)
connection.commit()
finally:
database.put_connection(connection)
cls._clean_last = datetime.now()
2019-03-09 17:03:38 +01:00
reset.clear()
2019-01-18 11:57:11 +01:00
@classmethod
def rollback(cls, transaction):
try:
cls._reset[transaction].clear()
except KeyError:
pass
2011-06-14 11:44:26 +02:00
@classmethod
def drop(cls, dbname):
2020-11-27 22:19:56 +01:00
pid = os.getpid()
with cls._listener_lock[pid]:
listener = cls._listener.pop((pid, dbname), None)
2019-03-09 17:03:38 +01:00
if listener:
2019-12-06 17:33:47 +01:00
database = backend.Database(dbname)
2019-03-09 17:03:38 +01:00
conn = database.get_connection()
try:
cursor = conn.cursor()
cursor.execute('NOTIFY "%s"' % cls._channel)
conn.commit()
finally:
database.put_connection(conn)
listener.join()
2019-01-18 11:57:11 +01:00
for inst in cls._instances.values():
2019-01-30 10:05:55 +01:00
inst._timestamp.pop(dbname, None)
2019-01-18 11:57:11 +01:00
inst._database_cache.pop(dbname, None)
inst._transaction_lower.pop(dbname, None)
2021-01-20 00:52:07 +01:00
@classmethod
def refresh_pool(cls, transaction):
database = transaction.database
dbname = database.name
if not _clear_timeout and database.has_channel():
database = backend.Database(dbname)
conn = database.get_connection()
try:
cursor = conn.cursor()
cursor.execute(
'NOTIFY "%s", %%s' % cls._channel, ('refresh pool',))
conn.commit()
finally:
database.put_connection(conn)
2019-03-09 17:03:38 +01:00
@classmethod
def _listen(cls, dbname):
2019-12-06 17:33:47 +01:00
database = backend.Database(dbname)
2019-03-09 17:03:38 +01:00
if not database.has_channel():
raise NotImplementedError
logger.info("listening on channel '%s' of '%s'", cls._channel, dbname)
2021-01-03 13:00:26 +01:00
conn = database.get_connection(autocommit=True)
2020-11-27 22:19:56 +01:00
pid = os.getpid()
current_thread = threading.current_thread()
2019-03-09 17:03:38 +01:00
try:
cursor = conn.cursor()
cursor.execute('LISTEN "%s"' % cls._channel)
2020-11-27 22:19:56 +01:00
while cls._listener.get((pid, dbname)) == current_thread:
2019-03-09 17:03:38 +01:00
readable, _, _ = select.select([conn], [], [])
if not readable:
continue
conn.poll()
while conn.notifies:
notification = conn.notifies.pop()
2021-01-20 00:52:07 +01:00
if notification.payload == 'refresh pool':
Pool(dbname).refresh(_get_modules(cursor))
elif notification.payload:
2019-03-09 17:03:38 +01:00
reset = json.loads(notification.payload)
for name in reset:
inst = cls._instances[name]
inst._clear(dbname)
cls._clean_last = datetime.now()
2019-03-09 17:03:38 +01:00
except Exception:
logger.error(
"cache listener on '%s' crashed", dbname, exc_info=True)
raise
finally:
database.put_connection(conn)
2020-11-27 22:19:56 +01:00
with cls._listener_lock[pid]:
if cls._listener.get((pid, dbname)) == current_thread:
del cls._listener[pid, dbname]
2019-03-09 17:03:38 +01:00
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.
2020-06-13 09:48:03 +02:00
The default_factory provides the same behavior as in standard
collections.defaultdict.
2011-06-14 11:44:26 +02:00
"""
2016-03-03 23:27:38 +01:00
__slots__ = ('size_limit',)
2020-06-13 09:48:03 +02:00
def __init__(self, size_limit, default_factory=None, *args, **kwargs):
2011-06-14 11:44:26 +02:00
assert size_limit > 0
self.size_limit = size_limit
super(LRUDict, self).__init__(*args, **kwargs)
2020-06-13 09:48:03 +02:00
self.default_factory = default_factory
2011-06-14 11:44:26 +02:00
self._check_size_limit()
def __setitem__(self, key, value):
super(LRUDict, self).__setitem__(key, value)
self._check_size_limit()
2020-06-13 09:48:03 +02:00
def __missing__(self, key):
if self.default_factory is None:
raise KeyError(key)
self[key] = value = self.default_factory()
return value
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):
"""
2020-06-13 09:48:03 +02:00
Dictionary with a size limit and default_factory. (see LRUDict)
2016-03-03 23:27:38 +01:00
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()