Skip to content

Commit a5e16a4

Browse files
Sliding Sync: Reset forgotten status when membership changes (like rejoining a room) (#17835)
Reset `sliding_sync_membership_snapshots` -> `forgotten` status when membership changes (like rejoining a room). Fix #17781 ### What was the problem before? Previously, if someone used `/forget` on one of their rooms, it would update `sliding_sync_membership_snapshots` as expected but when someone rejoined the room (or had any membership change), the upsert didn't overwrite and reset the `forgotten` status so it remained `forgotten` and invisible down the Sliding Sync endpoint.
1 parent 80ad02e commit a5e16a4

File tree

10 files changed

+433
-4
lines changed

10 files changed

+433
-4
lines changed

changelog.d/17835.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a bug in [MSC4186](https://github.com/matrix-org/matrix-spec-proposals/pull/4186) Sliding Sync that would cause rooms to stay forgotten and hidden even after rejoining.

synapse/storage/databases/main/events.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1863,10 +1863,10 @@ def _update_current_state_txn(
18631863
txn.execute_batch(
18641864
f"""
18651865
INSERT INTO sliding_sync_membership_snapshots
1866-
(room_id, user_id, sender, membership_event_id, membership, event_stream_ordering, event_instance_name
1866+
(room_id, user_id, sender, membership_event_id, membership, forgotten, event_stream_ordering, event_instance_name
18671867
{("," + ", ".join(sliding_sync_snapshot_keys)) if sliding_sync_snapshot_keys else ""})
18681868
VALUES (
1869-
?, ?, ?, ?, ?,
1869+
?, ?, ?, ?, ?, ?,
18701870
(SELECT stream_ordering FROM events WHERE event_id = ?),
18711871
(SELECT COALESCE(instance_name, 'master') FROM events WHERE event_id = ?)
18721872
{("," + ", ".join("?" for _ in sliding_sync_snapshot_values)) if sliding_sync_snapshot_values else ""}
@@ -1876,6 +1876,7 @@ def _update_current_state_txn(
18761876
sender = EXCLUDED.sender,
18771877
membership_event_id = EXCLUDED.membership_event_id,
18781878
membership = EXCLUDED.membership,
1879+
forgotten = EXCLUDED.forgotten,
18791880
event_stream_ordering = EXCLUDED.event_stream_ordering
18801881
{("," + ", ".join(f"{key} = EXCLUDED.{key}" for key in sliding_sync_snapshot_keys)) if sliding_sync_snapshot_keys else ""}
18811882
""",
@@ -1886,6 +1887,9 @@ def _update_current_state_txn(
18861887
membership_info.sender,
18871888
membership_info.membership_event_id,
18881889
membership_info.membership,
1890+
# Since this is a new membership, it isn't forgotten anymore (which
1891+
# matches how Synapse currently thinks about the forgotten status)
1892+
0,
18891893
# XXX: We do not use `membership_info.membership_event_stream_ordering` here
18901894
# because it is an unreliable value. See XXX note above.
18911895
membership_info.membership_event_id,
@@ -2901,6 +2905,9 @@ def _store_room_members_txn(
29012905
"sender": event.sender,
29022906
"membership_event_id": event.event_id,
29032907
"membership": event.membership,
2908+
# Since this is a new membership, it isn't forgotten anymore (which
2909+
# matches how Synapse currently thinks about the forgotten status)
2910+
"forgotten": 0,
29042911
"event_stream_ordering": event.internal_metadata.stream_ordering,
29052912
"event_instance_name": event.internal_metadata.instance_name,
29062913
}

synapse/storage/databases/main/events_bg_updates.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,12 @@ def __init__(
304304
_BackgroundUpdates.SLIDING_SYNC_MEMBERSHIP_SNAPSHOTS_BG_UPDATE,
305305
self._sliding_sync_membership_snapshots_bg_update,
306306
)
307+
# Add a background update to fix data integrity issue in the
308+
# `sliding_sync_membership_snapshots` -> `forgotten` column
309+
self.db_pool.updates.register_background_update_handler(
310+
_BackgroundUpdates.SLIDING_SYNC_MEMBERSHIP_SNAPSHOTS_FIX_FORGOTTEN_COLUMN_BG_UPDATE,
311+
self._sliding_sync_membership_snapshots_fix_forgotten_column_bg_update,
312+
)
307313

308314
# We want this to run on the main database at startup before we start processing
309315
# events.
@@ -2429,6 +2435,118 @@ def _fill_table_txn(txn: LoggingTransaction) -> None:
24292435

24302436
return len(memberships_to_update_rows)
24312437

2438+
async def _sliding_sync_membership_snapshots_fix_forgotten_column_bg_update(
2439+
self, progress: JsonDict, batch_size: int
2440+
) -> int:
2441+
"""
2442+
Background update to update the `sliding_sync_membership_snapshots` ->
2443+
`forgotten` column to be in sync with the `room_memberships` table.
2444+
2445+
Because of previously flawed code (now fixed); any room that someone has
2446+
forgotten and subsequently re-joined or had any new membership on, we need to go
2447+
and update the column to match the `room_memberships` table as it has fallen out
2448+
of sync.
2449+
"""
2450+
last_event_stream_ordering = progress.get(
2451+
"last_event_stream_ordering", -(1 << 31)
2452+
)
2453+
2454+
def _txn(
2455+
txn: LoggingTransaction,
2456+
) -> int:
2457+
"""
2458+
Returns:
2459+
The number of rows updated.
2460+
"""
2461+
2462+
# To simplify things, we can just recheck any row in
2463+
# `sliding_sync_membership_snapshots` with `forgotten=1`
2464+
txn.execute(
2465+
"""
2466+
SELECT
2467+
s.room_id,
2468+
s.user_id,
2469+
s.membership_event_id,
2470+
s.event_stream_ordering,
2471+
m.forgotten
2472+
FROM sliding_sync_membership_snapshots AS s
2473+
INNER JOIN room_memberships AS m ON (s.membership_event_id = m.event_id)
2474+
WHERE s.event_stream_ordering > ?
2475+
AND s.forgotten = 1
2476+
ORDER BY s.event_stream_ordering ASC
2477+
LIMIT ?
2478+
""",
2479+
(last_event_stream_ordering, batch_size),
2480+
)
2481+
2482+
memberships_to_update_rows = cast(
2483+
List[Tuple[str, str, str, int, int]],
2484+
txn.fetchall(),
2485+
)
2486+
if not memberships_to_update_rows:
2487+
return 0
2488+
2489+
# Assemble the values to update
2490+
#
2491+
# (room_id, user_id)
2492+
key_values: List[Tuple[str, str]] = []
2493+
# (forgotten,)
2494+
value_values: List[Tuple[int]] = []
2495+
for (
2496+
room_id,
2497+
user_id,
2498+
_membership_event_id,
2499+
_event_stream_ordering,
2500+
forgotten,
2501+
) in memberships_to_update_rows:
2502+
key_values.append(
2503+
(
2504+
room_id,
2505+
user_id,
2506+
)
2507+
)
2508+
value_values.append((forgotten,))
2509+
2510+
# Update all of the rows in one go
2511+
self.db_pool.simple_update_many_txn(
2512+
txn,
2513+
table="sliding_sync_membership_snapshots",
2514+
key_names=("room_id", "user_id"),
2515+
key_values=key_values,
2516+
value_names=("forgotten",),
2517+
value_values=value_values,
2518+
)
2519+
2520+
# Update the progress
2521+
(
2522+
_room_id,
2523+
_user_id,
2524+
_membership_event_id,
2525+
event_stream_ordering,
2526+
_forgotten,
2527+
) = memberships_to_update_rows[-1]
2528+
self.db_pool.updates._background_update_progress_txn(
2529+
txn,
2530+
_BackgroundUpdates.SLIDING_SYNC_MEMBERSHIP_SNAPSHOTS_FIX_FORGOTTEN_COLUMN_BG_UPDATE,
2531+
{
2532+
"last_event_stream_ordering": event_stream_ordering,
2533+
},
2534+
)
2535+
2536+
return len(memberships_to_update_rows)
2537+
2538+
num_rows = await self.db_pool.runInteraction(
2539+
"_sliding_sync_membership_snapshots_fix_forgotten_column_bg_update",
2540+
_txn,
2541+
)
2542+
2543+
if not num_rows:
2544+
await self.db_pool.updates._end_background_update(
2545+
_BackgroundUpdates.SLIDING_SYNC_MEMBERSHIP_SNAPSHOTS_FIX_FORGOTTEN_COLUMN_BG_UPDATE
2546+
)
2547+
2548+
return num_rows
2549+
24322550

24332551
def _resolve_stale_data_in_sliding_sync_tables(
24342552
txn: LoggingTransaction,

synapse/storage/databases/main/roommember.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1375,6 +1375,7 @@ def f(txn: LoggingTransaction) -> None:
13751375
keyvalues={"user_id": user_id, "room_id": room_id},
13761376
updatevalues={"forgotten": 1},
13771377
)
1378+
# Handle updating the `sliding_sync_membership_snapshots` table
13781379
self.db_pool.simple_update_txn(
13791380
txn,
13801381
table="sliding_sync_membership_snapshots",

synapse/storage/schema/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@
153153
Changes in SCHEMA_VERSION = 88
154154
- MSC4140: Add `delayed_events` table that keeps track of events that are to
155155
be posted in response to a resettable timeout or an on-demand action.
156+
- Add background update to fix data integrity issue in the
157+
`sliding_sync_membership_snapshots` -> `forgotten` column
156158
"""
157159

158160

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
--
2+
-- This file is licensed under the Affero General Public License (AGPL) version 3.
3+
--
4+
-- Copyright (C) 2024 New Vector, Ltd
5+
--
6+
-- This program is free software: you can redistribute it and/or modify
7+
-- it under the terms of the GNU Affero General Public License as
8+
-- published by the Free Software Foundation, either version 3 of the
9+
-- License, or (at your option) any later version.
10+
--
11+
-- See the GNU Affero General Public License for more details:
12+
-- <https://www.gnu.org/licenses/agpl-3.0.html>.
13+
14+
-- Add a background update to update the `sliding_sync_membership_snapshots` ->
15+
-- `forgotten` column to be in sync with the `room_memberships` table.
16+
--
17+
-- For any room that someone has forgotten and subsequently re-joined or had any new
18+
-- membership on, we need to go and update the column to match the `room_memberships`
19+
-- table as it has fallen out of sync.
20+
INSERT INTO background_updates (ordering, update_name, progress_json) VALUES
21+
(8802, 'sliding_sync_membership_snapshots_fix_forgotten_column_bg_update', '{}');

synapse/types/storage/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,6 @@ class _BackgroundUpdates:
4545
SLIDING_SYNC_MEMBERSHIP_SNAPSHOTS_BG_UPDATE = (
4646
"sliding_sync_membership_snapshots_bg_update"
4747
)
48+
SLIDING_SYNC_MEMBERSHIP_SNAPSHOTS_FIX_FORGOTTEN_COLUMN_BG_UPDATE = (
49+
"sliding_sync_membership_snapshots_fix_forgotten_column_bg_update"
50+
)

tests/rest/client/sliding_sync/test_sliding_sync.py

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ def _create_remote_invite_room_for_user(
240240
self,
241241
invitee_user_id: str,
242242
unsigned_invite_room_state: Optional[List[StrippedStateEvent]],
243+
invite_room_id: Optional[str] = None,
243244
) -> str:
244245
"""
245246
Create a fake invite for a remote room and persist it.
@@ -252,19 +253,23 @@ def _create_remote_invite_room_for_user(
252253
invitee_user_id: The person being invited
253254
unsigned_invite_room_state: List of stripped state events to assist the
254255
receiver in identifying the room.
256+
invite_room_id: Optional remote room ID to be invited to. When unset, we
257+
will generate one.
255258
256259
Returns:
257260
The room ID of the remote invite room
258261
"""
259262
store = self.hs.get_datastores().main
260263

261-
invite_room_id = f"!test_room{self._remote_invite_count}:remote_server"
264+
if invite_room_id is None:
265+
invite_room_id = f"!test_room{self._remote_invite_count}:remote_server"
262266

263267
invite_event_dict = {
264268
"room_id": invite_room_id,
265269
"sender": "@inviter:remote_server",
266270
"state_key": invitee_user_id,
267-
"depth": 1,
271+
# Just keep advancing the depth
272+
"depth": self._remote_invite_count,
268273
"origin_server_ts": 1,
269274
"type": EventTypes.Member,
270275
"content": {"membership": Membership.INVITE},
@@ -679,6 +684,112 @@ def test_forgotten_up_to_date(self) -> None:
679684
exact=True,
680685
)
681686

687+
def test_rejoin_forgotten_room(self) -> None:
688+
"""
689+
Make sure we can see a forgotten room again if we rejoin (or any new membership
690+
like an invite) (no longer forgotten)
691+
"""
692+
user1_id = self.register_user("user1", "pass")
693+
user1_tok = self.login(user1_id, "pass")
694+
user2_id = self.register_user("user2", "pass")
695+
user2_tok = self.login(user2_id, "pass")
696+
697+
room_id = self.helper.create_room_as(user2_id, tok=user2_tok, is_public=True)
698+
# User1 joins the room
699+
self.helper.join(room_id, user1_id, tok=user1_tok)
700+
701+
# Make the Sliding Sync request
702+
sync_body = {
703+
"lists": {
704+
"foo-list": {
705+
"ranges": [[0, 99]],
706+
"required_state": [],
707+
"timeline_limit": 0,
708+
}
709+
}
710+
}
711+
response_body, from_token = self.do_sync(sync_body, tok=user1_tok)
712+
# We should see the room (like normal)
713+
self.assertIncludes(
714+
set(response_body["lists"]["foo-list"]["ops"][0]["room_ids"]),
715+
{room_id},
716+
exact=True,
717+
)
718+
719+
# Leave and forget the room
720+
self.helper.leave(room_id, user1_id, tok=user1_tok)
721+
# User1 forgets the room
722+
channel = self.make_request(
723+
"POST",
724+
f"/_matrix/client/r0/rooms/{room_id}/forget",
725+
content={},
726+
access_token=user1_tok,
727+
)
728+
self.assertEqual(channel.code, 200, channel.result)
729+
730+
# Re-join the room
731+
self.helper.join(room_id, user1_id, tok=user1_tok)
732+
733+
# We should see the room again after re-joining
734+
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
735+
self.assertIncludes(
736+
set(response_body["lists"]["foo-list"]["ops"][0]["room_ids"]),
737+
{room_id},
738+
exact=True,
739+
)
740+
741+
def test_invited_to_forgotten_remote_room(self) -> None:
742+
"""
743+
Make sure we can see a forgotten room again if we are invited again
744+
(remote/federated out-of-band memberships)
745+
"""
746+
user1_id = self.register_user("user1", "pass")
747+
user1_tok = self.login(user1_id, "pass")
748+
749+
# Create a remote room invite (out-of-band membership)
750+
room_id = self._create_remote_invite_room_for_user(user1_id, None)
751+
752+
# Make the Sliding Sync request
753+
sync_body = {
754+
"lists": {
755+
"foo-list": {
756+
"ranges": [[0, 99]],
757+
"required_state": [],
758+
"timeline_limit": 0,
759+
}
760+
}
761+
}
762+
response_body, from_token = self.do_sync(sync_body, tok=user1_tok)
763+
# We should see the room (like normal)
764+
self.assertIncludes(
765+
set(response_body["lists"]["foo-list"]["ops"][0]["room_ids"]),
766+
{room_id},
767+
exact=True,
768+
)
769+
770+
# Leave and forget the room
771+
self.helper.leave(room_id, user1_id, tok=user1_tok)
772+
# User1 forgets the room
773+
channel = self.make_request(
774+
"POST",
775+
f"/_matrix/client/r0/rooms/{room_id}/forget",
776+
content={},
777+
access_token=user1_tok,
778+
)
779+
self.assertEqual(channel.code, 200, channel.result)
780+
781+
# Get invited to the room again
782+
# self.helper.join(room_id, user1_id, tok=user1_tok)
783+
self._create_remote_invite_room_for_user(user1_id, None, invite_room_id=room_id)
784+
785+
# We should see the room again after re-joining
786+
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
787+
self.assertIncludes(
788+
set(response_body["lists"]["foo-list"]["ops"][0]["room_ids"]),
789+
{room_id},
790+
exact=True,
791+
)
792+
682793
def test_ignored_user_invites_initial_sync(self) -> None:
683794
"""
684795
Make sure we ignore invites if they are from one of the `m.ignored_user_list` on

0 commit comments

Comments
 (0)