Skip to main content

zerodds_corba_ccm/
container.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! CCM-Container — Spec §9.
5//!
6//! Der Container haeltet die Component-Lifecycle, ruft die CIF-
7//! Lifecycle-Methods (`set_*_context`/`ccm_activate`/`ccm_passivate`/
8//! `ccm_remove`) und liefert `ComponentContext` an den Executor.
9
10use alloc::boxed::Box;
11use alloc::collections::BTreeMap;
12use alloc::string::String;
13use alloc::sync::Arc;
14use std::sync::Mutex;
15
16use crate::cif::{CifError, ComponentExecutor};
17use crate::port::PortRegistry;
18
19/// Container-Type — Spec §9.1.4.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
21pub enum ContainerType {
22    /// `Session` — non-persistent stateful.
23    Session,
24    /// `Service` — stateless.
25    Service,
26    /// `Process` — long-running.
27    Process,
28    /// `Entity` — persistent.
29    Entity,
30}
31
32/// Lifecycle-State eines Component-Executor.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub enum LifecycleState {
35    /// Erstellt, aber noch nicht initialisiert.
36    Created,
37    /// `set_*_context` aufgerufen, vor `ccm_activate`.
38    Configured,
39    /// `ccm_activate` aufgerufen.
40    Active,
41    /// `ccm_passivate` aufgerufen.
42    Passive,
43    /// `ccm_remove` aufgerufen — terminal.
44    Removed,
45}
46
47/// Container-Fehler.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum ContainerError {
50    /// Component-Instance unbekannt.
51    InstanceNotFound(String),
52    /// Operation passt nicht zum aktuellen Lifecycle-State.
53    InvalidState {
54        /// Aktueller State.
55        current: LifecycleState,
56        /// Welche Operation versucht wurde.
57        operation: String,
58    },
59    /// Cif-Fehler aus dem Executor.
60    Cif(CifError),
61}
62
63impl From<CifError> for ContainerError {
64    fn from(e: CifError) -> Self {
65        Self::Cif(e)
66    }
67}
68
69struct InstanceEntry {
70    state: LifecycleState,
71    executor: Box<dyn ComponentExecutor>,
72}
73
74/// CCM-Container.
75pub struct Container {
76    container_type: ContainerType,
77    instances: Mutex<BTreeMap<String, InstanceEntry>>,
78    /// Port-Registry, geshared mit Caller.
79    pub ports: Arc<PortRegistry>,
80}
81
82impl core::fmt::Debug for Container {
83    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
84        let n = self.instances.lock().ok().map(|g| g.len()).unwrap_or(0);
85        f.debug_struct("Container")
86            .field("type", &self.container_type)
87            .field("instances", &n)
88            .finish()
89    }
90}
91
92impl Container {
93    /// Konstruktor.
94    #[must_use]
95    pub fn new(container_type: ContainerType) -> Self {
96        Self {
97            container_type,
98            instances: Mutex::new(BTreeMap::new()),
99            ports: Arc::new(PortRegistry::new()),
100        }
101    }
102
103    /// Container-Type.
104    #[must_use]
105    pub fn container_type(&self) -> ContainerType {
106        self.container_type
107    }
108
109    /// Registriert einen neuen Component-Executor unter einer
110    /// Instance-Id und konfiguriert ihn.
111    ///
112    /// # Errors
113    /// `Cif` wenn der Executor-Setup fehlschlaegt.
114    pub fn install_component(
115        &self,
116        instance_id: String,
117        mut executor: Box<dyn ComponentExecutor>,
118        context: Box<dyn crate::context::ComponentContext>,
119    ) -> Result<(), ContainerError> {
120        executor.set_context(context);
121        let mut g = self.instances.lock().map_err(|_| {
122            ContainerError::Cif(CifError::CcmException("instances mutex poisoned".into()))
123        })?;
124        g.insert(
125            instance_id,
126            InstanceEntry {
127                state: LifecycleState::Configured,
128                executor,
129            },
130        );
131        Ok(())
132    }
133
134    /// `ccm_activate` — Spec §9.5.4.
135    ///
136    /// # Errors
137    /// `InstanceNotFound` / `InvalidState` / `Cif`.
138    pub fn activate(&self, instance_id: &str) -> Result<(), ContainerError> {
139        self.transition(instance_id, "ccm_activate", LifecycleState::Active, |e| {
140            e.ccm_activate()
141        })
142    }
143
144    /// `ccm_passivate` — Spec §9.5.5.
145    ///
146    /// # Errors
147    /// Wie oben.
148    pub fn passivate(&self, instance_id: &str) -> Result<(), ContainerError> {
149        self.transition(instance_id, "ccm_passivate", LifecycleState::Passive, |e| {
150            e.ccm_passivate()
151        })
152    }
153
154    /// `ccm_remove` — Spec §9.5.6.
155    ///
156    /// # Errors
157    /// Wie oben.
158    pub fn remove(&self, instance_id: &str) -> Result<(), ContainerError> {
159        self.transition(instance_id, "ccm_remove", LifecycleState::Removed, |e| {
160            e.ccm_remove()
161        })?;
162        if let Ok(mut g) = self.instances.lock() {
163            g.remove(instance_id);
164        }
165        Ok(())
166    }
167
168    /// Anzahl Instances.
169    #[must_use]
170    pub fn instance_count(&self) -> usize {
171        self.instances.lock().map(|g| g.len()).unwrap_or(0)
172    }
173
174    /// Lifecycle-State einer Instance.
175    #[must_use]
176    pub fn state_of(&self, instance_id: &str) -> Option<LifecycleState> {
177        self.instances
178            .lock()
179            .ok()
180            .and_then(|g| g.get(instance_id).map(|e| e.state))
181    }
182
183    fn transition<F>(
184        &self,
185        instance_id: &str,
186        op: &str,
187        target: LifecycleState,
188        f: F,
189    ) -> Result<(), ContainerError>
190    where
191        F: FnOnce(&mut Box<dyn ComponentExecutor>) -> Result<(), CifError>,
192    {
193        let mut g = self.instances.lock().map_err(|_| {
194            ContainerError::Cif(CifError::CcmException("instances mutex poisoned".into()))
195        })?;
196        let entry = g
197            .get_mut(instance_id)
198            .ok_or_else(|| ContainerError::InstanceNotFound(instance_id.to_string()))?;
199        // Spec §9.5: erlaubte Transitions:
200        // Configured -> Active (via activate)
201        // Active     -> Passive (via passivate)
202        // Active     -> Removed (via remove)
203        // Passive    -> Active (via activate)
204        // Passive    -> Removed (via remove)
205        let valid = matches!(
206            (entry.state, target),
207            (LifecycleState::Configured, LifecycleState::Active)
208                | (LifecycleState::Active, LifecycleState::Passive)
209                | (LifecycleState::Passive, LifecycleState::Active)
210                | (LifecycleState::Active, LifecycleState::Removed)
211                | (LifecycleState::Passive, LifecycleState::Removed)
212                | (LifecycleState::Configured, LifecycleState::Removed)
213        );
214        if !valid {
215            return Err(ContainerError::InvalidState {
216                current: entry.state,
217                operation: op.to_string(),
218            });
219        }
220        f(&mut entry.executor)?;
221        entry.state = target;
222        Ok(())
223    }
224}
225
226#[cfg(test)]
227#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
228mod tests {
229    use super::*;
230    use crate::context::ComponentContext;
231    use core::sync::atomic::{AtomicUsize, Ordering};
232
233    struct AnonContext;
234    impl ComponentContext for AnonContext {
235        fn get_caller_principal(&self) -> Option<alloc::vec::Vec<u8>> {
236            None
237        }
238    }
239
240    struct CountingExecutor {
241        ctx_set: AtomicUsize,
242        activations: AtomicUsize,
243        passivations: AtomicUsize,
244        removals: AtomicUsize,
245    }
246
247    impl ComponentExecutor for CountingExecutor {
248        fn set_context(&mut self, _: Box<dyn ComponentContext>) {
249            self.ctx_set.fetch_add(1, Ordering::Relaxed);
250        }
251        fn ccm_activate(&mut self) -> Result<(), CifError> {
252            self.activations.fetch_add(1, Ordering::Relaxed);
253            Ok(())
254        }
255        fn ccm_passivate(&mut self) -> Result<(), CifError> {
256            self.passivations.fetch_add(1, Ordering::Relaxed);
257            Ok(())
258        }
259        fn ccm_remove(&mut self) -> Result<(), CifError> {
260            self.removals.fetch_add(1, Ordering::Relaxed);
261            Ok(())
262        }
263    }
264
265    fn fresh_executor() -> Box<dyn ComponentExecutor> {
266        Box::new(CountingExecutor {
267            ctx_set: AtomicUsize::new(0),
268            activations: AtomicUsize::new(0),
269            passivations: AtomicUsize::new(0),
270            removals: AtomicUsize::new(0),
271        })
272    }
273
274    #[test]
275    fn install_and_full_lifecycle_round_trip() {
276        let c = Container::new(ContainerType::Session);
277        c.install_component("inst-1".into(), fresh_executor(), Box::new(AnonContext))
278            .unwrap();
279        assert_eq!(c.state_of("inst-1"), Some(LifecycleState::Configured));
280        c.activate("inst-1").unwrap();
281        assert_eq!(c.state_of("inst-1"), Some(LifecycleState::Active));
282        c.passivate("inst-1").unwrap();
283        assert_eq!(c.state_of("inst-1"), Some(LifecycleState::Passive));
284        c.activate("inst-1").unwrap(); // re-activate
285        assert_eq!(c.state_of("inst-1"), Some(LifecycleState::Active));
286        c.remove("inst-1").unwrap();
287        assert_eq!(c.state_of("inst-1"), None); // removed.
288    }
289
290    #[test]
291    fn passivate_in_configured_state_invalid() {
292        let c = Container::new(ContainerType::Session);
293        c.install_component("inst-1".into(), fresh_executor(), Box::new(AnonContext))
294            .unwrap();
295        let err = c.passivate("inst-1").unwrap_err();
296        assert!(matches!(err, ContainerError::InvalidState { .. }));
297    }
298
299    #[test]
300    fn unknown_instance_returns_not_found() {
301        let c = Container::new(ContainerType::Session);
302        let err = c.activate("inst-x").unwrap_err();
303        assert!(matches!(err, ContainerError::InstanceNotFound(_)));
304    }
305
306    #[test]
307    fn instance_count_tracks_install_and_remove() {
308        let c = Container::new(ContainerType::Service);
309        c.install_component("a".into(), fresh_executor(), Box::new(AnonContext))
310            .unwrap();
311        c.install_component("b".into(), fresh_executor(), Box::new(AnonContext))
312            .unwrap();
313        assert_eq!(c.instance_count(), 2);
314        c.activate("a").unwrap();
315        c.remove("a").unwrap();
316        assert_eq!(c.instance_count(), 1);
317    }
318}