1use std::collections::{BTreeMap, HashMap};
13use std::path::{Path, PathBuf};
14use std::sync::{Arc, Mutex, MutexGuard};
15
16use crate::config::templates::is_segment;
17use crate::config::types::ServiceTemplateConfig;
18
19#[path = "lifecycle_run.rs"]
20mod run;
21
22#[path = "plan.rs"]
23pub mod plan;
24#[path = "site/processes.rs"]
25mod processes;
26#[path = "site.rs"]
27pub mod site;
28#[path = "templates.rs"]
29pub mod templates;
30#[path = "trial.rs"]
31pub mod trial;
32
33const APP: &str = "liminal";
35
36#[path = "site/lifecycle_records.rs"]
37mod records;
38pub use records::{ServiceRecord, ServiceState, Transition};
39
40#[derive(Clone, Debug, PartialEq, Eq)]
42pub enum LifecycleError {
43 UnknownTemplate(String),
45 NameInvalid,
47 NameTaken(String),
49 UnknownService(String),
51 NotNow {
53 verb: &'static str,
55 state: ServiceState,
57 running: bool,
59 },
60 Placement(String),
62 NoPlacer,
64 Store(String),
66 Process(String),
68}
69
70impl std::fmt::Display for LifecycleError {
71 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 match self {
73 Self::UnknownTemplate(name) => write!(formatter, "unknown_template: {name}"),
74 Self::NameInvalid => formatter
75 .write_str("name_invalid: a name is one segment of letters, digits, '-' or '_'"),
76 Self::NameTaken(name) => write!(formatter, "name_taken: {name}"),
77 Self::UnknownService(id) => write!(formatter, "unknown_service: {id}"),
78 Self::NotNow {
79 verb,
80 state,
81 running,
82 } => write!(
83 formatter,
84 "not_now: {verb} does not apply to a service {state:?} with a process {}",
85 if *running { "running" } else { "stopped" }
86 ),
87 Self::Placement(why) => write!(formatter, "placement_refused: {why}"),
88 Self::NoPlacer => {
89 formatter.write_str("placement_unavailable: no app_credential_path is configured")
90 }
91 Self::Store(why) => write!(formatter, "registry_unavailable: {why}"),
92 Self::Process(why) => write!(formatter, "process_failed: {why}"),
93 }
94 }
95}
96
97impl std::error::Error for LifecycleError {}
98
99pub trait Placer: Send + Sync + std::fmt::Debug {
101 fn place(
107 &self,
108 operation: &str,
109 child: (&str, &str),
110 parent: (&str, &str),
111 ) -> Result<(), String>;
112}
113
114#[path = "lifecycle_place.rs"]
115mod place;
116#[path = "site/placement.rs"]
117mod placement;
118pub use placement::LysPlacer;
119
120#[derive(Clone, Debug, PartialEq, Eq)]
123pub struct ProcessGrant {
124 pub service: String,
126 pub subjects: Vec<String>,
128}
129
130impl ProcessGrant {
131 #[must_use]
133 pub fn serves(&self, subject: &str) -> bool {
134 self.subjects.iter().any(|own| own == subject)
135 }
136}
137
138const ADDRESS_WAIT: std::time::Duration = std::time::Duration::from_secs(5);
141
142pub struct Lifecycle {
144 templates: BTreeMap<String, ServiceTemplateConfig>,
145 dir: PathBuf,
146 network: String,
147 server: std::sync::OnceLock<String>,
149 told: (Mutex<()>, std::sync::Condvar),
153 placer: Option<Arc<dyn Placer>>,
154 registry: Mutex<Registry>,
155 site_config: std::sync::OnceLock<site::storage::SiteBindings>,
156 changed: std::sync::Condvar,
157 site_console: std::sync::OnceLock<std::sync::Weak<crate::console::ConsoleHub>>,
158}
159
160#[derive(Default)]
161struct Registry {
162 records: BTreeMap<String, ServiceRecord>,
163 running: HashMap<String, run::Running>,
164 tokens: HashMap<[u8; 32], ProcessGrant>,
165 stopping_all: bool,
166 construction: site::Construction,
167 construction_hash: Option<haematite::tree::Hash>,
168 trials: BTreeMap<String, BTreeMap<u64, trial::Registration>>,
169 poisoned: bool,
170}
171
172impl Lifecycle {
173 pub const OPEN_NETWORK: &str = "open";
176
177 pub fn open(
185 templates: &[ServiceTemplateConfig],
186 dir: PathBuf,
187 network: String,
188 placer: Option<Arc<dyn Placer>>,
189 ) -> Result<Arc<Self>, LifecycleError> {
190 site::create_private_dir(&dir).map_err(|error| store(&dir, &error))?;
191 let snapshot = site::storage::load(&dir)?;
192 Ok(Self::open_snapshot(
193 templates, dir, network, placer, snapshot,
194 ))
195 }
196
197 fn open_snapshot(
198 templates: &[ServiceTemplateConfig],
199 dir: PathBuf,
200 network: String,
201 placer: Option<Arc<dyn Placer>>,
202 snapshot: site::storage::Snapshot,
203 ) -> Arc<Self> {
204 Arc::new(Self {
205 templates: templates
206 .iter()
207 .map(|template| (template.name.clone(), template.clone()))
208 .collect(),
209 dir,
210 network,
211 server: std::sync::OnceLock::new(),
212 told: (Mutex::new(()), std::sync::Condvar::new()),
213 placer,
214 site_config: std::sync::OnceLock::new(),
215 changed: std::sync::Condvar::new(),
216 site_console: std::sync::OnceLock::new(),
217 registry: Mutex::new(Registry {
218 records: snapshot.records,
219 construction: snapshot.construction,
220 ..Registry::default()
221 }),
222 })
223 }
224
225 #[must_use]
228 pub fn places(&self) -> bool {
229 self.placer.is_some()
230 }
231
232 pub fn listening(&self, address: String) {
235 let _first = self.server.set(address);
236 let (lock, told) = &self.told;
237 drop(
238 lock.lock()
239 .unwrap_or_else(std::sync::PoisonError::into_inner),
240 );
241 told.notify_all();
242 }
243
244 pub(super) fn server_address(&self) -> Option<&str> {
249 if let Some(address) = self.server.get() {
250 return Some(address);
251 }
252 let (lock, told) = &self.told;
253 drop(
254 told.wait_timeout_while(
255 lock.lock()
256 .unwrap_or_else(std::sync::PoisonError::into_inner),
257 ADDRESS_WAIT,
258 |()| self.server.get().is_none(),
259 )
260 .unwrap_or_else(std::sync::PoisonError::into_inner),
261 );
262 self.server.get().map(String::as_str)
263 }
264
265 fn lock(&self) -> Result<MutexGuard<'_, Registry>, LifecycleError> {
266 let registry = self
267 .registry
268 .lock()
269 .map_err(|_| LifecycleError::Store("registry_lock_poisoned".into()))?;
270 if registry.poisoned {
271 return Err(LifecycleError::Store(
272 "registry_commit_uncertain: reopen required".into(),
273 ));
274 }
275 Ok(registry)
276 }
277
278 pub fn list(
284 &self,
285 visible: impl Fn(&ServiceRecord) -> bool,
286 ) -> Result<serde_json::Value, LifecycleError> {
287 let registry = self.lock()?;
288 let services: Vec<serde_json::Value> = registry
289 .records
290 .values()
291 .filter(|record| visible(record))
292 .map(|record| {
293 let mut value = serde_json::to_value(record)
294 .map_err(|_| LifecycleError::Store("service_encoding_failed".into()))?;
295 if let Some(object) = value.as_object_mut() {
296 object.insert(
297 "running".to_owned(),
298 registry.running.contains_key(&record.id).into(),
299 );
300 }
301 Ok(value)
302 })
303 .collect::<Result<_, LifecycleError>>()?;
304 let mut templates: Vec<serde_json::Value> = self
305 .templates
306 .values()
307 .map(|template| {
308 serde_json::json!({"name": template.name, "subjects": template.subjects})
309 })
310 .collect();
311 templates.extend(registry.construction.templates.values().filter(|record| !record.retired)
312 .map(|record| serde_json::json!({"name":record.proposal.name,"subjects":record.proposal.subjects})));
313 drop(registry);
314 Ok(serde_json::json!({"templates": templates, "services": services}))
315 }
316
317 pub fn record(&self, id: &str) -> Result<ServiceRecord, LifecycleError> {
322 self.lock()?
323 .records
324 .get(id)
325 .cloned()
326 .ok_or_else(|| LifecycleError::UnknownService(id.to_owned()))
327 }
328
329 pub fn create(
336 &self,
337 template: &str,
338 name: &str,
339 holder: &str,
340 operation: &str,
341 now_ms: u64,
342 ) -> Result<ServiceRecord, LifecycleError> {
343 let found = self
344 .templates
345 .get(template)
346 .ok_or_else(|| LifecycleError::UnknownTemplate(template.to_owned()))?;
347 if !is_segment(name) {
348 return Err(LifecycleError::NameInvalid);
349 }
350 let id = format!("svc-{}", digest(&["liminal-service/v1", holder, operation]));
351 {
352 let registry = self.lock()?;
353 if let Some(record) = registry.records.get(&id) {
354 return Ok(record.clone());
355 }
356 if registry
357 .records
358 .values()
359 .any(|record| record.template.as_deref() == Some(template) && record.name == name)
360 {
361 return Err(LifecycleError::NameTaken(name.to_owned()));
362 }
363 }
364 let subjects: Vec<String> = found
365 .subjects
366 .iter()
367 .map(|root| format!("{root}.{name}"))
368 .collect();
369 let placed = if let Some(placer) = self.placer.as_ref() {
372 self.place_children(placer.as_ref(), &id, &subjects)?;
373 true
374 } else if self.network == Self::OPEN_NETWORK {
375 false
376 } else {
377 return Err(LifecycleError::NoPlacer);
378 };
379 let record = ServiceRecord {
380 id: id.clone(),
381 name: name.to_owned(),
382 template: Some(template.to_owned()),
383 purpose: None,
384 contracts: BTreeMap::new(),
385 builder: None,
386 builder_grants: None,
387 builder_bound: false,
388 approved_by: Vec::new(),
389 responsible: None,
390 plan: plan::Plan::default(),
391 approvals: Vec::new(),
392 subjects,
393 state: ServiceState::Asleep,
394 created_by: holder.to_owned(),
395 transitions: vec![Transition {
396 from: ServiceState::Asleep,
397 to: ServiceState::Asleep,
398 holder: holder.to_owned(),
399 at_ms: now_ms,
400 note: Some("created".to_owned()),
401 }],
402 placed,
403 };
404 let mut registry = self.lock()?;
405 if let Some(raced) = registry.records.get(&id) {
406 return Ok(raced.clone());
407 }
408 self.keep(&mut registry, &record)?;
409 registry.records.insert(id, record.clone());
410 drop(registry);
411 Ok(record)
412 }
413
414 #[must_use]
418 pub fn placed_since(&self, subject: &str, since_ms: u64) -> bool {
419 self.lock().is_ok_and(|registry| {
420 registry.records.values().any(|record| {
421 record.state != ServiceState::Retired
422 && record.subjects.iter().any(|served| served == subject)
423 && record
424 .transitions
425 .first()
426 .is_some_and(|created| created.at_ms >= since_ms)
427 })
428 })
429 }
430
431 fn keep(&self, registry: &mut Registry, record: &ServiceRecord) -> Result<(), LifecycleError> {
432 site::storage::keep(self, registry, record)
433 }
434
435 pub(in crate::services) fn log_path(&self, id: &str) -> PathBuf {
437 self.dir.join(format!("{id}.log"))
438 }
439}
440
441fn revoke(registry: &mut Registry, id: &str) -> Option<run::Running> {
444 let process = registry.running.remove(id)?;
445 registry.tokens.remove(&process.digest);
446 Some(process)
447}
448
449fn store(path: &Path, error: &std::io::Error) -> LifecycleError {
450 LifecycleError::Store(format!("{}: {}", path.display(), error.kind()))
451}
452
453fn digest(parts: &[&str]) -> String {
455 let mut hasher = blake3::Hasher::new();
456 for part in parts {
457 hasher.update(&u64::try_from(part.len()).unwrap_or(u64::MAX).to_be_bytes());
458 hasher.update(part.as_bytes());
459 }
460 hasher.finalize().to_hex()[..32].to_owned()
461}
462
463#[cfg(test)]
464#[path = "lifecycle_place_tests.rs"]
465mod place_tests;
466#[cfg(test)]
467#[path = "lifecycle_tests.rs"]
468mod tests;
469
470impl std::fmt::Debug for Lifecycle {
471 fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
472 out.debug_struct("Lifecycle").finish_non_exhaustive()
473 }
474}