Skip to main content

tuwunel_api/client/sync/
v5.rs

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