def test_del_multi(self): cache = LruCache(4, 2, cache_type=TreeCache) cache[("animal", "cat")] = "mew" cache[("animal", "dog")] = "woof" cache[("vehicles", "car")] = "vroom" cache[("vehicles", "train")] = "chuff" self.assertEquals(len(cache), 4) self.assertEquals(cache.get(("animal", "cat")), "mew") self.assertEquals(cache.get(("vehicles", "car")), "vroom") cache.del_multi(("animal",)) self.assertEquals(len(cache), 2) self.assertEquals(cache.get(("animal", "cat")), None) self.assertEquals(cache.get(("animal", "dog")), None) self.assertEquals(cache.get(("vehicles", "car")), "vroom") self.assertEquals(cache.get(("vehicles", "train")), "chuff")
def test_del_multi(self): m1 = Mock() m2 = Mock() m3 = Mock() m4 = Mock() cache = LruCache(4, 2, cache_type=TreeCache) cache.set(("a", "1"), "value", callbacks=[m1]) cache.set(("a", "2"), "value", callbacks=[m2]) cache.set(("b", "1"), "value", callbacks=[m3]) cache.set(("b", "2"), "value", callbacks=[m4]) self.assertEquals(m1.call_count, 0) self.assertEquals(m2.call_count, 0) self.assertEquals(m3.call_count, 0) self.assertEquals(m4.call_count, 0) cache.del_multi(("a",)) self.assertEquals(m1.call_count, 1) self.assertEquals(m2.call_count, 1) self.assertEquals(m3.call_count, 0) self.assertEquals(m4.call_count, 0)
class Cache(object): __slots__ = ( "cache", "name", "keylen", "thread", "metrics", "_pending_deferred_cache", ) def __init__( self, name: str, max_entries: int = 1000, keylen: int = 1, tree: bool = False, iterable: bool = False, apply_cache_factor_from_config: bool = True, ): """ Args: name: The name of the cache max_entries: Maximum amount of entries that the cache will hold keylen: The length of the tuple used as the cache key tree: Use a TreeCache instead of a dict as the underlying cache type iterable: If True, count each item in the cached object as an entry, rather than each cached object apply_cache_factor_from_config: Whether cache factors specified in the config file affect `max_entries` Returns: Cache """ cache_type = TreeCache if tree else dict self._pending_deferred_cache = cache_type() self.cache = LruCache( max_size=max_entries, keylen=keylen, cache_type=cache_type, size_callback=(lambda d: len(d)) if iterable else None, evicted_callback=self._on_evicted, apply_cache_factor_from_config=apply_cache_factor_from_config, ) self.name = name self.keylen = keylen self.thread = None self.metrics = register_cache( "cache", name, self.cache, collect_callback=self._metrics_collection_callback, ) @property def max_entries(self): return self.cache.max_size def _on_evicted(self, evicted_count): self.metrics.inc_evictions(evicted_count) def _metrics_collection_callback(self): cache_pending_metric.labels(self.name).set( len(self._pending_deferred_cache)) def check_thread(self): expected_thread = self.thread if expected_thread is None: self.thread = threading.current_thread() else: if expected_thread is not threading.current_thread(): raise ValueError( "Cache objects can only be accessed from the main thread") def get(self, key, default=_CacheSentinel, callback=None, update_metrics=True): """Looks the key up in the caches. Args: key(tuple) default: What is returned if key is not in the caches. If not specified then function throws KeyError instead callback(fn): Gets called when the entry in the cache is invalidated update_metrics (bool): whether to update the cache hit rate metrics Returns: Either an ObservableDeferred or the raw result """ callbacks = [callback] if callback else [] val = self._pending_deferred_cache.get(key, _CacheSentinel) if val is not _CacheSentinel: val.callbacks.update(callbacks) if update_metrics: self.metrics.inc_hits() return val.deferred val = self.cache.get(key, _CacheSentinel, callbacks=callbacks) if val is not _CacheSentinel: self.metrics.inc_hits() return val if update_metrics: self.metrics.inc_misses() if default is _CacheSentinel: raise KeyError() else: return default def set(self, key, value, callback=None): if not isinstance(value, defer.Deferred): raise TypeError("not a Deferred") callbacks = [callback] if callback else [] self.check_thread() observable = ObservableDeferred(value, consumeErrors=True) observer = defer.maybeDeferred(observable.observe) entry = CacheEntry(deferred=observable, callbacks=callbacks) existing_entry = self._pending_deferred_cache.pop(key, None) if existing_entry: existing_entry.invalidate() self._pending_deferred_cache[key] = entry def compare_and_pop(): """Check if our entry is still the one in _pending_deferred_cache, and if so, pop it. Returns true if the entries matched. """ existing_entry = self._pending_deferred_cache.pop(key, None) if existing_entry is entry: return True # oops, the _pending_deferred_cache has been updated since # we started our query, so we are out of date. # # Better put back whatever we took out. (We do it this way # round, rather than peeking into the _pending_deferred_cache # and then removing on a match, to make the common case faster) if existing_entry is not None: self._pending_deferred_cache[key] = existing_entry return False def cb(result): if compare_and_pop(): self.cache.set(key, result, entry.callbacks) else: # we're not going to put this entry into the cache, so need # to make sure that the invalidation callbacks are called. # That was probably done when _pending_deferred_cache was # updated, but it's possible that `set` was called without # `invalidate` being previously called, in which case it may # not have been. Either way, let's double-check now. entry.invalidate() def eb(_fail): compare_and_pop() entry.invalidate() # once the deferred completes, we can move the entry from the # _pending_deferred_cache to the real cache. # observer.addCallbacks(cb, eb) return observable def prefill(self, key, value, callback=None): callbacks = [callback] if callback else [] self.cache.set(key, value, callbacks=callbacks) def invalidate(self, key): self.check_thread() self.cache.pop(key, None) # if we have a pending lookup for this key, remove it from the # _pending_deferred_cache, which will (a) stop it being returned # for future queries and (b) stop it being persisted as a proper entry # in self.cache. entry = self._pending_deferred_cache.pop(key, None) # run the invalidation callbacks now, rather than waiting for the # deferred to resolve. if entry: entry.invalidate() def invalidate_many(self, key): self.check_thread() if not isinstance(key, tuple): raise TypeError("The cache key must be a tuple not %r" % (type(key), )) self.cache.del_multi(key) # if we have a pending lookup for this key, remove it from the # _pending_deferred_cache, as above entry_dict = self._pending_deferred_cache.pop(key, None) if entry_dict is not None: for entry in iterate_tree_cache_entry(entry_dict): entry.invalidate() def invalidate_all(self): self.check_thread() self.cache.clear() for entry in self._pending_deferred_cache.values(): entry.invalidate() self._pending_deferred_cache.clear()
class Cache(object): __slots__ = ( "cache", "max_entries", "name", "keylen", "thread", "metrics", "_pending_deferred_cache", ) def __init__(self, name, max_entries=1000, keylen=1, tree=False, iterable=False): cache_type = TreeCache if tree else dict self._pending_deferred_cache = cache_type() self.cache = LruCache( max_size=max_entries, keylen=keylen, cache_type=cache_type, size_callback=(lambda d: len(d)) if iterable else None, evicted_callback=self._on_evicted, ) self.name = name self.keylen = keylen self.thread = None self.metrics = register_cache("cache", name, self.cache) def _on_evicted(self, evicted_count): self.metrics.inc_evictions(evicted_count) def check_thread(self): expected_thread = self.thread if expected_thread is None: self.thread = threading.current_thread() else: if expected_thread is not threading.current_thread(): raise ValueError( "Cache objects can only be accessed from the main thread") def get(self, key, default=_CacheSentinel, callback=None, update_metrics=True): """Looks the key up in the caches. Args: key(tuple) default: What is returned if key is not in the caches. If not specified then function throws KeyError instead callback(fn): Gets called when the entry in the cache is invalidated update_metrics (bool): whether to update the cache hit rate metrics Returns: Either a Deferred or the raw result """ callbacks = [callback] if callback else [] val = self._pending_deferred_cache.get(key, _CacheSentinel) if val is not _CacheSentinel: val.callbacks.update(callbacks) if update_metrics: self.metrics.inc_hits() return val.deferred val = self.cache.get(key, _CacheSentinel, callbacks=callbacks) if val is not _CacheSentinel: self.metrics.inc_hits() return val if update_metrics: self.metrics.inc_misses() if default is _CacheSentinel: raise KeyError() else: return default def set(self, key, value, callback=None): callbacks = [callback] if callback else [] self.check_thread() entry = CacheEntry( deferred=value, callbacks=callbacks, ) existing_entry = self._pending_deferred_cache.pop(key, None) if existing_entry: existing_entry.invalidate() self._pending_deferred_cache[key] = entry def shuffle(result): existing_entry = self._pending_deferred_cache.pop(key, None) if existing_entry is entry: self.cache.set(key, result, entry.callbacks) else: # oops, the _pending_deferred_cache has been updated since # we started our query, so we are out of date. # # Better put back whatever we took out. (We do it this way # round, rather than peeking into the _pending_deferred_cache # and then removing on a match, to make the common case faster) if existing_entry is not None: self._pending_deferred_cache[key] = existing_entry # we're not going to put this entry into the cache, so need # to make sure that the invalidation callbacks are called. # That was probably done when _pending_deferred_cache was # updated, but it's possible that `set` was called without # `invalidate` being previously called, in which case it may # not have been. Either way, let's double-check now. entry.invalidate() return result entry.deferred.addCallback(shuffle) def prefill(self, key, value, callback=None): callbacks = [callback] if callback else [] self.cache.set(key, value, callbacks=callbacks) def invalidate(self, key): self.check_thread() self.cache.pop(key, None) # if we have a pending lookup for this key, remove it from the # _pending_deferred_cache, which will (a) stop it being returned # for future queries and (b) stop it being persisted as a proper entry # in self.cache. entry = self._pending_deferred_cache.pop(key, None) # run the invalidation callbacks now, rather than waiting for the # deferred to resolve. if entry: entry.invalidate() def invalidate_many(self, key): self.check_thread() if not isinstance(key, tuple): raise TypeError("The cache key must be a tuple not %r" % (type(key), )) self.cache.del_multi(key) # if we have a pending lookup for this key, remove it from the # _pending_deferred_cache, as above entry_dict = self._pending_deferred_cache.pop(key, None) if entry_dict is not None: for entry in iterate_tree_cache_entry(entry_dict): entry.invalidate() def invalidate_all(self): self.check_thread() self.cache.clear() for entry in itervalues(self._pending_deferred_cache): entry.invalidate() self._pending_deferred_cache.clear()
class Cache(object): __slots__ = ( "cache", "max_entries", "name", "keylen", "thread", "metrics", "_pending_deferred_cache", ) def __init__(self, name, max_entries=1000, keylen=1, tree=False, iterable=False): cache_type = TreeCache if tree else dict self._pending_deferred_cache = cache_type() self.cache = LruCache( max_size=max_entries, keylen=keylen, cache_type=cache_type, size_callback=(lambda d: len(d)) if iterable else None, evicted_callback=self._on_evicted, ) self.name = name self.keylen = keylen self.thread = None self.metrics = register_cache("cache", name, self.cache) def _on_evicted(self, evicted_count): self.metrics.inc_evictions(evicted_count) def check_thread(self): expected_thread = self.thread if expected_thread is None: self.thread = threading.current_thread() else: if expected_thread is not threading.current_thread(): raise ValueError( "Cache objects can only be accessed from the main thread" ) def get(self, key, default=_CacheSentinel, callback=None, update_metrics=True): """Looks the key up in the caches. Args: key(tuple) default: What is returned if key is not in the caches. If not specified then function throws KeyError instead callback(fn): Gets called when the entry in the cache is invalidated update_metrics (bool): whether to update the cache hit rate metrics Returns: Either a Deferred or the raw result """ callbacks = [callback] if callback else [] val = self._pending_deferred_cache.get(key, _CacheSentinel) if val is not _CacheSentinel: val.callbacks.update(callbacks) if update_metrics: self.metrics.inc_hits() return val.deferred val = self.cache.get(key, _CacheSentinel, callbacks=callbacks) if val is not _CacheSentinel: self.metrics.inc_hits() return val if update_metrics: self.metrics.inc_misses() if default is _CacheSentinel: raise KeyError() else: return default def set(self, key, value, callback=None): callbacks = [callback] if callback else [] self.check_thread() entry = CacheEntry( deferred=value, callbacks=callbacks, ) existing_entry = self._pending_deferred_cache.pop(key, None) if existing_entry: existing_entry.invalidate() self._pending_deferred_cache[key] = entry def shuffle(result): existing_entry = self._pending_deferred_cache.pop(key, None) if existing_entry is entry: self.cache.set(key, result, entry.callbacks) else: # oops, the _pending_deferred_cache has been updated since # we started our query, so we are out of date. # # Better put back whatever we took out. (We do it this way # round, rather than peeking into the _pending_deferred_cache # and then removing on a match, to make the common case faster) if existing_entry is not None: self._pending_deferred_cache[key] = existing_entry # we're not going to put this entry into the cache, so need # to make sure that the invalidation callbacks are called. # That was probably done when _pending_deferred_cache was # updated, but it's possible that `set` was called without # `invalidate` being previously called, in which case it may # not have been. Either way, let's double-check now. entry.invalidate() return result entry.deferred.addCallback(shuffle) def prefill(self, key, value, callback=None): callbacks = [callback] if callback else [] self.cache.set(key, value, callbacks=callbacks) def invalidate(self, key): self.check_thread() self.cache.pop(key, None) # if we have a pending lookup for this key, remove it from the # _pending_deferred_cache, which will (a) stop it being returned # for future queries and (b) stop it being persisted as a proper entry # in self.cache. entry = self._pending_deferred_cache.pop(key, None) # run the invalidation callbacks now, rather than waiting for the # deferred to resolve. if entry: entry.invalidate() def invalidate_many(self, key): self.check_thread() if not isinstance(key, tuple): raise TypeError( "The cache key must be a tuple not %r" % (type(key),) ) self.cache.del_multi(key) # if we have a pending lookup for this key, remove it from the # _pending_deferred_cache, as above entry_dict = self._pending_deferred_cache.pop(key, None) if entry_dict is not None: for entry in iterate_tree_cache_entry(entry_dict): entry.invalidate() def invalidate_all(self): self.check_thread() self.cache.clear() for entry in itervalues(self._pending_deferred_cache): entry.invalidate() self._pending_deferred_cache.clear()
class Cache(object): __slots__ = ( "cache", "max_entries", "name", "keylen", "sequence", "thread", "metrics", "_pending_deferred_cache", ) def __init__(self, name, max_entries=1000, keylen=1, tree=False, iterable=False): cache_type = TreeCache if tree else dict self._pending_deferred_cache = cache_type() self.cache = LruCache( max_size=max_entries, keylen=keylen, cache_type=cache_type, size_callback=(lambda d: len(d)) if iterable else None, ) self.name = name self.keylen = keylen self.sequence = 0 self.thread = None self.metrics = register_cache(name, self.cache) def check_thread(self): expected_thread = self.thread if expected_thread is None: self.thread = threading.current_thread() else: if expected_thread is not threading.current_thread(): raise ValueError( "Cache objects can only be accessed from the main thread" ) def get(self, key, default=_CacheSentinel, callback=None, update_metrics=True): """Looks the key up in the caches. Args: key(tuple) default: What is returned if key is not in the caches. If not specified then function throws KeyError instead callback(fn): Gets called when the entry in the cache is invalidated update_metrics (bool): whether to update the cache hit rate metrics Returns: Either a Deferred or the raw result """ callbacks = [callback] if callback else [] val = self._pending_deferred_cache.get(key, _CacheSentinel) if val is not _CacheSentinel: if val.sequence == self.sequence: val.callbacks.update(callbacks) if update_metrics: self.metrics.inc_hits() return val.deferred val = self.cache.get(key, _CacheSentinel, callbacks=callbacks) if val is not _CacheSentinel: self.metrics.inc_hits() return val if update_metrics: self.metrics.inc_misses() if default is _CacheSentinel: raise KeyError() else: return default def set(self, key, value, callback=None): callbacks = [callback] if callback else [] self.check_thread() entry = CacheEntry( deferred=value, sequence=self.sequence, callbacks=callbacks, ) entry.callbacks.update(callbacks) existing_entry = self._pending_deferred_cache.pop(key, None) if existing_entry: existing_entry.invalidate() self._pending_deferred_cache[key] = entry def shuffle(result): if self.sequence == entry.sequence: existing_entry = self._pending_deferred_cache.pop(key, None) if existing_entry is entry: self.cache.set(key, result, entry.callbacks) else: entry.invalidate() else: entry.invalidate() return result entry.deferred.addCallback(shuffle) def prefill(self, key, value, callback=None): callbacks = [callback] if callback else [] self.cache.set(key, value, callbacks=callbacks) def invalidate(self, key): self.check_thread() # Increment the sequence number so that any SELECT statements that # raced with the INSERT don't update the cache (SYN-369) self.sequence += 1 entry = self._pending_deferred_cache.pop(key, None) if entry: entry.invalidate() self.cache.pop(key, None) def invalidate_many(self, key): self.check_thread() if not isinstance(key, tuple): raise TypeError( "The cache key must be a tuple not %r" % (type(key),) ) self.sequence += 1 self.cache.del_multi(key) entry_dict = self._pending_deferred_cache.pop(key, None) if entry_dict is not None: for entry in iterate_tree_cache_entry(entry_dict): entry.invalidate() def invalidate_all(self): self.check_thread() self.sequence += 1 self.cache.clear()
class DeferredCache(Generic[KT, VT]): """Wraps an LruCache, adding support for Deferred results. It expects that each entry added with set() will be a Deferred; likewise get() will return a Deferred. """ __slots__ = ( "cache", "thread", "_pending_deferred_cache", ) def __init__( self, name: str, max_entries: int = 1000, tree: bool = False, iterable: bool = False, apply_cache_factor_from_config: bool = True, ): """ Args: name: The name of the cache max_entries: Maximum amount of entries that the cache will hold keylen: The length of the tuple used as the cache key. Ignored unless `tree` is True. tree: Use a TreeCache instead of a dict as the underlying cache type iterable: If True, count each item in the cached object as an entry, rather than each cached object apply_cache_factor_from_config: Whether cache factors specified in the config file affect `max_entries` """ cache_type = TreeCache if tree else dict # _pending_deferred_cache maps from the key value to a `CacheEntry` object. self._pending_deferred_cache = ( cache_type() ) # type: Union[TreeCache, MutableMapping[KT, CacheEntry]] def metrics_cb(): cache_pending_metric.labels(name).set(len(self._pending_deferred_cache)) # cache is used for completed results and maps to the result itself, rather than # a Deferred. self.cache = LruCache( max_size=max_entries, cache_name=name, cache_type=cache_type, size_callback=(lambda d: len(d) or 1) if iterable else None, metrics_collection_callback=metrics_cb, apply_cache_factor_from_config=apply_cache_factor_from_config, ) # type: LruCache[KT, VT] self.thread = None # type: Optional[threading.Thread] @property def max_entries(self): return self.cache.max_size def check_thread(self): expected_thread = self.thread if expected_thread is None: self.thread = threading.current_thread() else: if expected_thread is not threading.current_thread(): raise ValueError( "Cache objects can only be accessed from the main thread" ) def get( self, key: KT, callback: Optional[Callable[[], None]] = None, update_metrics: bool = True, ) -> defer.Deferred: """Looks the key up in the caches. For symmetry with set(), this method does *not* follow the synapse logcontext rules: the logcontext will not be cleared on return, and the Deferred will run its callbacks in the sentinel context. In other words: wrap the result with make_deferred_yieldable() before `await`ing it. Args: key: callback: Gets called when the entry in the cache is invalidated update_metrics (bool): whether to update the cache hit rate metrics Returns: A Deferred which completes with the result. Note that this may later fail if there is an ongoing set() operation which later completes with a failure. Raises: KeyError if the key is not found in the cache """ callbacks = [callback] if callback else [] val = self._pending_deferred_cache.get(key, _Sentinel.sentinel) if val is not _Sentinel.sentinel: val.callbacks.update(callbacks) if update_metrics: m = self.cache.metrics assert m # we always have a name, so should always have metrics m.inc_hits() return val.deferred.observe() val2 = self.cache.get( key, _Sentinel.sentinel, callbacks=callbacks, update_metrics=update_metrics ) if val2 is _Sentinel.sentinel: raise KeyError() else: return defer.succeed(val2) def get_immediate( self, key: KT, default: T, update_metrics: bool = True ) -> Union[VT, T]: """If we have a *completed* cached value, return it.""" return self.cache.get(key, default, update_metrics=update_metrics) def set( self, key: KT, value: defer.Deferred, callback: Optional[Callable[[], None]] = None, ) -> defer.Deferred: """Adds a new entry to the cache (or updates an existing one). The given `value` *must* be a Deferred. First any existing entry for the same key is invalidated. Then a new entry is added to the cache for the given key. Until the `value` completes, calls to `get()` for the key will also result in an incomplete Deferred, which will ultimately complete with the same result as `value`. If `value` completes successfully, subsequent calls to `get()` will then return a completed deferred with the same result. If it *fails*, the cache is invalidated and subequent calls to `get()` will raise a KeyError. If another call to `set()` happens before `value` completes, then (a) any invalidation callbacks registered in the interim will be called, (b) any `get()`s in the interim will continue to complete with the result from the *original* `value`, (c) any future calls to `get()` will complete with the result from the *new* `value`. It is expected that `value` does *not* follow the synapse logcontext rules - ie, if it is incomplete, it runs its callbacks in the sentinel context. Args: key: Key to be set value: a deferred which will complete with a result to add to the cache callback: An optional callback to be called when the entry is invalidated """ if not isinstance(value, defer.Deferred): raise TypeError("not a Deferred") callbacks = [callback] if callback else [] self.check_thread() existing_entry = self._pending_deferred_cache.pop(key, None) if existing_entry: existing_entry.invalidate() # XXX: why don't we invalidate the entry in `self.cache` yet? # we can save a whole load of effort if the deferred is ready. if value.called: result = value.result if not isinstance(result, failure.Failure): self.cache.set(key, result, callbacks) return value # otherwise, we'll add an entry to the _pending_deferred_cache for now, # and add callbacks to add it to the cache properly later. observable = ObservableDeferred(value, consumeErrors=True) observer = observable.observe() entry = CacheEntry(deferred=observable, callbacks=callbacks) self._pending_deferred_cache[key] = entry def compare_and_pop(): """Check if our entry is still the one in _pending_deferred_cache, and if so, pop it. Returns true if the entries matched. """ existing_entry = self._pending_deferred_cache.pop(key, None) if existing_entry is entry: return True # oops, the _pending_deferred_cache has been updated since # we started our query, so we are out of date. # # Better put back whatever we took out. (We do it this way # round, rather than peeking into the _pending_deferred_cache # and then removing on a match, to make the common case faster) if existing_entry is not None: self._pending_deferred_cache[key] = existing_entry return False def cb(result): if compare_and_pop(): self.cache.set(key, result, entry.callbacks) else: # we're not going to put this entry into the cache, so need # to make sure that the invalidation callbacks are called. # That was probably done when _pending_deferred_cache was # updated, but it's possible that `set` was called without # `invalidate` being previously called, in which case it may # not have been. Either way, let's double-check now. entry.invalidate() def eb(_fail): compare_and_pop() entry.invalidate() # once the deferred completes, we can move the entry from the # _pending_deferred_cache to the real cache. # observer.addCallbacks(cb, eb) # we return a new Deferred which will be called before any subsequent observers. return observable.observe() def prefill( self, key: KT, value: VT, callback: Optional[Callable[[], None]] = None ): callbacks = [callback] if callback else [] self.cache.set(key, value, callbacks=callbacks) def invalidate(self, key): """Delete a key, or tree of entries If the cache is backed by a regular dict, then "key" must be of the right type for this cache If the cache is backed by a TreeCache, then "key" must be a tuple, but may be of lower cardinality than the TreeCache - in which case the whole subtree is deleted. """ self.check_thread() self.cache.del_multi(key) # if we have a pending lookup for this key, remove it from the # _pending_deferred_cache, which will (a) stop it being returned # for future queries and (b) stop it being persisted as a proper entry # in self.cache. entry = self._pending_deferred_cache.pop(key, None) # run the invalidation callbacks now, rather than waiting for the # deferred to resolve. if entry: # _pending_deferred_cache.pop should either return a CacheEntry, or, in the # case of a TreeCache, a dict of keys to cache entries. Either way calling # iterate_tree_cache_entry on it will do the right thing. for entry in iterate_tree_cache_entry(entry): entry.invalidate() def invalidate_all(self): self.check_thread() self.cache.clear() for entry in self._pending_deferred_cache.values(): entry.invalidate() self._pending_deferred_cache.clear()
class Cache(object): __slots__ = ( "cache", "max_entries", "name", "keylen", "sequence", "thread", "metrics", ) def __init__(self, name, max_entries=1000, keylen=1, tree=False): cache_type = TreeCache if tree else dict self.cache = LruCache( max_size=max_entries, keylen=keylen, cache_type=cache_type ) self.name = name self.keylen = keylen self.sequence = 0 self.thread = None self.metrics = register_cache(name, self.cache) def check_thread(self): expected_thread = self.thread if expected_thread is None: self.thread = threading.current_thread() else: if expected_thread is not threading.current_thread(): raise ValueError( "Cache objects can only be accessed from the main thread" ) def get(self, key, default=_CacheSentinel, callback=None): val = self.cache.get(key, _CacheSentinel, callback=callback) if val is not _CacheSentinel: self.metrics.inc_hits() return val self.metrics.inc_misses() if default is _CacheSentinel: raise KeyError() else: return default def update(self, sequence, key, value, callback=None): self.check_thread() if self.sequence == sequence: # Only update the cache if the caches sequence number matches the # number that the cache had before the SELECT was started (SYN-369) self.prefill(key, value, callback=callback) def prefill(self, key, value, callback=None): self.cache.set(key, value, callback=callback) def invalidate(self, key): self.check_thread() if not isinstance(key, tuple): raise TypeError( "The cache key must be a tuple not %r" % (type(key),) ) # Increment the sequence number so that any SELECT statements that # raced with the INSERT don't update the cache (SYN-369) self.sequence += 1 self.cache.pop(key, None) def invalidate_many(self, key): self.check_thread() if not isinstance(key, tuple): raise TypeError( "The cache key must be a tuple not %r" % (type(key),) ) self.sequence += 1 self.cache.del_multi(key) def invalidate_all(self): self.check_thread() self.sequence += 1 self.cache.clear()