store.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2014-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 collections import namedtuple
  17. from typing import Dict, Iterable, List, Set, Tuple
  18. from synapse.api.constants import EventTypes
  19. from synapse.storage._base import SQLBaseStore
  20. from synapse.storage.database import DatabasePool
  21. from synapse.storage.databases.state.bg_updates import StateBackgroundUpdateStore
  22. from synapse.storage.state import StateFilter
  23. from synapse.storage.types import Cursor
  24. from synapse.storage.util.sequence import build_sequence_generator
  25. from synapse.types import MutableStateMap, StateMap
  26. from synapse.util.caches.descriptors import cached
  27. from synapse.util.caches.dictionary_cache import DictionaryCache
  28. logger = logging.getLogger(__name__)
  29. MAX_STATE_DELTA_HOPS = 100
  30. class _GetStateGroupDelta(
  31. namedtuple("_GetStateGroupDelta", ("prev_group", "delta_ids"))
  32. ):
  33. """Return type of get_state_group_delta that implements __len__, which lets
  34. us use the itrable flag when caching
  35. """
  36. __slots__ = []
  37. def __len__(self):
  38. return len(self.delta_ids) if self.delta_ids else 0
  39. class StateGroupDataStore(StateBackgroundUpdateStore, SQLBaseStore):
  40. """A data store for fetching/storing state groups."""
  41. def __init__(self, database: DatabasePool, db_conn, hs):
  42. super().__init__(database, db_conn, hs)
  43. # Originally the state store used a single DictionaryCache to cache the
  44. # event IDs for the state types in a given state group to avoid hammering
  45. # on the state_group* tables.
  46. #
  47. # The point of using a DictionaryCache is that it can cache a subset
  48. # of the state events for a given state group (i.e. a subset of the keys for a
  49. # given dict which is an entry in the cache for a given state group ID).
  50. #
  51. # However, this poses problems when performing complicated queries
  52. # on the store - for instance: "give me all the state for this group, but
  53. # limit members to this subset of users", as DictionaryCache's API isn't
  54. # rich enough to say "please cache any of these fields, apart from this subset".
  55. # This is problematic when lazy loading members, which requires this behaviour,
  56. # as without it the cache has no choice but to speculatively load all
  57. # state events for the group, which negates the efficiency being sought.
  58. #
  59. # Rather than overcomplicating DictionaryCache's API, we instead split the
  60. # state_group_cache into two halves - one for tracking non-member events,
  61. # and the other for tracking member_events. This means that lazy loading
  62. # queries can be made in a cache-friendly manner by querying both caches
  63. # separately and then merging the result. So for the example above, you
  64. # would query the members cache for a specific subset of state keys
  65. # (which DictionaryCache will handle efficiently and fine) and the non-members
  66. # cache for all state (which DictionaryCache will similarly handle fine)
  67. # and then just merge the results together.
  68. #
  69. # We size the non-members cache to be smaller than the members cache as the
  70. # vast majority of state in Matrix (today) is member events.
  71. self._state_group_cache = DictionaryCache(
  72. "*stateGroupCache*",
  73. # TODO: this hasn't been tuned yet
  74. 50000,
  75. )
  76. self._state_group_members_cache = DictionaryCache(
  77. "*stateGroupMembersCache*",
  78. 500000,
  79. )
  80. def get_max_state_group_txn(txn: Cursor):
  81. txn.execute("SELECT COALESCE(max(id), 0) FROM state_groups")
  82. return txn.fetchone()[0]
  83. self._state_group_seq_gen = build_sequence_generator(
  84. db_conn,
  85. self.database_engine,
  86. get_max_state_group_txn,
  87. "state_group_id_seq",
  88. table="state_groups",
  89. id_column="id",
  90. )
  91. @cached(max_entries=10000, iterable=True)
  92. async def get_state_group_delta(self, state_group):
  93. """Given a state group try to return a previous group and a delta between
  94. the old and the new.
  95. Returns:
  96. (prev_group, delta_ids), where both may be None.
  97. """
  98. def _get_state_group_delta_txn(txn):
  99. prev_group = self.db_pool.simple_select_one_onecol_txn(
  100. txn,
  101. table="state_group_edges",
  102. keyvalues={"state_group": state_group},
  103. retcol="prev_state_group",
  104. allow_none=True,
  105. )
  106. if not prev_group:
  107. return _GetStateGroupDelta(None, None)
  108. delta_ids = self.db_pool.simple_select_list_txn(
  109. txn,
  110. table="state_groups_state",
  111. keyvalues={"state_group": state_group},
  112. retcols=("type", "state_key", "event_id"),
  113. )
  114. return _GetStateGroupDelta(
  115. prev_group,
  116. {(row["type"], row["state_key"]): row["event_id"] for row in delta_ids},
  117. )
  118. return await self.db_pool.runInteraction(
  119. "get_state_group_delta", _get_state_group_delta_txn
  120. )
  121. async def _get_state_groups_from_groups(
  122. self, groups: List[int], state_filter: StateFilter
  123. ) -> Dict[int, StateMap[str]]:
  124. """Returns the state groups for a given set of groups from the
  125. database, filtering on types of state events.
  126. Args:
  127. groups: list of state group IDs to query
  128. state_filter: The state filter used to fetch state
  129. from the database.
  130. Returns:
  131. Dict of state group to state map.
  132. """
  133. results = {}
  134. chunks = [groups[i : i + 100] for i in range(0, len(groups), 100)]
  135. for chunk in chunks:
  136. res = await self.db_pool.runInteraction(
  137. "_get_state_groups_from_groups",
  138. self._get_state_groups_from_groups_txn,
  139. chunk,
  140. state_filter,
  141. )
  142. results.update(res)
  143. return results
  144. def _get_state_for_group_using_cache(self, cache, group, state_filter):
  145. """Checks if group is in cache. See `_get_state_for_groups`
  146. Args:
  147. cache(DictionaryCache): the state group cache to use
  148. group(int): The state group to lookup
  149. state_filter (StateFilter): The state filter used to fetch state
  150. from the database.
  151. Returns 2-tuple (`state_dict`, `got_all`).
  152. `got_all` is a bool indicating if we successfully retrieved all
  153. requests state from the cache, if False we need to query the DB for the
  154. missing state.
  155. """
  156. is_all, known_absent, state_dict_ids = cache.get(group)
  157. if is_all or state_filter.is_full():
  158. # Either we have everything or want everything, either way
  159. # `is_all` tells us whether we've gotten everything.
  160. return state_filter.filter_state(state_dict_ids), is_all
  161. # tracks whether any of our requested types are missing from the cache
  162. missing_types = False
  163. if state_filter.has_wildcards():
  164. # We don't know if we fetched all the state keys for the types in
  165. # the filter that are wildcards, so we have to assume that we may
  166. # have missed some.
  167. missing_types = True
  168. else:
  169. # There aren't any wild cards, so `concrete_types()` returns the
  170. # complete list of event types we're wanting.
  171. for key in state_filter.concrete_types():
  172. if key not in state_dict_ids and key not in known_absent:
  173. missing_types = True
  174. break
  175. return state_filter.filter_state(state_dict_ids), not missing_types
  176. async def _get_state_for_groups(
  177. self, groups: Iterable[int], state_filter: StateFilter = StateFilter.all()
  178. ) -> Dict[int, MutableStateMap[str]]:
  179. """Gets the state at each of a list of state groups, optionally
  180. filtering by type/state_key
  181. Args:
  182. groups: list of state groups for which we want
  183. to get the state.
  184. state_filter: The state filter used to fetch state
  185. from the database.
  186. Returns:
  187. Dict of state group to state map.
  188. """
  189. member_filter, non_member_filter = state_filter.get_member_split()
  190. # Now we look them up in the member and non-member caches
  191. (
  192. non_member_state,
  193. incomplete_groups_nm,
  194. ) = self._get_state_for_groups_using_cache(
  195. groups, self._state_group_cache, state_filter=non_member_filter
  196. )
  197. (member_state, incomplete_groups_m,) = self._get_state_for_groups_using_cache(
  198. groups, self._state_group_members_cache, state_filter=member_filter
  199. )
  200. state = dict(non_member_state)
  201. for group in groups:
  202. state[group].update(member_state[group])
  203. # Now fetch any missing groups from the database
  204. incomplete_groups = incomplete_groups_m | incomplete_groups_nm
  205. if not incomplete_groups:
  206. return state
  207. cache_sequence_nm = self._state_group_cache.sequence
  208. cache_sequence_m = self._state_group_members_cache.sequence
  209. # Help the cache hit ratio by expanding the filter a bit
  210. db_state_filter = state_filter.return_expanded()
  211. group_to_state_dict = await self._get_state_groups_from_groups(
  212. list(incomplete_groups), state_filter=db_state_filter
  213. )
  214. # Now lets update the caches
  215. self._insert_into_cache(
  216. group_to_state_dict,
  217. db_state_filter,
  218. cache_seq_num_members=cache_sequence_m,
  219. cache_seq_num_non_members=cache_sequence_nm,
  220. )
  221. # And finally update the result dict, by filtering out any extra
  222. # stuff we pulled out of the database.
  223. for group, group_state_dict in group_to_state_dict.items():
  224. # We just replace any existing entries, as we will have loaded
  225. # everything we need from the database anyway.
  226. state[group] = state_filter.filter_state(group_state_dict)
  227. return state
  228. def _get_state_for_groups_using_cache(
  229. self, groups: Iterable[int], cache: DictionaryCache, state_filter: StateFilter
  230. ) -> Tuple[Dict[int, StateMap[str]], Set[int]]:
  231. """Gets the state at each of a list of state groups, optionally
  232. filtering by type/state_key, querying from a specific cache.
  233. Args:
  234. groups: list of state groups for which we want to get the state.
  235. cache: the cache of group ids to state dicts which
  236. we will pass through - either the normal state cache or the
  237. specific members state cache.
  238. state_filter: The state filter used to fetch state from the
  239. database.
  240. Returns:
  241. Tuple of dict of state_group_id to state map of entries in the
  242. cache, and the state group ids either missing from the cache or
  243. incomplete.
  244. """
  245. results = {}
  246. incomplete_groups = set()
  247. for group in set(groups):
  248. state_dict_ids, got_all = self._get_state_for_group_using_cache(
  249. cache, group, state_filter
  250. )
  251. results[group] = state_dict_ids
  252. if not got_all:
  253. incomplete_groups.add(group)
  254. return results, incomplete_groups
  255. def _insert_into_cache(
  256. self,
  257. group_to_state_dict,
  258. state_filter,
  259. cache_seq_num_members,
  260. cache_seq_num_non_members,
  261. ):
  262. """Inserts results from querying the database into the relevant cache.
  263. Args:
  264. group_to_state_dict (dict): The new entries pulled from database.
  265. Map from state group to state dict
  266. state_filter (StateFilter): The state filter used to fetch state
  267. from the database.
  268. cache_seq_num_members (int): Sequence number of member cache since
  269. last lookup in cache
  270. cache_seq_num_non_members (int): Sequence number of member cache since
  271. last lookup in cache
  272. """
  273. # We need to work out which types we've fetched from the DB for the
  274. # member vs non-member caches. This should be as accurate as possible,
  275. # but can be an underestimate (e.g. when we have wild cards)
  276. member_filter, non_member_filter = state_filter.get_member_split()
  277. if member_filter.is_full():
  278. # We fetched all member events
  279. member_types = None
  280. else:
  281. # `concrete_types()` will only return a subset when there are wild
  282. # cards in the filter, but that's fine.
  283. member_types = member_filter.concrete_types()
  284. if non_member_filter.is_full():
  285. # We fetched all non member events
  286. non_member_types = None
  287. else:
  288. non_member_types = non_member_filter.concrete_types()
  289. for group, group_state_dict in group_to_state_dict.items():
  290. state_dict_members = {}
  291. state_dict_non_members = {}
  292. for k, v in group_state_dict.items():
  293. if k[0] == EventTypes.Member:
  294. state_dict_members[k] = v
  295. else:
  296. state_dict_non_members[k] = v
  297. self._state_group_members_cache.update(
  298. cache_seq_num_members,
  299. key=group,
  300. value=state_dict_members,
  301. fetched_keys=member_types,
  302. )
  303. self._state_group_cache.update(
  304. cache_seq_num_non_members,
  305. key=group,
  306. value=state_dict_non_members,
  307. fetched_keys=non_member_types,
  308. )
  309. async def store_state_group(
  310. self, event_id, room_id, prev_group, delta_ids, current_state_ids
  311. ) -> int:
  312. """Store a new set of state, returning a newly assigned state group.
  313. Args:
  314. event_id (str): The event ID for which the state was calculated
  315. room_id (str)
  316. prev_group (int|None): A previous state group for the room, optional.
  317. delta_ids (dict|None): The delta between state at `prev_group` and
  318. `current_state_ids`, if `prev_group` was given. Same format as
  319. `current_state_ids`.
  320. current_state_ids (dict): The state to store. Map of (type, state_key)
  321. to event_id.
  322. Returns:
  323. The state group ID
  324. """
  325. def _store_state_group_txn(txn):
  326. if current_state_ids is None:
  327. # AFAIK, this can never happen
  328. raise Exception("current_state_ids cannot be None")
  329. state_group = self._state_group_seq_gen.get_next_id_txn(txn)
  330. self.db_pool.simple_insert_txn(
  331. txn,
  332. table="state_groups",
  333. values={"id": state_group, "room_id": room_id, "event_id": event_id},
  334. )
  335. # We persist as a delta if we can, while also ensuring the chain
  336. # of deltas isn't tooo long, as otherwise read performance degrades.
  337. if prev_group:
  338. is_in_db = self.db_pool.simple_select_one_onecol_txn(
  339. txn,
  340. table="state_groups",
  341. keyvalues={"id": prev_group},
  342. retcol="id",
  343. allow_none=True,
  344. )
  345. if not is_in_db:
  346. raise Exception(
  347. "Trying to persist state with unpersisted prev_group: %r"
  348. % (prev_group,)
  349. )
  350. potential_hops = self._count_state_group_hops_txn(txn, prev_group)
  351. if prev_group and potential_hops < MAX_STATE_DELTA_HOPS:
  352. self.db_pool.simple_insert_txn(
  353. txn,
  354. table="state_group_edges",
  355. values={"state_group": state_group, "prev_state_group": prev_group},
  356. )
  357. self.db_pool.simple_insert_many_txn(
  358. txn,
  359. table="state_groups_state",
  360. values=[
  361. {
  362. "state_group": state_group,
  363. "room_id": room_id,
  364. "type": key[0],
  365. "state_key": key[1],
  366. "event_id": state_id,
  367. }
  368. for key, state_id in delta_ids.items()
  369. ],
  370. )
  371. else:
  372. self.db_pool.simple_insert_many_txn(
  373. txn,
  374. table="state_groups_state",
  375. values=[
  376. {
  377. "state_group": state_group,
  378. "room_id": room_id,
  379. "type": key[0],
  380. "state_key": key[1],
  381. "event_id": state_id,
  382. }
  383. for key, state_id in current_state_ids.items()
  384. ],
  385. )
  386. # Prefill the state group caches with this group.
  387. # It's fine to use the sequence like this as the state group map
  388. # is immutable. (If the map wasn't immutable then this prefill could
  389. # race with another update)
  390. current_member_state_ids = {
  391. s: ev
  392. for (s, ev) in current_state_ids.items()
  393. if s[0] == EventTypes.Member
  394. }
  395. txn.call_after(
  396. self._state_group_members_cache.update,
  397. self._state_group_members_cache.sequence,
  398. key=state_group,
  399. value=dict(current_member_state_ids),
  400. )
  401. current_non_member_state_ids = {
  402. s: ev
  403. for (s, ev) in current_state_ids.items()
  404. if s[0] != EventTypes.Member
  405. }
  406. txn.call_after(
  407. self._state_group_cache.update,
  408. self._state_group_cache.sequence,
  409. key=state_group,
  410. value=dict(current_non_member_state_ids),
  411. )
  412. return state_group
  413. return await self.db_pool.runInteraction(
  414. "store_state_group", _store_state_group_txn
  415. )
  416. async def purge_unreferenced_state_groups(
  417. self, room_id: str, state_groups_to_delete
  418. ) -> None:
  419. """Deletes no longer referenced state groups and de-deltas any state
  420. groups that reference them.
  421. Args:
  422. room_id: The room the state groups belong to (must all be in the
  423. same room).
  424. state_groups_to_delete (Collection[int]): Set of all state groups
  425. to delete.
  426. """
  427. await self.db_pool.runInteraction(
  428. "purge_unreferenced_state_groups",
  429. self._purge_unreferenced_state_groups,
  430. room_id,
  431. state_groups_to_delete,
  432. )
  433. def _purge_unreferenced_state_groups(self, txn, room_id, state_groups_to_delete):
  434. logger.info(
  435. "[purge] found %i state groups to delete", len(state_groups_to_delete)
  436. )
  437. rows = self.db_pool.simple_select_many_txn(
  438. txn,
  439. table="state_group_edges",
  440. column="prev_state_group",
  441. iterable=state_groups_to_delete,
  442. keyvalues={},
  443. retcols=("state_group",),
  444. )
  445. remaining_state_groups = {
  446. row["state_group"]
  447. for row in rows
  448. if row["state_group"] not in state_groups_to_delete
  449. }
  450. logger.info(
  451. "[purge] de-delta-ing %i remaining state groups",
  452. len(remaining_state_groups),
  453. )
  454. # Now we turn the state groups that reference to-be-deleted state
  455. # groups to non delta versions.
  456. for sg in remaining_state_groups:
  457. logger.info("[purge] de-delta-ing remaining state group %s", sg)
  458. curr_state = self._get_state_groups_from_groups_txn(txn, [sg])
  459. curr_state = curr_state[sg]
  460. self.db_pool.simple_delete_txn(
  461. txn, table="state_groups_state", keyvalues={"state_group": sg}
  462. )
  463. self.db_pool.simple_delete_txn(
  464. txn, table="state_group_edges", keyvalues={"state_group": sg}
  465. )
  466. self.db_pool.simple_insert_many_txn(
  467. txn,
  468. table="state_groups_state",
  469. values=[
  470. {
  471. "state_group": sg,
  472. "room_id": room_id,
  473. "type": key[0],
  474. "state_key": key[1],
  475. "event_id": state_id,
  476. }
  477. for key, state_id in curr_state.items()
  478. ],
  479. )
  480. logger.info("[purge] removing redundant state groups")
  481. txn.execute_batch(
  482. "DELETE FROM state_groups_state WHERE state_group = ?",
  483. ((sg,) for sg in state_groups_to_delete),
  484. )
  485. txn.execute_batch(
  486. "DELETE FROM state_groups WHERE id = ?",
  487. ((sg,) for sg in state_groups_to_delete),
  488. )
  489. async def get_previous_state_groups(
  490. self, state_groups: Iterable[int]
  491. ) -> Dict[int, int]:
  492. """Fetch the previous groups of the given state groups.
  493. Args:
  494. state_groups
  495. Returns:
  496. A mapping from state group to previous state group.
  497. """
  498. rows = await self.db_pool.simple_select_many_batch(
  499. table="state_group_edges",
  500. column="prev_state_group",
  501. iterable=state_groups,
  502. keyvalues={},
  503. retcols=("prev_state_group", "state_group"),
  504. desc="get_previous_state_groups",
  505. )
  506. return {row["state_group"]: row["prev_state_group"] for row in rows}
  507. async def purge_room_state(self, room_id, state_groups_to_delete):
  508. """Deletes all record of a room from state tables
  509. Args:
  510. room_id (str):
  511. state_groups_to_delete (list[int]): State groups to delete
  512. """
  513. await self.db_pool.runInteraction(
  514. "purge_room_state",
  515. self._purge_room_state_txn,
  516. room_id,
  517. state_groups_to_delete,
  518. )
  519. def _purge_room_state_txn(self, txn, room_id, state_groups_to_delete):
  520. # first we have to delete the state groups states
  521. logger.info("[purge] removing %s from state_groups_state", room_id)
  522. self.db_pool.simple_delete_many_txn(
  523. txn,
  524. table="state_groups_state",
  525. column="state_group",
  526. iterable=state_groups_to_delete,
  527. keyvalues={},
  528. )
  529. # ... and the state group edges
  530. logger.info("[purge] removing %s from state_group_edges", room_id)
  531. self.db_pool.simple_delete_many_txn(
  532. txn,
  533. table="state_group_edges",
  534. column="state_group",
  535. iterable=state_groups_to_delete,
  536. keyvalues={},
  537. )
  538. # ... and the state groups
  539. logger.info("[purge] removing %s from state_groups", room_id)
  540. self.db_pool.simple_delete_many_txn(
  541. txn,
  542. table="state_groups",
  543. column="id",
  544. iterable=state_groups_to_delete,
  545. keyvalues={},
  546. )