blob: 711e2b84020a619058b9c7891287ba12690d6fa2 [file] [log] [blame]
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 Edgewall Software
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at http://trac.edgewall.com/license.html.
#
# This software consists of voluntary contributions made by many
# individuals. For the exact contribution history, see the revision
# history and logs, available at http://trac.edgewall.org/.
from __future__ import with_statement
from .core import Component
from .util import arity
from .util.concurrency import ThreadLocal, threading
__all__ = ['CacheManager', 'cached']
_id_to_key = {}
def key_to_id(s):
"""Return a hash of the given property key."""
# This is almost the same algorithm as Python's string hash,
# except we only keep a 31-bit result.
result = ord(s[0]) << 7 if s else 0
for c in s:
result = ((1000003 * result) & 0x7fffffff) ^ ord(c)
result ^= len(s)
_id_to_key[result] = s
return result
class CachedPropertyBase(object):
"""Base class for cached property descriptors"""
def __init__(self, retriever):
self.retriever = retriever
self.__doc__ = retriever.__doc__
def make_key(self, cls):
attr = self.retriever.__name__
for base in cls.mro():
if base.__dict__.get(attr) is self:
cls = base
break
return '%s.%s.%s' % (cls.__module__, cls.__name__, attr)
class CachedSingletonProperty(CachedPropertyBase):
"""Cached property descriptor for classes behaving as singletons
in the scope of one `~trac.env.Environment` instance.
This means there will be no more than one cache to monitor in the
database for this kind of cache. Therefore, using only "static"
information for the key is enough. For the same reason it is also
safe to store the corresponding id as a property of the descriptor
instance.
"""
def __get__(self, instance, owner):
if instance is None:
return self
try:
id = self.id
except AttributeError:
id = self.id = key_to_id(self.make_key(owner))
return CacheManager(instance.env).get(id, self.retriever, instance)
def __delete__(self, instance):
try:
id = self.id
except AttributeError:
id = self.id = key_to_id(self.make_key(instance.__class__))
CacheManager(instance.env).invalidate(id)
class CachedProperty(CachedPropertyBase):
"""Cached property descriptor for classes having potentially
multiple instances associated to a single `~trac.env.Environment`
instance.
As we'll have potentiall many different caches to monitor for this
kind of cache, the key needs to be augmented by a string unique to
each instance of the owner class. As the resulting id will be
different for each instance of the owner class, we can't store it
as a property of the descriptor class, so we store it back in the
attribute used for augmenting the key (``key_attr``).
"""
def __init__(self, retriever, key_attr):
super(CachedProperty, self).__init__(retriever)
self.key_attr = key_attr
def __get__(self, instance, owner):
if instance is None:
return self
id = getattr(instance, self.key_attr)
if isinstance(id, str):
id = key_to_id(self.make_key(owner) + ':' + id)
setattr(instance, self.key_attr, id)
return CacheManager(instance.env).get(id, self.retriever, instance)
def __delete__(self, instance):
id = getattr(instance, self.key_attr)
if isinstance(id, str):
id = key_to_id(self.make_key(instance.__class__) + ':' + id)
setattr(instance, self.key_attr, id)
CacheManager(instance.env).invalidate(id)
def cached(fn_or_attr=None):
"""Method decorator creating a cached attribute from a data
retrieval method.
Accessing the cached attribute gives back the cached value. The
data retrieval method is transparently called by the
`CacheManager` on first use after the program start or after the
cache has been invalidated. Invalidating the cache for this value
is done by ``del``\ eting the attribute.
Note that the cache validity is maintained using the `cache` table
in the database. Cache invalidation is performed within a
transaction block, and can be nested within another transaction
block.
When the decorator is used in a class for which instances behave
as singletons within the scope of a given `~trac.env.Environment`
(typically `~trac.core.Component` classes), the key used to
identify the attribute in the database is constructed from the
names of the containing module, class and retriever method::
class WikiSystem(Component):
@cached
def pages(self):
return set(name for name, in self.env.db_query(
"SELECT DISTINCT name FROM wiki"))
Otherwise, when the decorator is used in non-"singleton" objects,
a string specifying the name of an attribute containing a string
unique to the instance must be passed to the decorator. This value
will be appended to the key constructed from module, class and
method name::
class SomeClass(object):
def __init__(self, env, name):
self.env = env
self.name = name
self._metadata_id = name
@cached('_metadata_id')
def metadata(self):
...
Note that in this case the key attribute is overwritten with a
hash of the key on first access, so it should not be used for any
other purpose.
In either case, this decorator requires that the object on which
it is used has an ``env`` attribute containing the application
`~trac.env.Environment`.
.. versionchanged:: 1.0
The data retrieval method used to be called with a single
argument ``db`` containing a reference to a database
connection. This is the same connection that can be retrieved
via the normal `~trac.env.Environment.db_query` or
`~trac.env.Environment.db_transaction`, so this is no longer
needed, though methods supporting that argument are still
supported (but will be removed in version 1.1.1).
"""
if hasattr(fn_or_attr, '__call__'):
return CachedSingletonProperty(fn_or_attr)
def decorator(fn):
return CachedProperty(fn, fn_or_attr)
return decorator
class CacheManager(Component):
"""Cache manager."""
required = True
def __init__(self):
self._cache = {}
self._local = ThreadLocal(meta=None, cache=None)
self._lock = threading.RLock()
# Public interface
def reset_metadata(self):
"""Reset per-request cache metadata."""
self._local.meta = self._local.cache = None
def get(self, id, retriever, instance):
"""Get cached or fresh data for the given id."""
# Get cache metadata
local_meta = self._local.meta
local_cache = self._local.cache
if local_meta is None:
# First cache usage in this request, retrieve cache metadata
# from the database and make a thread-local copy of the cache
meta = self.env.db_query("SELECT id, generation FROM cache")
self._local.meta = local_meta = dict(meta)
self._local.cache = local_cache = self._cache.copy()
db_generation = local_meta.get(id, -1)
# Try the thread-local copy first
try:
(data, generation) = local_cache[id]
if generation == db_generation:
return data
except KeyError:
pass
with self.env.db_query as db:
with self._lock:
# Get data from the process cache
try:
(data, generation) = local_cache[id] = self._cache[id]
if generation == db_generation:
return data
except KeyError:
generation = None # Force retrieval from the database
# Check if the process cache has the newest version, as it may
# have been updated after the metadata retrieval
for db_generation, in db(
"SELECT generation FROM cache WHERE id=%s", (id,)):
break
else:
db_generation = -1
if db_generation == generation:
return data
# Retrieve data from the database
if arity(retriever) == 2:
data = retriever(instance, db)
else:
data = retriever(instance)
local_cache[id] = self._cache[id] = (data, db_generation)
local_meta[id] = db_generation
return data
def invalidate(self, id):
"""Invalidate cached data for the given id."""
with self.env.db_transaction as db:
with self._lock:
# Invalidate in other processes
# The row corresponding to the cache may not exist in the table
# yet.
# - If the row exists, the UPDATE increments the generation,
# the SELECT returns a row and we're done.
# - If the row doesn't exist, the UPDATE does nothing, but
# starts a transaction. The SELECT then returns nothing,
# and we can safely INSERT a new row.
db("UPDATE cache SET generation=generation+1 WHERE id=%s",
(id,))
if not db("SELECT generation FROM cache WHERE id=%s", (id,)):
db("INSERT INTO cache VALUES (%s, %s, %s)",
(id, 0, _id_to_key.get(id, '<unknown>')))
# Invalidate in this process
self._cache.pop(id, None)
# Invalidate in this thread
try:
del self._local.cache[id]
except (KeyError, TypeError):
pass