test_transactions.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. # Copyright 2018-2021 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 http import HTTPStatus
  15. from typing import Any, Generator, Tuple, cast
  16. from unittest.mock import AsyncMock, Mock, call
  17. from twisted.internet import defer, reactor as _reactor
  18. from synapse.logging.context import SENTINEL_CONTEXT, LoggingContext, current_context
  19. from synapse.rest.client.transactions import CLEANUP_PERIOD_MS, HttpTransactionCache
  20. from synapse.types import ISynapseReactor, JsonDict
  21. from synapse.util import Clock
  22. from tests import unittest
  23. from tests.utils import MockClock
  24. reactor = cast(ISynapseReactor, _reactor)
  25. class HttpTransactionCacheTestCase(unittest.TestCase):
  26. def setUp(self) -> None:
  27. self.clock = MockClock()
  28. self.hs = Mock()
  29. self.hs.get_clock = Mock(return_value=self.clock)
  30. self.hs.get_auth = Mock()
  31. self.cache = HttpTransactionCache(self.hs)
  32. self.mock_http_response = (HTTPStatus.OK, {"result": "GOOD JOB!"})
  33. # Here we make sure that we're setting all the fields that HttpTransactionCache
  34. # uses to build the transaction key.
  35. self.mock_request = Mock()
  36. self.mock_request.path = b"/foo/bar"
  37. self.mock_requester = Mock()
  38. self.mock_requester.app_service = None
  39. self.mock_requester.is_guest = False
  40. self.mock_requester.access_token_id = 1234
  41. @defer.inlineCallbacks
  42. def test_executes_given_function(
  43. self,
  44. ) -> Generator["defer.Deferred[Any]", object, None]:
  45. cb = AsyncMock(return_value=self.mock_http_response)
  46. res = yield self.cache.fetch_or_execute_request(
  47. self.mock_request, self.mock_requester, cb, "some_arg", keyword="arg"
  48. )
  49. cb.assert_called_once_with("some_arg", keyword="arg")
  50. self.assertEqual(res, self.mock_http_response)
  51. @defer.inlineCallbacks
  52. def test_deduplicates_based_on_key(
  53. self,
  54. ) -> Generator["defer.Deferred[Any]", object, None]:
  55. cb = AsyncMock(return_value=self.mock_http_response)
  56. for i in range(3): # invoke multiple times
  57. res = yield self.cache.fetch_or_execute_request(
  58. self.mock_request,
  59. self.mock_requester,
  60. cb,
  61. "some_arg",
  62. keyword="arg",
  63. changing_args=i,
  64. )
  65. self.assertEqual(res, self.mock_http_response)
  66. # expect only a single call to do the work
  67. cb.assert_called_once_with("some_arg", keyword="arg", changing_args=0)
  68. @defer.inlineCallbacks
  69. def test_logcontexts_with_async_result(
  70. self,
  71. ) -> Generator["defer.Deferred[Any]", object, None]:
  72. @defer.inlineCallbacks
  73. def cb() -> Generator["defer.Deferred[object]", object, Tuple[int, JsonDict]]:
  74. yield Clock(reactor).sleep(0)
  75. return 1, {}
  76. @defer.inlineCallbacks
  77. def test() -> Generator["defer.Deferred[Any]", object, None]:
  78. with LoggingContext("c") as c1:
  79. res = yield self.cache.fetch_or_execute_request(
  80. self.mock_request, self.mock_requester, cb
  81. )
  82. self.assertIs(current_context(), c1)
  83. self.assertEqual(res, (1, {}))
  84. # run the test twice in parallel
  85. d = defer.gatherResults([test(), test()])
  86. self.assertIs(current_context(), SENTINEL_CONTEXT)
  87. yield d
  88. self.assertIs(current_context(), SENTINEL_CONTEXT)
  89. @defer.inlineCallbacks
  90. def test_does_not_cache_exceptions(
  91. self,
  92. ) -> Generator["defer.Deferred[Any]", object, None]:
  93. """Checks that, if the callback throws an exception, it is called again
  94. for the next request.
  95. """
  96. called = [False]
  97. def cb() -> "defer.Deferred[Tuple[int, JsonDict]]":
  98. if called[0]:
  99. # return a valid result the second time
  100. return defer.succeed(self.mock_http_response)
  101. called[0] = True
  102. raise Exception("boo")
  103. with LoggingContext("test") as test_context:
  104. try:
  105. yield self.cache.fetch_or_execute_request(
  106. self.mock_request, self.mock_requester, cb
  107. )
  108. except Exception as e:
  109. self.assertEqual(e.args[0], "boo")
  110. self.assertIs(current_context(), test_context)
  111. res = yield self.cache.fetch_or_execute_request(
  112. self.mock_request, self.mock_requester, cb
  113. )
  114. self.assertEqual(res, self.mock_http_response)
  115. self.assertIs(current_context(), test_context)
  116. @defer.inlineCallbacks
  117. def test_does_not_cache_failures(
  118. self,
  119. ) -> Generator["defer.Deferred[Any]", object, None]:
  120. """Checks that, if the callback returns a failure, it is called again
  121. for the next request.
  122. """
  123. called = [False]
  124. def cb() -> "defer.Deferred[Tuple[int, JsonDict]]":
  125. if called[0]:
  126. # return a valid result the second time
  127. return defer.succeed(self.mock_http_response)
  128. called[0] = True
  129. return defer.fail(Exception("boo"))
  130. with LoggingContext("test") as test_context:
  131. try:
  132. yield self.cache.fetch_or_execute_request(
  133. self.mock_request, self.mock_requester, cb
  134. )
  135. except Exception as e:
  136. self.assertEqual(e.args[0], "boo")
  137. self.assertIs(current_context(), test_context)
  138. res = yield self.cache.fetch_or_execute_request(
  139. self.mock_request, self.mock_requester, cb
  140. )
  141. self.assertEqual(res, self.mock_http_response)
  142. self.assertIs(current_context(), test_context)
  143. @defer.inlineCallbacks
  144. def test_cleans_up(self) -> Generator["defer.Deferred[Any]", object, None]:
  145. cb = AsyncMock(return_value=self.mock_http_response)
  146. yield self.cache.fetch_or_execute_request(
  147. self.mock_request, self.mock_requester, cb, "an arg"
  148. )
  149. # should NOT have cleaned up yet
  150. self.clock.advance_time_msec(CLEANUP_PERIOD_MS / 2)
  151. yield self.cache.fetch_or_execute_request(
  152. self.mock_request, self.mock_requester, cb, "an arg"
  153. )
  154. # still using cache
  155. cb.assert_called_once_with("an arg")
  156. self.clock.advance_time_msec(CLEANUP_PERIOD_MS)
  157. yield self.cache.fetch_or_execute_request(
  158. self.mock_request, self.mock_requester, cb, "an arg"
  159. )
  160. # no longer using cache
  161. self.assertEqual(cb.call_count, 2)
  162. self.assertEqual(cb.call_args_list, [call("an arg"), call("an arg")])