From 4f4611dc582ac9410fcf1ffb4f5a5594eaace39e Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 16 Oct 2026 11:43:54 +0000 Subject: [PATCH] Port find_relevant_room_ids_for_extension to Rust `rust/src/sliding_sync/extensions/relevant.rs` holds the matching of an extension's `lists` and `rooms` against the response. Phase 5 uses it, and it is cheap to port now. A list without exactly one `SYNC` operation fails, as Python's assertion does: that is a list with no ranges or several, under an extension that names it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L51DoxB8VnH8UjXJFDgpNG --- rust/src/sliding_sync/extensions/mod.rs | 23 ++++ rust/src/sliding_sync/extensions/relevant.rs | 120 ++++++++++++++++++ rust/src/sliding_sync/mod.rs | 2 + synapse/synapse_rust/sliding_sync.pyi | 8 ++ tests/handlers/test_sliding_sync_rust_pure.py | 69 ++++++++++ 5 files changed, 222 insertions(+) create mode 100644 rust/src/sliding_sync/extensions/mod.rs create mode 100644 rust/src/sliding_sync/extensions/relevant.rs diff --git a/rust/src/sliding_sync/extensions/mod.rs b/rust/src/sliding_sync/extensions/mod.rs new file mode 100644 index 0000000000..3c483c3e42 --- /dev/null +++ b/rust/src/sliding_sync/extensions/mod.rs @@ -0,0 +1,23 @@ +/* + * This file is licensed under the Affero General Public License (AGPL) version 3. + * + * Copyright (C) 2026 Element Creations Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * See the GNU Affero General Public License for more details: + * . + */ + +//! The sliding sync extensions (`synapse.handlers.sliding_sync.extensions`). + +use pyo3::{prelude::*, types::PyModule}; + +pub mod relevant; + +pub fn register_test_functions(m: &Bound<'_, PyModule>) -> PyResult<()> { + relevant::register_test_functions(m) +} diff --git a/rust/src/sliding_sync/extensions/relevant.rs b/rust/src/sliding_sync/extensions/relevant.rs new file mode 100644 index 0000000000..6504759a65 --- /dev/null +++ b/rust/src/sliding_sync/extensions/relevant.rs @@ -0,0 +1,120 @@ +/* + * This file is licensed under the Affero General Public License (AGPL) version 3. + * + * Copyright (C) 2026 Element Creations Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * See the GNU Affero General Public License for more details: + * . + */ + +//! `find_relevant_room_ids_for_extension`: the rooms an extension covers, +//! from its `lists` and `rooms` and what the response holds. + +use std::collections::HashSet; +use std::sync::Arc; + +use anyhow::{bail, Context}; +use indexmap::IndexMap; +use pyo3::{exceptions::PyAssertionError, prelude::*, types::PyModule}; + +use crate::sliding_sync::room_lists::lists::SyncOp; + +/// The response's lists, by name, each with its operations. +pub type ActualLists = IndexMap, Vec>; + +/// The rooms of a list's single `SYNC` operation. Python asserts there is +/// exactly one, so a list with no ranges, or several, fails here. +fn single_op_rooms<'a>(name: &str, ops: &'a [SyncOp]) -> anyhow::Result<&'a [Arc]> { + match ops { + [op] => Ok(&op.room_ids), + _ => bail!("list {name} has {} operations, not one", ops.len()), + } +} + +/// `find_relevant_room_ids_for_extension`: of the response, the rooms +/// `requested_room_ids` names among `actual_room_ids` (the room +/// subscriptions), and the rooms of the lists `requested_lists` names. `*` +/// means all of them. +pub fn find_relevant_room_ids_for_extension( + requested_lists: Option<&[String]>, + requested_room_ids: Option<&[String]>, + actual_lists: &ActualLists, + actual_room_ids: &HashSet>, +) -> anyhow::Result>> { + let mut relevant: HashSet> = HashSet::new(); + + for room_id in requested_room_ids.unwrap_or_default() { + if room_id == "*" { + relevant.extend(actual_room_ids.iter().cloned()); + break; + } + if let Some(room_id) = actual_room_ids.get(room_id.as_str()) { + relevant.insert(Arc::clone(room_id)); + } + } + + for list_key in requested_lists.unwrap_or_default() { + if list_key == "*" { + for (name, ops) in actual_lists { + relevant.extend(single_op_rooms(name, ops)?.iter().cloned()); + } + break; + } + if let Some(ops) = actual_lists.get(list_key.as_str()) { + relevant.extend( + single_op_rooms(list_key, ops) + .context("an extension's list")? + .iter() + .cloned(), + ); + } + } + + Ok(relevant) +} + +/// `find_relevant_room_ids_for_extension` through the Rust implementation, +/// each list as its operations' room IDs. Raises `AssertionError` as +/// Python does. +#[pyfunction] +fn find_relevant_room_ids_for_test( + requested_lists: Option>, + requested_room_ids: Option>, + actual_lists: Vec<(String, Vec>)>, + actual_room_ids: HashSet, +) -> PyResult> { + let actual_lists: ActualLists = actual_lists + .into_iter() + .map(|(name, ops)| { + let ops = ops + .into_iter() + .map(|room_ids| SyncOp { + range: (0, 0), + room_ids: room_ids.into_iter().map(Arc::from).collect(), + }) + .collect(); + (Arc::from(name), ops) + }) + .collect(); + let actual_room_ids: HashSet> = actual_room_ids.into_iter().map(Arc::from).collect(); + let relevant = find_relevant_room_ids_for_extension( + requested_lists.as_deref(), + requested_room_ids.as_deref(), + &actual_lists, + &actual_room_ids, + ) + .map_err(|e| PyAssertionError::new_err(e.to_string()))?; + let mut relevant: Vec = relevant.iter().map(|r| r.to_string()).collect(); + relevant.sort(); + Ok(relevant) +} + +pub fn register_test_functions(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(find_relevant_room_ids_for_test, m)?)?; + Ok(()) +} diff --git a/rust/src/sliding_sync/mod.rs b/rust/src/sliding_sync/mod.rs index 08b57a6fcb..2943b4a3ee 100644 --- a/rust/src/sliding_sync/mod.rs +++ b/rust/src/sliding_sync/mod.rs @@ -38,6 +38,7 @@ use crate::storage::sync_store::{SyncDbStats, SyncStore}; pub mod config; pub mod connection; +pub mod extensions; pub mod request; pub mod required_state; pub mod room; @@ -168,6 +169,7 @@ pub fn register_module(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> child_module.add_class::()?; config::register_test_functions(&child_module)?; connection::register_test_functions(&child_module)?; + extensions::register_test_functions(&child_module)?; required_state::register_test_functions(&child_module)?; room::register_test_functions(&child_module)?; room_lists::register_test_functions(&child_module)?; diff --git a/synapse/synapse_rust/sliding_sync.pyi b/synapse/synapse_rust/sliding_sync.pyi index c4032492ab..260aacb305 100644 --- a/synapse/synapse_rust/sliding_sync.pyi +++ b/synapse/synapse_rust/sliding_sync.pyi @@ -240,3 +240,11 @@ def room_config_to_persist_for_test( ignore_timeline_bound: bool, limited: bool | None, ) -> tuple[int, dict[str, list[str]]] | None: ... +def find_relevant_room_ids_for_test( + requested_lists: list[str] | None, + requested_room_ids: list[str] | None, + actual_lists: list[tuple[str, list[list[str]]]], + actual_room_ids: set[str], +) -> list[str]: + """Each list as its operations' room IDs. Raises `AssertionError` as + Python does.""" diff --git a/tests/handlers/test_sliding_sync_rust_pure.py b/tests/handlers/test_sliding_sync_rust_pure.py index bd5fd604f1..852e0d7803 100644 --- a/tests/handlers/test_sliding_sync_rust_pure.py +++ b/tests/handlers/test_sliding_sync_rust_pure.py @@ -35,6 +35,7 @@ from synapse.handlers.sliding_sync import ( _stripped_state, _timeline_bounds, ) +from synapse.handlers.sliding_sync.extensions import SlidingSyncExtensionHandler from synapse.handlers.sliding_sync.room_lists import ( SlidingSyncRoomLists, _sync_list_ops, @@ -53,11 +54,13 @@ from synapse.types.handlers import SLIDING_SYNC_DEFAULT_BUMP_EVENT_TYPES from synapse.types.handlers.sliding_sync import ( HaveSentRoom, HaveSentRoomFlag, + OperationType, PerConnectionState, RoomLazyMembershipChanges, RoomStatusMap, RoomSyncConfig, SlidingSyncConfig, + SlidingSyncResult, StateValues, ) from synapse.types.state import StateFilter @@ -1210,3 +1213,69 @@ class RoomHelpersTestCase(unittest.TestCase): (python.timeline_limit, _as_sets(python.required_state_map)), context, ) + + +class RelevantRoomIdsTestCase(unittest.TestCase): + """`rust/src/sliding_sync/extensions/relevant.rs`""" + + def test_against_python(self) -> None: + rng = random.Random(18) + names = ["a", "b", "c", "*"] + raised = 0 + for _ in range(2000): + actual_lists = [ + ( + name, + [ + rng.sample(_ROOMS, rng.randint(0, 3)) + for _ in range(rng.choice([1, 1, 1, 1, 0, 2])) + ], + ) + for name in rng.sample(names[:3], rng.randint(0, 3)) + ] + actual_room_ids = set(rng.sample(_ROOMS, rng.randint(0, 3))) + requested_lists = rng.choice( + [None, rng.sample(names + ["missing"], rng.randint(0, 3))] + ) + requested_room_ids = rng.choice( + [None, rng.sample([*_ROOMS[:4], "*"], rng.randint(0, 3))] + ) + + try: + python: list[str] | None = sorted( + SlidingSyncExtensionHandler.find_relevant_room_ids_for_extension( + cast(SlidingSyncExtensionHandler, None), + requested_lists, + requested_room_ids, + { + name: SlidingSyncResult.SlidingWindowList( + count=0, + ops=[ + SlidingSyncResult.SlidingWindowList.Operation( + op=OperationType.SYNC, + range=(0, 0), + room_ids=room_ids, + ) + for room_ids in ops + ], + ) + for name, ops in actual_lists + }, + actual_room_ids, + ) + ) + except AssertionError: + python = None + raised += 1 + try: + result: list[str] | None = rust.find_relevant_room_ids_for_test( + requested_lists, requested_room_ids, actual_lists, actual_room_ids + ) + except AssertionError: + result = None + self.assertEqual( + result, + python, + (requested_lists, requested_room_ids, actual_lists, actual_room_ids), + ) + self.assertGreater(raised, 0)