Skip to main content

o_sfu_core/engine/room/
directory.rs

1//! Current-room indexes and lifecycle leases.
2//!
3//! [`RoomDirectory`] indexes one current room by UUID, issuer and instance ID.
4//! Each entry shares a [`RoomLifecycle`] gate. Accepted leases defer empty-room
5//! removal until the final mutation finishes. Reservation expiry removes only
6//! idle entries claimed by that gate.
7
8use std::{
9    collections::BTreeMap,
10    sync::{Arc, Mutex},
11    time::Duration,
12};
13
14use time::{OffsetDateTime, format_description::well_known::Rfc3339};
15use tokio::time::Instant;
16
17use super::Room;
18use crate::engine::{RoomInstanceId, sync::lock_unpoisoned};
19
20const UNKNOWN_REMOTE_ADDRESS: &str = "unknown";
21
22fn rfc3339_now() -> String {
23    match OffsetDateTime::now_utc().format(&Rfc3339) {
24        Ok(timestamp) => timestamp,
25        Err(_error) => String::from("1970-01-01T00:00:00Z"),
26    }
27}
28
29/// directory row for one current room instance
30///
31/// cloned entries carry the same lifecycle gate as the live directory row, so
32/// a manager snapshot can accept work without keeping the directory lock held
33#[derive(Debug, Clone)]
34pub(crate) struct RoomDirectoryEntry {
35    pub room: Arc<Room>,
36    pub lifecycle: RoomLifecycle,
37    pub create_date: String,
38    pub remote_address: String,
39}
40
41impl RoomDirectoryEntry {
42    fn new(room: Arc<Room>, remote_address: Option<&str>, reservation_ttl: Duration) -> Self {
43        Self {
44            room,
45            lifecycle: RoomLifecycle::new(reservation_ttl),
46            create_date: rfc3339_now(),
47            remote_address: remote_address.unwrap_or(UNKNOWN_REMOTE_ADDRESS).to_owned(),
48        }
49    }
50}
51
52/// mutable state behind one directory entry's lifecycle lease gate
53///
54/// this lock is synchronous and short lived
55/// callers may hold a
56/// [`RoomLifecycleLease`] while awaiting, but this mutex is only held while a
57/// lease is accepted or released
58#[derive(Debug)]
59struct RoomLifecycleState {
60    /// accepted room work that has not finished or been dropped
61    active_mutations: usize,
62    /// empty-room removal request waiting for accepted work to drain
63    remove_when_idle: bool,
64    /// terminal marker set once one finisher wins directory removal
65    closing: bool,
66    /// reservation deadline, or `None` once a successful join retired it
67    expires_at: Option<Instant>,
68    /// lease length this reservation was published with and is renewed by
69    reservation_ttl: Duration,
70}
71
72impl RoomLifecycleState {
73    fn new(reservation_ttl: Duration) -> Self {
74        Self {
75            active_mutations: 0,
76            remove_when_idle: false,
77            closing: false,
78            expires_at: Some(Instant::now() + reservation_ttl),
79            reservation_ttl,
80        }
81    }
82}
83
84/// cloneable admission gate for the current room stored in one directory row
85///
86/// this type coordinates manager-level liveness only
87/// room membership ordering
88/// remains owned by [`Room`] and its state transition methods
89#[derive(Debug, Clone)]
90pub(crate) struct RoomLifecycle {
91    state: Arc<Mutex<RoomLifecycleState>>,
92}
93
94impl RoomLifecycle {
95    pub(crate) fn new(reservation_ttl: Duration) -> Self {
96        Self {
97            state: Arc::new(Mutex::new(RoomLifecycleState::new(reservation_ttl))),
98        }
99    }
100
101    /// atomically claims cleanup responsibility for an expired, idle reservation.
102    pub(crate) fn claim_expired_reservation(&self) -> bool {
103        let mut state = lock_unpoisoned(&self.state);
104        let is_expired = state.expires_at.is_some_and(|t| Instant::now() >= t);
105        if !state.closing && state.active_mutations == 0 && is_expired {
106            state.closing = true;
107            drop(state);
108            return true;
109        }
110        false
111    }
112
113    /// extends a room reservation without rearming it
114    pub(crate) fn renew_reservation(&self) {
115        let mut state = lock_unpoisoned(&self.state);
116        if state.expires_at.is_some() {
117            state.expires_at = Some(Instant::now() + state.reservation_ttl);
118        }
119    }
120
121    #[cfg(any(test, feature = "testing-transport"))]
122    pub(crate) fn expire_reservation_now_for_test(&self) {
123        lock_unpoisoned(&self.state).expires_at = Some(Instant::now());
124    }
125
126    #[cfg(any(test, feature = "testing-transport"))]
127    #[must_use]
128    pub(crate) fn has_reservation_deadline_for_test(&self) -> bool {
129        lock_unpoisoned(&self.state).expires_at.is_some()
130    }
131
132    /// accept a new current-room operation
133    ///
134    /// `None` means empty-room removal is pending or already won
135    /// callers must
136    /// still validate that the room pointer is current after acquiring the lease
137    #[must_use]
138    pub(crate) fn begin(&self) -> Option<RoomLifecycleLease> {
139        let mut state = lock_unpoisoned(&self.state);
140        if state.closing || state.remove_when_idle {
141            return None;
142        }
143        state.active_mutations = state.active_mutations.checked_add(1)?;
144        let lease = RoomLifecycleLease {
145            state: Arc::clone(&self.state),
146            finished: false,
147        };
148        drop(state);
149        Some(lease)
150    }
151}
152
153/// cancellation-safe permit for work accepted against a directory entry
154///
155/// dropping the lease releases admission without requesting removal
156/// manager
157/// teardown paths call [`Self::finish`] after checking whether the room is empty
158#[derive(Debug)]
159pub(crate) struct RoomLifecycleLease {
160    /// shared lease state for the directory entry that accepted this work
161    state: Arc<Mutex<RoomLifecycleState>>,
162    /// guards against double release when `finish`, `cancel` or `Drop` overlap
163    finished: bool,
164}
165
166impl RoomLifecycleLease {
167    /// release a lease that was accepted for a stale directory row
168    ///
169    /// this is separate from `Drop` so stale-current validation can be explicit
170    /// at the manager boundary
171    pub(crate) fn cancel(mut self) {
172        let _ = self.release(false, false);
173    }
174
175    /// Releases the lease and returns whether this caller claimed directory removal.
176    #[must_use]
177    pub(crate) fn finish(mut self, remove_if_empty: bool, room_can_be_removed: bool) -> bool {
178        self.release(remove_if_empty, room_can_be_removed)
179    }
180
181    #[must_use]
182    fn release(&mut self, remove_if_empty: bool, room_can_be_removed: bool) -> bool {
183        if self.finished {
184            return false;
185        }
186        self.finished = true;
187        let mut state = lock_unpoisoned(&self.state);
188        if state.active_mutations > 0 {
189            state.active_mutations -= 1;
190        }
191        if remove_if_empty && room_can_be_removed {
192            state.remove_when_idle = true;
193        }
194        let idle_pending_removal = state.active_mutations == 0 && state.remove_when_idle;
195        let should_remove = idle_pending_removal && room_can_be_removed;
196        if should_remove {
197            state.closing = true;
198        } else if idle_pending_removal {
199            // The final lease either found the room non-empty or supplied no
200            // emptiness proof, so an earlier removal request cannot close it.
201            state.remove_when_idle = false;
202        }
203        drop(state);
204        should_remove
205    }
206
207    pub(crate) fn clear_expiration(&self) {
208        lock_unpoisoned(&self.state).expires_at = None;
209    }
210}
211
212impl Drop for RoomLifecycleLease {
213    fn drop(&mut self) {
214        let _ = self.release(false, false);
215    }
216}
217
218#[derive(Debug, Default)]
219pub(crate) struct RoomDirectory {
220    by_uuid: BTreeMap<String, RoomDirectoryEntry>,
221    uuid_by_instance: BTreeMap<RoomInstanceId, String>,
222    uuid_by_issuer: BTreeMap<String, String>,
223}
224
225impl RoomDirectory {
226    #[must_use]
227    pub(crate) fn get_by_uuid(&self, uuid: &str) -> Option<Arc<Room>> {
228        self.by_uuid.get(uuid).map(|entry| Arc::clone(&entry.room))
229    }
230
231    #[must_use]
232    pub(crate) fn entry(&self, uuid: &str) -> Option<RoomDirectoryEntry> {
233        self.by_uuid.get(uuid).cloned()
234    }
235
236    #[must_use]
237    pub(crate) fn entry_by_issuer(&self, issuer: &str) -> Option<RoomDirectoryEntry> {
238        let uuid = self.uuid_by_issuer.get(issuer)?;
239        self.entry(uuid)
240    }
241
242    #[must_use]
243    pub(crate) fn entry_by_instance_id(
244        &self,
245        room_instance_id: RoomInstanceId,
246    ) -> Option<RoomDirectoryEntry> {
247        let uuid = self.uuid_by_instance.get(&room_instance_id)?;
248        self.entry(uuid)
249    }
250
251    #[must_use]
252    pub(crate) fn entries(&self) -> Vec<RoomDirectoryEntry> {
253        self.by_uuid.values().cloned().collect()
254    }
255
256    #[must_use]
257    pub(crate) fn rooms(&self) -> Vec<Arc<Room>> {
258        self.by_uuid
259            .values()
260            .map(|entry| Arc::clone(&entry.room))
261            .collect()
262    }
263
264    pub(crate) fn insert(
265        &mut self,
266        room: Arc<Room>,
267        remote_address: Option<&str>,
268        reservation_ttl: Duration,
269    ) {
270        let room_id = room.uuid().to_owned();
271        self.uuid_by_issuer
272            .insert(room.issuer().to_owned(), room_id.clone());
273        self.uuid_by_instance
274            .insert(room.instance_id(), room_id.clone());
275        self.by_uuid.insert(
276            room_id,
277            RoomDirectoryEntry::new(room, remote_address, reservation_ttl),
278        );
279    }
280
281    #[must_use]
282    pub(crate) fn contains_current(&self, uuid: &str, room: &Arc<Room>) -> bool {
283        self.by_uuid
284            .get(uuid)
285            .is_some_and(|entry| Arc::ptr_eq(&entry.room, room))
286    }
287
288    pub(crate) fn remove_if_current(&mut self, uuid: &str, room: &Arc<Room>) {
289        if self.contains_current(uuid, room) {
290            self.by_uuid.remove(uuid);
291            self.uuid_by_issuer.remove(room.issuer());
292            self.uuid_by_instance.remove(&room.instance_id());
293        }
294    }
295}