module_orchestrator/domain/
service.rs1use std::collections::HashSet;
2use std::sync::Arc;
3
4use modkit::registry::ModuleRegistry;
5use modkit::runtime::ModuleManager;
6use modkit_macros::domain_model;
7
8use super::model::{DeploymentMode, InstanceInfo, ModuleInfo};
9
10#[domain_model]
12struct CompiledModule {
13 name: String,
14 capabilities: Vec<String>,
15 deps: Vec<String>,
16}
17
18#[domain_model]
20pub struct ModulesService {
21 compiled: Vec<CompiledModule>,
23 module_manager: Arc<ModuleManager>,
25}
26
27impl ModulesService {
28 #[must_use]
33 pub fn new(registry: &ModuleRegistry, module_manager: Arc<ModuleManager>) -> Self {
34 let compiled: Vec<CompiledModule> = registry
35 .modules()
36 .iter()
37 .map(|entry| CompiledModule {
38 name: entry.name().to_owned(),
39 capabilities: entry
40 .caps()
41 .labels()
42 .iter()
43 .map(|s| (*s).to_owned())
44 .collect(),
45 deps: entry.deps().iter().map(|d| (*d).to_owned()).collect(),
46 })
47 .collect();
48
49 Self {
50 compiled,
51 module_manager,
52 }
53 }
54
55 #[must_use]
57 pub fn list_modules(&self) -> Vec<ModuleInfo> {
58 let mut modules = Vec::new();
59 let mut seen_names = HashSet::new();
60
61 for cm in &self.compiled {
63 seen_names.insert(cm.name.clone());
64
65 let instances = self.get_module_instances(&cm.name);
66
67 modules.push(ModuleInfo {
68 name: cm.name.clone(),
69 capabilities: cm.capabilities.clone(),
70 dependencies: cm.deps.clone(),
71 deployment_mode: DeploymentMode::CompiledIn,
72 instances,
73 });
74 }
75
76 for instance in self.module_manager.all_instances() {
79 if seen_names.contains(&instance.module) {
80 continue;
81 }
82 seen_names.insert(instance.module.clone());
83
84 let instances = self.get_module_instances(&instance.module);
85
86 modules.push(ModuleInfo {
87 name: instance.module.clone(),
88 capabilities: vec![],
89 dependencies: vec![],
90 deployment_mode: DeploymentMode::OutOfProcess,
91 instances,
92 });
93 }
94
95 modules.sort_by(|a, b| a.name.cmp(&b.name));
97
98 modules
99 }
100
101 fn get_module_instances(&self, module_name: &str) -> Vec<InstanceInfo> {
102 self.module_manager
103 .instances_of(module_name)
104 .into_iter()
105 .map(|inst| {
106 let grpc_services = inst
107 .grpc_services
108 .iter()
109 .map(|(name, ep)| (name.clone(), ep.uri.clone()))
110 .collect();
111
112 InstanceInfo {
113 instance_id: inst.instance_id,
114 version: inst.version.clone(),
115 state: inst.state(),
116 grpc_services,
117 }
118 })
119 .collect()
120 }
121}
122
123#[cfg(test)]
124#[cfg_attr(coverage_nightly, coverage(off))]
125mod tests {
126 use super::*;
127 use modkit::registry::RegistryBuilder;
128 use modkit::runtime::{Endpoint, InstanceState, ModuleInstance, ModuleManager};
129 use uuid::Uuid;
130
131 type ModuleSpec = (&'static str, &'static [&'static str], bool, bool);
135
136 #[domain_model]
137 #[derive(Default)]
138 struct DummyCore;
139 #[async_trait::async_trait]
140 impl modkit::Module for DummyCore {
141 async fn init(&self, _ctx: &modkit::context::ModuleCtx) -> anyhow::Result<()> {
142 Ok(())
143 }
144 }
145
146 #[domain_model]
147 #[derive(Default, Clone)]
148 struct DummyRest;
149 impl modkit::contracts::RestApiCapability for DummyRest {
150 fn register_rest(
151 &self,
152 _ctx: &modkit::context::ModuleCtx,
153 _router: axum::Router,
154 _openapi: &dyn modkit::api::OpenApiRegistry,
155 ) -> anyhow::Result<axum::Router> {
156 Ok(axum::Router::new())
157 }
158 }
159
160 #[domain_model]
161 #[derive(Default)]
162 struct DummySystem;
163 #[async_trait::async_trait]
164 impl modkit::contracts::SystemCapability for DummySystem {}
165
166 fn build_registry(modules: &[ModuleSpec]) -> ModuleRegistry {
167 let mut b = RegistryBuilder::default();
168 for &(name, deps, has_rest, has_system) in modules {
169 b.register_core_with_meta(name, deps, Arc::new(DummyCore));
170 if has_rest {
171 b.register_rest_with_meta(name, Arc::new(DummyRest));
172 }
173 if has_system {
174 b.register_system_with_meta(name, Arc::new(DummySystem));
175 }
176 }
177 b.build_topo_sorted().unwrap()
178 }
179
180 #[test]
183 fn list_compiled_in_modules_from_registry() {
184 let registry = build_registry(&[
185 ("api_gateway", &[], true, true),
186 ("nodes_registry", &["api_gateway"], true, false),
187 ]);
188 let manager = Arc::new(ModuleManager::new());
189 let svc = ModulesService::new(®istry, manager);
190 let modules = svc.list_modules();
191
192 assert_eq!(modules.len(), 2);
193 assert_eq!(modules[0].name, "api_gateway");
195 assert_eq!(modules[0].deployment_mode, DeploymentMode::CompiledIn);
196 assert!(modules[0].capabilities.contains(&"rest".to_owned()));
197 assert!(modules[0].capabilities.contains(&"system".to_owned()));
198 assert!(modules[0].instances.is_empty());
199
200 assert_eq!(modules[1].name, "nodes_registry");
201 assert_eq!(modules[1].dependencies, vec!["api_gateway"]);
202 }
203
204 #[test]
205 fn dynamic_external_instances_appear_as_out_of_process() {
206 let registry = build_registry(&[]);
207 let manager = Arc::new(ModuleManager::new());
208
209 let instance = Arc::new(
210 ModuleInstance::new("external_svc", Uuid::new_v4())
211 .with_version("2.0.0")
212 .with_grpc_service("ext.Service", Endpoint::http("127.0.0.1", 9001)),
213 );
214 manager.register_instance(instance);
215
216 let svc = ModulesService::new(®istry, manager);
217 let modules = svc.list_modules();
218
219 assert_eq!(modules.len(), 1);
220 assert_eq!(modules[0].name, "external_svc");
221 assert_eq!(modules[0].deployment_mode, DeploymentMode::OutOfProcess);
222 assert_eq!(modules[0].instances.len(), 1);
223 assert_eq!(modules[0].instances[0].version, Some("2.0.0".to_owned()));
224 assert!(
225 modules[0].instances[0]
226 .grpc_services
227 .contains_key("ext.Service")
228 );
229 }
230
231 #[test]
232 fn compiled_in_modules_show_instances_from_manager() {
233 let registry = build_registry(&[("grpc_hub", &[], false, true)]);
234 let manager = Arc::new(ModuleManager::new());
235
236 let instance =
237 Arc::new(ModuleInstance::new("grpc_hub", Uuid::new_v4()).with_version("0.1.0"));
238 manager.register_instance(instance);
239
240 let svc = ModulesService::new(®istry, manager);
241 let modules = svc.list_modules();
242
243 assert_eq!(modules.len(), 1);
244 assert_eq!(modules[0].name, "grpc_hub");
245 assert_eq!(modules[0].deployment_mode, DeploymentMode::CompiledIn);
246 assert_eq!(modules[0].instances.len(), 1);
247 }
248
249 #[test]
250 fn instance_state_maps_correctly() {
251 let registry = build_registry(&[]);
252 let manager = Arc::new(ModuleManager::new());
253
254 let instance = Arc::new(ModuleInstance::new("svc", Uuid::new_v4()));
255 manager.register_instance(instance);
257
258 let svc = ModulesService::new(®istry, manager);
259 let modules = svc.list_modules();
260
261 assert_eq!(modules[0].instances[0].state, InstanceState::Registered);
262 }
263
264 #[test]
265 fn result_is_sorted_by_name() {
266 let registry =
267 build_registry(&[("zebra", &[], false, false), ("alpha", &[], false, false)]);
268 let manager = Arc::new(ModuleManager::new());
269
270 let svc = ModulesService::new(®istry, manager);
271 let modules = svc.list_modules();
272
273 assert_eq!(modules[0].name, "alpha");
274 assert_eq!(modules[1].name, "zebra");
275 }
276}