use crate::sql::connection_status::UserStatus; use std::collections::{HashMap, HashSet}; use std::sync::RwLock; fn select_primary_route(previous_primary: Option, connected: &HashSet) -> Option { previous_primary .filter(|id| connected.contains(id)) .or_else(|| connected.iter().min().copied()) } fn sorted_vec(values: HashSet) -> Vec { let mut values = values.into_iter().collect::>(); values.sort_unstable(); values } fn sorted_pairs(values: HashSet<(i64, i64)>) -> Vec<(i64, i64)> { let mut values = values.into_iter().collect::>(); values.sort_unstable(); values } #[derive(Clone, Debug, PartialEq, Eq)] pub struct ClientSessionRoute { pub omikron_id: i64, pub iota_id: i64, } #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct PresenceSubscriber { pub user_id: i64, pub session_id: i64, pub omikron_id: i64, } #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct PresenceIndices { pub sessions_by_user: HashMap>, pub sessions_by_omikron: HashMap>, pub targets_by_subscriber: HashMap<(i64, i64), HashSet>, pub subscribers_by_omikron: HashMap>, pub iotas_by_omikron: HashMap>, } #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct IotaRoute { pub primary_omikron_id: Option, pub connected_omikron_ids: HashSet, } #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct PresenceRoutes { pub iotas: HashMap, pub sessions: HashMap<(i64, i64), ClientSessionRoute>, pub subscriptions: HashMap>, pub indices: PresenceIndices, } #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct RemovedOmikronState { pub iota_ids: Vec, pub session_user_ids: Vec, pub subscriber_sessions: Vec<(i64, i64)>, } /// All live presence state is kept together so routing cannot be overwritten /// by a preference update. Preferences are account-scoped, routes are /// session-scoped, and effective state is always derived. pub struct PresenceTracker { pub(crate) routes: RwLock, preferences: RwLock>, } impl Default for PresenceTracker { fn default() -> Self { Self { routes: RwLock::new(PresenceRoutes::default()), preferences: RwLock::new(HashMap::new()), } } } impl PresenceTracker { fn remove_subscription_locked(routes: &mut PresenceRoutes, key: (i64, i64)) { let Some(targets) = routes.indices.targets_by_subscriber.remove(&key) else { return; }; let mut omikron_ids = HashSet::new(); for target_user_id in targets { if let Some(subscribers) = routes.subscriptions.get_mut(&target_user_id) { subscribers.retain(|subscriber| { if (subscriber.user_id, subscriber.session_id) == key { omikron_ids.insert(subscriber.omikron_id); false } else { true } }); if subscribers.is_empty() { routes.subscriptions.remove(&target_user_id); } } } for omikron_id in omikron_ids { if let Some(sessions) = routes.indices.subscribers_by_omikron.get_mut(&omikron_id) { sessions.remove(&key); if sessions.is_empty() { routes.indices.subscribers_by_omikron.remove(&omikron_id); } } } } fn remove_session_locked( routes: &mut PresenceRoutes, key: (i64, i64), expected_omikron_id: Option, ) -> bool { let Some(route) = routes.sessions.get(&key) else { return false; }; if expected_omikron_id.is_some_and(|id| route.omikron_id != id) { return false; } let route = routes.sessions.remove(&key).unwrap(); if let Some(session_ids) = routes.indices.sessions_by_user.get_mut(&key.0) { session_ids.remove(&key.1); if session_ids.is_empty() { routes.indices.sessions_by_user.remove(&key.0); } } if let Some(session_keys) = routes .indices .sessions_by_omikron .get_mut(&route.omikron_id) { session_keys.remove(&key); if session_keys.is_empty() { routes.indices.sessions_by_omikron.remove(&route.omikron_id); } } Self::remove_subscription_locked(routes, key); true } fn insert_session_locked( routes: &mut PresenceRoutes, key: (i64, i64), route: ClientSessionRoute, ) { Self::remove_session_locked(routes, key, None); routes.sessions.insert(key, route.clone()); routes .indices .sessions_by_user .entry(key.0) .or_default() .insert(key.1); routes .indices .sessions_by_omikron .entry(route.omikron_id) .or_default() .insert(key); } fn remove_iota_owner_locked( routes: &mut PresenceRoutes, iota_id: i64, omikron_id: i64, ) -> bool { let Some(route) = routes.iotas.get_mut(&iota_id) else { return false; }; route.connected_omikron_ids.remove(&omikron_id); route.primary_omikron_id = select_primary_route(route.primary_omikron_id, &route.connected_omikron_ids); let empty = route.connected_omikron_ids.is_empty(); if let Some(iotas) = routes.indices.iotas_by_omikron.get_mut(&omikron_id) { iotas.remove(&iota_id); if iotas.is_empty() { routes.indices.iotas_by_omikron.remove(&omikron_id); } } if empty { routes.iotas.remove(&iota_id); } empty } #[cfg(debug_assertions)] fn debug_assert_indices(routes: &PresenceRoutes) { debug_assert!(Self::check_indices(routes).is_ok()); } #[cfg(not(debug_assertions))] fn debug_assert_indices(_: &PresenceRoutes) {} fn check_indices(routes: &PresenceRoutes) -> Result<(), String> { for (key, route) in &routes.sessions { if !routes .indices .sessions_by_user .get(&key.0) .is_some_and(|sessions| sessions.contains(&key.1)) { return Err(format!("session {key:?} missing user index")); } if !routes .indices .sessions_by_omikron .get(&route.omikron_id) .is_some_and(|sessions| sessions.contains(key)) { return Err(format!("session {key:?} missing Omikron index")); } } for (user_id, session_ids) in &routes.indices.sessions_by_user { for session_id in session_ids { if !routes.sessions.contains_key(&(*user_id, *session_id)) { return Err(format!("stale user session index {user_id}:{session_id}")); } } } for (omikron_id, session_keys) in &routes.indices.sessions_by_omikron { for key in session_keys { if routes.sessions.get(key).map(|route| route.omikron_id) != Some(*omikron_id) { return Err(format!("stale Omikron session index {omikron_id}:{key:?}")); } } } for (target_user_id, subscribers) in &routes.subscriptions { for subscriber in subscribers { let key = (subscriber.user_id, subscriber.session_id); if !routes .indices .targets_by_subscriber .get(&key) .is_some_and(|targets| targets.contains(target_user_id)) { return Err(format!("subscription {key:?} missing target index")); } if !routes .indices .subscribers_by_omikron .get(&subscriber.omikron_id) .is_some_and(|sessions| sessions.contains(&key)) { return Err(format!("subscription {key:?} missing Omikron index")); } } } for (key, targets) in &routes.indices.targets_by_subscriber { for target in targets { if !routes.subscriptions.get(target).is_some_and(|subscribers| { subscribers .iter() .any(|subscriber| (subscriber.user_id, subscriber.session_id) == *key) }) { return Err(format!("stale target index {key:?}:{target}")); } } } for (omikron_id, iota_ids) in &routes.indices.iotas_by_omikron { for iota_id in iota_ids { if !routes .iotas .get(iota_id) .is_some_and(|route| route.connected_omikron_ids.contains(omikron_id)) { return Err(format!("stale Iota index {omikron_id}:{iota_id}")); } } } Ok(()) } pub fn set_preference(&self, user_id: i64, status: UserStatus) { self.preferences.write().unwrap().insert(user_id, status); } pub fn set_preferences(&self, preferences: HashMap) { self.preferences.write().unwrap().extend(preferences); } pub fn load_preference(&self, user_id: i64, raw: &str) -> bool { let Some(status) = UserStatus::from_client_preference(raw) else { self.set_preference(user_id, UserStatus::user_online); return false; }; self.set_preference(user_id, status); true } pub fn preference(&self, user_id: i64) -> UserStatus { self.preferences .read() .unwrap() .get(&user_id) .cloned() .unwrap_or(UserStatus::user_online) } pub fn has_iota_route(&self, iota_id: i64) -> bool { self.routes .read() .unwrap() .iotas .get(&iota_id) .is_some_and(|route| !route.connected_omikron_ids.is_empty()) } pub fn has_active_session_for_iota(&self, user_id: i64, iota_id: i64) -> bool { let routes = self.routes.read().unwrap(); routes .indices .sessions_by_user .get(&user_id) .into_iter() .flat_map(|session_ids| session_ids.iter()) .any(|session_id| { routes .sessions .get(&(user_id, *session_id)) .is_some_and(|route| { route.iota_id == iota_id && routes.iotas.get(&route.iota_id).is_some_and(|iota| { iota.connected_omikron_ids.contains(&route.omikron_id) }) }) }) } pub fn track_session(&self, user_id: i64, session_id: i64, omikron_id: i64, iota_id: i64) { let mut routes = self.routes.write().unwrap(); Self::insert_session_locked( &mut routes, (user_id, session_id), ClientSessionRoute { omikron_id, iota_id, }, ); Self::debug_assert_indices(&routes); } pub fn remove_session(&self, user_id: i64, session_id: i64, omikron_id: i64) -> bool { let mut routes = self.routes.write().unwrap(); let removed = Self::remove_session_locked(&mut routes, (user_id, session_id), Some(omikron_id)); Self::debug_assert_indices(&routes); removed } pub fn owns_session(&self, user_id: i64, session_id: i64, omikron_id: i64) -> bool { self.routes .read() .unwrap() .sessions .get(&(user_id, session_id)) .is_some_and(|route| route.omikron_id == omikron_id) } pub fn resolve_public_state(&self, user_id: i64, iota_id: i64) -> UserStatus { if !self.has_iota_route(iota_id) { return UserStatus::iota_offline; } if !self.has_active_session_for_iota(user_id, iota_id) { return UserStatus::user_offline; } self.preference(user_id).public_value() } pub fn resolve_private_state(&self, user_id: i64) -> UserStatus { self.preference(user_id) } pub fn remove_omikron(&self, omikron_id: i64) -> RemovedOmikronState { let mut routes = self.routes.write().unwrap(); let iota_ids = routes .indices .iotas_by_omikron .remove(&omikron_id) .unwrap_or_default(); for iota_id in &iota_ids { Self::remove_iota_owner_locked(&mut routes, *iota_id, omikron_id); } let subscriber_sessions = routes .indices .subscribers_by_omikron .remove(&omikron_id) .unwrap_or_default(); let session_keys = routes .indices .sessions_by_omikron .remove(&omikron_id) .unwrap_or_default(); let session_user_ids = session_keys .iter() .map(|(user_id, _)| *user_id) .collect::>(); for key in &session_keys { Self::remove_session_locked(&mut routes, *key, Some(omikron_id)); } for key in &subscriber_sessions { Self::remove_subscription_locked(&mut routes, *key); } Self::debug_assert_indices(&routes); RemovedOmikronState { iota_ids: sorted_vec(iota_ids), session_user_ids: sorted_vec(session_user_ids), subscriber_sessions: sorted_pairs(subscriber_sessions), } } pub fn remove_sessions_for_iota(&self, iota_id: i64, omikron_id: i64) { let mut routes = self.routes.write().unwrap(); let keys = routes .indices .sessions_by_omikron .get(&omikron_id) .into_iter() .flat_map(|keys| keys.iter()) .filter(|key| { routes .sessions .get(key) .is_some_and(|route| route.iota_id == iota_id) }) .copied() .collect::>(); for key in &keys { Self::remove_session_locked(&mut routes, *key, Some(omikron_id)); } Self::debug_assert_indices(&routes); } pub fn replace_subscription( &self, user_id: i64, session_id: i64, omikron_id: i64, user_ids: Vec, ) { let mut routes = self.routes.write().unwrap(); let key = (user_id, session_id); Self::remove_subscription_locked(&mut routes, key); for target_user_id in user_ids { let subscriber = PresenceSubscriber { user_id, session_id, omikron_id, }; routes .subscriptions .entry(target_user_id) .or_default() .insert(subscriber.clone()); routes .indices .targets_by_subscriber .entry(key) .or_default() .insert(target_user_id); routes .indices .subscribers_by_omikron .entry(omikron_id) .or_default() .insert(key); } Self::debug_assert_indices(&routes); } pub fn subscribers(&self, target_user_id: i64) -> Vec { let mut subscribers = self .routes .read() .unwrap() .subscriptions .get(&target_user_id) .cloned() .unwrap_or_default() .into_iter() .collect::>(); subscribers.sort_by_key(|subscriber| { ( subscriber.omikron_id, subscriber.user_id, subscriber.session_id, ) }); subscribers } pub fn sessions_owned_by(&self, omikron_id: i64) -> Vec<(i64, i64, ClientSessionRoute)> { let routes = self.routes.read().unwrap(); let mut sessions = routes .indices .sessions_by_omikron .get(&omikron_id) .into_iter() .flat_map(|keys| keys.iter()) .filter_map(|key| { routes .sessions .get(key) .map(|route| (key.0, key.1, route.clone())) }) .collect::>(); sessions.sort_by_key(|(user_id, session_id, _)| (*user_id, *session_id)); sessions } pub fn replace_omikron_snapshot( &self, omikron_id: i64, iota_ids: &[i64], sessions: &[(i64, i64, i64)], ) { let mut routes = self.routes.write().unwrap(); let old_sessions = routes .indices .sessions_by_omikron .get(&omikron_id) .cloned() .unwrap_or_default(); let old_subscriber_sessions = routes .indices .subscribers_by_omikron .remove(&omikron_id) .unwrap_or_default(); for key in &old_subscriber_sessions { Self::remove_subscription_locked(&mut routes, *key); } for key in &old_sessions { Self::remove_session_locked(&mut routes, *key, Some(omikron_id)); } let old_iota_ids = routes .indices .iotas_by_omikron .get(&omikron_id) .cloned() .unwrap_or_default(); for iota_id in old_iota_ids { Self::remove_iota_owner_locked(&mut routes, iota_id, omikron_id); } for iota_id in iota_ids { let route = routes.iotas.entry(*iota_id).or_default(); route.connected_omikron_ids.insert(omikron_id); route.primary_omikron_id = select_primary_route(route.primary_omikron_id, &route.connected_omikron_ids); routes .indices .iotas_by_omikron .entry(omikron_id) .or_default() .insert(*iota_id); } for (user_id, session_id, iota_id) in sessions { Self::insert_session_locked( &mut routes, (*user_id, *session_id), ClientSessionRoute { omikron_id, iota_id: *iota_id, }, ); } Self::debug_assert_indices(&routes); } pub fn iota_ids_owned_by(&self, omikron_id: i64) -> Vec { let routes = self.routes.read().unwrap(); sorted_vec( routes .indices .iotas_by_omikron .get(&omikron_id) .cloned() .unwrap_or_default(), ) } pub fn untrack_iota_connection(&self, iota_id: i64, omikron_id: i64) -> bool { let connections_empty = self.disconnect_iota(iota_id, omikron_id); self.remove_sessions_for_iota(iota_id, omikron_id); connections_empty } pub fn primary_iota_route(&self, iota_id: i64) -> Option { self.routes .read() .unwrap() .iotas .get(&iota_id) .and_then(|route| route.primary_omikron_id) } pub fn iota_connections(&self, iota_id: i64) -> Option> { self.all_iota_routes().get(&iota_id).map(|route| { let mut ids = route .connected_omikron_ids .iter() .copied() .collect::>(); ids.sort_unstable(); ids }) } pub fn routes_for_user(&self, user_id: i64) -> Vec { let routes = self.routes.read().unwrap(); let mut user_routes = routes .indices .sessions_by_user .get(&user_id) .into_iter() .flat_map(|session_ids| session_ids.iter()) .filter_map(|session_id| { routes .sessions .get(&(user_id, *session_id)) .map(|route| (*session_id, route.clone())) }) .collect::>(); user_routes.sort_by_key(|(session_id, route)| (*session_id, route.omikron_id)); user_routes.into_iter().map(|(_, route)| route).collect() } pub fn user_route(&self, user_id: i64) -> Option { self.routes_for_user(user_id).into_iter().next() } pub fn connect_iota(&self, iota_id: i64, omikron_id: i64) { self.connect_iota_with_priority(iota_id, omikron_id, false); } fn connect_iota_with_priority(&self, iota_id: i64, omikron_id: i64, primary: bool) { let mut routes = self.routes.write().unwrap(); let route = routes.iotas.entry(iota_id).or_default(); route.connected_omikron_ids.insert(omikron_id); if primary || route.primary_omikron_id.is_none() { route.primary_omikron_id = Some(omikron_id); } routes .indices .iotas_by_omikron .entry(omikron_id) .or_default() .insert(iota_id); Self::debug_assert_indices(&routes); } pub fn disconnect_iota(&self, iota_id: i64, omikron_id: i64) -> bool { let mut routes = self.routes.write().unwrap(); let empty = Self::remove_iota_owner_locked(&mut routes, iota_id, omikron_id); Self::debug_assert_indices(&routes); empty } pub fn all_iota_routes(&self) -> HashMap { self.routes.read().unwrap().iotas.clone() } pub fn connection_routes(&self) -> HashMap>> { let mut result: std::collections::HashMap>> = std::collections::HashMap::new(); let iotas = self.all_iota_routes(); for (iota_id, route) in &iotas { for omikron_id in &route.connected_omikron_ids { result .entry(*omikron_id) .or_default() .entry(*iota_id) .or_default(); } } result } } #[cfg(test)] mod tests { use super::{ClientSessionRoute, PresenceTracker}; use crate::sql::connection_status::UserStatus; fn check_index_consistency(tracker: &PresenceTracker) -> Result<(), String> { PresenceTracker::check_indices(&tracker.routes.read().unwrap()) } #[test] fn effective_state_is_derived_from_routes_and_preference() { let tracker = PresenceTracker::default(); tracker.set_preference(7, UserStatus::user_online); assert_eq!( tracker.resolve_public_state(7, 11), UserStatus::iota_offline ); tracker.connect_iota(11, 42); assert_eq!( tracker.resolve_public_state(7, 11), UserStatus::user_offline ); tracker.track_session(7, 3, 42, 11); assert_eq!(tracker.resolve_public_state(7, 11), UserStatus::user_online); tracker.set_preference(7, UserStatus::user_invisible); assert_eq!( tracker.resolve_public_state(7, 11), UserStatus::user_offline ); } #[test] fn session_must_match_assigned_iota_and_live_route() { let tracker = PresenceTracker::default(); tracker.connect_iota(11, 42); tracker.track_session(7, 3, 42, 12); assert!(!tracker.has_active_session_for_iota(7, 11)); assert_eq!( tracker.resolve_public_state(7, 11), UserStatus::user_offline ); tracker.track_session(7, 3, 43, 11); assert!(!tracker.has_active_session_for_iota(7, 11)); tracker.track_session(7, 3, 42, 11); assert!(tracker.has_active_session_for_iota(7, 11)); tracker.disconnect_iota(11, 42); assert!(!tracker.has_active_session_for_iota(7, 11)); assert_eq!( tracker.resolve_public_state(7, 11), UserStatus::iota_offline ); } #[test] fn resolver_covers_all_public_preference_states() { let tracker = PresenceTracker::default(); tracker.connect_iota(11, 42); tracker.track_session(7, 3, 42, 11); for (preference, expected) in [ (UserStatus::user_online, UserStatus::user_online), (UserStatus::user_idle, UserStatus::user_idle), (UserStatus::user_dnd, UserStatus::user_dnd), (UserStatus::user_wc, UserStatus::user_wc), (UserStatus::user_invisible, UserStatus::user_offline), ] { tracker.set_preference(7, preference); assert_eq!(tracker.resolve_public_state(7, 11), expected); } } #[test] fn session_routes_replace_by_session_without_replacing_preferences() { let tracker = PresenceTracker::default(); tracker.set_preference(7, UserStatus::user_dnd); tracker.connect_iota(11, 42); tracker.connect_iota(12, 43); tracker.track_session(7, 3, 42, 11); tracker.track_session(7, 4, 43, 12); assert_eq!(tracker.preference(7), UserStatus::user_dnd); assert_eq!( tracker .routes .read() .unwrap() .sessions .get(&(7, 3)) .cloned(), Some(ClientSessionRoute { omikron_id: 42, iota_id: 11, }) ); tracker.remove_session(7, 3, 42); assert!(!tracker.owns_session(7, 3, 42)); assert!(tracker.owns_session(7, 4, 43)); assert_eq!(tracker.preference(7), UserStatus::user_dnd); } #[test] fn subscriptions_are_authoritative_and_cleanup_is_scoped() { let tracker = PresenceTracker::default(); tracker.replace_subscription(7, 3, 42, vec![20, 21, 20]); tracker.replace_subscription(7, 3, 42, vec![21, 22]); tracker.replace_subscription(8, 4, 43, vec![21]); assert_eq!(tracker.subscribers(20).len(), 0); assert_eq!(tracker.subscribers(21).len(), 2); assert_eq!(tracker.subscribers(22).len(), 1); tracker.remove_omikron(42); assert!(tracker.subscribers(22).is_empty()); assert_eq!(tracker.subscribers(21).len(), 1); } #[test] fn losing_one_iota_route_keeps_iota_online() { let tracker = PresenceTracker::default(); tracker.connect_iota(11, 42); tracker.connect_iota(11, 43); assert!(tracker.has_iota_route(11)); assert!(!tracker.disconnect_iota(11, 42)); assert!(tracker.has_iota_route(11)); assert!(tracker.disconnect_iota(11, 43)); assert!(!tracker.has_iota_route(11)); } #[test] fn iota_connection_routes_track_primary_and_replacement() { let tracker = PresenceTracker::default(); tracker.connect_iota(11, 42); tracker.connect_iota(11, 43); assert_eq!(tracker.iota_connections(11), Some(vec![42, 43])); assert_eq!(tracker.primary_iota_route(11), Some(42)); tracker.untrack_iota_connection(11, 42); assert_eq!(tracker.iota_connections(11), Some(vec![43])); assert_eq!(tracker.primary_iota_route(11), Some(43)); } #[test] fn snapshot_preserves_primary_and_uses_deterministic_fallback() { let tracker = PresenceTracker::default(); tracker.connect_iota(11, 20); tracker.connect_iota(11, 30); assert_eq!(tracker.primary_iota_route(11), Some(20)); tracker.replace_omikron_snapshot(30, &[11], &[]); assert_eq!(tracker.primary_iota_route(11), Some(20)); tracker.replace_omikron_snapshot(20, &[11], &[]); assert_eq!(tracker.primary_iota_route(11), Some(30)); tracker.replace_omikron_snapshot(30, &[11], &[]); assert_eq!(tracker.primary_iota_route(11), Some(20)); } #[test] fn user_routes_are_sorted_by_session_then_omikron() { let tracker = PresenceTracker::default(); tracker.track_session(7, 20, 30, 11); tracker.track_session(7, 10, 40, 11); assert_eq!( tracker .routes_for_user(7) .iter() .map(|route| route.omikron_id) .collect::>(), vec![40, 30] ); tracker.remove_session(7, 10, 40); assert_eq!(tracker.routes_for_user(7).len(), 1); } #[test] fn stale_disconnect_cannot_remove_current_session_subscription() { let tracker = PresenceTracker::default(); tracker.track_session(7, 3, 42, 11); tracker.replace_subscription(7, 3, 42, vec![20]); assert!(!tracker.remove_session(7, 3, 99)); assert!(tracker.owns_session(7, 3, 42)); assert_eq!(tracker.subscribers(20).len(), 1); assert!(tracker.remove_session(7, 3, 42)); assert!(tracker.subscribers(20).is_empty()); } #[test] fn private_state_is_not_affected_by_connectivity() { let tracker = PresenceTracker::default(); tracker.set_preference(7, UserStatus::user_invisible); assert_eq!(tracker.resolve_private_state(7), UserStatus::user_invisible); } #[test] fn invalid_persisted_preference_falls_back_to_online() { let tracker = PresenceTracker::default(); assert!(!tracker.load_preference(7, "user_offline")); assert_eq!(tracker.preference(7), UserStatus::user_online); assert!(!tracker.load_preference(7, "iota_offline")); assert_eq!(tracker.preference(7), UserStatus::user_online); assert!(tracker.load_preference(7, "user_invisible")); assert_eq!(tracker.preference(7), UserStatus::user_invisible); assert!(tracker.load_preference(7, "user_dnd")); assert_eq!(tracker.preference(7), UserStatus::user_dnd); } #[test] fn active_session_can_use_one_of_several_live_iota_routes() { let tracker = PresenceTracker::default(); tracker.connect_iota(11, 42); tracker.connect_iota(11, 43); tracker.track_session(7, 3, 43, 11); assert!(tracker.has_active_session_for_iota(7, 11)); tracker.disconnect_iota(11, 43); assert!(!tracker.has_active_session_for_iota(7, 11)); } #[test] fn route_snapshot_replaces_only_one_omikron() { let tracker = PresenceTracker::default(); tracker.connect_iota(11, 42); tracker.connect_iota(12, 43); tracker.track_session(7, 3, 42, 11); tracker.track_session(8, 4, 43, 12); tracker.replace_subscription(7, 3, 42, vec![20]); tracker.replace_omikron_snapshot(42, &[12], &[(9, 5, 12)]); assert!(!tracker.owns_session(7, 3, 42)); assert!(tracker.owns_session(8, 4, 43)); assert!(tracker.owns_session(9, 5, 42)); assert!(tracker.subscribers(20).is_empty()); assert!(tracker.has_iota_route(12)); } #[test] fn omikron_cleanup_preserves_other_routes_and_preferences() { let tracker = PresenceTracker::default(); tracker.set_preference(7, UserStatus::user_dnd); tracker.connect_iota(11, 42); tracker.connect_iota(11, 43); tracker.track_session(7, 3, 42, 11); tracker.track_session(7, 4, 43, 11); tracker.replace_subscription(7, 3, 42, vec![20]); tracker.replace_subscription(7, 4, 43, vec![21]); tracker.remove_omikron(42); assert!(tracker.has_iota_route(11)); assert!(!tracker.owns_session(7, 3, 42)); assert!(tracker.owns_session(7, 4, 43)); assert!(tracker.subscribers(20).is_empty()); assert_eq!(tracker.subscribers(21).len(), 1); assert_eq!(tracker.preference(7), UserStatus::user_dnd); } #[test] fn secondary_indices_follow_session_and_subscription_mutations() { let tracker = PresenceTracker::default(); tracker.track_session(7, 3, 42, 11); tracker.replace_subscription(7, 3, 42, vec![20, 21, 20]); assert_eq!( tracker .routes .read() .unwrap() .indices .sessions_by_user .get(&7) .cloned(), Some([3].into_iter().collect()) ); assert_eq!( tracker .routes .read() .unwrap() .indices .targets_by_subscriber .get(&(7, 3)) .cloned(), Some([20, 21].into_iter().collect()) ); assert!(check_index_consistency(&tracker).is_ok()); tracker.replace_subscription(7, 3, 42, vec![22]); assert!(tracker.subscribers(20).is_empty()); assert!(tracker.subscribers(21).is_empty()); assert_eq!(tracker.subscribers(22).len(), 1); tracker.remove_session(7, 3, 42); assert!(check_index_consistency(&tracker).is_ok()); assert!( tracker .routes .read() .unwrap() .indices .sessions_by_user .is_empty() ); assert!( tracker .routes .read() .unwrap() .indices .targets_by_subscriber .is_empty() ); } #[test] fn omikron_removal_returns_all_affected_identifiers() { let tracker = PresenceTracker::default(); tracker.connect_iota(11, 42); tracker.track_session(7, 3, 42, 11); tracker.replace_subscription(7, 3, 42, vec![20]); let removed = tracker.remove_omikron(42); assert_eq!(removed.iota_ids, vec![11]); assert_eq!(removed.session_user_ids, vec![7]); assert_eq!(removed.subscriber_sessions, vec![(7, 3)]); assert!(check_index_consistency(&tracker).is_ok()); } #[test] fn consistency_check_reports_corrupted_reverse_index() { let tracker = PresenceTracker::default(); tracker.track_session(7, 3, 42, 11); tracker .routes .write() .unwrap() .indices .sessions_by_user .get_mut(&7) .unwrap() .clear(); assert!(check_index_consistency(&tracker).is_err()); } }