Make LruCache register its own metrics (#8561)

rather than have everything that instantiates an LruCache manage metrics
separately, have LruCache do it itself.
This commit is contained in:
Richard van der Hoff 2020-10-16 15:51:57 +01:00 committed by GitHub
parent da0090fdff
commit 3ee17585cd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 62 additions and 62 deletions

1
changelog.d/8561.misc Normal file
View file

@ -0,0 +1 @@
Move metric registration code down into `LruCache`.

View file

@ -34,7 +34,6 @@ from synapse.api.room_versions import KNOWN_ROOM_VERSIONS
from synapse.events import EventBase
from synapse.logging import opentracing as opentracing
from synapse.types import StateMap, UserID
from synapse.util.caches import register_cache
from synapse.util.caches.lrucache import LruCache
from synapse.util.metrics import Measure
@ -70,8 +69,7 @@ class Auth:
self.store = hs.get_datastore()
self.state = hs.get_state_handler()
self.token_cache = LruCache(10000)
register_cache("cache", "token_cache", self.token_cache)
self.token_cache = LruCache(10000, "token_cache")
self._auth_blocking = AuthBlocking(self.hs)

View file

@ -20,7 +20,6 @@ from typing import Any, Dict, List, Optional, Pattern, Union
from synapse.events import EventBase
from synapse.types import UserID
from synapse.util.caches import register_cache
from synapse.util.caches.lrucache import LruCache
logger = logging.getLogger(__name__)
@ -186,8 +185,7 @@ class PushRuleEvaluatorForEvent:
# Caches (string, is_glob, word_boundary) -> regex for push. See _glob_matches
regex_cache = LruCache(50000)
register_cache("cache", "regex_push_cache", regex_cache)
regex_cache = LruCache(50000, "regex_push_cache")
def _glob_matches(glob: str, value: str, word_boundary: bool = False) -> bool:

View file

@ -16,7 +16,7 @@
import logging
from sys import intern
from typing import Callable, Dict, Optional
from typing import Callable, Dict, Optional, Sized
import attr
from prometheus_client.core import Gauge
@ -92,7 +92,7 @@ class CacheMetric:
def register_cache(
cache_type: str,
cache_name: str,
cache,
cache: Sized,
collect_callback: Optional[Callable] = None,
resizable: bool = True,
resize_callback: Optional[Callable] = None,
@ -100,12 +100,15 @@ def register_cache(
"""Register a cache object for metric collection and resizing.
Args:
cache_type
cache_type: a string indicating the "type" of the cache. This is used
only for deduplication so isn't too important provided it's constant.
cache_name: name of the cache
cache: cache itself
cache: cache itself, which must implement __len__(), and may optionally implement
a max_size property
collect_callback: If given, a function which is called during metric
collection to update additional metrics.
resizable: Whether this cache supports being resized.
resizable: Whether this cache supports being resized, in which case either
resize_callback must be provided, or the cache must support set_max_size().
resize_callback: A function which can be called to resize the cache.
Returns:

View file

@ -24,7 +24,6 @@ from prometheus_client import Gauge
from twisted.internet import defer
from synapse.util.async_helpers import ObservableDeferred
from synapse.util.caches import register_cache
from synapse.util.caches.lrucache import LruCache
from synapse.util.caches.treecache import TreeCache, iterate_tree_cache_entry
@ -54,10 +53,7 @@ class DeferredCache(Generic[KT, VT]):
__slots__ = (
"cache",
"name",
"keylen",
"thread",
"metrics",
"_pending_deferred_cache",
)
@ -89,37 +85,27 @@ class DeferredCache(Generic[KT, VT]):
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,
keylen=keylen,
cache_name=name,
cache_type=cache_type,
size_callback=(lambda d: len(d)) if iterable else None,
evicted_callback=self._on_evicted,
metrics_collection_callback=metrics_cb,
apply_cache_factor_from_config=apply_cache_factor_from_config,
)
self.name = name
self.keylen = keylen
self.thread = None # type: Optional[threading.Thread]
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:
@ -154,21 +140,18 @@ class DeferredCache(Generic[KT, VT]):
if val is not _Sentinel.sentinel:
val.callbacks.update(callbacks)
if update_metrics:
self.metrics.inc_hits()
m = self.cache.metrics
assert m # we always have a name, so should always have metrics
m.inc_hits()
return val.deferred
val = self.cache.get(key, _Sentinel.sentinel, callbacks=callbacks)
if val is not _Sentinel.sentinel:
self.metrics.inc_hits()
return val
if update_metrics:
self.metrics.inc_misses()
if default is _Sentinel.sentinel:
val = self.cache.get(
key, default, callbacks=callbacks, update_metrics=update_metrics
)
if val is _Sentinel.sentinel:
raise KeyError()
else:
return default
return val
def set(
self,

View file

@ -19,8 +19,6 @@ from collections import namedtuple
from synapse.util.caches.lrucache import LruCache
from . import register_cache
logger = logging.getLogger(__name__)
@ -46,18 +44,16 @@ class DictionaryCache:
"""
def __init__(self, name, max_entries=1000):
self.cache = LruCache(max_size=max_entries, size_callback=len)
self.cache = LruCache(max_size=max_entries, cache_name=name, size_callback=len)
self.name = name
self.sequence = 0
self.thread = None
# caches_by_name[name] = self.cache
class Sentinel:
__slots__ = []
self.sentinel = Sentinel()
self.metrics = register_cache("dictionary", name, self.cache)
def check_thread(self):
expected_thread = self.thread
@ -82,8 +78,6 @@ class DictionaryCache:
"""
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)
@ -95,7 +89,6 @@ class DictionaryCache:
{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):

View file

@ -18,6 +18,7 @@ from functools import wraps
from typing import Callable, Optional, Type, Union
from synapse.config import cache as cache_config
from synapse.util.caches import CacheMetric, register_cache
from synapse.util.caches.treecache import TreeCache
@ -43,27 +44,29 @@ class _Node:
class LruCache:
"""
Least-recently-used cache.
Least-recently-used cache, supporting prometheus metrics and invalidation callbacks.
Supports del_multi only if cache_type=TreeCache
If cache_type=TreeCache, all keys must be tuples.
Can also set callbacks on objects when getting/setting which are fired
when that key gets invalidated/evicted.
"""
def __init__(
self,
max_size: int,
cache_name: Optional[str] = None,
keylen: int = 1,
cache_type: Type[Union[dict, TreeCache]] = dict,
size_callback: Optional[Callable] = None,
evicted_callback: Optional[Callable] = None,
metrics_collection_callback: Optional[Callable[[], None]] = None,
apply_cache_factor_from_config: bool = True,
):
"""
Args:
max_size: The maximum amount of entries the cache can hold
cache_name: The name of this cache, for the prometheus metrics. If unset,
no metrics will be reported on this cache.
keylen: The length of the tuple used as the cache key. Ignored unless
cache_type is `TreeCache`.
@ -73,9 +76,13 @@ class LruCache:
size_callback (func(V) -> int | None):
evicted_callback (func(int)|None):
if not None, called on eviction with the size of the evicted
entry
metrics_collection_callback:
metrics collection callback. This is called early in the metrics
collection process, before any of the metrics registered with the
prometheus Registry are collected, so can be used to update any dynamic
metrics.
Ignored if cache_name is None.
apply_cache_factor_from_config (bool): If true, `max_size` will be
multiplied by a cache factor derived from the homeserver config
@ -94,6 +101,19 @@ class LruCache:
else:
self.max_size = int(max_size)
if cache_name is not None:
metrics = register_cache(
"lru_cache",
cache_name,
self,
collect_callback=metrics_collection_callback,
) # type: Optional[CacheMetric]
else:
metrics = None
# this is exposed for access from outside this class
self.metrics = metrics
list_root = _Node(None, None, None, None)
list_root.next_node = list_root
list_root.prev_node = list_root
@ -105,8 +125,8 @@ class LruCache:
todelete = list_root.prev_node
evicted_len = delete_node(todelete)
cache.pop(todelete.key, None)
if evicted_callback:
evicted_callback(evicted_len)
if metrics:
metrics.inc_evictions(evicted_len)
def synchronized(f):
@wraps(f)
@ -169,13 +189,17 @@ class LruCache:
return deleted_len
@synchronized
def cache_get(key, default=None, callbacks=[]):
def cache_get(key, default=None, callbacks=[], update_metrics=True):
node = cache.get(key, None)
if node is not None:
move_node_to_front(node)
node.callbacks.update(callbacks)
if update_metrics and metrics:
metrics.inc_hits()
return node.value
else:
if update_metrics and metrics:
metrics.inc_misses()
return default
@synchronized

View file

@ -59,7 +59,7 @@ class LruCacheTestCase(unittest.HomeserverTestCase):
self.assertEquals(cache.pop("key"), None)
def test_del_multi(self):
cache = LruCache(4, 2, cache_type=TreeCache)
cache = LruCache(4, keylen=2, cache_type=TreeCache)
cache[("animal", "cat")] = "mew"
cache[("animal", "dog")] = "woof"
cache[("vehicles", "car")] = "vroom"
@ -160,7 +160,7 @@ class LruCacheCallbacksTestCase(unittest.HomeserverTestCase):
m2 = Mock()
m3 = Mock()
m4 = Mock()
cache = LruCache(4, 2, cache_type=TreeCache)
cache = LruCache(4, keylen=2, cache_type=TreeCache)
cache.set(("a", "1"), "value", callbacks=[m1])
cache.set(("a", "2"), "value", callbacks=[m2])