def test_pop(self): m = Mock() cache = LruCache(1) cache.set("key", "value", callbacks=[m]) self.assertFalse(m.called) cache.pop("key") self.assertEquals(m.call_count, 1) cache.set("key", "value") self.assertEquals(m.call_count, 1) cache.pop("key") self.assertEquals(m.call_count, 1)
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()
def test_pop(self): cache = LruCache(1) cache["key"] = 1 self.assertEquals(cache.pop("key"), 1) self.assertEquals(cache.pop("key"), None)
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 DictionaryCache(object): """Caches key -> dictionary lookups, supporting caching partial dicts, i.e. fetching a subset of dictionary keys for a particular key. """ def __init__(self, name, max_entries=1000): self.cache = LruCache(max_size=max_entries) self.name = name self.sequence = 0 self.thread = None # caches_by_name[name] = self.cache class Sentinel(object): __slots__ = [] self.sentinel = Sentinel() caches_by_name[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, dict_keys=None): entry = self.cache.get(key, self.sentinel) if entry is not self.sentinel: cache_counter.inc_hits(self.name) if dict_keys is None: return DictionaryEntry(entry.full, dict(entry.value)) else: return DictionaryEntry( entry.full, {k: entry.value[k] for k in dict_keys if k in entry.value}) cache_counter.inc_misses(self.name) return DictionaryEntry(False, {}) 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 self.cache.pop(key, None) def invalidate_all(self): self.check_thread() self.sequence += 1 self.cache.clear() def update(self, sequence, key, value, full=False): 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) if full: self._insert(key, value) else: self._update_or_insert(key, value) def _update_or_insert(self, key, value): entry = self.cache.setdefault(key, DictionaryEntry(False, {})) entry.value.update(value) def _insert(self, key, value): self.cache[key] = DictionaryEntry(True, value)
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 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: 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): 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: KT): self.check_thread() if not isinstance(key, tuple): raise TypeError("The cache key must be a tuple not %r" % (type(key), )) key = cast(KT, 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", "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 DictionaryCache: """Caches key -> dictionary lookups, supporting caching partial dicts, i.e. fetching a subset of dictionary keys for a particular key. """ def __init__(self, name, max_entries=1000): self.cache = LruCache( max_size=max_entries, cache_name=name, size_callback=len) # type: LruCache[Any, DictionaryEntry] self.name = name self.sequence = 0 self.thread = None 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, dict_keys=None): """Fetch an entry out of the cache Args: key dict_key(list): If given a set of keys then return only those keys that exist in the cache. Returns: DictionaryEntry """ entry = self.cache.get(key, _Sentinel.sentinel) if entry is not _Sentinel.sentinel: if dict_keys is None: return DictionaryEntry(entry.full, entry.known_absent, dict(entry.value)) else: return DictionaryEntry( entry.full, entry.known_absent, {k: entry.value[k] for k in dict_keys if k in entry.value}, ) return DictionaryEntry(False, set(), {}) 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 self.cache.pop(key, None) def invalidate_all(self): self.check_thread() self.sequence += 1 self.cache.clear() def update(self, sequence, key, value, fetched_keys=None): """Updates the entry in the cache Args: sequence key (K) value (dict[X,Y]): The value to update the cache with. fetched_keys (None|set[X]): All of the dictionary keys which were fetched from the database. If None, this is the complete value for key K. Otherwise, it is used to infer a list of keys which we know don't exist in the full dict. """ 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) if fetched_keys is None: self._insert(key, value, set()) else: self._update_or_insert(key, value, fetched_keys) def _update_or_insert(self, key, value, known_absent): # We pop and reinsert as we need to tell the cache the size may have # changed entry = self.cache.pop(key, DictionaryEntry(False, set(), {})) entry.value.update(value) entry.known_absent.update(known_absent) self.cache[key] = entry def _insert(self, key, value, known_absent): self.cache[key] = DictionaryEntry(True, known_absent, value)
class DictionaryCache(object): """Caches key -> dictionary lookups, supporting caching partial dicts, i.e. fetching a subset of dictionary keys for a particular key. """ def __init__(self, name, max_entries=1000): self.cache = LruCache(max_size=max_entries, size_callback=len) self.name = name self.sequence = 0 self.thread = None # caches_by_name[name] = self.cache class Sentinel(object): __slots__ = [] self.sentinel = Sentinel() 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, dict_keys=None): """Fetch an entry out of the cache Args: key dict_key(list): If given a set of keys then return only those keys that exist in the cache. Returns: DictionaryEntry """ entry = self.cache.get(key, self.sentinel) if entry is not self.sentinel: self.metrics.inc_hits() if dict_keys is None: return DictionaryEntry(entry.full, entry.known_absent, dict(entry.value)) else: return DictionaryEntry( entry.full, entry.known_absent, {k: entry.value[k] for k in dict_keys if k in entry.value}) self.metrics.inc_misses() return DictionaryEntry(False, set(), {}) 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 self.cache.pop(key, None) def invalidate_all(self): self.check_thread() self.sequence += 1 self.cache.clear() def update(self, sequence, key, value, full=False, known_absent=None): """Updates the entry in the cache Args: sequence key value (dict): The value to update the cache with. full (bool): Whether the given value is the full dict, or just a partial subset there of. If not full then any existing entries for the key will be updated. known_absent (set): Set of keys that we know don't exist in the full dict. """ 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) if known_absent is None: known_absent = set() if full: self._insert(key, value, known_absent) else: self._update_or_insert(key, value, known_absent) def _update_or_insert(self, key, value, known_absent): # We pop and reinsert as we need to tell the cache the size may have # changed entry = self.cache.pop(key, DictionaryEntry(False, set(), {})) entry.value.update(value) entry.known_absent.update(known_absent) self.cache[key] = entry def _insert(self, key, value, known_absent): self.cache[key] = DictionaryEntry(True, known_absent, value)
class DictionaryCache(object): """Caches key -> dictionary lookups, supporting caching partial dicts, i.e. fetching a subset of dictionary keys for a particular key. """ def __init__(self, name, max_entries=1000): self.cache = LruCache(max_size=max_entries) self.name = name self.sequence = 0 self.thread = None # caches_by_name[name] = self.cache class Sentinel(object): __slots__ = [] self.sentinel = Sentinel() caches_by_name[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, dict_keys=None): entry = self.cache.get(key, self.sentinel) if entry is not self.sentinel: cache_counter.inc_hits(self.name) if dict_keys is None: return DictionaryEntry(entry.full, dict(entry.value)) else: return DictionaryEntry(entry.full, { k: entry.value[k] for k in dict_keys if k in entry.value }) cache_counter.inc_misses(self.name) return DictionaryEntry(False, {}) 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 self.cache.pop(key, None) def invalidate_all(self): self.check_thread() self.sequence += 1 self.cache.clear() def update(self, sequence, key, value, full=False): 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) if full: self._insert(key, value) else: self._update_or_insert(key, value) def _update_or_insert(self, key, value): entry = self.cache.setdefault(key, DictionaryEntry(False, {})) entry.value.update(value) def _insert(self, key, value): self.cache[key] = DictionaryEntry(True, value)
class DictionaryCache(object): """Caches key -> dictionary lookups, supporting caching partial dicts, i.e. fetching a subset of dictionary keys for a particular key. """ def __init__(self, name, max_entries=1000): self.cache = LruCache(max_size=max_entries, size_callback=len) self.name = name self.sequence = 0 self.thread = None # caches_by_name[name] = self.cache class Sentinel(object): __slots__ = [] self.sentinel = Sentinel() 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, dict_keys=None): """Fetch an entry out of the cache Args: key dict_key(list): If given a set of keys then return only those keys that exist in the cache. Returns: DictionaryEntry """ entry = self.cache.get(key, self.sentinel) if entry is not self.sentinel: self.metrics.inc_hits() if dict_keys is None: return DictionaryEntry(entry.full, entry.known_absent, dict(entry.value)) else: return DictionaryEntry(entry.full, entry.known_absent, { k: entry.value[k] for k in dict_keys if k in entry.value }) self.metrics.inc_misses() return DictionaryEntry(False, set(), {}) 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 self.cache.pop(key, None) def invalidate_all(self): self.check_thread() self.sequence += 1 self.cache.clear() def update(self, sequence, key, value, full=False, known_absent=None): """Updates the entry in the cache Args: sequence key value (dict): The value to update the cache with. full (bool): Whether the given value is the full dict, or just a partial subset there of. If not full then any existing entries for the key will be updated. known_absent (set): Set of keys that we know don't exist in the full dict. """ 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) if known_absent is None: known_absent = set() if full: self._insert(key, value, known_absent) else: self._update_or_insert(key, value, known_absent) def _update_or_insert(self, key, value, known_absent): # We pop and reinsert as we need to tell the cache the size may have # changed entry = self.cache.pop(key, DictionaryEntry(False, set(), {})) entry.value.update(value) entry.known_absent.update(known_absent) self.cache[key] = entry def _insert(self, key, value, known_absent): self.cache[key] = DictionaryEntry(True, known_absent, value)
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()