1#[cfg(any(test, feature = "testing-transport"))]
9use std::sync::Mutex;
10use std::{collections::BTreeSet, future::Future, sync::Arc, time::Duration};
11
12use o_sfu_telemetry::schema::event as telemetry_event;
13use tokio::sync::RwLock;
14use tracing::{info, warn};
15
16#[cfg(any(test, feature = "testing-transport"))]
17pub use super::placement::JoinPlacementTestGate;
18use super::{
19 Room, RoomConfig, RoomJoinError, RoomManagerJoinError, RoomRuntimePolicy,
20 RoomUserStatsSnapshot,
21 directory::{RoomDirectory, RoomDirectoryEntry, RoomLifecycleLease},
22 effects::batch::RoomEffectContext,
23 factory::RoomFactory,
24 membership::JoinUserRequest,
25 placement::JoinAdmissionTurn,
26 source_policy::SourcePolicyTurn,
27};
28use crate::engine::{
29 ConnectionId, RoomInstanceId, UserId,
30 media_transport::{MediaTransport, TransportSessionKey},
31 metrics::{RoomGaugeValues, RuntimeMetrics},
32 room::instance::RoomManagerServeError,
33};
34
35#[cfg(any(test, feature = "testing-transport"))]
36#[path = "TESTS/support.rs"]
37mod test_support;
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct RuntimeRoomStatsSnapshot {
42 pub create_date: String,
44 pub uuid: String,
46 pub remote_address: String,
48 pub users_stats: RoomUserStatsSnapshot,
50 pub web_rtc_enabled: bool,
52}
53
54#[derive(Debug, Clone)]
56pub struct RoomUserAdmission {
57 pub room: Arc<Room>,
59 pub connection_id: ConnectionId,
61 pub transport_session_key: TransportSessionKey,
64}
65
66#[derive(Debug, Clone)]
68pub struct RuntimeRoomDirectorySnapshot {
69 pub room: Arc<Room>,
71 pub create_date: String,
73 pub remote_address: String,
75}
76
77fn retrieve_room_reservation(
78 directory: &RoomDirectory,
79 issuer: &str,
80 key: &str,
81 config: &RoomConfig,
82) -> Result<Option<Arc<Room>>, RoomManagerServeError> {
83 let Some(entry) = directory.entry_by_issuer(issuer) else {
84 return Ok(None);
85 };
86 if !entry.room.definition.matches_reservation(key, config) {
87 warn!(
88 event = telemetry_event::ROOM_RESERVATION_CONFLICT,
89 room_id = entry.room.uuid(),
90 issuer,
91 config = ?config,
92 "conflicting reservation request"
93 );
94 return Err(RoomManagerServeError::ConflictingReservation);
95 }
96 entry.lifecycle.renew_reservation();
97 Ok(Some(entry.room))
98}
99
100#[derive(Debug)]
102pub struct RoomManager {
103 directory: RwLock<RoomDirectory>,
104 factory: RoomFactory,
105 reservation_ttl: Duration,
106 #[cfg(any(test, feature = "testing-transport"))]
107 join_placement_gate: Mutex<Option<Arc<JoinPlacementTestGate>>>,
108}
109
110impl RoomManager {
111 #[must_use]
114 pub fn new(
115 runtime_policy: RoomRuntimePolicy,
116 metrics: Arc<RuntimeMetrics>,
117 reservation_ttl: Duration,
118 ) -> Self {
119 let factory = RoomFactory::new(runtime_policy, metrics);
120 Self {
121 directory: RwLock::new(RoomDirectory::default()),
122 factory,
123 reservation_ttl,
124 #[cfg(any(test, feature = "testing-transport"))]
125 join_placement_gate: Mutex::new(None),
126 }
127 }
128
129 pub async fn serve_room(
140 &self,
141 issuer: &str,
142 key: &str,
143 config: &RoomConfig,
144 remote_address: Option<&str>,
145 ) -> Result<Arc<Room>, RoomManagerServeError> {
146 {
147 let directory = self.directory.read().await;
148 if let Some(room) = retrieve_room_reservation(&directory, issuer, key, config)? {
149 return Ok(room);
150 }
151 }
152 let mut directory = self.directory.write().await;
153 if let Some(room) = retrieve_room_reservation(&directory, issuer, key, config)? {
154 return Ok(room);
155 }
156 let room = self.factory.create(issuer, key, config);
157 directory.insert(Arc::clone(&room), remote_address, self.reservation_ttl);
158 drop(directory);
159 info!(
160 event = telemetry_event::ROOM_CREATED,
161 room_id = room.uuid(),
162 remote_address = remote_address.unwrap_or("unknown"),
163 web_rtc_enabled = config.web_rtc_enabled,
164 "room created"
165 );
166 Ok(room)
167 }
168
169 pub async fn get_by_uuid(&self, uuid: &str) -> Option<Arc<Room>> {
171 let directory = self.directory.read().await;
172 directory.get_by_uuid(uuid)
173 }
174
175 pub async fn stats_snapshots(
181 &self,
182 media_transport: &MediaTransport,
183 ) -> Vec<RuntimeRoomStatsSnapshot> {
184 let entries = self.directory_entries().await;
185 let mut snapshots = Vec::with_capacity(entries.len());
186 for entry in entries {
187 snapshots.push(self.entry_stats_snapshot(entry, media_transport).await);
188 }
189 snapshots
190 }
191
192 pub async fn directory_snapshots(&self) -> Vec<RuntimeRoomDirectorySnapshot> {
194 self.directory_entries()
195 .await
196 .into_iter()
197 .map(|entry| RuntimeRoomDirectorySnapshot {
198 room: entry.room,
199 create_date: entry.create_date,
200 remote_address: entry.remote_address,
201 })
202 .collect()
203 }
204
205 pub async fn room_gauges(&self) -> RoomGaugeValues {
210 let rooms = self.directory.read().await.rooms();
211 let mut gauges = RoomGaugeValues {
212 rooms: rooms.len(),
213 ..RoomGaugeValues::default()
214 };
215 for room in rooms {
216 let state = room.state.read().await;
217 let media = state.media_counts();
218 gauges.users = gauges.users.saturating_add(state.user_count());
219 gauges.publications = gauges.publications.saturating_add(media.publications);
220 gauges.subscriptions = gauges.subscriptions.saturating_add(media.subscriptions);
221 gauges.recording_rooms = gauges
222 .recording_rooms
223 .saturating_add(usize::from(state.recording_state().recording == Some(true)));
224 }
225 gauges
226 }
227
228 pub async fn directory_snapshot(&self, room_id: &str) -> Option<RuntimeRoomDirectorySnapshot> {
230 let entry = self.entry(room_id).await?;
231 Some(RuntimeRoomDirectorySnapshot {
232 room: entry.room,
233 create_date: entry.create_date,
234 remote_address: entry.remote_address,
235 })
236 }
237
238 pub async fn sync_source_packet_selection_policies_for_runtime_ids(
244 &self,
245 room_instance_ids: &BTreeSet<RoomInstanceId>,
246 media_transport: &MediaTransport,
247 ) {
248 if room_instance_ids.is_empty() {
249 return;
250 }
251 let rooms = self
252 .directory_entries_for_instance_ids(room_instance_ids)
253 .await;
254 if rooms.is_empty() {
255 return;
256 }
257 let active_speaker_sources = media_transport.active_speaker_source_snapshot().await;
258 for room in rooms {
259 SourcePolicyTurn::packet_selection()
260 .execute(&room, Some(media_transport), Some(&active_speaker_sources))
261 .await;
262 }
263 }
264
265 pub async fn join_user(
281 &self,
282 room_id: &str,
283 request: JoinUserRequest,
284 media_transport: &MediaTransport,
285 ) -> Result<RoomUserAdmission, RoomManagerJoinError> {
286 let mutation = self
287 .begin_current_room_mutation(room_id)
288 .await
289 .ok_or(RoomManagerJoinError::MissingRoom)?;
290 let room = Arc::clone(&mutation.room);
291
292 let admission = JoinAdmissionTurn::from_factory(request, media_transport, &self.factory);
293 #[cfg(any(test, feature = "testing-transport"))]
294 let admission = admission.with_gate(self.join_placement_gate_for_test());
295 let join_commit = match room
296 .commit_admission(admission, RoomEffectContext::runtime(media_transport))
297 .await
298 {
299 Ok(commit) => commit,
300 Err(err) => {
301 self.finish_session_mutation(room_id, mutation, false).await;
302 return Err(match err {
303 RoomJoinError::RoomFull => RoomManagerJoinError::RoomFull,
304 RoomJoinError::RouterState => RoomManagerJoinError::RouterState,
305 });
306 }
307 };
308 mutation.lease.clear_expiration();
311 let receipt = room
312 .finalize_admission(join_commit, RoomEffectContext::runtime(media_transport))
313 .await;
314
315 self.finish_session_mutation(room_id, mutation, false).await;
316 Ok(RoomUserAdmission {
317 room,
318 connection_id: receipt.connection_id,
319 transport_session_key: receipt.transport_session_key,
320 })
321 }
322
323 pub async fn close_session(
329 &self,
330 room_id: &str,
331 user_id: &UserId,
332 connection_id: ConnectionId,
333 media_transport: &MediaTransport,
334 ) -> bool {
335 let Some((_room, did_remove_active_session)) = self
336 .run_current_room_mutation(
337 room_id,
338 |room| async move {
339 room.remove_user(user_id, connection_id, media_transport)
340 .await
341 },
342 true,
343 )
344 .await
345 else {
346 return false;
347 };
348 did_remove_active_session
349 }
350
351 pub async fn disconnect_users(
356 &self,
357 room_id: &str,
358 user_ids: &[UserId],
359 media_transport: &MediaTransport,
360 ) {
361 let _ = self
362 .run_current_room_mutation(
363 room_id,
364 |room| async move {
365 room.disconnect_users(user_ids, media_transport).await;
366 },
367 true,
368 )
369 .await;
370 }
371
372 pub async fn check_expired_room_reservations(&self) {
374 let mut directory = self.directory.write().await;
375 for entry in directory.entries() {
376 if entry.lifecycle.claim_expired_reservation() {
377 directory.remove_if_current(entry.room.uuid(), &entry.room);
378 info!(
379 event = telemetry_event::ROOM_RESERVATION_EXPIRED,
380 room_id = entry.room.uuid(),
381 "room reservation expired"
382 );
383 }
384 }
385 drop(directory);
386 }
387
388 #[cfg(test)]
389 pub(super) async fn with_current_room<T, F, Fut>(&self, room_id: &str, action: F) -> Option<T>
390 where
391 F: FnOnce(Arc<Room>) -> Fut,
392 Fut: Future<Output = T>,
393 {
394 self.run_current_room_mutation(room_id, action, false)
395 .await
396 .map(|(_, output)| output)
397 }
398
399 async fn run_current_room_mutation<T, F, Fut>(
400 &self,
401 room_id: &str,
402 action: F,
403 remove_if_empty: bool,
404 ) -> Option<(Arc<Room>, T)>
405 where
406 F: FnOnce(Arc<Room>) -> Fut,
407 Fut: Future<Output = T>,
408 {
409 let mutation = self.begin_current_room_mutation(room_id).await?;
410 let room = Arc::clone(&mutation.room);
411 let output = action(Arc::clone(&room)).await;
412 self.finish_session_mutation(room_id, mutation, remove_if_empty)
413 .await;
414 Some((room, output))
415 }
416
417 async fn finish_session_mutation(
418 &self,
419 room_id: &str,
420 mutation: CurrentRoomMutation,
421 remove_if_empty: bool,
422 ) {
423 let CurrentRoomMutation { room, lease } = mutation;
424 if lease.finish(remove_if_empty, room.is_empty().await) {
428 self.directory
429 .write()
430 .await
431 .remove_if_current(room_id, &room);
432 }
433 }
434
435 async fn begin_current_room_mutation(&self, room_id: &str) -> Option<CurrentRoomMutation> {
441 let entry = self.entry(room_id).await?;
442 let lease = entry.lifecycle.begin()?;
443 let room = entry.room;
444 if self.is_current_entry(room_id, &room).await {
445 return Some(CurrentRoomMutation { room, lease });
446 }
447 lease.cancel();
448 None
449 }
450
451 async fn entry(&self, room_id: &str) -> Option<RoomDirectoryEntry> {
452 let directory = self.directory.read().await;
453 directory.entry(room_id)
454 }
455
456 async fn directory_entries(&self) -> Vec<RoomDirectoryEntry> {
457 let directory = self.directory.read().await;
458 directory.entries()
459 }
460
461 async fn directory_entries_for_instance_ids(
462 &self,
463 room_instance_ids: &BTreeSet<RoomInstanceId>,
464 ) -> Vec<Arc<Room>> {
465 let directory = self.directory.read().await;
466 room_instance_ids
467 .iter()
468 .filter_map(|room_instance_id| directory.entry_by_instance_id(*room_instance_id))
469 .map(|entry| entry.room)
470 .collect()
471 }
472
473 async fn entry_stats_snapshot(
474 &self,
475 entry: RoomDirectoryEntry,
476 media_transport: &MediaTransport,
477 ) -> RuntimeRoomStatsSnapshot {
478 let room = entry.room;
479 let users_stats = room.session_stats_snapshot(media_transport).await;
480 RuntimeRoomStatsSnapshot {
481 create_date: entry.create_date,
482 uuid: room.uuid().to_owned(),
483 remote_address: entry.remote_address,
484 users_stats,
485 web_rtc_enabled: room.web_rtc_enabled(),
486 }
487 }
488
489 async fn is_current_entry(&self, room_id: &str, room: &Arc<Room>) -> bool {
490 let directory = self.directory.read().await;
491 directory.contains_current(room_id, room)
492 }
493}
494
495#[derive(Debug)]
500struct CurrentRoomMutation {
501 room: Arc<Room>,
502 lease: RoomLifecycleLease,
503}