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` is the top-level object; it offers
7//! `preparePlan(DPD) -> DomainApplicationManager`. Spec §9.1.2:
8//! `DomainApplicationManager` launches a plan on the nodes via
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. A running 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    /// Active 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 of a DomainApplication.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum AppState {
33    /// Plan is `prepared` (D&C §9.1.1 `preparePlan`).
34    Prepared,
35    /// `startLaunch` has been called.
36    Launched,
37    /// `finishLaunch` has completed.
38    Running,
39    /// `destroyApplication` has completed.
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    /// Constructor — D&C §9.1.1 `preparePlan`.
52    ///
53    /// # Errors
54    /// `PlanError` if the plan is inconsistent (validation fails).
55    pub fn prepare_plan(plan: DeploymentPlan) -> Result<Self, PlanError> {
56        plan.validate()?;
57        Ok(Self {
58            plan,
59            state: AppState::Prepared,
60        })
61    }
62
63    /// Spec D&C §9.1.2 `startLaunch` — prepares the instances and
64    /// requests resources on the nodes.
65    ///
66    /// # Errors
67    /// Static string if the manager is not in the `Prepared` state.
68    pub fn start_launch(&mut self) -> Result<DomainApplication, &'static str> {
69        if self.state != AppState::Prepared {
70            return Err("startLaunch requires Prepared state");
71        }
72        self.state = AppState::Launched;
73        let mut running = BTreeMap::new();
74        for inst in &self.plan.instances {
75            running.insert(inst.name.clone(), inst.node.clone());
76        }
77        Ok(DomainApplication {
78            plan_label: self.plan.label.clone(),
79            plan_uuid: self.plan.uuid.clone(),
80            running_instances: running,
81            state: AppState::Launched,
82        })
83    }
84
85    /// Spec D&C §9.1.2 `finishLaunch` — completes the connection phase
86    /// and returns `running` as the final state.
87    ///
88    /// # Errors
89    /// Static string if not in the `Launched` state.
90    pub fn finish_launch(&mut self, app: &mut DomainApplication) -> Result<(), &'static str> {
91        if self.state != AppState::Launched {
92            return Err("finishLaunch requires Launched state");
93        }
94        self.state = AppState::Running;
95        app.state = AppState::Running;
96        Ok(())
97    }
98
99    /// Spec D&C §9.1.2 `destroyApplication` — tear-down.
100    ///
101    /// # Errors
102    /// Static string if not in the `Running` state.
103    pub fn destroy_application(&mut self, app: &mut DomainApplication) -> Result<(), &'static str> {
104        if self.state != AppState::Running {
105            return Err("destroyApplication requires Running state");
106        }
107        self.state = AppState::Finished;
108        app.state = AppState::Finished;
109        app.running_instances.clear();
110        Ok(())
111    }
112
113    /// Current lifecycle state.
114    #[must_use]
115    pub fn state(&self) -> AppState {
116        self.state
117    }
118
119    /// Plan reference.
120    #[must_use]
121    pub fn plan(&self) -> &DeploymentPlan {
122        &self.plan
123    }
124}
125
126/// `ExecutionManager` — D&C §9.1.1. Top-level service.
127#[derive(Debug, Default, Clone, PartialEq, Eq)]
128pub struct ExecutionManager {
129    plans: Vec<String>,
130}
131
132impl ExecutionManager {
133    /// Constructor.
134    #[must_use]
135    pub fn new() -> Self {
136        Self::default()
137    }
138
139    /// Spec §9.1.1 `preparePlan(DeploymentPlan)` — creates a
140    /// `DomainApplicationManager`.
141    ///
142    /// # Errors
143    /// `PlanError` if the plan is inconsistent.
144    pub fn prepare_plan(
145        &mut self,
146        plan: DeploymentPlan,
147    ) -> Result<DomainApplicationManager, PlanError> {
148        let label = plan.label.clone();
149        let mgr = DomainApplicationManager::prepare_plan(plan)?;
150        self.plans.push(label);
151        Ok(mgr)
152    }
153
154    /// Spec §9.1.1 `getManagers` — list of all plan labels for which
155    /// this ExecutionManager has called preparePlan.
156    #[must_use]
157    pub fn managed_plans(&self) -> &[String] {
158        &self.plans
159    }
160}
161
162#[cfg(test)]
163#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
164mod tests {
165    use super::*;
166    use crate::plan::{ImplementationDescription, InstanceDeploymentDescription};
167    use alloc::string::ToString;
168
169    fn sample_plan() -> DeploymentPlan {
170        DeploymentPlan {
171            label: "P".into(),
172            uuid: "u".into(),
173            realizes: "P".into(),
174            implementations: alloc::vec![ImplementationDescription {
175                label: "I".into(),
176                uuid: "iu".into(),
177                artifacts: alloc::vec!["a.so".into()],
178                realizes: "IDL:X:1.0".into(),
179                ..ImplementationDescription::default()
180            }],
181            instances: alloc::vec![InstanceDeploymentDescription {
182                name: "x".into(),
183                implementation: "I".into(),
184                node: "N".into(),
185                ..InstanceDeploymentDescription::default()
186            }],
187            connections: alloc::vec![],
188        }
189    }
190
191    #[test]
192    fn full_lifecycle_round_trip() {
193        let mut em = ExecutionManager::new();
194        let mut mgr = em.prepare_plan(sample_plan()).unwrap();
195        assert_eq!(mgr.state(), AppState::Prepared);
196        let mut app = mgr.start_launch().unwrap();
197        assert_eq!(app.state, AppState::Launched);
198        mgr.finish_launch(&mut app).unwrap();
199        assert_eq!(app.state, AppState::Running);
200        assert_eq!(
201            app.running_instances.get("x").map(String::as_str),
202            Some("N")
203        );
204        mgr.destroy_application(&mut app).unwrap();
205        assert_eq!(app.state, AppState::Finished);
206        assert!(app.running_instances.is_empty());
207    }
208
209    #[test]
210    fn invalid_plan_rejected_at_prepare() {
211        let mut em = ExecutionManager::new();
212        let mut bad = sample_plan();
213        bad.instances[0].implementation = "MissingImpl".into();
214        assert!(em.prepare_plan(bad).is_err());
215    }
216
217    #[test]
218    fn double_start_fails() {
219        let mut mgr = DomainApplicationManager::prepare_plan(sample_plan()).unwrap();
220        let _ = mgr.start_launch().unwrap();
221        let mut second = DomainApplication {
222            plan_label: "P".into(),
223            plan_uuid: "u".into(),
224            running_instances: BTreeMap::new(),
225            state: AppState::Prepared,
226        };
227        assert!(mgr.start_launch().is_err());
228        assert!(mgr.finish_launch(&mut second).is_err() || mgr.state() != AppState::Launched);
229    }
230
231    #[test]
232    fn destroy_before_running_fails() {
233        let mut mgr = DomainApplicationManager::prepare_plan(sample_plan()).unwrap();
234        let mut app = mgr.start_launch().unwrap();
235        assert!(mgr.destroy_application(&mut app).is_err());
236    }
237
238    #[test]
239    fn execution_manager_lists_prepared_plans() {
240        let mut em = ExecutionManager::new();
241        em.prepare_plan(sample_plan()).unwrap();
242        assert_eq!(em.managed_plans(), &["P".to_string()]);
243    }
244}