Skip to main content

zerodds_corba_dnc/
execution.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! ExecutionManager / DomainApplicationManager — D&C §9.
5//!
6//! Spec §9.1.1: `ExecutionManager` ist das Top-Level-Objekt; bietet
7//! `preparePlan(DPD) -> DomainApplicationManager`. Spec §9.1.2:
8//! `DomainApplicationManager` startet einen Plan auf den Nodes mit
9//! `startLaunch` + `finishLaunch`.
10
11use alloc::collections::BTreeMap;
12use alloc::string::String;
13use alloc::vec::Vec;
14
15use crate::plan::{DeploymentPlan, PlanError};
16
17/// `DomainApplication` — D&C §9.1.3. Ein laufender Plan-Run.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct DomainApplication {
20    /// Plan-Label.
21    pub plan_label: String,
22    /// Plan-UUID.
23    pub plan_uuid: String,
24    /// Aktive Instances + Nodes (instance_name → node_name).
25    pub running_instances: BTreeMap<String, String>,
26    /// Lifecycle-Status (`Prepared`, `Launched`, `Finished`).
27    pub state: AppState,
28}
29
30/// Lifecycle-State einer DomainApplication.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum AppState {
33    /// Plan ist `prepared` (D&C §9.1.1 `preparePlan`).
34    Prepared,
35    /// `startLaunch` wurde gerufen.
36    Launched,
37    /// `finishLaunch` ist abgeschlossen.
38    Running,
39    /// `destroyApplication` ist abgeschlossen.
40    Finished,
41}
42
43/// `DomainApplicationManager` — D&C §9.1.2.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct DomainApplicationManager {
46    plan: DeploymentPlan,
47    state: AppState,
48}
49
50impl DomainApplicationManager {
51    /// Konstruktor — D&C §9.1.1 `preparePlan`.
52    ///
53    /// # Errors
54    /// `PlanError`, wenn der Plan inkonsistent ist (Validation
55    /// fehlschlaegt).
56    pub fn prepare_plan(plan: DeploymentPlan) -> Result<Self, PlanError> {
57        plan.validate()?;
58        Ok(Self {
59            plan,
60            state: AppState::Prepared,
61        })
62    }
63
64    /// Spec D&C §9.1.2 `startLaunch` — bereitet die Instances vor,
65    /// fordert Resources auf den Nodes an.
66    ///
67    /// # Errors
68    /// Static-String wenn der Manager nicht im `Prepared`-State ist.
69    pub fn start_launch(&mut self) -> Result<DomainApplication, &'static str> {
70        if self.state != AppState::Prepared {
71            return Err("startLaunch requires Prepared state");
72        }
73        self.state = AppState::Launched;
74        let mut running = BTreeMap::new();
75        for inst in &self.plan.instances {
76            running.insert(inst.name.clone(), inst.node.clone());
77        }
78        Ok(DomainApplication {
79            plan_label: self.plan.label.clone(),
80            plan_uuid: self.plan.uuid.clone(),
81            running_instances: running,
82            state: AppState::Launched,
83        })
84    }
85
86    /// Spec D&C §9.1.2 `finishLaunch` — schliesst die Connection-Phase
87    /// ab und gibt `running` als finalen Zustand zurueck.
88    ///
89    /// # Errors
90    /// Static-String wenn nicht im `Launched`-State.
91    pub fn finish_launch(&mut self, app: &mut DomainApplication) -> Result<(), &'static str> {
92        if self.state != AppState::Launched {
93            return Err("finishLaunch requires Launched state");
94        }
95        self.state = AppState::Running;
96        app.state = AppState::Running;
97        Ok(())
98    }
99
100    /// Spec D&C §9.1.2 `destroyApplication` — Tear-down.
101    ///
102    /// # Errors
103    /// Static-String wenn nicht im `Running`-State.
104    pub fn destroy_application(&mut self, app: &mut DomainApplication) -> Result<(), &'static str> {
105        if self.state != AppState::Running {
106            return Err("destroyApplication requires Running state");
107        }
108        self.state = AppState::Finished;
109        app.state = AppState::Finished;
110        app.running_instances.clear();
111        Ok(())
112    }
113
114    /// Aktueller Lifecycle-State.
115    #[must_use]
116    pub fn state(&self) -> AppState {
117        self.state
118    }
119
120    /// Plan-Reference.
121    #[must_use]
122    pub fn plan(&self) -> &DeploymentPlan {
123        &self.plan
124    }
125}
126
127/// `ExecutionManager` — D&C §9.1.1. Top-Level-Service.
128#[derive(Debug, Default, Clone, PartialEq, Eq)]
129pub struct ExecutionManager {
130    plans: Vec<String>,
131}
132
133impl ExecutionManager {
134    /// Konstruktor.
135    #[must_use]
136    pub fn new() -> Self {
137        Self::default()
138    }
139
140    /// Spec §9.1.1 `preparePlan(DeploymentPlan)` — erzeugt einen
141    /// `DomainApplicationManager`.
142    ///
143    /// # Errors
144    /// `PlanError` wenn der Plan inkonsistent ist.
145    pub fn prepare_plan(
146        &mut self,
147        plan: DeploymentPlan,
148    ) -> Result<DomainApplicationManager, PlanError> {
149        let label = plan.label.clone();
150        let mgr = DomainApplicationManager::prepare_plan(plan)?;
151        self.plans.push(label);
152        Ok(mgr)
153    }
154
155    /// Spec §9.1.1 `getManagers` — Liste aller Plan-Labels, fuer die
156    /// dieses ExecutionManager preparePlan aufgerufen hat.
157    #[must_use]
158    pub fn managed_plans(&self) -> &[String] {
159        &self.plans
160    }
161}
162
163#[cfg(test)]
164#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
165mod tests {
166    use super::*;
167    use crate::plan::{ImplementationDescription, InstanceDeploymentDescription};
168    use alloc::string::ToString;
169
170    fn sample_plan() -> DeploymentPlan {
171        DeploymentPlan {
172            label: "P".into(),
173            uuid: "u".into(),
174            realizes: "P".into(),
175            implementations: alloc::vec![ImplementationDescription {
176                label: "I".into(),
177                uuid: "iu".into(),
178                artifacts: alloc::vec!["a.so".into()],
179                realizes: "IDL:X:1.0".into(),
180                ..ImplementationDescription::default()
181            }],
182            instances: alloc::vec![InstanceDeploymentDescription {
183                name: "x".into(),
184                implementation: "I".into(),
185                node: "N".into(),
186                ..InstanceDeploymentDescription::default()
187            }],
188            connections: alloc::vec![],
189        }
190    }
191
192    #[test]
193    fn full_lifecycle_round_trip() {
194        let mut em = ExecutionManager::new();
195        let mut mgr = em.prepare_plan(sample_plan()).unwrap();
196        assert_eq!(mgr.state(), AppState::Prepared);
197        let mut app = mgr.start_launch().unwrap();
198        assert_eq!(app.state, AppState::Launched);
199        mgr.finish_launch(&mut app).unwrap();
200        assert_eq!(app.state, AppState::Running);
201        assert_eq!(
202            app.running_instances.get("x").map(String::as_str),
203            Some("N")
204        );
205        mgr.destroy_application(&mut app).unwrap();
206        assert_eq!(app.state, AppState::Finished);
207        assert!(app.running_instances.is_empty());
208    }
209
210    #[test]
211    fn invalid_plan_rejected_at_prepare() {
212        let mut em = ExecutionManager::new();
213        let mut bad = sample_plan();
214        bad.instances[0].implementation = "MissingImpl".into();
215        assert!(em.prepare_plan(bad).is_err());
216    }
217
218    #[test]
219    fn double_start_fails() {
220        let mut mgr = DomainApplicationManager::prepare_plan(sample_plan()).unwrap();
221        let _ = mgr.start_launch().unwrap();
222        let mut second = DomainApplication {
223            plan_label: "P".into(),
224            plan_uuid: "u".into(),
225            running_instances: BTreeMap::new(),
226            state: AppState::Prepared,
227        };
228        assert!(mgr.start_launch().is_err());
229        assert!(mgr.finish_launch(&mut second).is_err() || mgr.state() != AppState::Launched);
230    }
231
232    #[test]
233    fn destroy_before_running_fails() {
234        let mut mgr = DomainApplicationManager::prepare_plan(sample_plan()).unwrap();
235        let mut app = mgr.start_launch().unwrap();
236        assert!(mgr.destroy_application(&mut app).is_err());
237    }
238
239    #[test]
240    fn execution_manager_lists_prepared_plans() {
241        let mut em = ExecutionManager::new();
242        em.prepare_plan(sample_plan()).unwrap();
243        assert_eq!(em.managed_plans(), &["P".to_string()]);
244    }
245}