test_stats.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. # Copyright 2019 New Vector Ltd
  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 typing import Any, Dict, List, Optional, Tuple, cast
  15. from twisted.test.proto_helpers import MemoryReactor
  16. from synapse.rest import admin
  17. from synapse.rest.client import login, room
  18. from synapse.server import HomeServer
  19. from synapse.storage.databases.main import stats
  20. from synapse.util import Clock
  21. from tests import unittest
  22. # The expected number of state events in a fresh public room.
  23. EXPT_NUM_STATE_EVTS_IN_FRESH_PUBLIC_ROOM = 5
  24. # The expected number of state events in a fresh private room.
  25. EXPT_NUM_STATE_EVTS_IN_FRESH_PRIVATE_ROOM = 6
  26. class StatsRoomTests(unittest.HomeserverTestCase):
  27. servlets = [
  28. admin.register_servlets_for_client_rest_resource,
  29. room.register_servlets,
  30. login.register_servlets,
  31. ]
  32. def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
  33. self.store = hs.get_datastores().main
  34. self.handler = self.hs.get_stats_handler()
  35. def _add_background_updates(self) -> None:
  36. """
  37. Add the background updates we need to run.
  38. """
  39. # Ugh, have to reset this flag
  40. self.store.db_pool.updates._all_done = False
  41. self.get_success(
  42. self.store.db_pool.simple_insert(
  43. "background_updates",
  44. {
  45. "update_name": "populate_stats_process_rooms",
  46. "progress_json": "{}",
  47. },
  48. )
  49. )
  50. self.get_success(
  51. self.store.db_pool.simple_insert(
  52. "background_updates",
  53. {
  54. "update_name": "populate_stats_process_users",
  55. "progress_json": "{}",
  56. "depends_on": "populate_stats_process_rooms",
  57. },
  58. )
  59. )
  60. async def get_all_room_state(self) -> List[Optional[str]]:
  61. rows = cast(
  62. List[Tuple[Optional[str]]],
  63. await self.store.db_pool.simple_select_list(
  64. "room_stats_state", None, retcols=("topic",)
  65. ),
  66. )
  67. return [r[0] for r in rows]
  68. def _get_current_stats(
  69. self, stats_type: str, stat_id: str
  70. ) -> Optional[Dict[str, Any]]:
  71. table, id_col = stats.TYPE_TO_TABLE[stats_type]
  72. cols = list(stats.ABSOLUTE_STATS_FIELDS[stats_type])
  73. row = self.get_success(
  74. self.store.db_pool.simple_select_one(
  75. table + "_current",
  76. {id_col: stat_id},
  77. cols,
  78. allow_none=True,
  79. )
  80. )
  81. return None if row is None else dict(zip(cols, row))
  82. def _perform_background_initial_update(self) -> None:
  83. # Do the initial population of the stats via the background update
  84. self._add_background_updates()
  85. self.wait_for_background_updates()
  86. def test_initial_room(self) -> None:
  87. """
  88. The background updates will build the table from scratch.
  89. """
  90. r = self.get_success(self.get_all_room_state())
  91. self.assertEqual(len(r), 0)
  92. # Disable stats
  93. self.hs.config.stats.stats_enabled = False
  94. self.handler.stats_enabled = False
  95. u1 = self.register_user("u1", "pass")
  96. u1_token = self.login("u1", "pass")
  97. room_1 = self.helper.create_room_as(u1, tok=u1_token)
  98. self.helper.send_state(
  99. room_1, event_type="m.room.topic", body={"topic": "foo"}, tok=u1_token
  100. )
  101. # Stats disabled, shouldn't have done anything
  102. r = self.get_success(self.get_all_room_state())
  103. self.assertEqual(len(r), 0)
  104. # Enable stats
  105. self.hs.config.stats.stats_enabled = True
  106. self.handler.stats_enabled = True
  107. # Do the initial population of the user directory via the background update
  108. self._add_background_updates()
  109. self.wait_for_background_updates()
  110. r = self.get_success(self.get_all_room_state())
  111. self.assertEqual(len(r), 1)
  112. self.assertEqual(r[0], "foo")
  113. def test_create_user(self) -> None:
  114. """
  115. When we create a user, it should have statistics already ready.
  116. """
  117. u1 = self.register_user("u1", "pass")
  118. u1stats = self._get_current_stats("user", u1)
  119. assert u1stats is not None
  120. # not in any rooms by default
  121. self.assertEqual(u1stats["joined_rooms"], 0)
  122. def test_create_room(self) -> None:
  123. """
  124. When we create a room, it should have statistics already ready.
  125. """
  126. self._perform_background_initial_update()
  127. u1 = self.register_user("u1", "pass")
  128. u1token = self.login("u1", "pass")
  129. r1 = self.helper.create_room_as(u1, tok=u1token)
  130. r1stats = self._get_current_stats("room", r1)
  131. r2 = self.helper.create_room_as(u1, tok=u1token, is_public=False)
  132. r2stats = self._get_current_stats("room", r2)
  133. assert r1stats is not None
  134. assert r2stats is not None
  135. self.assertEqual(
  136. r1stats["current_state_events"], EXPT_NUM_STATE_EVTS_IN_FRESH_PUBLIC_ROOM
  137. )
  138. self.assertEqual(
  139. r2stats["current_state_events"], EXPT_NUM_STATE_EVTS_IN_FRESH_PRIVATE_ROOM
  140. )
  141. self.assertEqual(r1stats["joined_members"], 1)
  142. self.assertEqual(r1stats["invited_members"], 0)
  143. self.assertEqual(r1stats["banned_members"], 0)
  144. self.assertEqual(r2stats["joined_members"], 1)
  145. self.assertEqual(r2stats["invited_members"], 0)
  146. self.assertEqual(r2stats["banned_members"], 0)
  147. def test_updating_profile_information_does_not_increase_joined_members_count(
  148. self,
  149. ) -> None:
  150. """
  151. Check that the joined_members count does not increase when a user changes their
  152. profile information (which is done by sending another join membership event into
  153. the room.
  154. """
  155. self._perform_background_initial_update()
  156. # Create a user and room
  157. u1 = self.register_user("u1", "pass")
  158. u1token = self.login("u1", "pass")
  159. r1 = self.helper.create_room_as(u1, tok=u1token)
  160. # Get the current room stats
  161. r1stats_ante = self._get_current_stats("room", r1)
  162. assert r1stats_ante is not None
  163. # Send a profile update into the room
  164. new_profile = {"displayname": "bob"}
  165. self.helper.change_membership(
  166. r1, u1, u1, "join", extra_data=new_profile, tok=u1token
  167. )
  168. # Get the new room stats
  169. r1stats_post = self._get_current_stats("room", r1)
  170. assert r1stats_post is not None
  171. # Ensure that the user count did not changed
  172. self.assertEqual(r1stats_post["joined_members"], r1stats_ante["joined_members"])
  173. self.assertEqual(
  174. r1stats_post["local_users_in_room"], r1stats_ante["local_users_in_room"]
  175. )
  176. def test_send_state_event_nonoverwriting(self) -> None:
  177. """
  178. When we send a non-overwriting state event, it increments current_state_events
  179. """
  180. self._perform_background_initial_update()
  181. u1 = self.register_user("u1", "pass")
  182. u1token = self.login("u1", "pass")
  183. r1 = self.helper.create_room_as(u1, tok=u1token)
  184. self.helper.send_state(
  185. r1, "cat.hissing", {"value": True}, tok=u1token, state_key="tabby"
  186. )
  187. r1stats_ante = self._get_current_stats("room", r1)
  188. assert r1stats_ante is not None
  189. self.helper.send_state(
  190. r1, "cat.hissing", {"value": False}, tok=u1token, state_key="moggy"
  191. )
  192. r1stats_post = self._get_current_stats("room", r1)
  193. assert r1stats_post is not None
  194. self.assertEqual(
  195. r1stats_post["current_state_events"] - r1stats_ante["current_state_events"],
  196. 1,
  197. )
  198. def test_join_first_time(self) -> None:
  199. """
  200. When a user joins a room for the first time, current_state_events and
  201. joined_members should increase by exactly 1.
  202. """
  203. self._perform_background_initial_update()
  204. u1 = self.register_user("u1", "pass")
  205. u1token = self.login("u1", "pass")
  206. r1 = self.helper.create_room_as(u1, tok=u1token)
  207. u2 = self.register_user("u2", "pass")
  208. u2token = self.login("u2", "pass")
  209. r1stats_ante = self._get_current_stats("room", r1)
  210. assert r1stats_ante is not None
  211. self.helper.join(r1, u2, tok=u2token)
  212. r1stats_post = self._get_current_stats("room", r1)
  213. assert r1stats_post is not None
  214. self.assertEqual(
  215. r1stats_post["current_state_events"] - r1stats_ante["current_state_events"],
  216. 1,
  217. )
  218. self.assertEqual(
  219. r1stats_post["joined_members"] - r1stats_ante["joined_members"], 1
  220. )
  221. def test_join_after_leave(self) -> None:
  222. """
  223. When a user joins a room after being previously left,
  224. joined_members should increase by exactly 1.
  225. current_state_events should not increase.
  226. left_members should decrease by exactly 1.
  227. """
  228. self._perform_background_initial_update()
  229. u1 = self.register_user("u1", "pass")
  230. u1token = self.login("u1", "pass")
  231. r1 = self.helper.create_room_as(u1, tok=u1token)
  232. u2 = self.register_user("u2", "pass")
  233. u2token = self.login("u2", "pass")
  234. self.helper.join(r1, u2, tok=u2token)
  235. self.helper.leave(r1, u2, tok=u2token)
  236. r1stats_ante = self._get_current_stats("room", r1)
  237. assert r1stats_ante is not None
  238. self.helper.join(r1, u2, tok=u2token)
  239. r1stats_post = self._get_current_stats("room", r1)
  240. assert r1stats_post is not None
  241. self.assertEqual(
  242. r1stats_post["current_state_events"] - r1stats_ante["current_state_events"],
  243. 0,
  244. )
  245. self.assertEqual(
  246. r1stats_post["joined_members"] - r1stats_ante["joined_members"], +1
  247. )
  248. self.assertEqual(
  249. r1stats_post["left_members"] - r1stats_ante["left_members"], -1
  250. )
  251. def test_invited(self) -> None:
  252. """
  253. When a user invites another user, current_state_events and
  254. invited_members should increase by exactly 1.
  255. """
  256. self._perform_background_initial_update()
  257. u1 = self.register_user("u1", "pass")
  258. u1token = self.login("u1", "pass")
  259. r1 = self.helper.create_room_as(u1, tok=u1token)
  260. u2 = self.register_user("u2", "pass")
  261. r1stats_ante = self._get_current_stats("room", r1)
  262. assert r1stats_ante is not None
  263. self.helper.invite(r1, u1, u2, tok=u1token)
  264. r1stats_post = self._get_current_stats("room", r1)
  265. assert r1stats_post is not None
  266. self.assertEqual(
  267. r1stats_post["current_state_events"] - r1stats_ante["current_state_events"],
  268. 1,
  269. )
  270. self.assertEqual(
  271. r1stats_post["invited_members"] - r1stats_ante["invited_members"], +1
  272. )
  273. def test_join_after_invite(self) -> None:
  274. """
  275. When a user joins a room after being invited and
  276. joined_members should increase by exactly 1.
  277. current_state_events should not increase.
  278. invited_members should decrease by exactly 1.
  279. """
  280. self._perform_background_initial_update()
  281. u1 = self.register_user("u1", "pass")
  282. u1token = self.login("u1", "pass")
  283. r1 = self.helper.create_room_as(u1, tok=u1token)
  284. u2 = self.register_user("u2", "pass")
  285. u2token = self.login("u2", "pass")
  286. self.helper.invite(r1, u1, u2, tok=u1token)
  287. r1stats_ante = self._get_current_stats("room", r1)
  288. assert r1stats_ante is not None
  289. self.helper.join(r1, u2, tok=u2token)
  290. r1stats_post = self._get_current_stats("room", r1)
  291. assert r1stats_post is not None
  292. self.assertEqual(
  293. r1stats_post["current_state_events"] - r1stats_ante["current_state_events"],
  294. 0,
  295. )
  296. self.assertEqual(
  297. r1stats_post["joined_members"] - r1stats_ante["joined_members"], +1
  298. )
  299. self.assertEqual(
  300. r1stats_post["invited_members"] - r1stats_ante["invited_members"], -1
  301. )
  302. def test_left(self) -> None:
  303. """
  304. When a user leaves a room after joining and
  305. left_members should increase by exactly 1.
  306. current_state_events should not increase.
  307. joined_members should decrease by exactly 1.
  308. """
  309. self._perform_background_initial_update()
  310. u1 = self.register_user("u1", "pass")
  311. u1token = self.login("u1", "pass")
  312. r1 = self.helper.create_room_as(u1, tok=u1token)
  313. u2 = self.register_user("u2", "pass")
  314. u2token = self.login("u2", "pass")
  315. self.helper.join(r1, u2, tok=u2token)
  316. r1stats_ante = self._get_current_stats("room", r1)
  317. assert r1stats_ante is not None
  318. self.helper.leave(r1, u2, tok=u2token)
  319. r1stats_post = self._get_current_stats("room", r1)
  320. assert r1stats_post is not None
  321. self.assertEqual(
  322. r1stats_post["current_state_events"] - r1stats_ante["current_state_events"],
  323. 0,
  324. )
  325. self.assertEqual(
  326. r1stats_post["left_members"] - r1stats_ante["left_members"], +1
  327. )
  328. self.assertEqual(
  329. r1stats_post["joined_members"] - r1stats_ante["joined_members"], -1
  330. )
  331. def test_banned(self) -> None:
  332. """
  333. When a user is banned from a room after joining and
  334. left_members should increase by exactly 1.
  335. current_state_events should not increase.
  336. banned_members should decrease by exactly 1.
  337. """
  338. self._perform_background_initial_update()
  339. u1 = self.register_user("u1", "pass")
  340. u1token = self.login("u1", "pass")
  341. r1 = self.helper.create_room_as(u1, tok=u1token)
  342. u2 = self.register_user("u2", "pass")
  343. u2token = self.login("u2", "pass")
  344. self.helper.join(r1, u2, tok=u2token)
  345. r1stats_ante = self._get_current_stats("room", r1)
  346. assert r1stats_ante is not None
  347. self.helper.change_membership(r1, u1, u2, "ban", tok=u1token)
  348. r1stats_post = self._get_current_stats("room", r1)
  349. assert r1stats_post is not None
  350. self.assertEqual(
  351. r1stats_post["current_state_events"] - r1stats_ante["current_state_events"],
  352. 0,
  353. )
  354. self.assertEqual(
  355. r1stats_post["banned_members"] - r1stats_ante["banned_members"], +1
  356. )
  357. self.assertEqual(
  358. r1stats_post["joined_members"] - r1stats_ante["joined_members"], -1
  359. )
  360. def test_initial_background_update(self) -> None:
  361. """
  362. Test that statistics can be generated by the initial background update
  363. handler.
  364. This test also tests that stats rows are not created for new subjects
  365. when stats are disabled. However, it may be desirable to change this
  366. behaviour eventually to still keep current rows.
  367. """
  368. self.hs.config.stats.stats_enabled = False
  369. self.handler.stats_enabled = False
  370. u1 = self.register_user("u1", "pass")
  371. u1token = self.login("u1", "pass")
  372. r1 = self.helper.create_room_as(u1, tok=u1token)
  373. # test that these subjects, which were created during a time of disabled
  374. # stats, do not have stats.
  375. self.assertIsNone(self._get_current_stats("room", r1))
  376. self.assertIsNone(self._get_current_stats("user", u1))
  377. self.hs.config.stats.stats_enabled = True
  378. self.handler.stats_enabled = True
  379. self._perform_background_initial_update()
  380. r1stats = self._get_current_stats("room", r1)
  381. u1stats = self._get_current_stats("user", u1)
  382. assert r1stats is not None
  383. assert u1stats is not None
  384. self.assertEqual(r1stats["joined_members"], 1)
  385. self.assertEqual(
  386. r1stats["current_state_events"], EXPT_NUM_STATE_EVTS_IN_FRESH_PUBLIC_ROOM
  387. )
  388. self.assertEqual(u1stats["joined_rooms"], 1)
  389. def test_incomplete_stats(self) -> None:
  390. """
  391. This tests that we track incomplete statistics.
  392. We first test that incomplete stats are incrementally generated,
  393. following the preparation of a background regen.
  394. We then test that these incomplete rows are completed by the background
  395. regen.
  396. """
  397. u1 = self.register_user("u1", "pass")
  398. u1token = self.login("u1", "pass")
  399. u2 = self.register_user("u2", "pass")
  400. u2token = self.login("u2", "pass")
  401. u3 = self.register_user("u3", "pass")
  402. r1 = self.helper.create_room_as(u1, tok=u1token, is_public=False)
  403. # preparation stage of the initial background update
  404. # Ugh, have to reset this flag
  405. self.store.db_pool.updates._all_done = False
  406. self.get_success(
  407. self.store.db_pool.simple_delete(
  408. "room_stats_current", {"1": 1}, "test_delete_stats"
  409. )
  410. )
  411. self.get_success(
  412. self.store.db_pool.simple_delete(
  413. "user_stats_current", {"1": 1}, "test_delete_stats"
  414. )
  415. )
  416. self.helper.invite(r1, u1, u2, tok=u1token)
  417. self.helper.join(r1, u2, tok=u2token)
  418. self.helper.invite(r1, u1, u3, tok=u1token)
  419. self.helper.send(r1, "thou shalt yield", tok=u1token)
  420. # now do the background updates
  421. self.store.db_pool.updates._all_done = False
  422. self.get_success(
  423. self.store.db_pool.simple_insert(
  424. "background_updates",
  425. {
  426. "update_name": "populate_stats_process_rooms",
  427. "progress_json": "{}",
  428. },
  429. )
  430. )
  431. self.get_success(
  432. self.store.db_pool.simple_insert(
  433. "background_updates",
  434. {
  435. "update_name": "populate_stats_process_users",
  436. "progress_json": "{}",
  437. "depends_on": "populate_stats_process_rooms",
  438. },
  439. )
  440. )
  441. self.wait_for_background_updates()
  442. r1stats_complete = self._get_current_stats("room", r1)
  443. assert r1stats_complete is not None
  444. u1stats_complete = self._get_current_stats("user", u1)
  445. assert u1stats_complete is not None
  446. u2stats_complete = self._get_current_stats("user", u2)
  447. assert u2stats_complete is not None
  448. # now we make our assertions
  449. # check that _complete rows are complete and correct
  450. self.assertEqual(r1stats_complete["joined_members"], 2)
  451. self.assertEqual(r1stats_complete["invited_members"], 1)
  452. self.assertEqual(
  453. r1stats_complete["current_state_events"],
  454. 2 + EXPT_NUM_STATE_EVTS_IN_FRESH_PRIVATE_ROOM,
  455. )
  456. self.assertEqual(u1stats_complete["joined_rooms"], 1)
  457. self.assertEqual(u2stats_complete["joined_rooms"], 1)