test_deferred_cache.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. # Copyright 2020 The Matrix.org Foundation C.I.C.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. from functools import partial
  15. from typing import List, Tuple
  16. from twisted.internet import defer
  17. from synapse.util.caches.deferred_cache import DeferredCache
  18. from tests.unittest import TestCase
  19. class DeferredCacheTestCase(TestCase):
  20. def test_empty(self) -> None:
  21. cache: DeferredCache[str, int] = DeferredCache("test")
  22. with self.assertRaises(KeyError):
  23. cache.get("foo") # type: ignore[unused-awaitable]
  24. def test_hit(self) -> None:
  25. cache: DeferredCache[str, int] = DeferredCache("test")
  26. cache.prefill("foo", 123)
  27. self.assertEqual(self.successResultOf(cache.get("foo")), 123)
  28. def test_hit_deferred(self) -> None:
  29. cache: DeferredCache[str, int] = DeferredCache("test")
  30. origin_d: "defer.Deferred[int]" = defer.Deferred()
  31. set_d = cache.set("k1", origin_d)
  32. # get should return an incomplete deferred
  33. get_d = cache.get("k1")
  34. self.assertFalse(get_d.called)
  35. # add a callback that will make sure that the set_d gets called before the get_d
  36. def check1(r: str) -> str:
  37. self.assertTrue(set_d.called)
  38. return r
  39. get_d.addCallback(check1) # type: ignore[unused-awaitable]
  40. # now fire off all the deferreds
  41. origin_d.callback(99)
  42. self.assertEqual(self.successResultOf(origin_d), 99)
  43. self.assertEqual(self.successResultOf(set_d), 99)
  44. self.assertEqual(self.successResultOf(get_d), 99)
  45. def test_callbacks(self) -> None:
  46. """Invalidation callbacks are called at the right time"""
  47. cache: DeferredCache[str, int] = DeferredCache("test")
  48. callbacks = set()
  49. # start with an entry, with a callback
  50. cache.prefill("k1", 10, callback=lambda: callbacks.add("prefill"))
  51. # now replace that entry with a pending result
  52. origin_d: "defer.Deferred[int]" = defer.Deferred()
  53. set_d = cache.set("k1", origin_d, callback=lambda: callbacks.add("set"))
  54. # ... and also make a get request
  55. get_d = cache.get("k1", callback=lambda: callbacks.add("get"))
  56. # we don't expect the invalidation callback for the original value to have
  57. # been called yet, even though get() will now return a different result.
  58. # I'm not sure if that is by design or not.
  59. self.assertEqual(callbacks, set())
  60. # now fire off all the deferreds
  61. origin_d.callback(20)
  62. self.assertEqual(self.successResultOf(set_d), 20)
  63. self.assertEqual(self.successResultOf(get_d), 20)
  64. # now the original invalidation callback should have been called, but none of
  65. # the others
  66. self.assertEqual(callbacks, {"prefill"})
  67. callbacks.clear()
  68. # another update should invalidate both the previous results
  69. cache.prefill("k1", 30)
  70. self.assertEqual(callbacks, {"set", "get"})
  71. def test_set_fail(self) -> None:
  72. cache: DeferredCache[str, int] = DeferredCache("test")
  73. callbacks = set()
  74. # start with an entry, with a callback
  75. cache.prefill("k1", 10, callback=lambda: callbacks.add("prefill"))
  76. # now replace that entry with a pending result
  77. origin_d: defer.Deferred = defer.Deferred()
  78. set_d = cache.set("k1", origin_d, callback=lambda: callbacks.add("set"))
  79. # ... and also make a get request
  80. get_d = cache.get("k1", callback=lambda: callbacks.add("get"))
  81. # none of the callbacks should have been called yet
  82. self.assertEqual(callbacks, set())
  83. # oh noes! fails!
  84. e = Exception("oops")
  85. origin_d.errback(e)
  86. self.assertIs(self.failureResultOf(set_d, Exception).value, e)
  87. self.assertIs(self.failureResultOf(get_d, Exception).value, e)
  88. # the callbacks for the failed requests should have been called.
  89. # I'm not sure if this is deliberate or not.
  90. self.assertEqual(callbacks, {"get", "set"})
  91. callbacks.clear()
  92. # the old value should still be returned now?
  93. get_d2 = cache.get("k1", callback=lambda: callbacks.add("get2"))
  94. self.assertEqual(self.successResultOf(get_d2), 10)
  95. # replacing the value now should run the callbacks for those requests
  96. # which got the original result
  97. cache.prefill("k1", 30)
  98. self.assertEqual(callbacks, {"prefill", "get2"})
  99. def test_get_immediate(self) -> None:
  100. cache: DeferredCache[str, int] = DeferredCache("test")
  101. d1: "defer.Deferred[int]" = defer.Deferred()
  102. cache.set("key1", d1) # type: ignore[unused-awaitable]
  103. # get_immediate should return default
  104. v = cache.get_immediate("key1", 1)
  105. self.assertEqual(v, 1)
  106. # now complete the set
  107. d1.callback(2)
  108. # get_immediate should return result
  109. v = cache.get_immediate("key1", 1)
  110. self.assertEqual(v, 2)
  111. def test_invalidate(self) -> None:
  112. cache: DeferredCache[Tuple[str], int] = DeferredCache("test")
  113. cache.prefill(("foo",), 123)
  114. cache.invalidate(("foo",))
  115. with self.assertRaises(KeyError):
  116. cache.get(("foo",)) # type: ignore[unused-awaitable]
  117. def test_invalidate_all(self) -> None:
  118. cache: DeferredCache[str, str] = DeferredCache("testcache")
  119. callback_record = [False, False]
  120. def record_callback(idx: int) -> None:
  121. callback_record[idx] = True
  122. # add a couple of pending entries
  123. d1: "defer.Deferred[str]" = defer.Deferred()
  124. cache.set("key1", d1, partial(record_callback, 0)) # type: ignore[unused-awaitable]
  125. d2: "defer.Deferred[str]" = defer.Deferred()
  126. cache.set("key2", d2, partial(record_callback, 1)) # type: ignore[unused-awaitable]
  127. # lookup should return pending deferreds
  128. self.assertFalse(cache.get("key1").called)
  129. self.assertFalse(cache.get("key2").called)
  130. # let one of the lookups complete
  131. d2.callback("result2")
  132. # now the cache will return a completed deferred
  133. self.assertEqual(self.successResultOf(cache.get("key2")), "result2")
  134. # now do the invalidation
  135. cache.invalidate_all()
  136. # lookup should fail
  137. with self.assertRaises(KeyError):
  138. cache.get("key1") # type: ignore[unused-awaitable]
  139. with self.assertRaises(KeyError):
  140. cache.get("key2") # type: ignore[unused-awaitable]
  141. # both callbacks should have been callbacked
  142. self.assertTrue(callback_record[0], "Invalidation callback for key1 not called")
  143. self.assertTrue(callback_record[1], "Invalidation callback for key2 not called")
  144. # letting the other lookup complete should do nothing
  145. d1.callback("result1")
  146. with self.assertRaises(KeyError):
  147. cache.get("key1", None) # type: ignore[unused-awaitable]
  148. def test_eviction(self) -> None:
  149. cache: DeferredCache[int, str] = DeferredCache(
  150. "test", max_entries=2, apply_cache_factor_from_config=False
  151. )
  152. cache.prefill(1, "one")
  153. cache.prefill(2, "two")
  154. cache.prefill(3, "three") # 1 will be evicted
  155. with self.assertRaises(KeyError):
  156. cache.get(1) # type: ignore[unused-awaitable]
  157. cache.get(2) # type: ignore[unused-awaitable]
  158. cache.get(3) # type: ignore[unused-awaitable]
  159. def test_eviction_lru(self) -> None:
  160. cache: DeferredCache[int, str] = DeferredCache(
  161. "test", max_entries=2, apply_cache_factor_from_config=False
  162. )
  163. cache.prefill(1, "one")
  164. cache.prefill(2, "two")
  165. # Now access 1 again, thus causing 2 to be least-recently used
  166. cache.get(1) # type: ignore[unused-awaitable]
  167. cache.prefill(3, "three")
  168. with self.assertRaises(KeyError):
  169. cache.get(2) # type: ignore[unused-awaitable]
  170. cache.get(1) # type: ignore[unused-awaitable]
  171. cache.get(3) # type: ignore[unused-awaitable]
  172. def test_eviction_iterable(self) -> None:
  173. cache: DeferredCache[int, List[str]] = DeferredCache(
  174. "test",
  175. max_entries=3,
  176. apply_cache_factor_from_config=False,
  177. iterable=True,
  178. )
  179. cache.prefill(1, ["one", "two"])
  180. cache.prefill(2, ["three"])
  181. # Now access 1 again, thus causing 2 to be least-recently used
  182. cache.get(1) # type: ignore[unused-awaitable]
  183. # Now add an item to the cache, which evicts 2.
  184. cache.prefill(3, ["four"])
  185. with self.assertRaises(KeyError):
  186. cache.get(2) # type: ignore[unused-awaitable]
  187. # Ensure 1 & 3 are in the cache.
  188. cache.get(1) # type: ignore[unused-awaitable]
  189. cache.get(3) # type: ignore[unused-awaitable]
  190. # Now access 1 again, thus causing 3 to be least-recently used
  191. cache.get(1) # type: ignore[unused-awaitable]
  192. # Now add an item with multiple elements to the cache
  193. cache.prefill(4, ["five", "six"])
  194. # Both 1 and 3 are evicted since there's too many elements.
  195. with self.assertRaises(KeyError):
  196. cache.get(1) # type: ignore[unused-awaitable]
  197. with self.assertRaises(KeyError):
  198. cache.get(3) # type: ignore[unused-awaitable]
  199. # Now add another item to fill the cache again.
  200. cache.prefill(5, ["seven"])
  201. # Now access 4, thus causing 5 to be least-recently used
  202. cache.get(4) # type: ignore[unused-awaitable]
  203. # Add an empty item.
  204. cache.prefill(6, [])
  205. # 5 gets evicted and replaced since an empty element counts as an item.
  206. with self.assertRaises(KeyError):
  207. cache.get(5) # type: ignore[unused-awaitable]
  208. cache.get(4) # type: ignore[unused-awaitable]
  209. cache.get(6) # type: ignore[unused-awaitable]