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 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#[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 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 let needs_pass = config_changed || conn.own_profile_owed();
178
179 if config_change_needs_position(needs_pass, advancing, caught_up, since) {
180 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}