use std::{ collections::{BTreeMap, BTreeSet, HashMap}, sync::Arc, }; use conduwuit::{Result, SyncMutex, trace}; use futures::StreamExt; use ruma::{ OwnedDeviceId, OwnedRoomId, OwnedUserId, RoomId, UserId, api::client::sync::sync_events::v5, }; use tokio::sync::{Mutex, watch}; use crate::{Dep, rooms}; pub struct Service { services: Services, wakers: Wakers, snake_connections: DbConnections, } struct Services { state_cache: Dep, } #[allow(unused, reason = "TODO refactor")] struct SlidingSyncCache { lists: BTreeMap, subscriptions: BTreeMap, // For every room, the roomsince number known_rooms: BTreeMap>, extensions: v5::request::Extensions, } #[derive(Default)] struct SnakeSyncCache { lists: BTreeMap, subscriptions: BTreeMap, known_rooms: BTreeMap>, acknowledged_pos: u64, pending: BTreeMap>>, extensions: v5::request::Extensions, } impl SnakeSyncCache { fn accepts_pos(&self, pos: u64) -> bool { self.acknowledged_pos == pos || self.pending.contains_key(&pos) } fn acknowledge(&mut self, pos: u64) { if let Some(known_rooms) = self.pending.remove(&pos) { self.known_rooms = known_rooms; self.acknowledged_pos = pos; } } } type DbConnections = SyncMutex>; type DbConnectionsKey = (OwnedUserId, OwnedDeviceId, String); pub type SnakeConnectionsKey = (OwnedUserId, OwnedDeviceId, Option); type SnakeConnectionsVal = Arc>; #[derive(Default)] struct Wakers(Mutex>>); impl Wakers { async fn subscribe(&self, user: &UserId) -> watch::Receiver { self.0 .lock() .await .entry(user.to_owned()) .or_insert_with(|| watch::channel(0).0) .subscribe() } async fn wake(&self, user: &UserId) { let mut wakers = self.0.lock().await; wake_waker(&mut wakers, user); } async fn wake_all(&self, users: I) where I: IntoIterator + Send, { let mut wakers = self.0.lock().await; for user in users { wake_waker(&mut wakers, &user); } } } /// Bumps the generation observed by anyone long-polling for this user, reaping /// the entry once its last subscriber is gone. fn wake_waker(wakers: &mut HashMap>, user: &UserId) { let Some(waker) = wakers.get(user) else { return; }; if waker.receiver_count() == 0 { wakers.remove(user); return; } waker.send_modify(|generation| *generation = generation.wrapping_add(1)); } impl crate::Service for Service { fn build(args: crate::Args<'_>) -> Result> { Ok(Arc::new(Self { services: Services { state_cache: args.depend::("rooms::state_cache"), }, wakers: Wakers::default(), snake_connections: SyncMutex::new(BTreeMap::new()), })) } fn name(&self) -> &str { crate::service::make_name(std::module_path!()) } } impl Service { pub async fn subscribe_to_wake(&self, user: &UserId) -> watch::Receiver { self.wakers.subscribe(user).await } /// Wake the target user's sync loop. Call this when something /// that gets included in a legacy sync response changes. /// /// Be careful where you call this function! In particular, don't call /// it in any function that's called by `append_pdu`. `append_pdu` will call /// it _after_ it's done appending a PDU, and calling it earlier can cause /// hard-to-diagnose race conditions. pub async fn wake(&self, user: &UserId) { trace!(?user, "Waking user's sync loops"); self.wakers.wake(user).await; } /// Wake all of our users who are joined to the specified room. pub async fn wake_all_joined(&self, room: &RoomId) { trace!(?room, "Waking all joined users' sync loops"); let users: Vec<_> = self .services .state_cache .active_local_users_in_room(room) .collect() .await; self.wakers.wake_all(users).await; } pub fn snake_connection_cached(&self, key: &SnakeConnectionsKey) -> bool { self.snake_connections.lock().contains_key(key) } pub fn snake_connection_accepts_pos(&self, key: &SnakeConnectionsKey, pos: u64) -> bool { self.snake_connections .lock() .get(key) .is_some_and(|cache| cache.lock().accepts_pos(pos)) } pub fn forget_snake_sync_connection(&self, key: &SnakeConnectionsKey) { self.snake_connections.lock().remove(key); } pub fn update_snake_sync_request_with_cache( &self, snake_key: &SnakeConnectionsKey, pos: u64, request: &mut v5::Request, ) -> BTreeMap> { let mut cache = self.snake_connections.lock(); let cached = Arc::clone( cache .entry(snake_key.clone()) .or_insert_with(|| Arc::new(SyncMutex::new(SnakeSyncCache::default()))), ); let cached = &mut cached.lock(); drop(cache); cached.acknowledge(pos); //v5::Request::try_from_http_request(req, path_args); for (list_id, list) in &mut request.lists { if let Some(cached_list) = cached.lists.get(list_id) { list_or_sticky( &mut list.room_details.required_state, &cached_list.room_details.required_state, ); match (&mut list.filters, cached_list.filters.clone()) { | (Some(filters), Some(cached_filters)) => { some_or_sticky(&mut filters.is_invite, cached_filters.is_invite); // TODO (morguldir): Find out how a client can unset this, probably need // to change into an option inside ruma list_or_sticky( &mut filters.not_room_types, &cached_filters.not_room_types, ); }, | (_, Some(cached_filters)) => list.filters = Some(cached_filters), | (Some(list_filters), _) => list.filters = Some(list_filters.clone()), | (..) => {}, } } cached.lists.insert(list_id.clone(), list.clone()); } cached .subscriptions .extend(request.room_subscriptions.clone()); request .room_subscriptions .extend(cached.subscriptions.clone()); request.extensions.e2ee.enabled = request .extensions .e2ee .enabled .or(cached.extensions.e2ee.enabled); request.extensions.to_device.enabled = request .extensions .to_device .enabled .or(cached.extensions.to_device.enabled); request.extensions.account_data.enabled = request .extensions .account_data .enabled .or(cached.extensions.account_data.enabled); request.extensions.account_data.lists = request .extensions .account_data .lists .clone() .or_else(|| cached.extensions.account_data.lists.clone()); request.extensions.account_data.rooms = request .extensions .account_data .rooms .clone() .or_else(|| cached.extensions.account_data.rooms.clone()); some_or_sticky(&mut request.extensions.typing.enabled, cached.extensions.typing.enabled); some_or_sticky( &mut request.extensions.typing.rooms, cached.extensions.typing.rooms.clone(), ); some_or_sticky( &mut request.extensions.typing.lists, cached.extensions.typing.lists.clone(), ); some_or_sticky( &mut request.extensions.receipts.enabled, cached.extensions.receipts.enabled, ); some_or_sticky( &mut request.extensions.receipts.rooms, cached.extensions.receipts.rooms.clone(), ); some_or_sticky( &mut request.extensions.receipts.lists, cached.extensions.receipts.lists.clone(), ); cached.extensions = request.extensions.clone(); cached.known_rooms.clone() } pub fn store_snake_sync_pending( &self, key: &SnakeConnectionsKey, pos: u64, known_rooms: BTreeMap>, ) { let mut cache = self.snake_connections.lock(); let cached = Arc::clone( cache .entry(key.clone()) .or_insert_with(|| Arc::new(SyncMutex::new(SnakeSyncCache::default()))), ); let cached = &mut cached.lock(); drop(cache); let mut pending_rooms = cached.known_rooms.clone(); for (list_id, rooms) in known_rooms { let list = pending_rooms.entry(list_id).or_default(); for last_seen in list.values_mut() { *last_seen = 0; } for room_id in rooms { list.insert(room_id, pos); } } cached.pending.insert(pos, pending_rooms); } pub fn update_snake_sync_subscriptions( &self, key: &SnakeConnectionsKey, subscriptions: BTreeMap, ) { let mut cache = self.snake_connections.lock(); let cached = Arc::clone( cache .entry(key.clone()) .or_insert_with(|| Arc::new(SyncMutex::new(SnakeSyncCache::default()))), ); let cached = &mut cached.lock(); drop(cache); cached.subscriptions = subscriptions; } } #[inline] pub fn into_snake_key(user_id: U, device_id: D, conn_id: C) -> SnakeConnectionsKey where U: Into, D: Into, C: Into>, { (user_id.into(), device_id.into(), conn_id.into()) } #[inline] pub fn into_db_key(user_id: U, device_id: D, conn_id: C) -> DbConnectionsKey where U: Into, D: Into, C: Into, { (user_id.into(), device_id.into(), conn_id.into()) } /// load params from cache if body doesn't contain it, as long as it's allowed /// in some cases we may need to allow an empty list as an actual value fn list_or_sticky(target: &mut Vec, cached: &Vec) { if target.is_empty() { target.clone_from(cached); } } fn some_or_sticky(target: &mut Option, cached: Option) { if target.is_none() { *target = cached; } } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn wake_after_subscription_is_observable() { let wakers = Wakers::default(); let user = UserId::parse("@alice:example.com").unwrap(); let mut receiver = wakers.subscribe(&user).await; let generation = *receiver.borrow_and_update(); wakers.wake(&user).await; wakers.wake(&user).await; receiver.changed().await.unwrap(); assert_eq!(*receiver.borrow_and_update(), generation.wrapping_add(2)); } #[tokio::test] async fn wake_notifies_all_subscribers() { let wakers = Wakers::default(); let user = UserId::parse("@alice:example.com").unwrap(); let mut first = wakers.subscribe(&user).await; let mut second = wakers.subscribe(&user).await; wakers.wake(&user).await; first.changed().await.unwrap(); second.changed().await.unwrap(); } #[tokio::test] async fn waker_is_not_retained_without_subscribers() { let wakers = Wakers::default(); let user = UserId::parse("@alice:example.com").unwrap(); wakers.wake(&user).await; assert!(wakers.0.lock().await.is_empty()); drop(wakers.subscribe(&user).await); assert_eq!(wakers.0.lock().await.len(), 1); wakers.wake(&user).await; assert!(wakers.0.lock().await.is_empty()); } #[tokio::test] async fn subscriber_still_woken_after_a_sibling_disconnects() { let wakers = Wakers::default(); let user = UserId::parse("@alice:example.com").unwrap(); let mut receiver = wakers.subscribe(&user).await; drop(wakers.subscribe(&user).await); wakers.wake(&user).await; receiver.changed().await.unwrap(); } #[test] fn pending_rooms_are_committed_only_when_the_response_pos_is_acknowledged() { let room_id = OwnedRoomId::try_from("!room:example.com").unwrap(); let mut cache = SnakeSyncCache { pending: BTreeMap::from([( 42, BTreeMap::from([("list".to_owned(), BTreeMap::from([(room_id, 42)]))]), )]), ..Default::default() }; assert!(cache.known_rooms.is_empty()); assert!(cache.accepts_pos(0)); assert!(cache.accepts_pos(42)); assert!(!cache.accepts_pos(41)); cache.acknowledge(0); assert!(cache.known_rooms.is_empty()); cache.acknowledge(42); assert_eq!(cache.acknowledged_pos, 42); assert_eq!(cache.known_rooms["list"].len(), 1); } #[test] fn overlapping_responses_accept_either_response_position() { let mut cache = SnakeSyncCache { acknowledged_pos: 100, pending: BTreeMap::from([(200, BTreeMap::new()), (201, BTreeMap::new())]), ..Default::default() }; assert!(cache.accepts_pos(200)); assert!(cache.accepts_pos(201)); cache.acknowledge(200); assert!(cache.accepts_pos(201)); } }