Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 80d4406

Browse files
David RobertsonMathieu Veltensquahtx
authored
Faster joins: omit partial rooms from eager syncs until the resync completes (#14870)
* Allow `AbstractSet` in `StrCollection` Or else frozensets are excluded. This will be useful in an upcoming commit where I plan to change a function that accepts `List[str]` to accept `StrCollection` instead. * `rooms_to_exclude` -> `rooms_to_exclude_globally` I am about to make use of this exclusion mechanism to exclude rooms for a specific user and a specific sync. This rename helps to clarify the distinction between the global config and the rooms to exclude for a specific sync. * Better function names for internal sync methods * Track a list of excluded rooms on SyncResultBuilder I plan to feed a list of partially stated rooms for this sync to ignore * Exclude partial state rooms during eager sync using the mechanism established in the previous commit * Track un-partial-state stream in sync tokens So that we can work out which rooms have become fully-stated during a given sync period. * Fix mutation of `@cached` return value This was fouling up a complement test added alongside this PR. Excluding a room would mean the set of forgotten rooms in the cache would be extended. This means that room could be erroneously considered forgotten in the future. Introduced in #12310, Synapse 1.57.0. I don't think this had any user-visible side effects (until now). * SyncResultBuilder: track rooms to force as newly joined Similar plan as before. We've omitted rooms from certain sync responses; now we establish the mechanism to reintroduce them into future syncs. * Read new field, to present rooms as newly joined * Force un-partial-stated rooms to be newly-joined for eager incremental syncs only, provided they're still fully stated * Notify user stream listeners to wake up long polling syncs * Changelog * Typo fix Co-authored-by: Sean Quah <[email protected]> * Unnecessary list cast Co-authored-by: Sean Quah <[email protected]> * Rephrase comment Co-authored-by: Sean Quah <[email protected]> * Another comment Co-authored-by: Sean Quah <[email protected]> * Fixup merge(?) * Poke notifier when receiving un-partial-stated msg over replication * Fixup merge whoops Thanks MV :) Co-authored-by: Mathieu Velen <[email protected]> Co-authored-by: Mathieu Velten <[email protected]> Co-authored-by: Sean Quah <[email protected]>
1 parent 5e75771 commit 80d4406

File tree

13 files changed

+170
-44
lines changed

13 files changed

+170
-44
lines changed

changelog.d/14870.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Faster joins: allow non-lazy-loading ("eager") syncs to complete after a partial join by omitting partial state rooms until they become fully stated.

synapse/handlers/federation.py

+5-10
Original file line numberDiff line numberDiff line change
@@ -1868,22 +1868,17 @@ async def _sync_partial_state_room(
18681868

18691869
async with self._is_partial_state_room_linearizer.queue(room_id):
18701870
logger.info("Clearing partial-state flag for %s", room_id)
1871-
success = await self.store.clear_partial_state_room(room_id)
1871+
new_stream_id = await self.store.clear_partial_state_room(room_id)
18721872

1873-
# Poke the notifier so that other workers see the write to
1874-
# the un-partial-stated rooms stream.
1875-
self._notifier.notify_replication()
1876-
1877-
if success:
1873+
if new_stream_id is not None:
18781874
logger.info("State resync complete for %s", room_id)
18791875
self._storage_controllers.state.notify_room_un_partial_stated(
18801876
room_id
18811877
)
18821878

1883-
# Poke the notifier so that other workers see the write to
1884-
# the un-partial-stated rooms stream.
1885-
self._notifier.notify_replication()
1886-
1879+
await self._notifier.on_un_partial_stated_room(
1880+
room_id, new_stream_id
1881+
)
18871882
return
18881883

18891884
# we raced against more events arriving with partial state. Go round

synapse/handlers/sync.py

+55-10
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ def __init__(self, hs: "HomeServer"):
290290
expiry_ms=LAZY_LOADED_MEMBERS_CACHE_MAX_AGE,
291291
)
292292

293-
self.rooms_to_exclude = hs.config.server.rooms_to_exclude_from_sync
293+
self.rooms_to_exclude_globally = hs.config.server.rooms_to_exclude_from_sync
294294

295295
async def wait_for_sync_for_user(
296296
self,
@@ -1340,7 +1340,10 @@ async def generate_sync_result(
13401340
membership_change_events = []
13411341
if since_token:
13421342
membership_change_events = await self.store.get_membership_changes_for_user(
1343-
user_id, since_token.room_key, now_token.room_key, self.rooms_to_exclude
1343+
user_id,
1344+
since_token.room_key,
1345+
now_token.room_key,
1346+
self.rooms_to_exclude_globally,
13441347
)
13451348

13461349
mem_last_change_by_room_id: Dict[str, EventBase] = {}
@@ -1375,12 +1378,39 @@ async def generate_sync_result(
13751378
else:
13761379
mutable_joined_room_ids.discard(room_id)
13771380

1381+
# Tweak the set of rooms to return to the client for eager (non-lazy) syncs.
1382+
mutable_rooms_to_exclude = set(self.rooms_to_exclude_globally)
1383+
if not sync_config.filter_collection.lazy_load_members():
1384+
# Non-lazy syncs should never include partially stated rooms.
1385+
# Exclude all partially stated rooms from this sync.
1386+
for room_id in mutable_joined_room_ids:
1387+
if await self.store.is_partial_state_room(room_id):
1388+
mutable_rooms_to_exclude.add(room_id)
1389+
1390+
# Incremental eager syncs should additionally include rooms that
1391+
# - we are joined to
1392+
# - are full-stated
1393+
# - became fully-stated at some point during the sync period
1394+
# (These rooms will have been omitted during a previous eager sync.)
1395+
forced_newly_joined_room_ids = set()
1396+
if since_token and not sync_config.filter_collection.lazy_load_members():
1397+
un_partial_stated_rooms = (
1398+
await self.store.get_un_partial_stated_rooms_between(
1399+
since_token.un_partial_stated_rooms_key,
1400+
now_token.un_partial_stated_rooms_key,
1401+
mutable_joined_room_ids,
1402+
)
1403+
)
1404+
for room_id in un_partial_stated_rooms:
1405+
if not await self.store.is_partial_state_room(room_id):
1406+
forced_newly_joined_room_ids.add(room_id)
1407+
13781408
# Now we have our list of joined room IDs, exclude as configured and freeze
13791409
joined_room_ids = frozenset(
13801410
(
13811411
room_id
13821412
for room_id in mutable_joined_room_ids
1383-
if room_id not in self.rooms_to_exclude
1413+
if room_id not in mutable_rooms_to_exclude
13841414
)
13851415
)
13861416

@@ -1397,6 +1427,8 @@ async def generate_sync_result(
13971427
since_token=since_token,
13981428
now_token=now_token,
13991429
joined_room_ids=joined_room_ids,
1430+
excluded_room_ids=frozenset(mutable_rooms_to_exclude),
1431+
forced_newly_joined_room_ids=frozenset(forced_newly_joined_room_ids),
14001432
membership_change_events=membership_change_events,
14011433
)
14021434

@@ -1834,14 +1866,16 @@ async def _generate_sync_entry_for_rooms(
18341866
# 3. Work out which rooms need reporting in the sync response.
18351867
ignored_users = await self.store.ignored_users(user_id)
18361868
if since_token:
1837-
room_changes = await self._get_rooms_changed(
1869+
room_changes = await self._get_room_changes_for_incremental_sync(
18381870
sync_result_builder, ignored_users
18391871
)
18401872
tags_by_room = await self.store.get_updated_tags(
18411873
user_id, since_token.account_data_key
18421874
)
18431875
else:
1844-
room_changes = await self._get_all_rooms(sync_result_builder, ignored_users)
1876+
room_changes = await self._get_room_changes_for_initial_sync(
1877+
sync_result_builder, ignored_users
1878+
)
18451879
tags_by_room = await self.store.get_tags_for_user(user_id)
18461880

18471881
log_kv({"rooms_changed": len(room_changes.room_entries)})
@@ -1900,7 +1934,7 @@ async def _have_rooms_changed(
19001934

19011935
assert since_token
19021936

1903-
if membership_change_events:
1937+
if membership_change_events or sync_result_builder.forced_newly_joined_room_ids:
19041938
return True
19051939

19061940
stream_id = since_token.room_key.stream
@@ -1909,7 +1943,7 @@ async def _have_rooms_changed(
19091943
return True
19101944
return False
19111945

1912-
async def _get_rooms_changed(
1946+
async def _get_room_changes_for_incremental_sync(
19131947
self,
19141948
sync_result_builder: "SyncResultBuilder",
19151949
ignored_users: FrozenSet[str],
@@ -1947,7 +1981,9 @@ async def _get_rooms_changed(
19471981
for event in membership_change_events:
19481982
mem_change_events_by_room_id.setdefault(event.room_id, []).append(event)
19491983

1950-
newly_joined_rooms: List[str] = []
1984+
newly_joined_rooms: List[str] = list(
1985+
sync_result_builder.forced_newly_joined_room_ids
1986+
)
19511987
newly_left_rooms: List[str] = []
19521988
room_entries: List[RoomSyncResultBuilder] = []
19531989
invited: List[InvitedSyncResult] = []
@@ -2153,7 +2189,7 @@ async def _get_rooms_changed(
21532189
newly_left_rooms,
21542190
)
21552191

2156-
async def _get_all_rooms(
2192+
async def _get_room_changes_for_initial_sync(
21572193
self,
21582194
sync_result_builder: "SyncResultBuilder",
21592195
ignored_users: FrozenSet[str],
@@ -2178,7 +2214,7 @@ async def _get_all_rooms(
21782214
room_list = await self.store.get_rooms_for_local_user_where_membership_is(
21792215
user_id=user_id,
21802216
membership_list=Membership.LIST,
2181-
excluded_rooms=self.rooms_to_exclude,
2217+
excluded_rooms=sync_result_builder.excluded_room_ids,
21822218
)
21832219

21842220
room_entries = []
@@ -2549,6 +2585,13 @@ class SyncResultBuilder:
25492585
since_token: The token supplied by user, or None.
25502586
now_token: The token to sync up to.
25512587
joined_room_ids: List of rooms the user is joined to
2588+
excluded_room_ids: Set of room ids we should omit from the /sync response.
2589+
forced_newly_joined_room_ids:
2590+
Rooms that should be presented in the /sync response as if they were
2591+
newly joined during the sync period, even if that's not the case.
2592+
(This is useful if the room was previously excluded from a /sync response,
2593+
and now the client should be made aware of it.)
2594+
Only used by incremental syncs.
25522595
25532596
# The following mirror the fields in a sync response
25542597
presence
@@ -2565,6 +2608,8 @@ class SyncResultBuilder:
25652608
since_token: Optional[StreamToken]
25662609
now_token: StreamToken
25672610
joined_room_ids: FrozenSet[str]
2611+
excluded_room_ids: FrozenSet[str]
2612+
forced_newly_joined_room_ids: FrozenSet[str]
25682613
membership_change_events: List[EventBase]
25692614

25702615
presence: List[UserPresenceState] = attr.Factory(list)

synapse/notifier.py

+26
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,32 @@ async def on_new_room_events(
314314
event_entries.append((entry, event.event_id))
315315
await self.notify_new_room_events(event_entries, max_room_stream_token)
316316

317+
async def on_un_partial_stated_room(
318+
self,
319+
room_id: str,
320+
new_token: int,
321+
) -> None:
322+
"""Used by the resync background processes to wake up all listeners
323+
of this room when it is un-partial-stated.
324+
325+
It will also notify replication listeners of the change in stream.
326+
"""
327+
328+
# Wake up all related user stream notifiers
329+
user_streams = self.room_to_user_streams.get(room_id, set())
330+
time_now_ms = self.clock.time_msec()
331+
for user_stream in user_streams:
332+
try:
333+
user_stream.notify(
334+
StreamKeyType.UN_PARTIAL_STATED_ROOMS, new_token, time_now_ms
335+
)
336+
except Exception:
337+
logger.exception("Failed to notify listener")
338+
339+
# Poke the replication so that other workers also see the write to
340+
# the un-partial-stated rooms stream.
341+
self.notify_replication()
342+
317343
async def notify_new_room_events(
318344
self,
319345
event_entries: List[Tuple[_PendingRoomEventEntry, str]],

synapse/replication/tcp/client.py

+1
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ async def on_rdata(
260260
self._state_storage_controller.notify_room_un_partial_stated(
261261
row.room_id
262262
)
263+
await self.notifier.on_un_partial_stated_room(row.room_id, token)
263264
elif stream_name == UnPartialStatedEventStream.NAME:
264265
for row in rows:
265266
assert isinstance(row, UnPartialStatedEventStreamRow)

synapse/storage/databases/main/relations.py

+1
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ def _get_recent_references_for_event_txn(
292292
to_device_key=0,
293293
device_list_key=0,
294294
groups_key=0,
295+
un_partial_stated_rooms_key=0,
295296
)
296297

297298
return events[:limit], next_token

synapse/storage/databases/main/room.py

+41-6
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
Mapping,
2727
Optional,
2828
Sequence,
29+
Set,
2930
Tuple,
3031
Union,
3132
cast,
@@ -1294,10 +1295,44 @@ def get_un_partial_stated_rooms_token(self, instance_name: str) -> int:
12941295
instance_name
12951296
)
12961297

1298+
async def get_un_partial_stated_rooms_between(
1299+
self, last_id: int, current_id: int, room_ids: Collection[str]
1300+
) -> Set[str]:
1301+
"""Get all rooms that got un partial stated between `last_id` exclusive and
1302+
`current_id` inclusive.
1303+
1304+
Returns:
1305+
The list of room ids.
1306+
"""
1307+
1308+
if last_id == current_id:
1309+
return set()
1310+
1311+
def _get_un_partial_stated_rooms_between_txn(
1312+
txn: LoggingTransaction,
1313+
) -> Set[str]:
1314+
sql = """
1315+
SELECT DISTINCT room_id FROM un_partial_stated_room_stream
1316+
WHERE ? < stream_id AND stream_id <= ? AND
1317+
"""
1318+
1319+
clause, args = make_in_list_sql_clause(
1320+
self.database_engine, "room_id", room_ids
1321+
)
1322+
1323+
txn.execute(sql + clause, [last_id, current_id] + args)
1324+
1325+
return {r[0] for r in txn}
1326+
1327+
return await self.db_pool.runInteraction(
1328+
"get_un_partial_stated_rooms_between",
1329+
_get_un_partial_stated_rooms_between_txn,
1330+
)
1331+
12971332
async def get_un_partial_stated_rooms_from_stream(
12981333
self, instance_name: str, last_id: int, current_id: int, limit: int
12991334
) -> Tuple[List[Tuple[int, Tuple[str]]], int, bool]:
1300-
"""Get updates for caches replication stream.
1335+
"""Get updates for un partial stated rooms replication stream.
13011336
13021337
Args:
13031338
instance_name: The writer we want to fetch updates from. Unused
@@ -2304,16 +2339,16 @@ async def unblock_room(self, room_id: str) -> None:
23042339
(room_id,),
23052340
)
23062341

2307-
async def clear_partial_state_room(self, room_id: str) -> bool:
2342+
async def clear_partial_state_room(self, room_id: str) -> Optional[int]:
23082343
"""Clears the partial state flag for a room.
23092344
23102345
Args:
23112346
room_id: The room whose partial state flag is to be cleared.
23122347
23132348
Returns:
2314-
`True` if the partial state flag has been cleared successfully.
2349+
The corresponding stream id for the un-partial-stated rooms stream.
23152350
2316-
`False` if the partial state flag could not be cleared because the room
2351+
`None` if the partial state flag could not be cleared because the room
23172352
still contains events with partial state.
23182353
"""
23192354
try:
@@ -2324,15 +2359,15 @@ async def clear_partial_state_room(self, room_id: str) -> bool:
23242359
room_id,
23252360
un_partial_state_room_stream_id,
23262361
)
2327-
return True
2362+
return un_partial_state_room_stream_id
23282363
except self.db_pool.engine.module.IntegrityError as e:
23292364
# Assume that any `IntegrityError`s are due to partial state events.
23302365
logger.info(
23312366
"Exception while clearing lazy partial-state-room %s, retrying: %s",
23322367
room_id,
23332368
e,
23342369
)
2335-
return False
2370+
return None
23362371

23372372
def _clear_partial_state_room_txn(
23382373
self,

synapse/storage/databases/main/roommember.py

+14-5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import logging
1616
from typing import (
1717
TYPE_CHECKING,
18+
AbstractSet,
1819
Collection,
1920
Dict,
2021
FrozenSet,
@@ -47,7 +48,13 @@
4748
ProfileInfo,
4849
RoomsForUser,
4950
)
50-
from synapse.types import JsonDict, PersistedEventPosition, StateMap, get_domain_from_id
51+
from synapse.types import (
52+
JsonDict,
53+
PersistedEventPosition,
54+
StateMap,
55+
StrCollection,
56+
get_domain_from_id,
57+
)
5158
from synapse.util.async_helpers import Linearizer
5259
from synapse.util.caches import intern_string
5360
from synapse.util.caches.descriptors import _CacheContext, cached, cachedList
@@ -385,7 +392,7 @@ async def get_rooms_for_local_user_where_membership_is(
385392
self,
386393
user_id: str,
387394
membership_list: Collection[str],
388-
excluded_rooms: Optional[List[str]] = None,
395+
excluded_rooms: StrCollection = (),
389396
) -> List[RoomsForUser]:
390397
"""Get all the rooms for this *local* user where the membership for this user
391398
matches one in the membership list.
@@ -412,10 +419,12 @@ async def get_rooms_for_local_user_where_membership_is(
412419
)
413420

414421
# Now we filter out forgotten and excluded rooms
415-
rooms_to_exclude: Set[str] = await self.get_forgotten_rooms_for_user(user_id)
422+
rooms_to_exclude = await self.get_forgotten_rooms_for_user(user_id)
416423

417424
if excluded_rooms is not None:
418-
rooms_to_exclude.update(set(excluded_rooms))
425+
# Take a copy to avoid mutating the in-cache set
426+
rooms_to_exclude = set(rooms_to_exclude)
427+
rooms_to_exclude.update(excluded_rooms)
419428

420429
return [room for room in rooms if room.room_id not in rooms_to_exclude]
421430

@@ -1169,7 +1178,7 @@ def f(txn: LoggingTransaction) -> int:
11691178
return count == 0
11701179

11711180
@cached()
1172-
async def get_forgotten_rooms_for_user(self, user_id: str) -> Set[str]:
1181+
async def get_forgotten_rooms_for_user(self, user_id: str) -> AbstractSet[str]:
11731182
"""Gets all rooms the user has forgotten.
11741183
11751184
Args:

0 commit comments

Comments
 (0)