516 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			516 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Python
		
	
	
| # Copyright 2015, 2016 OpenMarket Ltd
 | |
| # Copyright 2018 New Vector Ltd
 | |
| # Copyright 2020 The Matrix.org Foundation C.I.C.
 | |
| #
 | |
| # Licensed under the Apache License, Version 2.0 (the "License");
 | |
| # you may not use this file except in compliance with the License.
 | |
| # You may obtain a copy of the License at
 | |
| #
 | |
| #     http://www.apache.org/licenses/LICENSE-2.0
 | |
| #
 | |
| # Unless required by applicable law or agreed to in writing, software
 | |
| # distributed under the License is distributed on an "AS IS" BASIS,
 | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| # See the License for the specific language governing permissions and
 | |
| # limitations under the License.
 | |
| 
 | |
| import abc
 | |
| import enum
 | |
| import threading
 | |
| from typing import (
 | |
|     Callable,
 | |
|     Collection,
 | |
|     Dict,
 | |
|     Generic,
 | |
|     MutableMapping,
 | |
|     Optional,
 | |
|     Set,
 | |
|     Sized,
 | |
|     Tuple,
 | |
|     TypeVar,
 | |
|     Union,
 | |
|     cast,
 | |
| )
 | |
| 
 | |
| from prometheus_client import Gauge
 | |
| 
 | |
| from twisted.internet import defer
 | |
| from twisted.python.failure import Failure
 | |
| 
 | |
| from synapse.util.async_helpers import ObservableDeferred
 | |
| from synapse.util.caches.lrucache import LruCache
 | |
| from synapse.util.caches.treecache import TreeCache, iterate_tree_cache_entry
 | |
| 
 | |
| cache_pending_metric = Gauge(
 | |
|     "synapse_util_caches_cache_pending",
 | |
|     "Number of lookups currently pending for this cache",
 | |
|     ["name"],
 | |
| )
 | |
| 
 | |
| T = TypeVar("T")
 | |
| KT = TypeVar("KT")
 | |
| VT = TypeVar("VT")
 | |
| 
 | |
| 
 | |
| class _Sentinel(enum.Enum):
 | |
|     # defining a sentinel in this way allows mypy to correctly handle the
 | |
|     # type of a dictionary lookup.
 | |
|     sentinel = object()
 | |
| 
 | |
| 
 | |
| 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,
 | |
|         prune_unread_entries: bool = True,
 | |
|     ):
 | |
|         """
 | |
|         Args:
 | |
|             name: The name of the cache
 | |
|             max_entries: Maximum amount of entries that the cache will hold
 | |
|             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`
 | |
|             prune_unread_entries: If True, cache entries that haven't been read recently
 | |
|                 will be evicted from the cache in the background. Set to False to
 | |
|                 opt-out of this behaviour.
 | |
|         """
 | |
|         cache_type = TreeCache if tree else dict
 | |
| 
 | |
|         # _pending_deferred_cache maps from the key value to a `CacheEntry` object.
 | |
|         self._pending_deferred_cache: Union[
 | |
|             TreeCache, "MutableMapping[KT, CacheEntry[KT, VT]]"
 | |
|         ] = cache_type()
 | |
| 
 | |
|         def metrics_cb() -> None:
 | |
|             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[KT, VT] = LruCache(
 | |
|             max_size=max_entries,
 | |
|             cache_name=name,
 | |
|             cache_type=cache_type,
 | |
|             size_callback=(
 | |
|                 (lambda d: len(cast(Sized, d)) or 1)
 | |
|                 # Argument 1 to "len" has incompatible type "VT"; expected "Sized"
 | |
|                 # We trust that `VT` is `Sized` when `iterable` is `True`
 | |
|                 if iterable
 | |
|                 else None
 | |
|             ),
 | |
|             metrics_collection_callback=metrics_cb,
 | |
|             apply_cache_factor_from_config=apply_cache_factor_from_config,
 | |
|             prune_unread_entries=prune_unread_entries,
 | |
|         )
 | |
| 
 | |
|         self.thread: Optional[threading.Thread] = None
 | |
| 
 | |
|     @property
 | |
|     def max_entries(self) -> int:
 | |
|         return self.cache.max_size
 | |
| 
 | |
|     def check_thread(self) -> None:
 | |
|         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: 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
 | |
|         """
 | |
|         val = self._pending_deferred_cache.get(key, _Sentinel.sentinel)
 | |
|         if val is not _Sentinel.sentinel:
 | |
|             val.add_invalidation_callback(key, callback)
 | |
|             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(key)
 | |
| 
 | |
|         callbacks = (callback,) if callback else ()
 | |
| 
 | |
|         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_bulk(
 | |
|         self,
 | |
|         keys: Collection[KT],
 | |
|         callback: Optional[Callable[[], None]] = None,
 | |
|     ) -> Tuple[Dict[KT, VT], Optional["defer.Deferred[Dict[KT, VT]]"], Collection[KT]]:
 | |
|         """Bulk lookup of items in the cache.
 | |
| 
 | |
|         Returns:
 | |
|             A 3-tuple of:
 | |
|                 1. a dict of key/value of items already cached;
 | |
|                 2. a deferred that resolves to a dict of key/value of items
 | |
|                    we're already fetching; and
 | |
|                 3. a collection of keys that don't appear in the previous two.
 | |
|         """
 | |
| 
 | |
|         # The cached results
 | |
|         cached = {}
 | |
| 
 | |
|         # List of pending deferreds
 | |
|         pending = []
 | |
| 
 | |
|         # Dict that gets filled out when the pending deferreds complete
 | |
|         pending_results = {}
 | |
| 
 | |
|         # List of keys that aren't in either cache
 | |
|         missing = []
 | |
| 
 | |
|         callbacks = (callback,) if callback else ()
 | |
| 
 | |
|         for key in keys:
 | |
|             # Check if its in the main cache.
 | |
|             immediate_value = self.cache.get(
 | |
|                 key,
 | |
|                 _Sentinel.sentinel,
 | |
|                 callbacks=callbacks,
 | |
|             )
 | |
|             if immediate_value is not _Sentinel.sentinel:
 | |
|                 cached[key] = immediate_value
 | |
|                 continue
 | |
| 
 | |
|             # Check if its in the pending cache
 | |
|             pending_value = self._pending_deferred_cache.get(key, _Sentinel.sentinel)
 | |
|             if pending_value is not _Sentinel.sentinel:
 | |
|                 pending_value.add_invalidation_callback(key, callback)
 | |
| 
 | |
|                 def completed_cb(value: VT, key: KT) -> VT:
 | |
|                     pending_results[key] = value
 | |
|                     return value
 | |
| 
 | |
|                 # Add a callback to fill out `pending_results` when that completes
 | |
|                 d = pending_value.deferred(key).addCallback(completed_cb, key)
 | |
|                 pending.append(d)
 | |
|                 continue
 | |
| 
 | |
|             # Not in either cache
 | |
|             missing.append(key)
 | |
| 
 | |
|         # If we've got pending deferreds, squash them into a single one that
 | |
|         # returns `pending_results`.
 | |
|         pending_deferred = None
 | |
|         if pending:
 | |
|             pending_deferred = defer.gatherResults(
 | |
|                 pending, consumeErrors=True
 | |
|             ).addCallback(lambda _: pending_results)
 | |
| 
 | |
|         return (cached, pending_deferred, missing)
 | |
| 
 | |
|     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[VT]",
 | |
|         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
 | |
|         """
 | |
|         self.check_thread()
 | |
| 
 | |
|         self._pending_deferred_cache.pop(key, None)
 | |
| 
 | |
|         # XXX: why don't we invalidate the entry in `self.cache` yet?
 | |
| 
 | |
|         # otherwise, we'll add an entry to the _pending_deferred_cache for now,
 | |
|         # and add callbacks to add it to the cache properly later.
 | |
|         entry = CacheEntrySingle[KT, VT](value)
 | |
|         entry.add_invalidation_callback(key, callback)
 | |
|         self._pending_deferred_cache[key] = entry
 | |
|         deferred = entry.deferred(key).addCallbacks(
 | |
|             self._completed_callback,
 | |
|             self._error_callback,
 | |
|             callbackArgs=(entry, key),
 | |
|             errbackArgs=(entry, key),
 | |
|         )
 | |
| 
 | |
|         # we return a new Deferred which will be called before any subsequent observers.
 | |
|         return deferred
 | |
| 
 | |
|     def start_bulk_input(
 | |
|         self,
 | |
|         keys: Collection[KT],
 | |
|         callback: Optional[Callable[[], None]] = None,
 | |
|     ) -> "CacheMultipleEntries[KT, VT]":
 | |
|         """Bulk set API for use when fetching multiple keys at once from the DB.
 | |
| 
 | |
|         Called *before* starting the fetch from the DB, and the caller *must*
 | |
|         call either `complete_bulk(..)` or `error_bulk(..)` on the return value.
 | |
|         """
 | |
| 
 | |
|         entry = CacheMultipleEntries[KT, VT]()
 | |
|         entry.add_global_invalidation_callback(callback)
 | |
| 
 | |
|         for key in keys:
 | |
|             self._pending_deferred_cache[key] = entry
 | |
| 
 | |
|         return entry
 | |
| 
 | |
|     def _completed_callback(
 | |
|         self, value: VT, entry: "CacheEntry[KT, VT]", key: KT
 | |
|     ) -> VT:
 | |
|         """Called when a deferred is completed."""
 | |
|         # We check if the current entry matches the entry associated with the
 | |
|         # deferred. If they don't match then it got invalidated.
 | |
|         current_entry = self._pending_deferred_cache.pop(key, None)
 | |
|         if current_entry is not entry:
 | |
|             if current_entry:
 | |
|                 self._pending_deferred_cache[key] = current_entry
 | |
|             return value
 | |
| 
 | |
|         self.cache.set(key, value, entry.get_invalidation_callbacks(key))
 | |
| 
 | |
|         return value
 | |
| 
 | |
|     def _error_callback(
 | |
|         self,
 | |
|         failure: Failure,
 | |
|         entry: "CacheEntry[KT, VT]",
 | |
|         key: KT,
 | |
|     ) -> Failure:
 | |
|         """Called when a deferred errors."""
 | |
| 
 | |
|         # We check if the current entry matches the entry associated with the
 | |
|         # deferred. If they don't match then it got invalidated.
 | |
|         current_entry = self._pending_deferred_cache.pop(key, None)
 | |
|         if current_entry is not entry:
 | |
|             if current_entry:
 | |
|                 self._pending_deferred_cache[key] = current_entry
 | |
|             return failure
 | |
| 
 | |
|         for cb in entry.get_invalidation_callbacks(key):
 | |
|             cb()
 | |
| 
 | |
|         return failure
 | |
| 
 | |
|     def prefill(
 | |
|         self, key: KT, value: VT, callback: Optional[Callable[[], None]] = None
 | |
|     ) -> None:
 | |
|         callbacks = (callback,) if callback else ()
 | |
|         self.cache.set(key, value, callbacks=callbacks)
 | |
|         self._pending_deferred_cache.pop(key, None)
 | |
| 
 | |
|     def invalidate(self, key: KT) -> None:
 | |
|         """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)
 | |
|         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 iter_entry in iterate_tree_cache_entry(entry):
 | |
|                 for cb in iter_entry.get_invalidation_callbacks(key):
 | |
|                     cb()
 | |
| 
 | |
|     def invalidate_all(self) -> None:
 | |
|         self.check_thread()
 | |
|         self.cache.clear()
 | |
|         for key, entry in self._pending_deferred_cache.items():
 | |
|             for cb in entry.get_invalidation_callbacks(key):
 | |
|                 cb()
 | |
| 
 | |
|         self._pending_deferred_cache.clear()
 | |
| 
 | |
| 
 | |
| class CacheEntry(Generic[KT, VT], metaclass=abc.ABCMeta):
 | |
|     """Abstract class for entries in `DeferredCache[KT, VT]`"""
 | |
| 
 | |
|     @abc.abstractmethod
 | |
|     def deferred(self, key: KT) -> "defer.Deferred[VT]":
 | |
|         """Get a deferred that a caller can wait on to get the value at the
 | |
|         given key"""
 | |
|         ...
 | |
| 
 | |
|     @abc.abstractmethod
 | |
|     def add_invalidation_callback(
 | |
|         self, key: KT, callback: Optional[Callable[[], None]]
 | |
|     ) -> None:
 | |
|         """Add an invalidation callback"""
 | |
|         ...
 | |
| 
 | |
|     @abc.abstractmethod
 | |
|     def get_invalidation_callbacks(self, key: KT) -> Collection[Callable[[], None]]:
 | |
|         """Get all invalidation callbacks"""
 | |
|         ...
 | |
| 
 | |
| 
 | |
| class CacheEntrySingle(CacheEntry[KT, VT]):
 | |
|     """An implementation of `CacheEntry` wrapping a deferred that results in a
 | |
|     single cache entry.
 | |
|     """
 | |
| 
 | |
|     __slots__ = ["_deferred", "_callbacks"]
 | |
| 
 | |
|     def __init__(self, deferred: "defer.Deferred[VT]") -> None:
 | |
|         self._deferred = ObservableDeferred(deferred, consumeErrors=True)
 | |
|         self._callbacks: Set[Callable[[], None]] = set()
 | |
| 
 | |
|     def deferred(self, key: KT) -> "defer.Deferred[VT]":
 | |
|         return self._deferred.observe()
 | |
| 
 | |
|     def add_invalidation_callback(
 | |
|         self, key: KT, callback: Optional[Callable[[], None]]
 | |
|     ) -> None:
 | |
|         if callback is None:
 | |
|             return
 | |
| 
 | |
|         self._callbacks.add(callback)
 | |
| 
 | |
|     def get_invalidation_callbacks(self, key: KT) -> Collection[Callable[[], None]]:
 | |
|         return self._callbacks
 | |
| 
 | |
| 
 | |
| class CacheMultipleEntries(CacheEntry[KT, VT]):
 | |
|     """Cache entry that is used for bulk lookups and insertions."""
 | |
| 
 | |
|     __slots__ = ["_deferred", "_callbacks", "_global_callbacks"]
 | |
| 
 | |
|     def __init__(self) -> None:
 | |
|         self._deferred: Optional[ObservableDeferred[Dict[KT, VT]]] = None
 | |
|         self._callbacks: Dict[KT, Set[Callable[[], None]]] = {}
 | |
|         self._global_callbacks: Set[Callable[[], None]] = set()
 | |
| 
 | |
|     def deferred(self, key: KT) -> "defer.Deferred[VT]":
 | |
|         if not self._deferred:
 | |
|             self._deferred = ObservableDeferred(defer.Deferred(), consumeErrors=True)
 | |
|         return self._deferred.observe().addCallback(lambda res: res.get(key))
 | |
| 
 | |
|     def add_invalidation_callback(
 | |
|         self, key: KT, callback: Optional[Callable[[], None]]
 | |
|     ) -> None:
 | |
|         if callback is None:
 | |
|             return
 | |
| 
 | |
|         self._callbacks.setdefault(key, set()).add(callback)
 | |
| 
 | |
|     def get_invalidation_callbacks(self, key: KT) -> Collection[Callable[[], None]]:
 | |
|         return self._callbacks.get(key, set()) | self._global_callbacks
 | |
| 
 | |
|     def add_global_invalidation_callback(
 | |
|         self, callback: Optional[Callable[[], None]]
 | |
|     ) -> None:
 | |
|         """Add a callback for when any keys get invalidated."""
 | |
|         if callback is None:
 | |
|             return
 | |
| 
 | |
|         self._global_callbacks.add(callback)
 | |
| 
 | |
|     def complete_bulk(
 | |
|         self,
 | |
|         cache: DeferredCache[KT, VT],
 | |
|         result: Dict[KT, VT],
 | |
|     ) -> None:
 | |
|         """Called when there is a result"""
 | |
|         for key, value in result.items():
 | |
|             cache._completed_callback(value, self, key)
 | |
| 
 | |
|         if self._deferred:
 | |
|             self._deferred.callback(result)
 | |
| 
 | |
|     def error_bulk(
 | |
|         self, cache: DeferredCache[KT, VT], keys: Collection[KT], failure: Failure
 | |
|     ) -> None:
 | |
|         """Called when bulk lookup failed."""
 | |
|         for key in keys:
 | |
|             cache._error_callback(failure, self, key)
 | |
| 
 | |
|         if self._deferred:
 | |
|             self._deferred.errback(failure)
 |