Skip to main content

o_sfu_core/engine/room/
factory.rs

1//! Room construction for rooms that are new to the runtime directory.
2//!
3//! `RoomManager` owns idempotent lookup, directory publication and creation
4//! diagnostics. This module contains the cold-path allocation step used
5//! after lookup misses, before the new room is visible to other runtime
6//! entrypoints.
7//!
8//! A factory-created room receives fresh process-local placement, the immutable
9//! runtime policy selected at boot and the shared runtime metrics catalog. It
10//! does not register the room or emit creation events.
11//!
12//! Same-room worker placement is not decided here. The factory
13//! gives a room its stable instance id and primary router id, while
14//! `RoomManager::join_user` assigns workers from packet-loop heartbeat delays
15//! when sessions arrive.
16
17use std::sync::{Arc, Mutex};
18
19use o_sfu_router::{RouterId, rtp::MediaCapabilities};
20
21use super::{Room, RoomRuntimeContext};
22use crate::{
23    RoomMediaLimits, RoomWorkerPolicy, RuntimeFeatureFlags, VideoAdaptationTuning,
24    engine::{RoomInstanceId, metrics::RuntimeMetrics, sync::lock_unpoisoned},
25};
26
27/// admission limits that stay fixed for one room lifetime
28///
29/// this is kept separate from the wider runtime policy because admission is a
30/// narrow concern with its own tests and state checks
31///
32/// the policy is passed into `RoomState` at construction time and then treated
33/// as immutable room configuration
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct RoomAdmissionPolicy {
36    /// maximum number of live users the room accepts at once
37    ///
38    /// replaced connections still consume this budget until the room transition
39    /// finishes and the old live user has been removed
40    pub max_sessions: usize,
41}
42
43impl RoomAdmissionPolicy {
44    #[must_use]
45    pub const fn new(max_sessions: usize) -> Self {
46        Self { max_sessions }
47    }
48}
49
50/// stable runtime policy bundle shared by the room and its state model
51///
52/// this groups the room rules that are fixed for the room lifetime and read by
53/// more than one boundary during join, negotiation and observability work
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct RoomRuntimePolicy {
56    /// room-level admission limits enforced by room state
57    pub admission_policy: RoomAdmissionPolicy,
58    /// feature surface the room advertises to clients
59    pub feature_flags: RuntimeFeatureFlags,
60    /// router-native capability baseline used for negotiation and bootstrap
61    pub router_rtp_capabilities: MediaCapabilities,
62    /// same-room local worker-placement policy selected at runtime boot
63    pub room_worker_policy: RoomWorkerPolicy,
64    /// room media activation caps applied by source policy
65    pub media_limits: RoomMediaLimits,
66    /// receiver video adaptation knobs applied by source policy
67    pub video_adaptation_tuning: VideoAdaptationTuning,
68}
69
70impl RoomRuntimePolicy {
71    #[must_use]
72    pub fn new(
73        admission_policy: RoomAdmissionPolicy,
74        feature_flags: RuntimeFeatureFlags,
75        router_rtp_capabilities: MediaCapabilities,
76    ) -> Self {
77        Self {
78            admission_policy,
79            feature_flags,
80            router_rtp_capabilities,
81            room_worker_policy: RoomWorkerPolicy::strict_single_router(),
82            media_limits: RoomMediaLimits::default(),
83            video_adaptation_tuning: VideoAdaptationTuning::default(),
84        }
85    }
86
87    /// return a room policy that uses the provided same-room worker policy
88    #[must_use]
89    pub fn with_room_worker_policy(mut self, room_worker_policy: RoomWorkerPolicy) -> Self {
90        self.room_worker_policy = room_worker_policy;
91        self
92    }
93
94    /// return a room policy that uses the provided media activation limits
95    #[must_use]
96    pub fn with_media_limits(mut self, media_limits: RoomMediaLimits) -> Self {
97        self.media_limits = media_limits;
98        self
99    }
100
101    #[must_use]
102    pub fn with_video_adaptation_tuning(
103        mut self,
104        video_adaptation_tuning: VideoAdaptationTuning,
105    ) -> Self {
106        self.video_adaptation_tuning = video_adaptation_tuning;
107        self
108    }
109}
110
111/// external room config passed in from the http or runtime edge
112///
113/// this type keeps room identity separate from operator-facing knobs and
114/// compatibility toggles that may be chosen per room at creation time
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct RoomConfig {
117    /// whether this room should expose WebRTC to clients at all
118    pub web_rtc_enabled: bool,
119    /// compatibility recording address from `/v1/channel`
120    pub recording_address: Option<String>,
121}
122
123impl Default for RoomConfig {
124    fn default() -> Self {
125        Self {
126            web_rtc_enabled: true,
127            recording_address: None,
128        }
129    }
130}
131
132pub(super) struct RoomInit {
133    /// runtime-local instance and primary placement for the room
134    pub(super) runtime_context: RoomRuntimeContext,
135    /// validated room policy copied from runtime startup
136    pub(super) runtime_policy: RoomRuntimePolicy,
137    /// compatibility-facing issuer captured at room creation
138    pub(super) issuer: String,
139    /// room key captured from the first create request
140    pub(super) key: String,
141    /// room-level compatibility configuration
142    pub(super) config: RoomConfig,
143    /// process metric catalog used by room observers
144    pub(super) metrics: Arc<RuntimeMetrics>,
145}
146
147/// Monotonic placement counters assigned by the current process.
148///
149/// Room instance ids and router ids are allocated under one lock so every
150/// new room receives one coherent runtime placement. The counters are not a
151/// distributed identity source and must not leak into the Odoo-facing room
152/// contract.
153#[derive(Debug)]
154struct RoomRuntimeAllocator {
155    next_room_instance_id: u64,
156    /// Next router id to allocate for room-local topology.
157    ///
158    /// Room creation consumes one primary router id. Dynamic spillover
159    /// placement consumes additional router ids when sessions join.
160    next_router_id: u64,
161}
162
163/// Cold-path constructor for rooms that are new to the directory.
164///
165/// `RoomFactory` keeps runtime-wide creation dependencies behind the manager
166/// so `RoomManager::serve_room` can focus on idempotent lookup and
167/// publication. Each call to [`Self::create`] returns an unpublished
168/// [`Room`] with fresh process-local placement. The caller must insert it in
169/// the directory before exposing it to other runtime entrypoints.
170#[derive(Debug)]
171pub(crate) struct RoomFactory {
172    /// Runtime-wide room rules cloned into each room.
173    ///
174    /// Keeping the policy here makes every room start from the validated
175    /// boot-time policy while still letting the room own its copy.
176    runtime_policy: RoomRuntimePolicy,
177    /// Process metric catalog cloned into each room.
178    metrics: Arc<RuntimeMetrics>,
179    /// Serialized allocator for process-local placement ids.
180    ///
181    /// This keeps concurrent create requests from receiving the same runtime
182    /// placement.
183    allocator: Mutex<RoomRuntimeAllocator>,
184}
185
186impl RoomFactory {
187    /// Builds the factory for one [`RoomManager`](super::RoomManager) lifetime.
188    #[must_use]
189    pub(crate) fn new(runtime_policy: RoomRuntimePolicy, metrics: Arc<RuntimeMetrics>) -> Self {
190        Self {
191            runtime_policy,
192            metrics,
193            allocator: Mutex::new(RoomRuntimeAllocator {
194                next_room_instance_id: 0,
195                next_router_id: 0,
196            }),
197        }
198    }
199
200    /// Creates an unpublished room from one manager lookup miss
201    ///
202    /// The room emits no creation diagnostics. `RoomManager` publishes it
203    /// before emitting its creation event
204    #[must_use]
205    pub(crate) fn create(&self, issuer: &str, key: &str, config: &RoomConfig) -> Arc<Room> {
206        Arc::new(Room::new(RoomInit {
207            runtime_context: self.allocate_runtime_context(),
208            runtime_policy: self.runtime_policy.clone(),
209            issuer: issuer.to_owned(),
210            key: key.to_owned(),
211            config: config.clone(),
212            metrics: Arc::clone(&self.metrics),
213        }))
214    }
215
216    /// Reserves runtime-local placement for one new room.
217    ///
218    /// The primary router id is allocated here, but worker placement remains
219    /// unset until the first session join assigns the room from live load data.
220    ///
221    /// The mutex is poisoned-tolerant because placement allocation has no
222    /// partial side effect beyond the counters themselves. Recovering the inner
223    /// value keeps later room creation possible after an unrelated panic.
224    fn allocate_runtime_context(&self) -> RoomRuntimeContext {
225        let (room_instance_id, primary_router_id) = {
226            let mut allocator = lock_unpoisoned(&self.allocator);
227            let room_instance_id = RoomInstanceId::allocate(&mut allocator.next_room_instance_id);
228            let primary_router_id = RouterId(allocator.next_router_id);
229            allocator.next_router_id = allocator.next_router_id.saturating_add(1);
230            drop(allocator);
231            (room_instance_id, primary_router_id)
232        };
233        RoomRuntimeContext::new_unassigned(room_instance_id, primary_router_id)
234    }
235
236    /// reserve a new process-unique identifier for a spillover router
237    ///
238    /// this provides the room engine with a thread-safe way to allocate new router
239    /// identities on the fly when media load exceeds the primary worker capacity.
240    /// the allocator lock is held only long enough to increment the counter, keeping
241    /// the cold-path creation from blocking active request loops
242    pub(super) fn allocate_spillover_router(&self) -> RouterId {
243        let mut allocator = lock_unpoisoned(&self.allocator);
244        let router_id = RouterId(allocator.next_router_id);
245        allocator.next_router_id = allocator.next_router_id.saturating_add(1);
246        router_id
247    }
248}