Files
synapse/rust/src/sliding_sync/result.rs
T
Erik JohnstonandClaude Opus 5.5 106d7fcb60 Compute the sliding sync room lists and own the connection state in Rust
In `rust_sliding_sync: on` mode, `current_sync_for_user` is now four Rust
calls around the Python extensions: load the connection state, compute
the interested rooms (the port of `_compute_interested_rooms_new_tables`),
compute the rooms, and finish the request (unsent and sent rooms, then
the persist).

The connection state is a Rust pyclass. The extensions read and update it
through views with the surface of Python's `PerConnectionState` and
`MutablePerConnectionState`. Receipts tokens stay in their database form
in Rust and become `MultiWriterStreamToken`s only when Python asks.

The whole request falls back to Python when the sliding sync tables are
not filled, for a filter Python does not implement either, and on any
Rust error. Rooms Rust leaves to Python run `get_room_sync_data` over a
Python copy of their slice of the state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L51DoxB8VnH8UjXJFDgpNG
2026-10-16 16:36:08 +00:00

256 lines
8.2 KiB
Rust

/*
* 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:
* <https://www.gnu.org/licenses/agpl-3.0.html>.
*/
//! What crosses to Python for the rooms of a request: [`RoomsResult`], and a
//! [`RoomResult`] per room Rust computed.
//!
//! A `RoomResult`'s events never become Python objects, except for module
//! callbacks: Python reads only what `current_sync_for_user` and the
//! extensions need, and the servlet encodes the room in Rust.
use std::collections::BTreeSet;
use std::sync::{Arc, Mutex};
use pyo3::{intern, prelude::*, types::PyList};
use crate::caches::lock_ignoring_poison;
use crate::events::Event;
use crate::sliding_sync::config::{RequiredStateMap, RoomSyncConfig, MEMBER};
use crate::sliding_sync::connection::{MutablePerConnectionState, RoomLazyMembershipChanges};
use crate::sliding_sync::room_entry::{RoomEntry, RoomOutcome};
use crate::sliding_sync::SlidingSyncStats;
use crate::sync::relations::Bundle;
/// A `required_state_map` from Python: a mapping of types to sets of
/// state keys.
pub fn required_state_map_from_py(map: &Bound<'_, PyAny>) -> PyResult<RequiredStateMap> {
let py = map.py();
let mut result = RequiredStateMap::new();
for item in map.call_method0(intern!(py, "items"))?.try_iter()? {
let (state_type, state_keys): (String, Bound<'_, PyAny>) = item?.extract()?;
let mut keys = BTreeSet::new();
for key in state_keys.try_iter()? {
keys.insert(key?.extract::<String>()?);
}
result.insert(state_type, keys);
}
Ok(result)
}
/// One room's entry, computed in Rust: `SlidingSyncResult.RoomResult` as
/// far as `current_sync_for_user` and the extensions read it.
#[pyclass(frozen, skip_from_py_object)]
pub struct RoomResult {
pub(crate) entry: Arc<RoomEntry>,
}
#[pymethods]
impl RoomResult {
fn __bool__(&self) -> bool {
self.entry.has_updates()
}
#[getter]
fn room_id(&self) -> &str {
&self.entry.room_id
}
#[getter]
fn initial(&self) -> bool {
self.entry.initial
}
#[getter]
fn unstable_expanded_timeline(&self) -> bool {
self.entry.unstable_expanded_timeline
}
#[getter]
fn timeline_event_ids(&self) -> Vec<&str> {
self.entry
.timeline
.iter()
.map(|filtered| filtered.event.event_id())
.collect()
}
#[getter]
fn timeline_senders(&self) -> Vec<&str> {
self.entry
.timeline
.iter()
.map(|filtered| filtered.event.sender())
.collect()
}
/// The state keys of the membership events in `required_state`.
#[getter]
fn required_state_member_keys(&self) -> Vec<&str> {
self.entry
.required_state
.iter()
.filter(|event| event.type_() == MEMBER)
.filter_map(Event::state_key)
.collect()
}
#[getter]
fn hero_user_ids(&self) -> Vec<&str> {
self.entry
.heroes
.iter()
.map(|hero| hero.user_id.as_str())
.collect()
}
/// Every event the room's encoding serializes, for the module
/// callbacks that add to their unsigned data.
fn events_to_serialize<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
let bundled = self
.entry
.bundles
.iter()
.flat_map(|bundles| bundles.values().flat_map(Bundle::events));
let events = self
.entry
.timeline
.iter()
.map(|filtered| &filtered.event)
.chain(&self.entry.required_state)
.chain(bundled)
.map(|event| Bound::new(py, event.shallow_clone()))
.collect::<PyResult<Vec<_>>>()?;
PyList::new(py, events)
}
}
/// What [`crate::sliding_sync::room_entry::generate_room_entries`] made of
/// the rooms.
#[pyclass(frozen, skip_from_py_object)]
pub struct RoomsResult {
/// The entries to send. Taken by [`RoomsResult::take_entries`].
entries: Mutex<Vec<Arc<RoomEntry>>>,
/// The rooms computed with nothing to send.
empty: Vec<Arc<str>>,
/// `room_configs[room_id]` for the computed rooms whose config changes.
room_config_updates: Vec<(Arc<str>, RoomSyncConfig)>,
/// `room_lazy_membership[room_id]` for every computed room.
lazy_memberships: Vec<(Arc<str>, RoomLazyMembershipChanges)>,
fallbacks: Vec<(Arc<str>, String)>,
errors: Vec<(Arc<str>, String)>,
stats: SlidingSyncStats,
}
impl RoomsResult {
/// The result of `outcomes`. An entry with nothing to send is dropped
/// from an incremental response.
pub fn new(
outcomes: Vec<(Arc<str>, RoomOutcome)>,
incremental: bool,
mut stats: SlidingSyncStats,
) -> Self {
let mut entries = Vec::new();
let mut empty = Vec::new();
let mut room_config_updates = Vec::new();
let mut lazy_memberships = Vec::new();
let mut fallbacks = Vec::new();
let mut errors = Vec::new();
for (room_id, outcome) in outcomes {
match outcome {
RoomOutcome::Computed(computed) => {
stats.rooms_computed += 1;
if computed.lazy_member_lookup {
stats.lazy_member_lookups += 1;
}
let mutations = computed.mutations;
if let Some(config) = mutations.room_config {
room_config_updates.push((Arc::clone(&room_id), config));
}
lazy_memberships.push((Arc::clone(&room_id), mutations.lazy_membership));
if computed.entry.has_updates() || !incremental {
entries.push(Arc::new(computed.entry));
} else {
empty.push(room_id);
}
}
RoomOutcome::Fallback(reason) => fallbacks.push((room_id, reason)),
RoomOutcome::Error(error) => errors.push((room_id, error)),
}
}
RoomsResult {
entries: Mutex::new(entries),
empty,
room_config_updates,
lazy_memberships,
fallbacks,
errors,
stats,
}
}
/// Record the computed rooms' changes in the connection state.
pub fn apply_to<R: Clone>(&self, state: &mut MutablePerConnectionState<R>) {
for (room_id, config) in &self.room_config_updates {
state.set_room_config(room_id, config.clone());
}
for (room_id, changes) in &self.lazy_memberships {
state
.room_lazy_membership
.insert(room_id.to_string(), changes.clone());
}
}
}
#[pymethods]
impl RoomsResult {
/// The entries to send as `RoomResult`s. A second call returns an
/// empty list.
fn take_entries<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
let entries = std::mem::take(&mut *lock_ignoring_poison(&self.entries));
let items = entries
.into_iter()
.map(|entry| Bound::new(py, RoomResult { entry }))
.collect::<PyResult<Vec<_>>>()?;
PyList::new(py, items)
}
#[getter]
fn empty(&self) -> Vec<&str> {
self.empty.iter().map(|room_id| &**room_id).collect()
}
/// `(room_id, reason)` for each room Python must compute.
#[getter]
fn fallbacks(&self) -> Vec<(&str, &str)> {
self.fallbacks
.iter()
.map(|(room_id, reason)| (&**room_id, reason.as_str()))
.collect()
}
/// `(room_id, error)` for each room Rust failed on.
#[getter]
fn errors(&self) -> Vec<(&str, &str)> {
self.errors
.iter()
.map(|(room_id, error)| (&**room_id, error.as_str()))
.collect()
}
#[getter]
fn stats(&self) -> SlidingSyncStats {
self.stats
}
}