"Fossies" - the Fresh Open Source Software Archive

Member "PURELIB/trac/cache.py" (27 Aug 2019, 10418 Bytes) of package /windows/misc/Trac-1.4.win-amd64.exe:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. See also the last Fossies "Diffs" side-by-side code changes report for "cache.py": 1.3.5_vs_1.3.6.

    1 # -*- coding: utf-8 -*-
    2 #
    3 # Copyright (C) 2009-2019 Edgewall Software
    4 # All rights reserved.
    5 #
    6 # This software is licensed as described in the file COPYING, which
    7 # you should have received as part of this distribution. The terms
    8 # are also available at https://trac.edgewall.org/wiki/TracLicense.
    9 #
   10 # This software consists of voluntary contributions made by many
   11 # individuals. For the exact contribution history, see the revision
   12 # history and logs, available at https://trac.edgewall.org/.
   13 
   14 import functools
   15 
   16 from trac.core import Component
   17 from trac.util.concurrency import ThreadLocal, threading
   18 
   19 __all__ = ['CacheManager', 'cached']
   20 
   21 _id_to_key = {}
   22 
   23 
   24 def key_to_id(s):
   25     """Return a hash of the given property key."""
   26     # This is almost the same algorithm as Python's string hash,
   27     # except we only keep a 31-bit result.
   28     result = ord(s[0]) << 7 if s else 0
   29     for c in s:
   30         result = ((1000003 * result) & 0x7fffffff) ^ ord(c)
   31     result ^= len(s)
   32     _id_to_key[result] = s
   33     return result
   34 
   35 
   36 class CachedPropertyBase(property):
   37     """Base class for cached property descriptors.
   38 
   39     :since 1.0.2: inherits from `property`.
   40     """
   41 
   42     def __init__(self, retriever):
   43         self.retriever = retriever
   44         functools.update_wrapper(self, retriever)
   45 
   46     def make_key(self, cls):
   47         attr = self.retriever.__name__
   48         for base in cls.mro():
   49             if base.__dict__.get(attr) is self:
   50                 cls = base
   51                 break
   52         return '%s.%s.%s' % (cls.__module__, cls.__name__, attr)
   53 
   54 
   55 class CachedSingletonProperty(CachedPropertyBase):
   56     """Cached property descriptor for classes behaving as singletons
   57     in the scope of one `~trac.env.Environment` instance.
   58 
   59     This means there will be no more than one cache to monitor in the
   60     database for this kind of cache. Therefore, using only "static"
   61     information for the key is enough. For the same reason it is also
   62     safe to store the corresponding id as a property of the descriptor
   63     instance.
   64     """
   65 
   66     def __get__(self, instance, owner):
   67         if instance is None:
   68             return self
   69         try:
   70             id = self.id
   71         except AttributeError:
   72             id = self.id = key_to_id(self.make_key(owner))
   73         return CacheManager(instance.env).get(id, self.retriever, instance)
   74 
   75     def __delete__(self, instance):
   76         try:
   77             id = self.id
   78         except AttributeError:
   79             id = self.id = key_to_id(self.make_key(instance.__class__))
   80         CacheManager(instance.env).invalidate(id)
   81 
   82 
   83 class CachedProperty(CachedPropertyBase):
   84     """Cached property descriptor for classes having potentially
   85     multiple instances associated to a single `~trac.env.Environment`
   86     instance.
   87 
   88     As we'll have potentially many different caches to monitor for this
   89     kind of cache, the key needs to be augmented by a string unique to
   90     each instance of the owner class.  As the resulting id will be
   91     different for each instance of the owner class, we can't store it
   92     as a property of the descriptor class, so we store it back in the
   93     attribute used for augmenting the key (``key_attr``).
   94     """
   95 
   96     def __init__(self, retriever, key_attr):
   97         super(CachedProperty, self).__init__(retriever)
   98         self.key_attr = key_attr
   99 
  100     def __get__(self, instance, owner):
  101         if instance is None:
  102             return self
  103         id = getattr(instance, self.key_attr)
  104         if isinstance(id, str):
  105             id = key_to_id(self.make_key(owner) + ':' + id)
  106             setattr(instance, self.key_attr, id)
  107         return CacheManager(instance.env).get(id, self.retriever, instance)
  108 
  109     def __delete__(self, instance):
  110         id = getattr(instance, self.key_attr)
  111         if isinstance(id, str):
  112             id = key_to_id(self.make_key(instance.__class__) + ':' + id)
  113             setattr(instance, self.key_attr, id)
  114         CacheManager(instance.env).invalidate(id)
  115 
  116 
  117 def cached(fn_or_attr=None):
  118     """Method decorator creating a cached attribute from a data
  119     retrieval method.
  120 
  121     Accessing the cached attribute gives back the cached value.  The
  122     data retrieval method is transparently called by the
  123     `CacheManager` on first use after the program start or after the
  124     cache has been invalidated.  Invalidating the cache for this value
  125     is done by ``del``\ eting the attribute.
  126 
  127     Note that the cache validity is maintained using the `cache` table
  128     in the database.  Cache invalidation is performed within a
  129     transaction block, and can be nested within another transaction
  130     block.
  131 
  132     When the decorator is used in a class for which instances behave
  133     as singletons within the scope of a given `~trac.env.Environment`
  134     (typically `~trac.core.Component` classes), the key used to
  135     identify the attribute in the database is constructed from the
  136     names of the containing module, class and retriever method::
  137 
  138         class WikiSystem(Component):
  139             @cached
  140             def pages(self):
  141                 return {name for name, in self.env.db_query(
  142                             "SELECT DISTINCT name FROM wiki")}
  143 
  144     Otherwise, when the decorator is used in non-"singleton" objects,
  145     a string specifying the name of an attribute containing a string
  146     unique to the instance must be passed to the decorator. This value
  147     will be appended to the key constructed from module, class and
  148     method name::
  149 
  150         class SomeClass(object):
  151             def __init__(self, env, name):
  152                 self.env = env
  153                 self.name = name
  154                 self._metadata_id = name
  155 
  156             @cached('_metadata_id')
  157             def metadata(self):
  158                 ...
  159 
  160     Note that in this case the key attribute is overwritten with a
  161     hash of the key on first access, so it should not be used for any
  162     other purpose.
  163 
  164     In either case, this decorator requires that the object on which
  165     it is used has an ``env`` attribute containing the application
  166     `~trac.env.Environment`.
  167 
  168     .. versionchanged:: 1.0
  169         The data retrieval method used to be called with a single
  170         argument ``db`` containing a reference to a database
  171         connection.  This is the same connection that can be retrieved
  172         via the normal `~trac.env.Environment.db_query` or
  173         `~trac.env.Environment.db_transaction`, so this is no longer
  174         needed and is not supported.
  175     """
  176     if hasattr(fn_or_attr, '__call__'):
  177         return CachedSingletonProperty(fn_or_attr)
  178     def decorator(fn):
  179         return CachedProperty(fn, fn_or_attr)
  180     return decorator
  181 
  182 
  183 class CacheManager(Component):
  184     """Cache manager."""
  185 
  186     required = True
  187 
  188     def __init__(self):
  189         self._cache = {}
  190         self._local = ThreadLocal(meta=None, cache=None)
  191         self._lock = threading.RLock()
  192 
  193     # Public interface
  194 
  195     def reset_metadata(self):
  196         """Reset per-request cache metadata."""
  197         self._local.meta = self._local.cache = None
  198 
  199     def get(self, id, retriever, instance):
  200         """Get cached or fresh data for the given id."""
  201         # Get cache metadata
  202         local_meta = self._local.meta
  203         local_cache = self._local.cache
  204         if local_meta is None:
  205             # First cache usage in this request, retrieve cache metadata
  206             # from the database and make a thread-local copy of the cache
  207             meta = self.env.db_query("SELECT id, generation FROM cache")
  208             self._local.meta = local_meta = dict(meta)
  209             self._local.cache = local_cache = self._cache.copy()
  210 
  211         db_generation = local_meta.get(id, -1)
  212 
  213         # Try the thread-local copy first
  214         try:
  215             data, generation = local_cache[id]
  216             if generation == db_generation:
  217                 return data
  218         except KeyError:
  219             pass
  220 
  221         with self.env.db_query as db:
  222             with self._lock:
  223                 # Get data from the process cache
  224                 try:
  225                     data, generation = local_cache[id] = self._cache[id]
  226                     if generation == db_generation:
  227                         return data
  228                 except KeyError:
  229                     generation = None   # Force retrieval from the database
  230 
  231                 # Check if the process cache has the newest version, as it may
  232                 # have been updated after the metadata retrieval
  233                 for db_generation, in db(
  234                         "SELECT generation FROM cache WHERE id=%s", (id,)):
  235                     break
  236                 else:
  237                     db_generation = -1
  238                 if db_generation == generation:
  239                     return data
  240 
  241                 # Retrieve data from the database
  242                 data = retriever(instance)
  243                 local_cache[id] = self._cache[id] = data, db_generation
  244                 local_meta[id] = db_generation
  245                 return data
  246 
  247     def invalidate(self, id):
  248         """Invalidate cached data for the given id."""
  249         with self.env.db_transaction as db:
  250             with self._lock:
  251                 # Invalidate in other processes
  252 
  253                 # The row corresponding to the cache may not exist in the table
  254                 # yet.
  255                 #  - If the row exists, the UPDATE increments the generation,
  256                 #    the SELECT returns a row and we're done.
  257                 #  - If the row doesn't exist, the UPDATE does nothing, but
  258                 #    starts a transaction. The SELECT then returns nothing,
  259                 #    and we can safely INSERT a new row.
  260                 db("UPDATE cache SET generation=generation+1 WHERE id=%s",
  261                    (id,))
  262                 if not db("SELECT generation FROM cache WHERE id=%s", (id,)):
  263                     db("INSERT INTO cache VALUES (%s, %s, %s)",
  264                        (id, 0, _id_to_key.get(id, '<unknown>')))
  265 
  266                 # Invalidate in this process
  267                 self._cache.pop(id, None)
  268 
  269                 # Invalidate in this thread
  270                 try:
  271                     del self._local.cache[id]
  272                 except (KeyError, TypeError):
  273                     pass