test_deferred_cache.py 8.9 KB

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