def test_cache_metric(self): """ Caches produce metrics reflecting their state when scraped. """ CACHE_NAME = "cache_metrics_test_fgjkbdfg" cache = DeferredCache(CACHE_NAME, max_entries=777) items = { x.split(b"{")[0].decode("ascii"): x.split(b" ")[1].decode("ascii") for x in filter( lambda x: b"cache_metrics_test_fgjkbdfg" in x, generate_latest(REGISTRY).split(b"\n"), ) } self.assertEqual(items["synapse_util_caches_cache_size"], "0.0") self.assertEqual(items["synapse_util_caches_cache_max_size"], "777.0") cache.prefill("1", "hi") items = { x.split(b"{")[0].decode("ascii"): x.split(b" ")[1].decode("ascii") for x in filter( lambda x: b"cache_metrics_test_fgjkbdfg" in x, generate_latest(REGISTRY).split(b"\n"), ) } self.assertEqual(items["synapse_util_caches_cache_size"], "1.0") self.assertEqual(items["synapse_util_caches_cache_max_size"], "777.0")
def test_empty(self): cache = DeferredCache("test") failed = False try: cache.get("foo") except KeyError: failed = True self.assertTrue(failed)
def __get__(self, obj, owner): cache = DeferredCache( name=self.orig.__name__, max_entries=self.max_entries, keylen=self.num_args, tree=self.tree, iterable=self.iterable, ) # type: DeferredCache[CacheKey, Any] get_cache_key = self.cache_key_builder @functools.wraps(self.orig) def _wrapped(*args, **kwargs): # If we're passed a cache_context then we'll want to call its invalidate() # whenever we are invalidated invalidate_callback = kwargs.pop("on_invalidate", None) cache_key = get_cache_key(args, kwargs) try: ret = cache.get(cache_key, callback=invalidate_callback) except KeyError: # Add our own `cache_context` to argument list if the wrapped function # has asked for one if self.add_cache_context: kwargs["cache_context"] = _CacheContext.get_instance( cache, cache_key ) ret = defer.maybeDeferred(preserve_fn(self.orig), obj, *args, **kwargs) ret = cache.set(cache_key, ret, callback=invalidate_callback) return make_deferred_yieldable(ret) wrapped = cast(_CachedFunction, _wrapped) if self.num_args == 1: wrapped.invalidate = lambda key: cache.invalidate(key[0]) wrapped.prefill = lambda key, val: cache.prefill(key[0], val) else: wrapped.invalidate = cache.invalidate wrapped.invalidate_many = cache.invalidate_many wrapped.prefill = cache.prefill wrapped.invalidate_all = cache.invalidate_all wrapped.cache = cache wrapped.num_args = self.num_args obj.__dict__[self.orig.__name__] = wrapped return wrapped
def test_eviction(self): cache = DeferredCache( "test", max_entries=2, apply_cache_factor_from_config=False ) cache.prefill(1, "one") cache.prefill(2, "two") cache.prefill(3, "three") # 1 will be evicted with self.assertRaises(KeyError): cache.get(1) cache.get(2) cache.get(3)
def test_callbacks(self): """Invalidation callbacks are called at the right time""" cache = DeferredCache("test") callbacks = set() # start with an entry, with a callback cache.prefill("k1", 10, callback=lambda: callbacks.add("prefill")) # now replace that entry with a pending result origin_d = defer.Deferred() set_d = cache.set("k1", origin_d, callback=lambda: callbacks.add("set")) # ... and also make a get request get_d = cache.get("k1", callback=lambda: callbacks.add("get")) # we don't expect the invalidation callback for the original value to have # been called yet, even though get() will now return a different result. # I'm not sure if that is by design or not. self.assertEqual(callbacks, set()) # now fire off all the deferreds origin_d.callback(20) self.assertEqual(self.successResultOf(set_d), 20) self.assertEqual(self.successResultOf(get_d), 20) # now the original invalidation callback should have been called, but none of # the others self.assertEqual(callbacks, {"prefill"}) callbacks.clear() # another update should invalidate both the previous results cache.prefill("k1", 30) self.assertEqual(callbacks, {"set", "get"})
def test_invalidate(self): cache = DeferredCache("test") cache.prefill(("foo",), 123) cache.invalidate(("foo",)) with self.assertRaises(KeyError): cache.get(("foo",))
def __get__(self, obj: Optional[Any], owner: Optional[Type]) -> Callable[..., Any]: cache: DeferredCache[CacheKey, Any] = DeferredCache( name=self.orig.__name__, max_entries=self.max_entries, tree=self.tree, iterable=self.iterable, prune_unread_entries=self.prune_unread_entries, ) get_cache_key = self.cache_key_builder @functools.wraps(self.orig) def _wrapped(*args: Any, **kwargs: Any) -> Any: # If we're passed a cache_context then we'll want to call its invalidate() # whenever we are invalidated invalidate_callback = kwargs.pop("on_invalidate", None) cache_key = get_cache_key(args, kwargs) try: ret = cache.get(cache_key, callback=invalidate_callback) except KeyError: # Add our own `cache_context` to argument list if the wrapped function # has asked for one if self.add_cache_context: kwargs["cache_context"] = _CacheContext.get_instance( cache, cache_key) ret = defer.maybeDeferred(preserve_fn(self.orig), obj, *args, **kwargs) ret = cache.set(cache_key, ret, callback=invalidate_callback) # We started a new call to `self.orig`, so we must always wait for it to # complete. Otherwise we might mark our current logging context as # finished while `self.orig` is still using it in the background. ret = delay_cancellation(ret) return make_deferred_yieldable(ret) wrapped = cast(_CachedFunction, _wrapped) if self.num_args == 1: assert not self.tree wrapped.invalidate = lambda key: cache.invalidate(key[0]) wrapped.prefill = lambda key, val: cache.prefill(key[0], val) else: wrapped.invalidate = cache.invalidate wrapped.prefill = cache.prefill wrapped.invalidate_all = cache.invalidate_all wrapped.cache = cache wrapped.num_args = self.num_args obj.__dict__[self.orig.__name__] = wrapped return wrapped
def test_invalidate(self): cache = DeferredCache("test") cache.prefill(("foo", ), 123) cache.invalidate(("foo", )) failed = False try: cache.get(("foo", )) except KeyError: failed = True self.assertTrue(failed)
def test_eviction(self): cache = DeferredCache("test", max_entries=2, apply_cache_factor_from_config=False) cache.prefill(1, "one") cache.prefill(2, "two") cache.prefill(3, "three") # 1 will be evicted failed = False try: cache.get(1) except KeyError: failed = True self.assertTrue(failed) cache.get(2) cache.get(3)
def test_hit_deferred(self): cache = DeferredCache("test") origin_d = defer.Deferred() set_d = cache.set("k1", origin_d) # get should return an incomplete deferred get_d = cache.get("k1") self.assertFalse(get_d.called) # add a callback that will make sure that the set_d gets called before the get_d def check1(r): self.assertTrue(set_d.called) return r get_d.addCallback(check1) # now fire off all the deferreds origin_d.callback(99) self.assertEqual(self.successResultOf(origin_d), 99) self.assertEqual(self.successResultOf(set_d), 99) self.assertEqual(self.successResultOf(get_d), 99)
def test_hit_deferred(self): cache = DeferredCache("test") origin_d = defer.Deferred() set_d = cache.set("k1", origin_d) # get should return an incomplete deferred get_d = cache.get("k1") self.assertFalse(get_d.called) # add a callback that will make sure that the set_d gets called before the get_d def check1(r): self.assertTrue(set_d.called) return r # TODO: Actually ObservableDeferred *doesn't* run its tests in order on py3.8. # maybe we should fix that? # get_d.addCallback(check1) # now fire off all the deferreds origin_d.callback(99) self.assertEqual(self.successResultOf(origin_d), 99) self.assertEqual(self.successResultOf(set_d), 99) self.assertEqual(self.successResultOf(get_d), 99)
def test_set_fail(self): cache = DeferredCache("test") callbacks = set() # start with an entry, with a callback cache.prefill("k1", 10, callback=lambda: callbacks.add("prefill")) # now replace that entry with a pending result origin_d = defer.Deferred() set_d = cache.set("k1", origin_d, callback=lambda: callbacks.add("set")) # ... and also make a get request get_d = cache.get("k1", callback=lambda: callbacks.add("get")) # none of the callbacks should have been called yet self.assertEqual(callbacks, set()) # oh noes! fails! e = Exception("oops") origin_d.errback(e) self.assertIs(self.failureResultOf(set_d, Exception).value, e) self.assertIs(self.failureResultOf(get_d, Exception).value, e) # the callbacks for the failed requests should have been called. # I'm not sure if this is deliberate or not. self.assertEqual(callbacks, {"get", "set"}) callbacks.clear() # the old value should still be returned now? get_d2 = cache.get("k1", callback=lambda: callbacks.add("get2")) self.assertEqual(self.successResultOf(get_d2), 10) # replacing the value now should run the callbacks for those requests # which got the original result cache.prefill("k1", 30) self.assertEqual(callbacks, {"prefill", "get2"})
def test_get_immediate(self): cache = DeferredCache("test") d1 = defer.Deferred() cache.set("key1", d1) # get_immediate should return default v = cache.get_immediate("key1", 1) self.assertEqual(v, 1) # now complete the set d1.callback(2) # get_immediate should return result v = cache.get_immediate("key1", 1) self.assertEqual(v, 2)
def test_invalidate_all(self): cache = DeferredCache("testcache") callback_record = [False, False] def record_callback(idx): callback_record[idx] = True # add a couple of pending entries d1 = defer.Deferred() cache.set("key1", d1, partial(record_callback, 0)) d2 = defer.Deferred() cache.set("key2", d2, partial(record_callback, 1)) # lookup should return pending deferreds self.assertFalse(cache.get("key1").called) self.assertFalse(cache.get("key2").called) # let one of the lookups complete d2.callback("result2") # now the cache will return a completed deferred self.assertEqual(self.successResultOf(cache.get("key2")), "result2") # now do the invalidation cache.invalidate_all() # lookup should fail with self.assertRaises(KeyError): cache.get("key1") with self.assertRaises(KeyError): cache.get("key2") # both callbacks should have been callbacked self.assertTrue(callback_record[0], "Invalidation callback for key1 not called") self.assertTrue(callback_record[1], "Invalidation callback for key2 not called") # letting the other lookup complete should do nothing d1.callback("result1") with self.assertRaises(KeyError): cache.get("key1", None)
def test_eviction_lru(self): cache = DeferredCache("test", max_entries=2, apply_cache_factor_from_config=False) cache.prefill(1, "one") cache.prefill(2, "two") # Now access 1 again, thus causing 2 to be least-recently used cache.get(1) cache.prefill(3, "three") failed = False try: cache.get(2) except KeyError: failed = True self.assertTrue(failed) cache.get(1) cache.get(3)
def test_hit(self): cache = DeferredCache("test") cache.prefill("foo", 123) self.assertEquals(self.successResultOf(cache.get("foo")), 123)
def test_empty(self): cache = DeferredCache("test") with self.assertRaises(KeyError): cache.get("foo")
def test_eviction_iterable(self): cache = DeferredCache( "test", max_entries=3, apply_cache_factor_from_config=False, iterable=True, ) cache.prefill(1, ["one", "two"]) cache.prefill(2, ["three"]) # Now access 1 again, thus causing 2 to be least-recently used cache.get(1) # Now add an item to the cache, which evicts 2. cache.prefill(3, ["four"]) with self.assertRaises(KeyError): cache.get(2) # Ensure 1 & 3 are in the cache. cache.get(1) cache.get(3) # Now access 1 again, thus causing 3 to be least-recently used cache.get(1) # Now add an item with multiple elements to the cache cache.prefill(4, ["five", "six"]) # Both 1 and 3 are evicted since there's too many elements. with self.assertRaises(KeyError): cache.get(1) with self.assertRaises(KeyError): cache.get(3) # Now add another item to fill the cache again. cache.prefill(5, ["seven"]) # Now access 4, thus causing 5 to be least-recently used cache.get(4) # Add an empty item. cache.prefill(6, []) # 5 gets evicted and replaced since an empty element counts as an item. with self.assertRaises(KeyError): cache.get(5) cache.get(4) cache.get(6)
def test_eviction_lru(self): cache = DeferredCache( "test", max_entries=2, apply_cache_factor_from_config=False ) cache.prefill(1, "one") cache.prefill(2, "two") # Now access 1 again, thus causing 2 to be least-recently used cache.get(1) cache.prefill(3, "three") with self.assertRaises(KeyError): cache.get(2) cache.get(1) cache.get(3)
def __get__(self, obj, owner): cache = DeferredCache( name=self.orig.__name__, max_entries=self.max_entries, keylen=self.num_args, tree=self.tree, iterable=self.iterable, ) # type: DeferredCache[CacheKey, Any] def get_cache_key_gen(args, kwargs): """Given some args/kwargs return a generator that resolves into the cache_key. We loop through each arg name, looking up if its in the `kwargs`, otherwise using the next argument in `args`. If there are no more args then we try looking the arg name up in the defaults """ pos = 0 for nm in self.arg_names: if nm in kwargs: yield kwargs[nm] elif pos < len(args): yield args[pos] pos += 1 else: yield self.arg_defaults[nm] # By default our cache key is a tuple, but if there is only one item # then don't bother wrapping in a tuple. This is to save memory. if self.num_args == 1: nm = self.arg_names[0] def get_cache_key(args, kwargs): if nm in kwargs: return kwargs[nm] elif len(args): return args[0] else: return self.arg_defaults[nm] else: def get_cache_key(args, kwargs): return tuple(get_cache_key_gen(args, kwargs)) @functools.wraps(self.orig) def _wrapped(*args, **kwargs): # If we're passed a cache_context then we'll want to call its invalidate() # whenever we are invalidated invalidate_callback = kwargs.pop("on_invalidate", None) cache_key = get_cache_key(args, kwargs) try: ret = cache.get(cache_key, callback=invalidate_callback) except KeyError: # Add our own `cache_context` to argument list if the wrapped function # has asked for one if self.add_cache_context: kwargs["cache_context"] = _CacheContext.get_instance( cache, cache_key) ret = defer.maybeDeferred(preserve_fn(self.orig), obj, *args, **kwargs) ret = cache.set(cache_key, ret, callback=invalidate_callback) return make_deferred_yieldable(ret) wrapped = cast(_CachedFunction, _wrapped) if self.num_args == 1: wrapped.invalidate = lambda key: cache.invalidate(key[0]) wrapped.prefill = lambda key, val: cache.prefill(key[0], val) else: wrapped.invalidate = cache.invalidate wrapped.invalidate_all = cache.invalidate_all wrapped.invalidate_many = cache.invalidate_many wrapped.prefill = cache.prefill wrapped.invalidate_all = cache.invalidate_all wrapped.cache = cache wrapped.num_args = self.num_args obj.__dict__[self.orig.__name__] = wrapped return wrapped