purge_events.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. # Copyright 2020 The Matrix.org Foundation C.I.C.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import logging
  15. from typing import Any, List, Set, Tuple, cast
  16. from synapse.api.errors import SynapseError
  17. from synapse.storage.database import LoggingTransaction
  18. from synapse.storage.databases.main import CacheInvalidationWorkerStore
  19. from synapse.storage.databases.main.state import StateGroupWorkerStore
  20. from synapse.types import RoomStreamToken
  21. logger = logging.getLogger(__name__)
  22. class PurgeEventsStore(StateGroupWorkerStore, CacheInvalidationWorkerStore):
  23. async def purge_history(
  24. self, room_id: str, token: str, delete_local_events: bool
  25. ) -> Set[int]:
  26. """Deletes room history before a certain point.
  27. Note that only a single purge can occur at once, this is guaranteed via
  28. a higher level (in the PaginationHandler).
  29. Args:
  30. room_id:
  31. token: A topological token to delete events before
  32. delete_local_events:
  33. if True, we will delete local events as well as remote ones
  34. (instead of just marking them as outliers and deleting their
  35. state groups).
  36. Returns:
  37. The set of state groups that are referenced by deleted events.
  38. """
  39. parsed_token = await RoomStreamToken.parse(self, token)
  40. return await self.db_pool.runInteraction(
  41. "purge_history",
  42. self._purge_history_txn,
  43. room_id,
  44. parsed_token,
  45. delete_local_events,
  46. )
  47. def _purge_history_txn(
  48. self,
  49. txn: LoggingTransaction,
  50. room_id: str,
  51. token: RoomStreamToken,
  52. delete_local_events: bool,
  53. ) -> Set[int]:
  54. # Tables that should be pruned:
  55. # event_auth
  56. # event_backward_extremities
  57. # event_edges
  58. # event_forward_extremities
  59. # event_json
  60. # event_push_actions
  61. # event_relations
  62. # event_search
  63. # event_to_state_groups
  64. # events
  65. # rejections
  66. # room_depth
  67. # state_groups
  68. # state_groups_state
  69. # destination_rooms
  70. # we will build a temporary table listing the events so that we don't
  71. # have to keep shovelling the list back and forth across the
  72. # connection. Annoyingly the python sqlite driver commits the
  73. # transaction on CREATE, so let's do this first.
  74. #
  75. # furthermore, we might already have the table from a previous (failed)
  76. # purge attempt, so let's drop the table first.
  77. txn.execute("DROP TABLE IF EXISTS events_to_purge")
  78. txn.execute(
  79. "CREATE TEMPORARY TABLE events_to_purge ("
  80. " event_id TEXT NOT NULL,"
  81. " should_delete BOOLEAN NOT NULL"
  82. ")"
  83. )
  84. # First ensure that we're not about to delete all the forward extremeties
  85. txn.execute(
  86. "SELECT e.event_id, e.depth FROM events as e "
  87. "INNER JOIN event_forward_extremities as f "
  88. "ON e.event_id = f.event_id "
  89. "AND e.room_id = f.room_id "
  90. "WHERE f.room_id = ?",
  91. (room_id,),
  92. )
  93. rows = txn.fetchall()
  94. # if we already have no forwards extremities (for example because they were
  95. # cleared out by the `delete_old_current_state_events` background database
  96. # update), then we may as well carry on.
  97. if rows:
  98. max_depth = max(row[1] for row in rows)
  99. if max_depth < token.topological:
  100. # We need to ensure we don't delete all the events from the database
  101. # otherwise we wouldn't be able to send any events (due to not
  102. # having any backwards extremities)
  103. raise SynapseError(
  104. 400, "topological_ordering is greater than forward extremities"
  105. )
  106. logger.info("[purge] looking for events to delete")
  107. should_delete_expr = "state_events.state_key IS NULL"
  108. should_delete_params: Tuple[Any, ...] = ()
  109. if not delete_local_events:
  110. should_delete_expr += " AND event_id NOT LIKE ?"
  111. # We include the parameter twice since we use the expression twice
  112. should_delete_params += ("%:" + self.hs.hostname, "%:" + self.hs.hostname)
  113. should_delete_params += (room_id, token.topological)
  114. # Note that we insert events that are outliers and aren't going to be
  115. # deleted, as nothing will happen to them.
  116. txn.execute(
  117. "INSERT INTO events_to_purge"
  118. " SELECT event_id, %s"
  119. " FROM events AS e LEFT JOIN state_events USING (event_id)"
  120. " WHERE (NOT outlier OR (%s)) AND e.room_id = ? AND topological_ordering < ?"
  121. % (should_delete_expr, should_delete_expr),
  122. should_delete_params,
  123. )
  124. # We create the indices *after* insertion as that's a lot faster.
  125. # create an index on should_delete because later we'll be looking for
  126. # the should_delete / shouldn't_delete subsets
  127. txn.execute(
  128. "CREATE INDEX events_to_purge_should_delete"
  129. " ON events_to_purge(should_delete)"
  130. )
  131. # We do joins against events_to_purge for e.g. calculating state
  132. # groups to purge, etc., so lets make an index.
  133. txn.execute("CREATE INDEX events_to_purge_id ON events_to_purge(event_id)")
  134. txn.execute("SELECT event_id, should_delete FROM events_to_purge")
  135. event_rows = txn.fetchall()
  136. logger.info(
  137. "[purge] found %i events before cutoff, of which %i can be deleted",
  138. len(event_rows),
  139. sum(1 for e in event_rows if e[1]),
  140. )
  141. logger.info("[purge] Finding new backward extremities")
  142. # We calculate the new entries for the backward extremities by finding
  143. # events to be purged that are pointed to by events we're not going to
  144. # purge.
  145. txn.execute(
  146. "SELECT DISTINCT e.event_id FROM events_to_purge AS e"
  147. " INNER JOIN event_edges AS ed ON e.event_id = ed.prev_event_id"
  148. " LEFT JOIN events_to_purge AS ep2 ON ed.event_id = ep2.event_id"
  149. " WHERE ep2.event_id IS NULL"
  150. )
  151. new_backwards_extrems = txn.fetchall()
  152. logger.info("[purge] replacing backward extremities: %r", new_backwards_extrems)
  153. txn.execute(
  154. "DELETE FROM event_backward_extremities WHERE room_id = ?", (room_id,)
  155. )
  156. # Update backward extremeties
  157. txn.execute_batch(
  158. "INSERT INTO event_backward_extremities (room_id, event_id)"
  159. " VALUES (?, ?)",
  160. [(room_id, event_id) for event_id, in new_backwards_extrems],
  161. )
  162. logger.info("[purge] finding state groups referenced by deleted events")
  163. # Get all state groups that are referenced by events that are to be
  164. # deleted.
  165. txn.execute(
  166. """
  167. SELECT DISTINCT state_group FROM events_to_purge
  168. INNER JOIN event_to_state_groups USING (event_id)
  169. """
  170. )
  171. referenced_state_groups = {sg for sg, in txn}
  172. logger.info(
  173. "[purge] found %i referenced state groups", len(referenced_state_groups)
  174. )
  175. logger.info("[purge] removing events from event_to_state_groups")
  176. txn.execute(
  177. "DELETE FROM event_to_state_groups "
  178. "WHERE event_id IN (SELECT event_id from events_to_purge)"
  179. )
  180. # Delete all remote non-state events
  181. for table in (
  182. "event_edges",
  183. "events",
  184. "event_json",
  185. "event_auth",
  186. "event_forward_extremities",
  187. "event_relations",
  188. "event_search",
  189. "rejections",
  190. "redactions",
  191. ):
  192. logger.info("[purge] removing events from %s", table)
  193. txn.execute(
  194. "DELETE FROM %s WHERE event_id IN ("
  195. " SELECT event_id FROM events_to_purge WHERE should_delete"
  196. ")" % (table,)
  197. )
  198. # event_push_actions lacks an index on event_id, and has one on
  199. # (room_id, event_id) instead.
  200. for table in ("event_push_actions",):
  201. logger.info("[purge] removing events from %s", table)
  202. txn.execute(
  203. "DELETE FROM %s WHERE room_id = ? AND event_id IN ("
  204. " SELECT event_id FROM events_to_purge WHERE should_delete"
  205. ")" % (table,),
  206. (room_id,),
  207. )
  208. # Mark all state and own events as outliers
  209. logger.info("[purge] marking remaining events as outliers")
  210. txn.execute(
  211. "UPDATE events SET outlier = ?"
  212. " WHERE event_id IN ("
  213. " SELECT event_id FROM events_to_purge "
  214. " WHERE NOT should_delete"
  215. ")",
  216. (True,),
  217. )
  218. # synapse tries to take out an exclusive lock on room_depth whenever it
  219. # persists events (because upsert), and once we run this update, we
  220. # will block that for the rest of our transaction.
  221. #
  222. # So, let's stick it at the end so that we don't block event
  223. # persistence.
  224. #
  225. # We do this by calculating the minimum depth of the backwards
  226. # extremities. However, the events in event_backward_extremities
  227. # are ones we don't have yet so we need to look at the events that
  228. # point to it via event_edges table.
  229. txn.execute(
  230. """
  231. SELECT COALESCE(MIN(depth), 0)
  232. FROM event_backward_extremities AS eb
  233. INNER JOIN event_edges AS eg ON eg.prev_event_id = eb.event_id
  234. INNER JOIN events AS e ON e.event_id = eg.event_id
  235. WHERE eb.room_id = ?
  236. """,
  237. (room_id,),
  238. )
  239. (min_depth,) = cast(Tuple[int], txn.fetchone())
  240. logger.info("[purge] updating room_depth to %d", min_depth)
  241. txn.execute(
  242. "UPDATE room_depth SET min_depth = ? WHERE room_id = ?",
  243. (min_depth, room_id),
  244. )
  245. # finally, drop the temp table. this will commit the txn in sqlite,
  246. # so make sure to keep this actually last.
  247. txn.execute("DROP TABLE events_to_purge")
  248. for event_id, should_delete in event_rows:
  249. self._invalidate_cache_and_stream(
  250. txn, self._get_state_group_for_event, (event_id,)
  251. )
  252. # XXX: This is racy, since have_seen_events could be called between the
  253. # transaction completing and the invalidation running. On the other hand,
  254. # that's no different to calling `have_seen_events` just before the
  255. # event is deleted from the database.
  256. if should_delete:
  257. self._invalidate_cache_and_stream(
  258. txn, self.have_seen_event, (room_id, event_id)
  259. )
  260. txn.call_after(self._invalidate_get_event_cache, event_id)
  261. logger.info("[purge] done")
  262. return referenced_state_groups
  263. async def purge_room(self, room_id: str) -> List[int]:
  264. """Deletes all record of a room
  265. Args:
  266. room_id
  267. Returns:
  268. The list of state groups to delete.
  269. """
  270. return await self.db_pool.runInteraction(
  271. "purge_room", self._purge_room_txn, room_id
  272. )
  273. def _purge_room_txn(self, txn: LoggingTransaction, room_id: str) -> List[int]:
  274. # First, fetch all the state groups that should be deleted, before
  275. # we delete that information.
  276. txn.execute(
  277. """
  278. SELECT DISTINCT state_group FROM events
  279. INNER JOIN event_to_state_groups USING(event_id)
  280. WHERE events.room_id = ?
  281. """,
  282. (room_id,),
  283. )
  284. state_groups = [row[0] for row in txn]
  285. # Get all the auth chains that are referenced by events that are to be
  286. # deleted.
  287. txn.execute(
  288. """
  289. SELECT chain_id, sequence_number FROM events
  290. LEFT JOIN event_auth_chains USING (event_id)
  291. WHERE room_id = ?
  292. """,
  293. (room_id,),
  294. )
  295. referenced_chain_id_tuples = list(txn)
  296. logger.info("[purge] removing events from event_auth_chain_links")
  297. txn.executemany(
  298. """
  299. DELETE FROM event_auth_chain_links WHERE
  300. origin_chain_id = ? AND origin_sequence_number = ?
  301. """,
  302. referenced_chain_id_tuples,
  303. )
  304. # Now we delete tables which lack an index on room_id but have one on event_id
  305. for table in (
  306. "event_auth",
  307. "event_edges",
  308. "event_json",
  309. "event_push_actions_staging",
  310. "event_relations",
  311. "event_to_state_groups",
  312. "event_auth_chains",
  313. "event_auth_chain_to_calculate",
  314. "redactions",
  315. "rejections",
  316. "state_events",
  317. ):
  318. logger.info("[purge] removing %s from %s", room_id, table)
  319. txn.execute(
  320. """
  321. DELETE FROM %s WHERE event_id IN (
  322. SELECT event_id FROM events WHERE room_id=?
  323. )
  324. """
  325. % (table,),
  326. (room_id,),
  327. )
  328. # next, the tables with an index on room_id (or no useful index)
  329. for table in (
  330. "current_state_events",
  331. "destination_rooms",
  332. "event_backward_extremities",
  333. "event_forward_extremities",
  334. "event_push_actions",
  335. "event_search",
  336. "partial_state_events",
  337. "events",
  338. "federation_inbound_events_staging",
  339. "local_current_membership",
  340. "partial_state_rooms_servers",
  341. "partial_state_rooms",
  342. "receipts_graph",
  343. "receipts_linearized",
  344. "room_aliases",
  345. "room_depth",
  346. "room_memberships",
  347. "room_stats_state",
  348. "room_stats_current",
  349. "room_stats_earliest_token",
  350. "stream_ordering_to_exterm",
  351. "users_in_public_rooms",
  352. "users_who_share_private_rooms",
  353. # no useful index, but let's clear them anyway
  354. "appservice_room_list",
  355. "e2e_room_keys",
  356. "event_push_summary",
  357. "pusher_throttle",
  358. "room_account_data",
  359. "room_tags",
  360. # "rooms" happens last, to keep the foreign keys in the other tables
  361. # happy
  362. "rooms",
  363. ):
  364. logger.info("[purge] removing %s from %s", room_id, table)
  365. txn.execute("DELETE FROM %s WHERE room_id=?" % (table,), (room_id,))
  366. # Other tables we do NOT need to clear out:
  367. #
  368. # - blocked_rooms
  369. # This is important, to make sure that we don't accidentally rejoin a blocked
  370. # room after it was purged
  371. #
  372. # - user_directory
  373. # This has a room_id column, but it is unused
  374. #
  375. # Other tables that we might want to consider clearing out include:
  376. #
  377. # - event_reports
  378. # Given that these are intended for abuse management my initial
  379. # inclination is to leave them in place.
  380. #
  381. # - current_state_delta_stream
  382. # - ex_outlier_stream
  383. # - room_tags_revisions
  384. # The problem with these is that they are largeish and there is no room_id
  385. # index on them. In any case we should be clearing out 'stream' tables
  386. # periodically anyway (#5888)
  387. # TODO: we could probably usefully do a bunch more cache invalidation here
  388. # XXX: as with purge_history, this is racy, but no worse than other races
  389. # that already exist.
  390. self._invalidate_cache_and_stream(txn, self.have_seen_event, (room_id,))
  391. logger.info("[purge] done")
  392. return state_groups