response_cache.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2016 OpenMarket Ltd
  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. import logging
  16. from twisted.internet import defer
  17. from synapse.util.async import ObservableDeferred
  18. from synapse.util.caches import register_cache
  19. from synapse.util.logcontext import make_deferred_yieldable, run_in_background
  20. logger = logging.getLogger(__name__)
  21. class ResponseCache(object):
  22. """
  23. This caches a deferred response. Until the deferred completes it will be
  24. returned from the cache. This means that if the client retries the request
  25. while the response is still being computed, that original response will be
  26. used rather than trying to compute a new response.
  27. """
  28. def __init__(self, hs, name, timeout_ms=0):
  29. self.pending_result_cache = {} # Requests that haven't finished yet.
  30. self.clock = hs.get_clock()
  31. self.timeout_sec = timeout_ms / 1000.
  32. self._name = name
  33. self._metrics = register_cache(
  34. "response_cache", name, self
  35. )
  36. def size(self):
  37. return len(self.pending_result_cache)
  38. def __len__(self):
  39. return self.size()
  40. def get(self, key):
  41. """Look up the given key.
  42. Can return either a new Deferred (which also doesn't follow the synapse
  43. logcontext rules), or, if the request has completed, the actual
  44. result. You will probably want to make_deferred_yieldable the result.
  45. If there is no entry for the key, returns None. It is worth noting that
  46. this means there is no way to distinguish a completed result of None
  47. from an absent cache entry.
  48. Args:
  49. key (hashable):
  50. Returns:
  51. twisted.internet.defer.Deferred|None|E: None if there is no entry
  52. for this key; otherwise either a deferred result or the result
  53. itself.
  54. """
  55. result = self.pending_result_cache.get(key)
  56. if result is not None:
  57. self._metrics.inc_hits()
  58. return result.observe()
  59. else:
  60. self._metrics.inc_misses()
  61. return None
  62. def set(self, key, deferred):
  63. """Set the entry for the given key to the given deferred.
  64. *deferred* should run its callbacks in the sentinel logcontext (ie,
  65. you should wrap normal synapse deferreds with
  66. logcontext.run_in_background).
  67. Can return either a new Deferred (which also doesn't follow the synapse
  68. logcontext rules), or, if *deferred* was already complete, the actual
  69. result. You will probably want to make_deferred_yieldable the result.
  70. Args:
  71. key (hashable):
  72. deferred (twisted.internet.defer.Deferred[T):
  73. Returns:
  74. twisted.internet.defer.Deferred[T]|T: a new deferred, or the actual
  75. result.
  76. """
  77. result = ObservableDeferred(deferred, consumeErrors=True)
  78. self.pending_result_cache[key] = result
  79. def remove(r):
  80. if self.timeout_sec:
  81. self.clock.call_later(
  82. self.timeout_sec,
  83. self.pending_result_cache.pop, key, None,
  84. )
  85. else:
  86. self.pending_result_cache.pop(key, None)
  87. return r
  88. result.addBoth(remove)
  89. return result.observe()
  90. def wrap(self, key, callback, *args, **kwargs):
  91. """Wrap together a *get* and *set* call, taking care of logcontexts
  92. First looks up the key in the cache, and if it is present makes it
  93. follow the synapse logcontext rules and returns it.
  94. Otherwise, makes a call to *callback(*args, **kwargs)*, which should
  95. follow the synapse logcontext rules, and adds the result to the cache.
  96. Example usage:
  97. @defer.inlineCallbacks
  98. def handle_request(request):
  99. # etc
  100. defer.returnValue(result)
  101. result = yield response_cache.wrap(
  102. key,
  103. handle_request,
  104. request,
  105. )
  106. Args:
  107. key (hashable): key to get/set in the cache
  108. callback (callable): function to call if the key is not found in
  109. the cache
  110. *args: positional parameters to pass to the callback, if it is used
  111. **kwargs: named paramters to pass to the callback, if it is used
  112. Returns:
  113. twisted.internet.defer.Deferred: yieldable result
  114. """
  115. result = self.get(key)
  116. if not result:
  117. logger.info("[%s]: no cached result for [%s], calculating new one",
  118. self._name, key)
  119. d = run_in_background(callback, *args, **kwargs)
  120. result = self.set(key, d)
  121. elif not isinstance(result, defer.Deferred) or result.called:
  122. logger.info("[%s]: using completed cached result for [%s]",
  123. self._name, key)
  124. else:
  125. logger.info("[%s]: using incomplete cached result for [%s]",
  126. self._name, key)
  127. return make_deferred_yieldable(result)