store.py 25 KB

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