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.
 
 
 
 
 
 

734 lines
27 KiB

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