Skip to main content

zerodds_dcps/
factory.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! `DomainParticipantFactory` — singleton for creating participants
4//! (Spec OMG DDS 1.4 §2.2.2.2.1).
5//!
6//! The spec requires: "The DomainParticipantFactory is a singleton.
7//! The get_instance() method returns a reference to the only
8//! instance of the factory."
9//!
10//! Singleton via `OnceLock`. `create_participant(domain_id, qos)`
11//! creates a new `DomainParticipant`, starts a live runtime, and
12//! returns a cloneable handle. In addition there is
13//! `create_participant_offline(domain_id, qos)` for skeleton tests
14//! without a network, and `create_participant_with_config` for tests
15//! with a custom `RuntimeConfig`.
16
17extern crate alloc;
18
19use alloc::collections::BTreeMap;
20use alloc::vec::Vec;
21
22#[cfg(feature = "std")]
23use std::sync::{Mutex, OnceLock, Weak};
24
25use crate::error::{DdsError, Result};
26use crate::participant::{DomainId, DomainParticipant, ParticipantInner};
27use crate::qos::DomainParticipantQos;
28use crate::runtime::RuntimeConfig;
29
30/// QoS policy for the DomainParticipantFactory itself (Spec
31/// §2.2.2.2.2.6 `DomainParticipantFactoryQos`). Currently a single
32/// field `autoenable_created_entities` (default `true`); later spec
33/// extensions are added here.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct DomainParticipantFactoryQos {
36    /// If `true`, newly created entities are automatically `enable()`-d
37    /// on creation. Spec default: `true`.
38    pub autoenable_created_entities: bool,
39}
40
41impl Default for DomainParticipantFactoryQos {
42    fn default() -> Self {
43        Self {
44            autoenable_created_entities: true,
45        }
46    }
47}
48
49/// Factory singleton.
50#[cfg(feature = "std")]
51#[derive(Debug)]
52pub struct DomainParticipantFactory {
53    /// Registry of all participants created via `create_participant*` —
54    /// indexed by `DomainId`, multiple participants per domain allowed
55    /// (Spec §2.2.2.2.2.4 `lookup_participant` returns "any" participant for
56    /// the given domain).
57    ///
58    /// Stored as **weak** references so the factory never keeps a participant
59    /// alive past the user's last strong handle: dropping the user's
60    /// `DomainParticipant` releases its runtime (threads + sockets) via RAII.
61    /// `lookup_participant` upgrades on demand and garbage-collects dead weaks;
62    /// `delete_participant` stays available for explicit, spec-symmetric cleanup
63    /// but is a no-op once the participant has already dropped.
64    participants: Mutex<BTreeMap<DomainId, Vec<Weak<ParticipantInner>>>>,
65    /// Factory default QoS for newly created participants (Spec
66    /// §2.2.2.2.2.5 `set_default_participant_qos`).
67    default_participant_qos: Mutex<DomainParticipantQos>,
68    /// Factory's own QoS (Spec §2.2.2.2.2.6 `set_qos`/`get_qos`).
69    factory_qos: Mutex<DomainParticipantFactoryQos>,
70}
71
72#[cfg(not(feature = "std"))]
73#[derive(Debug, Default)]
74pub struct DomainParticipantFactory {}
75
76#[cfg(feature = "std")]
77impl DomainParticipantFactory {
78    /// Returns the process-wide factory singleton (Spec §2.2.2.2.2.1
79    /// `get_instance`).
80    pub fn instance() -> &'static Self {
81        static INSTANCE: OnceLock<DomainParticipantFactory> = OnceLock::new();
82        INSTANCE.get_or_init(|| Self {
83            participants: Mutex::new(BTreeMap::new()),
84            default_participant_qos: Mutex::new(DomainParticipantQos::default()),
85            factory_qos: Mutex::new(DomainParticipantFactoryQos::default()),
86        })
87    }
88
89    fn track(&self, p: &DomainParticipant) {
90        if let Ok(mut reg) = self.participants.lock() {
91            reg.entry(p.domain_id()).or_default().push(p.downgrade());
92        }
93    }
94
95    /// Creates a new `DomainParticipant` for the given domain id.
96    /// Starts the `DcpsRuntime` with the default config — UDP sockets +
97    /// SPDP/SEDP threads.
98    ///
99    /// # Errors
100    /// `DdsError::TransportError` if the UDP sockets do not bind.
101    pub fn create_participant(
102        &self,
103        domain_id: DomainId,
104        qos: DomainParticipantQos,
105    ) -> Result<DomainParticipant> {
106        // DDS 1.4 §2.2.3.1 UserDataQosPolicy → SPDP-Beacon PID_USER_DATA.
107        let config = RuntimeConfig {
108            user_data: qos.user_data.value.clone(),
109            ..RuntimeConfig::default()
110        };
111        let p = DomainParticipant::new_with_runtime(domain_id, qos, config)?;
112        self.track(&p);
113        Ok(p)
114    }
115
116    /// Variant with an explicitly passed `RuntimeConfig` (e.g. for
117    /// tests with short SPDP periods).
118    ///
119    /// # Errors
120    /// `DdsError::TransportError` if the UDP sockets do not bind.
121    pub fn create_participant_with_config(
122        &self,
123        domain_id: DomainId,
124        qos: DomainParticipantQos,
125        config: RuntimeConfig,
126    ) -> Result<DomainParticipant> {
127        let p = DomainParticipant::new_with_runtime(domain_id, qos, config)?;
128        self.track(&p);
129        Ok(p)
130    }
131
132    /// Offline variant without a runtime — only for unit tests that
133    /// don't want a network. The returned participant can create
134    /// topics, but no DataWriters/Readers.
135    #[must_use]
136    pub fn create_participant_offline(
137        &self,
138        domain_id: DomainId,
139        qos: DomainParticipantQos,
140    ) -> DomainParticipant {
141        let p = DomainParticipant::new(domain_id, qos);
142        self.track(&p);
143        p
144    }
145
146    /// Spec §2.2.2.2.2.4 `lookup_participant(domain_id)` — returns a
147    /// previously created participant for the same domain id, or `None`
148    /// if none is registered. With several participants for the same
149    /// domain, the implementation returns the first.
150    #[must_use]
151    pub fn lookup_participant(&self, domain_id: DomainId) -> Option<DomainParticipant> {
152        let mut reg = self.participants.lock().ok()?;
153        let mut found = None;
154        let mut now_empty = false;
155        if let Some(vec) = reg.get_mut(&domain_id) {
156            // Upgrade weaks; return the first still-live participant and garbage
157            // -collect any that the user has already dropped.
158            vec.retain(|w| match w.upgrade() {
159                Some(inner) => {
160                    if found.is_none() {
161                        found = Some(DomainParticipant::from_inner(inner));
162                    }
163                    true
164                }
165                None => false,
166            });
167            now_empty = vec.is_empty();
168        }
169        if now_empty {
170            reg.remove(&domain_id);
171        }
172        found
173    }
174
175    /// Spec §2.2.2.2.2.3 `delete_participant`. Removes the participant
176    /// from the factory registry and calls `delete_contained_entities`.
177    /// Returns `PreconditionNotMet` if the participant is not in the
178    /// registry.
179    ///
180    /// # Errors
181    /// `DdsError::PreconditionNotMet` if the participant is not
182    /// registered.
183    pub fn delete_participant(&self, p: &DomainParticipant) -> Result<()> {
184        let mut reg = self
185            .participants
186            .lock()
187            .map_err(|_| DdsError::PreconditionNotMet {
188                reason: "factory participants poisoned",
189            })?;
190        let target_handle = p.instance_handle();
191        let did = p.domain_id();
192        let mut found = false;
193        let mut now_empty = false;
194        if let Some(vec) = reg.get_mut(&did) {
195            // Remove the target (matched by handle through an upgrade) and GC any
196            // already-dropped weaks while we hold the lock.
197            vec.retain(|w| match w.upgrade() {
198                Some(inner) => {
199                    let is_target =
200                        DomainParticipant::from_inner(inner).instance_handle() == target_handle;
201                    if is_target {
202                        found = true;
203                    }
204                    !is_target
205                }
206                None => false,
207            });
208            now_empty = vec.is_empty();
209        }
210        if now_empty {
211            reg.remove(&did);
212        }
213        drop(reg);
214        if !found {
215            return Err(DdsError::PreconditionNotMet {
216                reason: "participant not registered with this factory",
217            });
218        }
219        p.delete_contained_entities()
220    }
221
222    /// Spec §2.2.2.2.2.5 `set_default_participant_qos` — default QoS
223    /// for participants created from now on.
224    ///
225    /// # Errors
226    /// `DdsError::PreconditionNotMet` on lock poisoning.
227    pub fn set_default_participant_qos(&self, qos: DomainParticipantQos) -> Result<()> {
228        let mut current =
229            self.default_participant_qos
230                .lock()
231                .map_err(|_| DdsError::PreconditionNotMet {
232                    reason: "default qos poisoned",
233                })?;
234        *current = qos;
235        Ok(())
236    }
237
238    /// Spec §2.2.2.2.2.5 `get_default_participant_qos`.
239    #[must_use]
240    pub fn get_default_participant_qos(&self) -> DomainParticipantQos {
241        self.default_participant_qos
242            .lock()
243            .map(|q| q.clone())
244            .unwrap_or_default()
245    }
246
247    /// Spec §2.2.2.2.2.6 `set_qos` (factory-level QoS).
248    ///
249    /// # Errors
250    /// `DdsError::PreconditionNotMet` on lock poisoning.
251    pub fn set_qos(&self, qos: DomainParticipantFactoryQos) -> Result<()> {
252        let mut current = self
253            .factory_qos
254            .lock()
255            .map_err(|_| DdsError::PreconditionNotMet {
256                reason: "factory qos poisoned",
257            })?;
258        *current = qos;
259        Ok(())
260    }
261
262    /// Spec §2.2.2.2.2.6 `get_qos` (factory-level QoS).
263    #[must_use]
264    pub fn get_qos(&self) -> DomainParticipantFactoryQos {
265        self.factory_qos.lock().map(|q| *q).unwrap_or_default()
266    }
267
268    /// Fluent entry point for creating a participant: returns a
269    /// [`ParticipantBuilder`] that collects QoS, a custom
270    /// [`RuntimeConfig`], and (with the `security` feature) a
271    /// [`SecurityBundle`](zerodds_security_runtime::SecurityBundle), then
272    /// materializes the participant on [`ParticipantBuilder::build`].
273    ///
274    /// This is a convenience facade over
275    /// [`Self::create_participant_with_config`] — `build()` resolves to
276    /// the same live runtime path. It exists so security wiring reads as
277    /// one chain:
278    ///
279    /// ```no_run
280    /// # #[cfg(feature = "security")] {
281    /// use zerodds_dcps::DomainParticipantFactory;
282    /// # let security_bundle = zerodds_security_runtime::SecurityBundle::builder().build();
283    /// let participant = DomainParticipantFactory::create(0)
284    ///     .with_security(security_bundle)
285    ///     .build()?;
286    /// # }
287    /// # Ok::<(), zerodds_dcps::error::DdsError>(())
288    /// ```
289    #[must_use]
290    pub fn create(domain_id: DomainId) -> ParticipantBuilder {
291        ParticipantBuilder::new(domain_id)
292    }
293}
294
295/// Fluent builder for a [`DomainParticipant`], returned by
296/// [`DomainParticipantFactory::create`]. Collects optional QoS, a custom
297/// [`RuntimeConfig`], and a [`SecurityBundle`](zerodds_security_runtime::SecurityBundle)
298/// (under the `security` feature) and creates the participant via the
299/// factory singleton on [`Self::build`].
300#[cfg(feature = "std")]
301#[derive(Debug)]
302pub struct ParticipantBuilder {
303    domain_id: DomainId,
304    qos: DomainParticipantQos,
305    config: Option<RuntimeConfig>,
306}
307
308#[cfg(feature = "std")]
309impl ParticipantBuilder {
310    fn new(domain_id: DomainId) -> Self {
311        Self {
312            domain_id,
313            qos: DomainParticipantQos::default(),
314            config: None,
315        }
316    }
317
318    /// Sets the participant QoS (default: [`DomainParticipantQos::default`]).
319    #[must_use]
320    pub fn with_qos(mut self, qos: DomainParticipantQos) -> Self {
321        self.qos = qos;
322        self
323    }
324
325    /// Sets an explicit [`RuntimeConfig`]. Overrides the config derived
326    /// from QoS / security wiring; subsequent [`Self::with_security`]
327    /// calls mutate this config.
328    #[must_use]
329    pub fn with_config(mut self, config: RuntimeConfig) -> Self {
330        self.config = Some(config);
331        self
332    }
333
334    /// Wires a [`SecurityBundle`](zerodds_security_runtime::SecurityBundle)
335    /// (logging plugin + security profile) into the participant's runtime
336    /// config — see [`RuntimeConfig::with_security_bundle`].
337    #[cfg(feature = "security")]
338    #[must_use]
339    pub fn with_security(mut self, bundle: zerodds_security_runtime::SecurityBundle) -> Self {
340        let base = self.config.take().unwrap_or_default();
341        self.config = Some(base.with_security_bundle(&bundle));
342        self
343    }
344
345    /// Creates the live participant via the factory singleton.
346    ///
347    /// # Errors
348    /// `DdsError::TransportError` if the UDP sockets do not bind.
349    pub fn build(self) -> Result<DomainParticipant> {
350        let factory = DomainParticipantFactory::instance();
351        match self.config {
352            Some(config) => {
353                factory.create_participant_with_config(self.domain_id, self.qos, config)
354            }
355            None => factory.create_participant(self.domain_id, self.qos),
356        }
357    }
358}
359
360#[cfg(test)]
361#[allow(clippy::expect_used, clippy::unwrap_used)]
362mod tests {
363    use super::*;
364
365    #[test]
366    fn factory_is_singleton() {
367        let a = DomainParticipantFactory::instance();
368        let b = DomainParticipantFactory::instance();
369        assert!(core::ptr::eq(a, b));
370    }
371
372    #[test]
373    fn factory_creates_participant_with_correct_domain_id() {
374        // Offline variant — no UDP bind in the unit test. The live
375        // variant is tested in runtime.rs.
376        let p = DomainParticipantFactory::instance()
377            .create_participant_offline(7, DomainParticipantQos::default());
378        assert_eq!(p.domain_id(), 7);
379    }
380
381    // ---- §2.2.2.2.2.4 lookup_participant ----
382
383    #[test]
384    fn lookup_participant_finds_registered_offline_participant() {
385        let f = DomainParticipantFactory::instance();
386        // Unique domain id so that no other test participant affects
387        // the lookup.
388        let domain = 91;
389        let _p = f.create_participant_offline(domain, DomainParticipantQos::default());
390        let found = f.lookup_participant(domain);
391        assert!(found.is_some());
392        assert_eq!(found.unwrap().domain_id(), domain);
393    }
394
395    #[test]
396    fn dropping_participant_releases_it_from_the_factory() {
397        // The factory holds only weak references: once the user drops their last
398        // strong handle, the participant is gone (RAII) and `lookup_participant`
399        // returns `None`, garbage-collecting the dead weak. (Pre-weak-refs the
400        // factory kept a strong clone and this would still return `Some`.)
401        let f = DomainParticipantFactory::instance();
402        let domain = 9182; // isolated from other tests
403        {
404            let _p = f.create_participant_offline(domain, DomainParticipantQos::default());
405            assert!(
406                f.lookup_participant(domain).is_some(),
407                "tracked while a strong handle is alive"
408            );
409        } // _p dropped here
410        assert!(
411            f.lookup_participant(domain).is_none(),
412            "factory must not keep the participant alive after the user drops it"
413        );
414    }
415
416    #[test]
417    fn lookup_participant_returns_none_for_unknown_domain() {
418        let f = DomainParticipantFactory::instance();
419        // Very high domain_id value for which nothing likely exists.
420        assert!(f.lookup_participant(60_001).is_none());
421    }
422
423    // ---- §2.2.2.2.2.3 delete_participant ----
424
425    #[test]
426    fn delete_participant_removes_from_registry() {
427        let f = DomainParticipantFactory::instance();
428        let domain = 92;
429        let p = f.create_participant_offline(domain, DomainParticipantQos::default());
430        assert!(f.lookup_participant(domain).is_some());
431        f.delete_participant(&p).unwrap();
432        assert!(f.lookup_participant(domain).is_none());
433    }
434
435    #[test]
436    fn delete_participant_unknown_returns_precondition_not_met() {
437        let f = DomainParticipantFactory::instance();
438        // Create a participant without tracking it via the factory
439        // (DomainParticipant::new directly).
440        let detached =
441            crate::participant::DomainParticipant::new(93, DomainParticipantQos::default());
442        let res = f.delete_participant(&detached);
443        assert!(matches!(
444            res,
445            Err(crate::error::DdsError::PreconditionNotMet { .. })
446        ));
447    }
448
449    // ---- §2.2.2.2.2.5 default_participant_qos ----
450
451    #[test]
452    fn default_participant_qos_roundtrips() {
453        let f = DomainParticipantFactory::instance();
454        let mut new_qos = f.get_default_participant_qos();
455        // We mutate some recognizable default. DomainParticipantQos is
456        // default-constructible; to verify the roundtrip it is enough
457        // to run set/get.
458        new_qos = new_qos.clone();
459        f.set_default_participant_qos(new_qos.clone()).unwrap();
460        let got = f.get_default_participant_qos();
461        // Equality modulo the Equality impl of DomainParticipantQos.
462        assert_eq!(format!("{got:?}"), format!("{new_qos:?}"));
463    }
464
465    // ---- §2.2.2.2.2.6 factory's own QoS ----
466
467    #[test]
468    fn factory_qos_default_is_autoenable_true() {
469        // Spec default §2.2.2.2.2.6: autoenable_created_entities = TRUE.
470        let q = DomainParticipantFactoryQos::default();
471        assert!(q.autoenable_created_entities);
472    }
473
474    #[test]
475    fn factory_set_get_qos_roundtrip() {
476        let f = DomainParticipantFactory::instance();
477        let q = DomainParticipantFactoryQos {
478            autoenable_created_entities: false,
479        };
480        f.set_qos(q).unwrap();
481        let got = f.get_qos();
482        assert!(!got.autoenable_created_entities);
483        // Restore default for other tests.
484        f.set_qos(DomainParticipantFactoryQos::default()).unwrap();
485    }
486}