test_deferred_cache.py 8.8 KB

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