You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

635 lines
22 KiB

  1. # Copyright 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 unittest.mock import AsyncMock, Mock
  15. import yaml
  16. from twisted.internet.defer import Deferred, ensureDeferred
  17. from twisted.test.proto_helpers import MemoryReactor
  18. from synapse.server import HomeServer
  19. from synapse.storage.background_updates import (
  20. BackgroundUpdater,
  21. ForeignKeyConstraint,
  22. NotNullConstraint,
  23. run_validate_constraint_and_delete_rows_schema_delta,
  24. )
  25. from synapse.storage.database import LoggingTransaction
  26. from synapse.storage.engines import PostgresEngine, Sqlite3Engine
  27. from synapse.types import JsonDict
  28. from synapse.util import Clock
  29. from tests import unittest
  30. from tests.unittest import override_config
  31. class BackgroundUpdateTestCase(unittest.HomeserverTestCase):
  32. def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
  33. self.updates: BackgroundUpdater = self.hs.get_datastores().main.db_pool.updates
  34. # the base test class should have run the real bg updates for us
  35. self.assertTrue(
  36. self.get_success(self.updates.has_completed_background_updates())
  37. )
  38. self.update_handler = Mock()
  39. self.updates.register_background_update_handler(
  40. "test_update", self.update_handler
  41. )
  42. self.store = self.hs.get_datastores().main
  43. async def update(self, progress: JsonDict, count: int) -> int:
  44. duration_ms = 10
  45. await self.clock.sleep((count * duration_ms) / 1000)
  46. progress = {"my_key": progress["my_key"] + 1}
  47. await self.store.db_pool.runInteraction(
  48. "update_progress",
  49. self.updates._background_update_progress_txn,
  50. "test_update",
  51. progress,
  52. )
  53. return count
  54. def test_do_background_update(self) -> None:
  55. # the time we claim it takes to update one item when running the update
  56. duration_ms = 10
  57. # the target runtime for each bg update
  58. target_background_update_duration_ms = 100
  59. self.get_success(
  60. self.store.db_pool.simple_insert(
  61. "background_updates",
  62. values={"update_name": "test_update", "progress_json": '{"my_key": 1}'},
  63. )
  64. )
  65. self.update_handler.side_effect = self.update
  66. self.update_handler.reset_mock()
  67. res = self.get_success(
  68. self.updates.do_next_background_update(False),
  69. by=0.02,
  70. )
  71. self.assertFalse(res)
  72. # on the first call, we should get run with the default background update size
  73. self.update_handler.assert_called_once_with(
  74. {"my_key": 1}, self.updates.default_background_batch_size
  75. )
  76. # second step: complete the update
  77. # we should now get run with a much bigger number of items to update
  78. async def update(progress: JsonDict, count: int) -> int:
  79. self.assertEqual(progress, {"my_key": 2})
  80. self.assertAlmostEqual(
  81. count,
  82. target_background_update_duration_ms / duration_ms,
  83. places=0,
  84. )
  85. await self.updates._end_background_update("test_update")
  86. return count
  87. self.update_handler.side_effect = update
  88. self.update_handler.reset_mock()
  89. result = self.get_success(self.updates.do_next_background_update(False))
  90. self.assertFalse(result)
  91. self.update_handler.assert_called_once()
  92. # third step: we don't expect to be called any more
  93. self.update_handler.reset_mock()
  94. result = self.get_success(self.updates.do_next_background_update(False))
  95. self.assertTrue(result)
  96. self.assertFalse(self.update_handler.called)
  97. @override_config(
  98. yaml.safe_load(
  99. """
  100. background_updates:
  101. default_batch_size: 20
  102. """
  103. )
  104. )
  105. def test_background_update_default_batch_set_by_config(self) -> None:
  106. """
  107. Test that the background update is run with the default_batch_size set by the config
  108. """
  109. self.get_success(
  110. self.store.db_pool.simple_insert(
  111. "background_updates",
  112. values={"update_name": "test_update", "progress_json": '{"my_key": 1}'},
  113. )
  114. )
  115. self.update_handler.side_effect = self.update
  116. self.update_handler.reset_mock()
  117. res = self.get_success(
  118. self.updates.do_next_background_update(False),
  119. by=0.01,
  120. )
  121. self.assertFalse(res)
  122. # on the first call, we should get run with the default background update size specified in the config
  123. self.update_handler.assert_called_once_with({"my_key": 1}, 20)
  124. def test_background_update_default_sleep_behavior(self) -> None:
  125. """
  126. Test default background update behavior, which is to sleep
  127. """
  128. self.get_success(
  129. self.store.db_pool.simple_insert(
  130. "background_updates",
  131. values={"update_name": "test_update", "progress_json": '{"my_key": 1}'},
  132. )
  133. )
  134. self.update_handler.side_effect = self.update
  135. self.update_handler.reset_mock()
  136. self.updates.start_doing_background_updates()
  137. # 2: advance the reactor less than the default sleep duration (1000ms)
  138. self.reactor.pump([0.5])
  139. # check that an update has not been run
  140. self.update_handler.assert_not_called()
  141. # advance reactor past default sleep duration
  142. self.reactor.pump([1])
  143. # check that update has been run
  144. self.update_handler.assert_called()
  145. @override_config(
  146. yaml.safe_load(
  147. """
  148. background_updates:
  149. sleep_duration_ms: 500
  150. """
  151. )
  152. )
  153. def test_background_update_sleep_set_in_config(self) -> None:
  154. """
  155. Test that changing the sleep time in the config changes how long it sleeps
  156. """
  157. self.get_success(
  158. self.store.db_pool.simple_insert(
  159. "background_updates",
  160. values={"update_name": "test_update", "progress_json": '{"my_key": 1}'},
  161. )
  162. )
  163. self.update_handler.side_effect = self.update
  164. self.update_handler.reset_mock()
  165. self.updates.start_doing_background_updates()
  166. # 2: advance the reactor less than the configured sleep duration (500ms)
  167. self.reactor.pump([0.45])
  168. # check that an update has not been run
  169. self.update_handler.assert_not_called()
  170. # advance reactor past config sleep duration but less than default duration
  171. self.reactor.pump([0.75])
  172. # check that update has been run
  173. self.update_handler.assert_called()
  174. @override_config(
  175. yaml.safe_load(
  176. """
  177. background_updates:
  178. sleep_enabled: false
  179. """
  180. )
  181. )
  182. def test_disabling_background_update_sleep(self) -> None:
  183. """
  184. Test that disabling sleep in the config results in bg update not sleeping
  185. """
  186. self.get_success(
  187. self.store.db_pool.simple_insert(
  188. "background_updates",
  189. values={"update_name": "test_update", "progress_json": '{"my_key": 1}'},
  190. )
  191. )
  192. self.update_handler.side_effect = self.update
  193. self.update_handler.reset_mock()
  194. self.updates.start_doing_background_updates()
  195. # 2: advance the reactor very little
  196. self.reactor.pump([0.025])
  197. # check that an update has run
  198. self.update_handler.assert_called()
  199. @override_config(
  200. yaml.safe_load(
  201. """
  202. background_updates:
  203. background_update_duration_ms: 500
  204. """
  205. )
  206. )
  207. def test_background_update_duration_set_in_config(self) -> None:
  208. """
  209. Test that the desired duration set in the config is used in determining batch size
  210. """
  211. # Duration of one background update item
  212. duration_ms = 10
  213. self.get_success(
  214. self.store.db_pool.simple_insert(
  215. "background_updates",
  216. values={"update_name": "test_update", "progress_json": '{"my_key": 1}'},
  217. )
  218. )
  219. self.update_handler.side_effect = self.update
  220. self.update_handler.reset_mock()
  221. res = self.get_success(
  222. self.updates.do_next_background_update(False),
  223. by=0.02,
  224. )
  225. self.assertFalse(res)
  226. # the first update was run with the default batch size, this should be run with 500ms as the
  227. # desired duration
  228. async def update(progress: JsonDict, count: int) -> int:
  229. self.assertEqual(progress, {"my_key": 2})
  230. self.assertAlmostEqual(
  231. count,
  232. 500 / duration_ms,
  233. places=0,
  234. )
  235. await self.updates._end_background_update("test_update")
  236. return count
  237. self.update_handler.side_effect = update
  238. self.get_success(self.updates.do_next_background_update(False))
  239. @override_config(
  240. yaml.safe_load(
  241. """
  242. background_updates:
  243. min_batch_size: 5
  244. """
  245. )
  246. )
  247. def test_background_update_min_batch_set_in_config(self) -> None:
  248. """
  249. Test that the minimum batch size set in the config is used
  250. """
  251. # a very long-running individual update
  252. duration_ms = 50
  253. self.get_success(
  254. self.store.db_pool.simple_insert(
  255. "background_updates",
  256. values={"update_name": "test_update", "progress_json": '{"my_key": 1}'},
  257. )
  258. )
  259. # Run the update with the long-running update item
  260. async def update_long(progress: JsonDict, count: int) -> int:
  261. await self.clock.sleep((count * duration_ms) / 1000)
  262. progress = {"my_key": progress["my_key"] + 1}
  263. await self.store.db_pool.runInteraction(
  264. "update_progress",
  265. self.updates._background_update_progress_txn,
  266. "test_update",
  267. progress,
  268. )
  269. return count
  270. self.update_handler.side_effect = update_long
  271. self.update_handler.reset_mock()
  272. res = self.get_success(
  273. self.updates.do_next_background_update(False),
  274. by=1,
  275. )
  276. self.assertFalse(res)
  277. # the first update was run with the default batch size, this should be run with minimum batch size
  278. # as the first items took a very long time
  279. async def update_short(progress: JsonDict, count: int) -> int:
  280. self.assertEqual(progress, {"my_key": 2})
  281. self.assertEqual(count, 5)
  282. await self.updates._end_background_update("test_update")
  283. return count
  284. self.update_handler.side_effect = update_short
  285. self.get_success(self.updates.do_next_background_update(False))
  286. class BackgroundUpdateControllerTestCase(unittest.HomeserverTestCase):
  287. def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
  288. self.updates: BackgroundUpdater = self.hs.get_datastores().main.db_pool.updates
  289. # the base test class should have run the real bg updates for us
  290. self.assertTrue(
  291. self.get_success(self.updates.has_completed_background_updates())
  292. )
  293. self.update_deferred: Deferred[int] = Deferred()
  294. self.update_handler = Mock(return_value=self.update_deferred)
  295. self.updates.register_background_update_handler(
  296. "test_update", self.update_handler
  297. )
  298. # Mock out the AsyncContextManager
  299. class MockCM:
  300. __aenter__ = AsyncMock(return_value=None)
  301. __aexit__ = AsyncMock(return_value=None)
  302. self._update_ctx_manager = MockCM
  303. # Mock out the `update_handler` callback
  304. self._on_update = Mock(return_value=self._update_ctx_manager())
  305. # Define a default batch size value that's not the same as the internal default
  306. # value (100).
  307. self._default_batch_size = 500
  308. # Register the callbacks with more mocks
  309. self.hs.get_module_api().register_background_update_controller_callbacks(
  310. on_update=self._on_update,
  311. min_batch_size=AsyncMock(return_value=self._default_batch_size),
  312. default_batch_size=AsyncMock(
  313. return_value=self._default_batch_size,
  314. ),
  315. )
  316. def test_controller(self) -> None:
  317. store = self.hs.get_datastores().main
  318. self.get_success(
  319. store.db_pool.simple_insert(
  320. "background_updates",
  321. values={"update_name": "test_update", "progress_json": "{}"},
  322. )
  323. )
  324. # Set the return value for the context manager.
  325. enter_defer: Deferred[int] = Deferred()
  326. self._update_ctx_manager.__aenter__ = Mock(return_value=enter_defer)
  327. # Start the background update.
  328. do_update_d = ensureDeferred(self.updates.do_next_background_update(True))
  329. self.pump()
  330. # `run_update` should have been called, but the update handler won't be
  331. # called until the `enter_defer` (returned by `__aenter__`) is resolved.
  332. self._on_update.assert_called_once_with(
  333. "test_update",
  334. "master",
  335. False,
  336. )
  337. self.assertFalse(do_update_d.called)
  338. self.assertFalse(self.update_deferred.called)
  339. # Resolving the `enter_defer` should call the update handler, which then
  340. # blocks.
  341. enter_defer.callback(100)
  342. self.pump()
  343. self.update_handler.assert_called_once_with({}, self._default_batch_size)
  344. self.assertFalse(self.update_deferred.called)
  345. self._update_ctx_manager.__aexit__.assert_not_called()
  346. # Resolving the update handler deferred should cause the
  347. # `do_next_background_update` to finish and return
  348. self.update_deferred.callback(100)
  349. self.pump()
  350. self._update_ctx_manager.__aexit__.assert_called()
  351. self.get_success(do_update_d)
  352. class BackgroundUpdateValidateConstraintTestCase(unittest.HomeserverTestCase):
  353. """Tests the validate contraint and delete background handlers."""
  354. def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
  355. self.updates: BackgroundUpdater = self.hs.get_datastores().main.db_pool.updates
  356. # the base test class should have run the real bg updates for us
  357. self.assertTrue(
  358. self.get_success(self.updates.has_completed_background_updates())
  359. )
  360. self.store = self.hs.get_datastores().main
  361. def test_not_null_constraint(self) -> None:
  362. # Create the initial tables, where we have some invalid data.
  363. """Tests adding a not null constraint."""
  364. table_sql = """
  365. CREATE TABLE test_constraint(
  366. a INT PRIMARY KEY,
  367. b INT
  368. );
  369. """
  370. self.get_success(
  371. self.store.db_pool.execute(
  372. "test_not_null_constraint", lambda _: None, table_sql
  373. )
  374. )
  375. # We add an index so that we can check that its correctly recreated when
  376. # using SQLite.
  377. index_sql = "CREATE INDEX test_index ON test_constraint(a)"
  378. self.get_success(
  379. self.store.db_pool.execute(
  380. "test_not_null_constraint", lambda _: None, index_sql
  381. )
  382. )
  383. self.get_success(
  384. self.store.db_pool.simple_insert("test_constraint", {"a": 1, "b": 1})
  385. )
  386. self.get_success(
  387. self.store.db_pool.simple_insert("test_constraint", {"a": 2, "b": None})
  388. )
  389. self.get_success(
  390. self.store.db_pool.simple_insert("test_constraint", {"a": 3, "b": 3})
  391. )
  392. # Now lets do the migration
  393. table2_sqlite = """
  394. CREATE TABLE test_constraint2(
  395. a INT PRIMARY KEY,
  396. b INT,
  397. CONSTRAINT test_constraint_name CHECK (b is NOT NULL)
  398. );
  399. """
  400. def delta(txn: LoggingTransaction) -> None:
  401. run_validate_constraint_and_delete_rows_schema_delta(
  402. txn,
  403. ordering=1000,
  404. update_name="test_bg_update",
  405. table="test_constraint",
  406. constraint_name="test_constraint_name",
  407. constraint=NotNullConstraint("b"),
  408. sqlite_table_name="test_constraint2",
  409. sqlite_table_schema=table2_sqlite,
  410. )
  411. self.get_success(
  412. self.store.db_pool.runInteraction(
  413. "test_not_null_constraint",
  414. delta,
  415. )
  416. )
  417. if isinstance(self.store.database_engine, PostgresEngine):
  418. # Postgres uses a background update
  419. self.updates.register_background_validate_constraint_and_delete_rows(
  420. "test_bg_update",
  421. table="test_constraint",
  422. constraint_name="test_constraint_name",
  423. constraint=NotNullConstraint("b"),
  424. unique_columns=["a"],
  425. )
  426. # Tell the DataStore that it hasn't finished all updates yet
  427. self.store.db_pool.updates._all_done = False
  428. # Now let's actually drive the updates to completion
  429. self.wait_for_background_updates()
  430. # Check the correct values are in the new table.
  431. rows = self.get_success(
  432. self.store.db_pool.simple_select_list(
  433. table="test_constraint",
  434. keyvalues={},
  435. retcols=("a", "b"),
  436. )
  437. )
  438. self.assertCountEqual(rows, [{"a": 1, "b": 1}, {"a": 3, "b": 3}])
  439. # And check that invalid rows get correctly rejected.
  440. self.get_failure(
  441. self.store.db_pool.simple_insert("test_constraint", {"a": 2, "b": None}),
  442. exc=self.store.database_engine.module.IntegrityError,
  443. )
  444. # Check the index is still there for SQLite.
  445. if isinstance(self.store.database_engine, Sqlite3Engine):
  446. # Ensure the index exists in the schema.
  447. self.get_success(
  448. self.store.db_pool.simple_select_one_onecol(
  449. table="sqlite_master",
  450. keyvalues={"tbl_name": "test_constraint"},
  451. retcol="name",
  452. )
  453. )
  454. def test_foreign_constraint(self) -> None:
  455. """Tests adding a not foreign key constraint."""
  456. # Create the initial tables, where we have some invalid data.
  457. base_sql = """
  458. CREATE TABLE base_table(
  459. b INT PRIMARY KEY
  460. );
  461. """
  462. table_sql = """
  463. CREATE TABLE test_constraint(
  464. a INT PRIMARY KEY,
  465. b INT NOT NULL
  466. );
  467. """
  468. self.get_success(
  469. self.store.db_pool.execute(
  470. "test_foreign_key_constraint", lambda _: None, base_sql
  471. )
  472. )
  473. self.get_success(
  474. self.store.db_pool.execute(
  475. "test_foreign_key_constraint", lambda _: None, table_sql
  476. )
  477. )
  478. self.get_success(self.store.db_pool.simple_insert("base_table", {"b": 1}))
  479. self.get_success(
  480. self.store.db_pool.simple_insert("test_constraint", {"a": 1, "b": 1})
  481. )
  482. self.get_success(
  483. self.store.db_pool.simple_insert("test_constraint", {"a": 2, "b": 2})
  484. )
  485. self.get_success(self.store.db_pool.simple_insert("base_table", {"b": 3}))
  486. self.get_success(
  487. self.store.db_pool.simple_insert("test_constraint", {"a": 3, "b": 3})
  488. )
  489. table2_sqlite = """
  490. CREATE TABLE test_constraint2(
  491. a INT PRIMARY KEY,
  492. b INT NOT NULL,
  493. CONSTRAINT test_constraint_name FOREIGN KEY (b) REFERENCES base_table (b)
  494. );
  495. """
  496. def delta(txn: LoggingTransaction) -> None:
  497. run_validate_constraint_and_delete_rows_schema_delta(
  498. txn,
  499. ordering=1000,
  500. update_name="test_bg_update",
  501. table="test_constraint",
  502. constraint_name="test_constraint_name",
  503. constraint=ForeignKeyConstraint(
  504. "base_table", [("b", "b")], deferred=False
  505. ),
  506. sqlite_table_name="test_constraint2",
  507. sqlite_table_schema=table2_sqlite,
  508. )
  509. self.get_success(
  510. self.store.db_pool.runInteraction(
  511. "test_foreign_key_constraint",
  512. delta,
  513. )
  514. )
  515. if isinstance(self.store.database_engine, PostgresEngine):
  516. # Postgres uses a background update
  517. self.updates.register_background_validate_constraint_and_delete_rows(
  518. "test_bg_update",
  519. table="test_constraint",
  520. constraint_name="test_constraint_name",
  521. constraint=ForeignKeyConstraint(
  522. "base_table", [("b", "b")], deferred=False
  523. ),
  524. unique_columns=["a"],
  525. )
  526. # Tell the DataStore that it hasn't finished all updates yet
  527. self.store.db_pool.updates._all_done = False
  528. # Now let's actually drive the updates to completion
  529. self.wait_for_background_updates()
  530. # Check the correct values are in the new table.
  531. rows = self.get_success(
  532. self.store.db_pool.simple_select_list(
  533. table="test_constraint",
  534. keyvalues={},
  535. retcols=("a", "b"),
  536. )
  537. )
  538. self.assertCountEqual(rows, [{"a": 1, "b": 1}, {"a": 3, "b": 3}])
  539. # And check that invalid rows get correctly rejected.
  540. self.get_failure(
  541. self.store.db_pool.simple_insert("test_constraint", {"a": 2, "b": 2}),
  542. exc=self.store.database_engine.module.IntegrityError,
  543. )