Skip to main content

tuwunel_api/client/sync/
v5.rs

1mod extensions;
2mod filter;
3mod range;
4mod rooms;
5mod selector;
6
7use std::{
8	collections::{BTreeMap, BTreeSet},
9	fmt::Debug,
10	sync::Arc,
11	time::Duration,
12};
13
14use axum::extract::{Extension, State};
15use futures::{FutureExt, TryFutureExt, future::join};
16use ruma::{
17	DeviceId, OwnedRoomId, UserId,
18	api::client::sync::sync_events::v5::{ListId, Request, Response, response},
19	events::room::member::MembershipState,
20};
21use tokio::{
22	sync::Notify,
23	time::{Instant, timeout_at},
24};
25use tuwunel_core::{
26	Err, Result, debug,
27	debug::INFO_SPAN_LEVEL,
28	debug_warn,
29	error::inspect_log,
30	smallvec::SmallVec,
31	trace,
32	utils::{TryFutureExtExt, result::FlatOk},
33};
34use tuwunel_service::{
35	Services,
36	presence::Ping,
37	sync::{Connection, into_connection_key},
38};
39
40use self::{
41	extensions::{apply_profiles, apply_ranges, handle as handle_extensions},
42	range::collect as collect_ranges,
43};
44use super::share_encrypted_room;
45use crate::{ClientIp, Ruma};
46
47#[derive(Copy, Clone)]
48struct SyncInfo<'a> {
49	services: &'a Services,
50	sender_user: &'a UserId,
51	sender_device: Option<&'a DeviceId>,
52	previous_connection_pos: Option<u64>,
53
54	/// The rooms the user calls direct, read once for the whole pass.
55	///
56	/// Both the list filters and every room payload answer `is_dm` from this.
57	direct_rooms: &'a BTreeSet<OwnedRoomId>,
58}
59
60#[derive(Clone, Debug)]
61struct WindowRoom {
62	room_id: OwnedRoomId,
63	membership: Option<MembershipState>,
64	lists: ListIds,
65	event_count: u64,
66	payload_count: u64,
67}
68
69impl WindowRoom {
70	#[inline]
71	fn payload_is_fresh(&self, roomsince: u64) -> bool {
72		roomsince == 0 || self.payload_count > roomsince
73	}
74}
75
76type Window = BTreeMap<OwnedRoomId, WindowRoom>;
77type ResponseLists = BTreeMap<ListId, response::List>;
78type ListIds = SmallVec<[ListId; 1]>;
79
80/// `POST /_matrix/client/unstable/org.matrix.simplified_msc3575/sync`
81/// ([MSC4186])
82///
83/// A simplified version of sliding sync ([MSC3575]).
84///
85/// Get all new events in a sliding window of rooms since the last sync or a
86/// given point in time.
87///
88/// [MSC3575]: https://github.com/matrix-org/matrix-spec-proposals/pull/3575
89/// [MSC4186]: https://github.com/matrix-org/matrix-spec-proposals/pull/4186
90#[tracing::instrument(
91	name = "sync",
92	level = INFO_SPAN_LEVEL,
93	skip_all,
94	fields(
95		user_id = %body.sender_user().localpart(),
96		device_id = %body.sender_device.as_deref().map_or("<no device>", |x| x.as_str()),
97		conn_id = ?body.body.conn_id.clone().unwrap_or_default(),
98		since = ?body.body.pos.clone().unwrap_or_default(),
99	)
100)]
101pub(crate) async fn sync_events_v5_route(
102	Extension(interrupted): Extension<Arc<Notify>>,
103	ClientIp(client): ClientIp,
104	State(ref services): State<crate::State>,
105	body: Ruma<Request>,
106) -> Result<Response> {
107	let sender_user = body.sender_user();
108	let sender_device = body.sender_device.as_deref();
109	let request = &body.body;
110	let since = request
111		.pos
112		.as_ref()
113		.and_then(|string| string.parse().ok())
114		.unwrap_or(0);
115
116	let timeout = request
117		.timeout
118		.as_ref()
119		.map(Duration::as_millis)
120		.map(TryInto::try_into)
121		.flat_ok()
122		.map(|timeout: u64| timeout.min(services.config.client_sync_timeout_max))
123		.unwrap_or(0);
124
125	let conn_key = into_connection_key(sender_user, sender_device, request.conn_id.as_deref());
126	let conn_val = services
127		.sync
128		.load_or_init_connection(&conn_key)
129		.await;
130
131	let conn = conn_val.lock();
132	let ping = Ping {
133		device_id: sender_device,
134		client_ip: Some(client),
135		new_state: Some(&request.set_presence),
136		appservice: body.appservice_info.as_ref(),
137	};
138
139	let ping_presence = services
140		.presence
141		.maybe_ping_presence(sender_user, ping)
142		.inspect_err(inspect_log)
143		.ok();
144
145	let (mut conn, _) = join(conn, ping_presence).await;
146
147	if since != 0 && conn.next_batch == 0 {
148		return Err!(Request(UnknownPos(warn!("Connection lost; restarting sync stream."))));
149	}
150
151	if since == 0 {
152		*conn = Connection::default();
153		conn.store(&services.sync, &conn_key);
154		debug_warn!(?conn_key, "Client cleared cache and reloaded.");
155	}
156
157	let advancing = since == conn.next_batch;
158	let retarding = since != 0 && since <= conn.globalsince;
159	if !advancing && !retarding {
160		return Err!(Request(UnknownPos(warn!(
161			"Requesting unknown or invalid stream position."
162		))));
163	}
164
165	debug_assert!(
166		advancing || retarding,
167		"Request should either be advancing or replaying the since token."
168	);
169
170	// Update parameters regardless of replay or advance
171	conn.next_batch = services.globals.wait_pending().await?;
172	conn.globalsince = since.min(conn.next_batch);
173	let config_changed = conn.update_cache(request);
174	let caught_up = conn.globalsince == conn.next_batch;
175
176	// A whole profile owed to a caught-up connection needs a pass like a new list.
177	let needs_pass = config_changed || conn.own_profile_owed();
178
179	if config_change_needs_position(needs_pass, advancing, caught_up, since) {
180		// The permit publishes the reserved position when it retires on drop.
181		drop(services.globals.next_count());
182	}
183
184	conn.update_rooms_prologue(retarding.then_some(since));
185
186	let mut response = Response {
187		txn_id: request.txn_id.clone(),
188		lists: Default::default(),
189		pos: Default::default(),
190		rooms: Default::default(),
191		extensions: Default::default(),
192	};
193
194	let stop_at = Instant::now()
195		.checked_add(Duration::from_millis(timeout))
196		.expect("configuration must limit maximum timeout");
197
198	loop {
199		debug_assert!(
200			conn.globalsince <= conn.next_batch,
201			"since should not be greater than next_batch."
202		);
203
204		let window;
205		let watchers = services
206			.sync
207			.watch(sender_user, sender_device, services.state_cache.rooms_joined(sender_user))
208			.await;
209
210		conn.next_batch = services.globals.wait_pending().await?;
211		let direct_rooms = services
212			.account_data
213			.direct_rooms(sender_user)
214			.await;
215
216		let sync_info = SyncInfo {
217			services,
218			sender_user,
219			sender_device,
220			previous_connection_pos: since.ne(&0).then_some(since),
221			direct_rooms: &direct_rooms,
222		};
223
224		(window, response.lists) = selector::selector(&mut conn, sync_info)
225			.boxed()
226			.await;
227
228		if conn.globalsince < conn.next_batch {
229			let ranges = collect_ranges(sync_info, &conn, &window);
230			let extensions = handle_extensions(sync_info, &conn, &window);
231			let (mut ranges, extensions) = join(ranges, extensions).boxed().await;
232
233			let mut extensions = extensions?;
234
235			apply_profiles(sync_info, &conn, &window, &ranges, &mut extensions).await?;
236			apply_ranges(&conn, &window, &mut ranges, &mut extensions);
237			conn.update_rooms_epilogue(ranges.room_updates());
238			conn.update_profiles_epilogue();
239			response.rooms = ranges.into_payloads();
240			response.extensions = extensions.into_response(&response.rooms);
241
242			if !is_empty_response(&response) {
243				response.pos = conn.next_batch.to_string().into();
244				trace!(conn.globalsince, conn.next_batch, "response {response:?}");
245				conn.store(&services.sync, &conn_key);
246				return Ok(response);
247			}
248		}
249
250		let waiter = async || {
251			tokio::select! {
252				() = interrupted.notified() => true,
253				watch = timeout_at(stop_at, watchers) => watch.is_err(),
254			}
255		};
256
257		if timeout == 0 || services.server.is_stopping() || waiter().boxed().await {
258			response.pos = conn.next_batch.to_string().into();
259			trace!(conn.globalsince, conn.next_batch, "empty response {response:?}");
260			conn.store(&services.sync, &conn_key);
261			return Ok(response);
262		}
263
264		debug!(
265			?timeout,
266			last_since = conn.globalsince,
267			last_batch = conn.next_batch,
268			pend_count = ?services.globals.pending_count(),
269			"notified by watcher"
270		);
271
272		conn.globalsince = conn.next_batch;
273	}
274}
275
276fn config_change_needs_position(
277	config_changed: bool,
278	advancing: bool,
279	caught_up: bool,
280	since: u64,
281) -> bool {
282	config_changed && advancing && caught_up && since != 0
283}
284
285fn is_empty_response(response: &Response) -> bool {
286	response.extensions.is_empty() && response.rooms.is_empty()
287}
288
289#[cfg(test)]
290mod tests {
291	use super::config_change_needs_position;
292
293	#[test]
294	fn caught_up_advancing_config_change_needs_position() {
295		assert!(config_change_needs_position(true, true, true, 1));
296		assert!(!config_change_needs_position(false, true, true, 1));
297		assert!(!config_change_needs_position(true, false, true, 1));
298		assert!(!config_change_needs_position(true, true, false, 1));
299		assert!(!config_change_needs_position(true, true, true, 0));
300	}
301}