Compare commits

...

5 Commits

Author SHA1 Message Date
Eric Eastwood
8dd27d37e4 Fix lints 2025-08-08 12:13:02 -05:00
Eric Eastwood
d3268e087d More accurate name distinction
See https://github.com/element-hq/synapse/pull/18794#discussion_r2263543402
2025-08-08 11:50:22 -05:00
Eric Eastwood
5ed63accb4 Add changelog 2025-08-08 11:38:35 -05:00
Eric Eastwood
1558b2edc8 Test all tables being purged 2025-08-08 11:35:38 -05:00
Eric Eastwood
deaf90afde Add local_current_membership to check when purging rooms in tests
Spawning from wanting to confirm my reply in https://github.com/element-hq/synapse/issues/18489
2025-08-08 10:56:21 -05:00
3 changed files with 107 additions and 93 deletions

1
changelog.d/18794.misc Normal file
View File

@@ -0,0 +1 @@
Update tests to ensure all database tables are emptied when purging a room.

View File

@@ -33,6 +33,73 @@ from synapse.types import RoomStreamToken
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
purge_room_tables_with_event_id_index = (
"event_auth",
"event_edges",
"event_json",
"event_push_actions_staging",
"event_relations",
"event_to_state_groups",
"event_auth_chains",
"event_auth_chain_to_calculate",
"redactions",
"rejections",
"state_events",
)
"""
Tables which lack an index on `room_id` but have one on `event_id`
"""
purge_room_tables_with_room_id_column = (
"current_state_events",
"destination_rooms",
"event_backward_extremities",
"event_forward_extremities",
"event_push_actions",
"event_search",
"event_failed_pull_attempts",
# Note: the partial state tables have foreign keys between each other, and to
# `events` and `rooms`. We need to delete from them in the right order.
"partial_state_events",
"partial_state_rooms_servers",
"partial_state_rooms",
# Note: the _membership(s) tables have foreign keys to the `events` table
# so must be deleted first.
"local_current_membership",
"room_memberships",
# Note: the sliding_sync_ tables have foreign keys to the `events` table
# so must be deleted first.
"sliding_sync_joined_rooms",
"sliding_sync_membership_snapshots",
"events",
"federation_inbound_events_staging",
"receipts_graph",
"receipts_linearized",
"room_aliases",
"room_depth",
"room_stats_state",
"room_stats_current",
"room_stats_earliest_token",
"stream_ordering_to_exterm",
"users_in_public_rooms",
"users_who_share_private_rooms",
# no useful index, but let's clear them anyway
"appservice_room_list",
"e2e_room_keys",
"event_push_summary",
"pusher_throttle",
"room_account_data",
"room_tags",
# "rooms" happens last, to keep the foreign keys in the other tables
# happy
"rooms",
)
"""
The tables with a `room_id` column regardless of whether they have a useful index on
`room_id`.
"""
class PurgeEventsStore(StateGroupWorkerStore, CacheInvalidationWorkerStore): class PurgeEventsStore(StateGroupWorkerStore, CacheInvalidationWorkerStore):
async def purge_history( async def purge_history(
self, room_id: str, token: str, delete_local_events: bool self, room_id: str, token: str, delete_local_events: bool
@@ -398,20 +465,8 @@ class PurgeEventsStore(StateGroupWorkerStore, CacheInvalidationWorkerStore):
referenced_chain_id_tuples, referenced_chain_id_tuples,
) )
# Now we delete tables which lack an index on room_id but have one on event_id # Now we delete tables which lack an index on `room_id` but have one on `event_id`
for table in ( for table in purge_room_tables_with_event_id_index:
"event_auth",
"event_edges",
"event_json",
"event_push_actions_staging",
"event_relations",
"event_to_state_groups",
"event_auth_chains",
"event_auth_chain_to_calculate",
"redactions",
"rejections",
"state_events",
):
logger.info("[purge] removing from %s", table) logger.info("[purge] removing from %s", table)
txn.execute( txn.execute(
@@ -424,51 +479,9 @@ class PurgeEventsStore(StateGroupWorkerStore, CacheInvalidationWorkerStore):
(room_id,), (room_id,),
) )
# next, the tables with an index on room_id (or no useful index) # next, the tables with a `room_id` column regardless of whether they have a
for table in ( # useful index on `room_id`
"current_state_events", for table in purge_room_tables_with_room_id_column:
"destination_rooms",
"event_backward_extremities",
"event_forward_extremities",
"event_push_actions",
"event_search",
"event_failed_pull_attempts",
# Note: the partial state tables have foreign keys between each other, and to
# `events` and `rooms`. We need to delete from them in the right order.
"partial_state_events",
"partial_state_rooms_servers",
"partial_state_rooms",
# Note: the _membership(s) tables have foreign keys to the `events` table
# so must be deleted first.
"local_current_membership",
"room_memberships",
# Note: the sliding_sync_ tables have foreign keys to the `events` table
# so must be deleted first.
"sliding_sync_joined_rooms",
"sliding_sync_membership_snapshots",
"events",
"federation_inbound_events_staging",
"receipts_graph",
"receipts_linearized",
"room_aliases",
"room_depth",
"room_stats_state",
"room_stats_current",
"room_stats_earliest_token",
"stream_ordering_to_exterm",
"users_in_public_rooms",
"users_who_share_private_rooms",
# no useful index, but let's clear them anyway
"appservice_room_list",
"e2e_room_keys",
"event_push_summary",
"pusher_throttle",
"room_account_data",
"room_tags",
# "rooms" happens last, to keep the foreign keys in the other tables
# happy
"rooms",
):
logger.info("[purge] removing from %s", table) logger.info("[purge] removing from %s", table)
txn.execute("DELETE FROM %s WHERE room_id=?" % (table,), (room_id,)) txn.execute("DELETE FROM %s WHERE room_id=?" % (table,), (room_id,))

View File

@@ -39,6 +39,10 @@ from synapse.handlers.pagination import (
) )
from synapse.rest.client import directory, events, knock, login, room, sync from synapse.rest.client import directory, events, knock, login, room, sync
from synapse.server import HomeServer from synapse.server import HomeServer
from synapse.storage.databases.main.purge_events import (
purge_room_tables_with_event_id_index,
purge_room_tables_with_room_id_column,
)
from synapse.types import UserID from synapse.types import UserID
from synapse.util import Clock from synapse.util import Clock
from synapse.util.task_scheduler import TaskScheduler from synapse.util.task_scheduler import TaskScheduler
@@ -546,7 +550,7 @@ class DeleteRoomTestCase(unittest.HomeserverTestCase):
def _is_purged(self, room_id: str) -> None: def _is_purged(self, room_id: str) -> None:
"""Test that the following tables have been purged of all rows related to the room.""" """Test that the following tables have been purged of all rows related to the room."""
for table in PURGE_TABLES: for table in purge_room_tables_with_room_id_column:
count = self.get_success( count = self.get_success(
self.store.db_pool.simple_select_one_onecol( self.store.db_pool.simple_select_one_onecol(
table=table, table=table,
@@ -555,7 +559,21 @@ class DeleteRoomTestCase(unittest.HomeserverTestCase):
desc="test_purge_room", desc="test_purge_room",
) )
) )
self.assertEqual(count, 0, msg=f"Rows not purged in {table}")
for table in purge_room_tables_with_event_id_index:
rows = self.get_success(
self.store.db_pool.execute(
"find_event_count_for_table",
f"""
SELECT COUNT(*) FROM {table} WHERE event_id IN (
SELECT event_id FROM events WHERE room_id=?
)
""",
room_id,
)
)
count = rows[0][0]
self.assertEqual(count, 0, msg=f"Rows not purged in {table}") self.assertEqual(count, 0, msg=f"Rows not purged in {table}")
def _assert_peek(self, room_id: str, expect_code: int) -> None: def _assert_peek(self, room_id: str, expect_code: int) -> None:
@@ -1228,7 +1246,7 @@ class DeleteRoomV2TestCase(unittest.HomeserverTestCase):
def _is_purged(self, room_id: str) -> None: def _is_purged(self, room_id: str) -> None:
"""Test that the following tables have been purged of all rows related to the room.""" """Test that the following tables have been purged of all rows related to the room."""
for table in PURGE_TABLES: for table in purge_room_tables_with_room_id_column:
count = self.get_success( count = self.get_success(
self.store.db_pool.simple_select_one_onecol( self.store.db_pool.simple_select_one_onecol(
table=table, table=table,
@@ -1237,7 +1255,21 @@ class DeleteRoomV2TestCase(unittest.HomeserverTestCase):
desc="test_purge_room", desc="test_purge_room",
) )
) )
self.assertEqual(count, 0, msg=f"Rows not purged in {table}")
for table in purge_room_tables_with_event_id_index:
rows = self.get_success(
self.store.db_pool.execute(
"find_event_count_for_table",
f"""
SELECT COUNT(*) FROM {table} WHERE event_id IN (
SELECT event_id FROM events WHERE room_id=?
)
""",
room_id,
)
)
count = rows[0][0]
self.assertEqual(count, 0, msg=f"Rows not purged in {table}") self.assertEqual(count, 0, msg=f"Rows not purged in {table}")
def _assert_peek(self, room_id: str, expect_code: int) -> None: def _assert_peek(self, room_id: str, expect_code: int) -> None:
@@ -3119,35 +3151,3 @@ class BlockRoomTestCase(unittest.HomeserverTestCase):
"""Block a room in database""" """Block a room in database"""
self.get_success(self._store.block_room(room_id, self.other_user)) self.get_success(self._store.block_room(room_id, self.other_user))
self._is_blocked(room_id, expect=True) self._is_blocked(room_id, expect=True)
PURGE_TABLES = [
"current_state_events",
"event_backward_extremities",
"event_forward_extremities",
"event_json",
"event_push_actions",
"event_search",
"events",
"receipts_graph",
"receipts_linearized",
"room_aliases",
"room_depth",
"room_memberships",
"room_stats_state",
"room_stats_current",
"room_stats_earliest_token",
"rooms",
"stream_ordering_to_exterm",
"users_in_public_rooms",
"users_who_share_private_rooms",
"appservice_room_list",
"e2e_room_keys",
"event_push_summary",
"pusher_throttle",
"room_account_data",
"room_tags",
"state_groups",
"state_groups_state",
"federation_inbound_events_staging",
]