quantrs2_device/distributed/
orchestrator.rs1use std::collections::HashMap;
13use std::sync::{Arc, Mutex};
14use std::time::{Duration, Instant};
15
16use petgraph::algo::toposort;
17use petgraph::graph::DiGraph;
18
19use super::config::*;
20use super::types::*;
21
22impl DistributedQuantumOrchestrator {
23 pub fn new(config: DistributedOrchestratorConfig) -> Self {
25 Self {
26 config,
27 nodes: Arc::new(Mutex::new(HashMap::new())),
28 workflows: Arc::new(Mutex::new(HashMap::new())),
29 execution_counter: Arc::new(Mutex::new(0)),
30 }
31 }
32
33 pub fn execute_distributed(&self, circuit: &str) -> Result<DistributedExecutionResult, String> {
41 if circuit.trim().is_empty() {
42 return Err("execute_distributed: empty circuit description provided".to_string());
43 }
44
45 let nodes = self
46 .nodes
47 .lock()
48 .unwrap_or_else(std::sync::PoisonError::into_inner);
49 let selected = nodes
50 .values()
51 .filter(|node| matches!(node.status, NodeStatus::Available))
52 .min_by_key(|node| node.node_id.clone());
53 let selected = match selected {
54 Some(node) => node.clone(),
55 None => {
56 return Err(format!(
57 "execute_distributed: no available nodes registered ({} nodes known, none Available)",
58 nodes.len()
59 ));
60 }
61 };
62 drop(nodes);
63
64 let mut counter = self
65 .execution_counter
66 .lock()
67 .unwrap_or_else(std::sync::PoisonError::into_inner);
68 *counter += 1;
69 let execution_id = format!("dist-exec-{:08}", *counter);
70 drop(counter);
71
72 Err(format!(
77 "execute_distributed[{execution_id}]: node '{}' selected and available, but network \
78 dispatch is not implemented in this build (requires a real inter-node transport); \
79 refusing to fabricate execution results",
80 selected.node_id
81 ))
82 }
83
84 pub fn add_node(&mut self, node_info: NodeInfo) -> Result<(), String> {
86 if node_info.node_id.trim().is_empty() {
87 return Err("add_node: node_id must not be empty".to_string());
88 }
89 let mut nodes = self
90 .nodes
91 .lock()
92 .unwrap_or_else(std::sync::PoisonError::into_inner);
93 nodes.insert(node_info.node_id.clone(), node_info);
94 Ok(())
95 }
96
97 pub fn remove_node(&mut self, node_id: &str) -> Result<(), String> {
99 let mut nodes = self
100 .nodes
101 .lock()
102 .unwrap_or_else(std::sync::PoisonError::into_inner);
103 match nodes.remove(node_id) {
104 Some(_) => Ok(()),
105 None => Err(format!("remove_node: node '{node_id}' is not registered")),
106 }
107 }
108
109 pub fn get_node_status(&self, node_id: &str) -> Result<NodeStatus, String> {
111 let nodes = self
112 .nodes
113 .lock()
114 .unwrap_or_else(std::sync::PoisonError::into_inner);
115 nodes
116 .get(node_id)
117 .map(|node| node.status.clone())
118 .ok_or_else(|| format!("get_node_status: node '{node_id}' is not registered"))
119 }
120
121 pub fn schedule_workflow(&self, workflow: DistributedWorkflow) -> Result<String, String> {
127 if workflow.steps.is_empty() {
128 return Err("schedule_workflow: workflow has no steps".to_string());
129 }
130
131 let mut graph = DiGraph::<&str, ()>::new();
134 let mut node_index = HashMap::new();
135 for step in &workflow.steps {
136 let idx = graph.add_node(step.as_str());
137 node_index.insert(step.as_str(), idx);
138 }
139 for (step, deps) in &workflow.dependencies {
140 let Some(&step_idx) = node_index.get(step.as_str()) else {
141 return Err(format!(
142 "schedule_workflow: dependency entry references unknown step '{step}'"
143 ));
144 };
145 for dep in deps {
146 let Some(&dep_idx) = node_index.get(dep.as_str()) else {
147 return Err(format!(
148 "schedule_workflow: step '{step}' depends on unknown step '{dep}'"
149 ));
150 };
151 graph.add_edge(dep_idx, step_idx, ());
152 }
153 }
154 if toposort(&graph, None).is_err() {
155 return Err(
156 "schedule_workflow: workflow dependency graph contains a cycle".to_string(),
157 );
158 }
159
160 let mut counter = self
161 .execution_counter
162 .lock()
163 .unwrap_or_else(std::sync::PoisonError::into_inner);
164 *counter += 1;
165 let generated_id = if workflow.workflow_id.trim().is_empty() {
166 format!("workflow-{:08}", *counter)
167 } else {
168 workflow.workflow_id.clone()
169 };
170 drop(counter);
171
172 let mut workflows = self
173 .workflows
174 .lock()
175 .unwrap_or_else(std::sync::PoisonError::into_inner);
176 workflows.insert(generated_id.clone(), workflow);
177 Ok(generated_id)
178 }
179
180 pub fn node_count(&self) -> usize {
183 self.nodes
184 .lock()
185 .unwrap_or_else(std::sync::PoisonError::into_inner)
186 .len()
187 }
188
189 pub fn workflow_count(&self) -> usize {
191 self.workflows
192 .lock()
193 .unwrap_or_else(std::sync::PoisonError::into_inner)
194 .len()
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use std::net::SocketAddr;
202
203 fn make_node(id: &str, status: NodeStatus) -> NodeInfo {
204 NodeInfo {
205 node_id: id.to_string(),
206 address: "127.0.0.1:8000".parse::<SocketAddr>().unwrap(),
207 capabilities: NodeCapabilities::default(),
208 status,
209 last_heartbeat: Some(Instant::now()),
210 }
211 }
212
213 #[test]
214 fn test_orchestrator_real_node_registry() {
215 let mut orchestrator =
216 DistributedQuantumOrchestrator::new(DistributedOrchestratorConfig::default());
217 assert_eq!(orchestrator.node_count(), 0);
218
219 orchestrator
220 .add_node(make_node("node_a", NodeStatus::Available))
221 .unwrap();
222 assert_eq!(orchestrator.node_count(), 1);
223 assert!(matches!(
224 orchestrator.get_node_status("node_a").unwrap(),
225 NodeStatus::Available
226 ));
227
228 let err = orchestrator.get_node_status("node_missing").unwrap_err();
231 assert!(err.contains("not registered"));
232
233 orchestrator.remove_node("node_a").unwrap();
234 assert_eq!(orchestrator.node_count(), 0);
235 assert!(orchestrator.remove_node("node_a").is_err());
236 }
237
238 #[test]
239 fn test_execute_distributed_honest_when_no_nodes() {
240 let orchestrator =
241 DistributedQuantumOrchestrator::new(DistributedOrchestratorConfig::default());
242 let result = orchestrator.execute_distributed("H 0; CNOT 0 1");
243 let err = result.unwrap_err();
244 assert!(err.contains("no available nodes"));
245 }
246
247 #[test]
248 fn test_execute_distributed_honest_error_no_fake_success() {
249 let mut orchestrator =
250 DistributedQuantumOrchestrator::new(DistributedOrchestratorConfig::default());
251 orchestrator
252 .add_node(make_node("node_a", NodeStatus::Available))
253 .unwrap();
254 let result = orchestrator.execute_distributed("H 0");
258 let err = result.unwrap_err();
259 assert!(err.contains("network"));
260 assert!(err.contains("node_a"));
261 }
262
263 #[test]
264 fn test_schedule_workflow_rejects_cycle() {
265 let orchestrator =
266 DistributedQuantumOrchestrator::new(DistributedOrchestratorConfig::default());
267 let mut dependencies = HashMap::new();
268 dependencies.insert("step_a".to_string(), vec!["step_b".to_string()]);
269 dependencies.insert("step_b".to_string(), vec!["step_a".to_string()]);
270 let workflow = DistributedWorkflow {
271 workflow_id: String::new(),
272 workflow_type: DistributedWorkflowType::Sequential,
273 steps: vec!["step_a".to_string(), "step_b".to_string()],
274 dependencies,
275 };
276 let err = orchestrator.schedule_workflow(workflow).unwrap_err();
277 assert!(err.contains("cycle"));
278 }
279
280 #[test]
281 fn test_schedule_workflow_accepts_valid_dag_and_generates_id() {
282 let orchestrator =
283 DistributedQuantumOrchestrator::new(DistributedOrchestratorConfig::default());
284 let mut dependencies = HashMap::new();
285 dependencies.insert("step_b".to_string(), vec!["step_a".to_string()]);
286 let workflow = DistributedWorkflow {
287 workflow_id: String::new(),
288 workflow_type: DistributedWorkflowType::Sequential,
289 steps: vec!["step_a".to_string(), "step_b".to_string()],
290 dependencies,
291 };
292 let id = orchestrator.schedule_workflow(workflow).unwrap();
293 assert!(id.starts_with("workflow-"));
294 assert_eq!(orchestrator.workflow_count(), 1);
295 }
296}