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#[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 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}