1use std::collections::{BTreeMap, HashMap};
16use std::io::Write;
17use std::path::{Path, PathBuf};
18use std::sync::{Arc, Mutex, MutexGuard};
19
20use serde::{Deserialize, Serialize};
21
22use super::lys_http::LysHttp;
23use crate::config::templates::is_segment;
24use crate::config::types::ServiceTemplateConfig;
25
26#[path = "lifecycle_run.rs"]
27mod run;
28
29const APP: &str = "liminal";
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum ServiceState {
36 Asleep,
38 Live,
40 Held,
42 Retired,
44}
45
46#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
48pub struct Transition {
49 pub from: ServiceState,
51 pub to: ServiceState,
53 pub holder: String,
55 pub at_ms: u64,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub note: Option<String>,
60}
61
62#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
64pub struct ServiceRecord {
65 pub id: String,
67 pub name: String,
69 pub template: String,
71 pub subjects: Vec<String>,
73 pub state: ServiceState,
75 pub created_by: String,
77 pub transitions: Vec<Transition>,
79}
80
81#[derive(Clone, Debug, PartialEq, Eq)]
83pub enum LifecycleError {
84 UnknownTemplate(String),
86 NameInvalid,
88 NameTaken(String),
90 UnknownService(String),
92 NotNow {
94 verb: &'static str,
96 state: ServiceState,
98 running: bool,
100 },
101 Placement(String),
103 NoPlacer,
105 Store(String),
107 Process(String),
109}
110
111impl std::fmt::Display for LifecycleError {
112 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 match self {
114 Self::UnknownTemplate(name) => write!(formatter, "unknown_template: {name}"),
115 Self::NameInvalid => formatter
116 .write_str("name_invalid: a name is one segment of letters, digits, '-' or '_'"),
117 Self::NameTaken(name) => write!(formatter, "name_taken: {name}"),
118 Self::UnknownService(id) => write!(formatter, "unknown_service: {id}"),
119 Self::NotNow {
120 verb,
121 state,
122 running,
123 } => write!(
124 formatter,
125 "not_now: {verb} does not apply to a service {state:?} with a process {}",
126 if *running { "running" } else { "stopped" }
127 ),
128 Self::Placement(why) => write!(formatter, "placement_refused: {why}"),
129 Self::NoPlacer => {
130 formatter.write_str("placement_unavailable: no app_credential_path is configured")
131 }
132 Self::Store(why) => write!(formatter, "registry_unavailable: {why}"),
133 Self::Process(why) => write!(formatter, "process_failed: {why}"),
134 }
135 }
136}
137
138impl std::error::Error for LifecycleError {}
139
140pub trait Placer: Send + Sync + std::fmt::Debug {
142 fn place(
148 &self,
149 operation: &str,
150 child: (&str, &str),
151 parent: (&str, &str),
152 ) -> Result<(), String>;
153}
154
155pub struct LysPlacer {
157 http: Arc<dyn LysHttp>,
158 credential: String,
159}
160
161impl LysPlacer {
162 #[must_use]
164 pub fn new(http: Arc<dyn LysHttp>, credential: String) -> Self {
165 Self { http, credential }
166 }
167}
168
169impl std::fmt::Debug for LysPlacer {
170 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 formatter.debug_struct("LysPlacer").finish_non_exhaustive()
172 }
173}
174
175impl Placer for LysPlacer {
176 fn place(
177 &self,
178 operation: &str,
179 child: (&str, &str),
180 parent: (&str, &str),
181 ) -> Result<(), String> {
182 let route = format!("apps/{APP}/placements");
183 let body = serde_json::json!({
184 "operation": operation,
185 "child": {"kind": child.0, "id": child.1},
186 "parent": {"kind": parent.0, "id": parent.1},
187 });
188 let answer = self
189 .http
190 .post(&self.credential, &route, &body)
191 .map_err(|error| error.0)?;
192 if answer.succeeded() {
193 Ok(())
194 } else {
195 Err(answer.refusal())
196 }
197 }
198}
199
200#[derive(Clone, Debug, PartialEq, Eq)]
203pub struct ProcessGrant {
204 pub service: String,
206 pub subjects: Vec<String>,
208}
209
210impl ProcessGrant {
211 #[must_use]
213 pub fn serves(&self, subject: &str) -> bool {
214 self.subjects.iter().any(|own| own == subject)
215 }
216}
217
218const ADDRESS_WAIT: std::time::Duration = std::time::Duration::from_secs(5);
221
222#[derive(Debug)]
224pub struct Lifecycle {
225 templates: BTreeMap<String, ServiceTemplateConfig>,
226 dir: PathBuf,
227 network: String,
228 server: std::sync::OnceLock<String>,
230 told: (Mutex<()>, std::sync::Condvar),
234 placer: Option<Arc<dyn Placer>>,
235 registry: Mutex<Registry>,
236}
237
238#[derive(Debug, Default)]
239struct Registry {
240 records: BTreeMap<String, ServiceRecord>,
241 running: HashMap<String, run::Running>,
242 tokens: HashMap<[u8; 32], ProcessGrant>,
243 stopping_all: bool,
244}
245
246impl Lifecycle {
247 pub fn open(
255 templates: &[ServiceTemplateConfig],
256 dir: PathBuf,
257 network: String,
258 placer: Option<Arc<dyn Placer>>,
259 ) -> Result<Arc<Self>, LifecycleError> {
260 std::fs::create_dir_all(&dir).map_err(|error| store(&dir, &error))?;
261 let mut records = BTreeMap::new();
262 for entry in std::fs::read_dir(&dir).map_err(|error| store(&dir, &error))? {
263 let path = entry.map_err(|error| store(&dir, &error))?.path();
264 if path.extension().is_none_or(|ext| ext != "json") {
265 continue;
266 }
267 let bytes = std::fs::read(&path).map_err(|error| store(&path, &error))?;
268 let record: ServiceRecord = serde_json::from_slice(&bytes).map_err(|error| {
269 LifecycleError::Store(format!(
270 "{} is not a service record: {error}",
271 path.display()
272 ))
273 })?;
274 records.insert(record.id.clone(), record);
275 }
276 Ok(Arc::new(Self {
277 templates: templates
278 .iter()
279 .map(|template| (template.name.clone(), template.clone()))
280 .collect(),
281 dir,
282 network,
283 server: std::sync::OnceLock::new(),
284 told: (Mutex::new(()), std::sync::Condvar::new()),
285 placer,
286 registry: Mutex::new(Registry {
287 records,
288 ..Registry::default()
289 }),
290 }))
291 }
292
293 pub fn listening(&self, address: String) {
296 let _first = self.server.set(address);
297 let (lock, told) = &self.told;
298 drop(
299 lock.lock()
300 .unwrap_or_else(std::sync::PoisonError::into_inner),
301 );
302 told.notify_all();
303 }
304
305 pub(super) fn server_address(&self) -> Option<&str> {
310 if let Some(address) = self.server.get() {
311 return Some(address);
312 }
313 let (lock, told) = &self.told;
314 drop(
315 told.wait_timeout_while(
316 lock.lock()
317 .unwrap_or_else(std::sync::PoisonError::into_inner),
318 ADDRESS_WAIT,
319 |()| self.server.get().is_none(),
320 )
321 .unwrap_or_else(std::sync::PoisonError::into_inner),
322 );
323 self.server.get().map(String::as_str)
324 }
325
326 fn lock(&self) -> Result<MutexGuard<'_, Registry>, LifecycleError> {
327 self.registry
328 .lock()
329 .map_err(|_| LifecycleError::Store("the registry lock is poisoned".to_owned()))
330 }
331
332 pub fn list(
338 &self,
339 visible: impl Fn(&ServiceRecord) -> bool,
340 ) -> Result<serde_json::Value, LifecycleError> {
341 let registry = self.lock()?;
342 let services: Vec<serde_json::Value> = registry
343 .records
344 .values()
345 .filter(|record| visible(record))
346 .map(|record| {
347 let mut value = serde_json::to_value(record).unwrap_or_default();
348 if let Some(object) = value.as_object_mut() {
349 object.insert(
350 "running".to_owned(),
351 registry.running.contains_key(&record.id).into(),
352 );
353 }
354 value
355 })
356 .collect();
357 drop(registry);
358 let templates: Vec<serde_json::Value> = self
359 .templates
360 .values()
361 .map(|template| {
362 serde_json::json!({"name": template.name, "subjects": template.subjects})
363 })
364 .collect();
365 Ok(serde_json::json!({"templates": templates, "services": services}))
366 }
367
368 pub fn record(&self, id: &str) -> Result<ServiceRecord, LifecycleError> {
373 self.lock()?
374 .records
375 .get(id)
376 .cloned()
377 .ok_or_else(|| LifecycleError::UnknownService(id.to_owned()))
378 }
379
380 pub fn create(
387 &self,
388 template: &str,
389 name: &str,
390 holder: &str,
391 operation: &str,
392 now_ms: u64,
393 ) -> Result<ServiceRecord, LifecycleError> {
394 let found = self
395 .templates
396 .get(template)
397 .ok_or_else(|| LifecycleError::UnknownTemplate(template.to_owned()))?;
398 if !is_segment(name) {
399 return Err(LifecycleError::NameInvalid);
400 }
401 let id = format!("svc-{}", digest(&["liminal-service/v1", holder, operation]));
402 {
403 let registry = self.lock()?;
404 if let Some(record) = registry.records.get(&id) {
405 return Ok(record.clone());
406 }
407 if registry
408 .records
409 .values()
410 .any(|record| record.template == template && record.name == name)
411 {
412 return Err(LifecycleError::NameTaken(name.to_owned()));
413 }
414 }
415 let subjects: Vec<String> = found
416 .subjects
417 .iter()
418 .map(|root| format!("{root}.{name}"))
419 .collect();
420 let placer = self.placer.as_ref().ok_or(LifecycleError::NoPlacer)?;
421 let network = ("liminal.network", self.network.as_str());
422 let mut children = vec![("liminal.service", id.as_str())];
423 children.extend(
424 subjects
425 .iter()
426 .map(|subject| ("liminal.subject", subject.as_str())),
427 );
428 for child in children {
429 let op = format!("op-{}", digest(&["liminal-place/v1", child.0, child.1]));
430 placer
431 .place(&op, child, network)
432 .map_err(LifecycleError::Placement)?;
433 }
434 let record = ServiceRecord {
435 id: id.clone(),
436 name: name.to_owned(),
437 template: template.to_owned(),
438 subjects,
439 state: ServiceState::Asleep,
440 created_by: holder.to_owned(),
441 transitions: vec![Transition {
442 from: ServiceState::Asleep,
443 to: ServiceState::Asleep,
444 holder: holder.to_owned(),
445 at_ms: now_ms,
446 note: Some("created".to_owned()),
447 }],
448 };
449 let mut registry = self.lock()?;
450 if let Some(raced) = registry.records.get(&id) {
451 return Ok(raced.clone());
452 }
453 self.keep(&record)?;
454 registry.records.insert(id, record.clone());
455 drop(registry);
456 Ok(record)
457 }
458
459 pub fn deploy(
465 self: &Arc<Self>,
466 id: &str,
467 holder: &str,
468 ) -> Result<ServiceRecord, LifecycleError> {
469 self.start(id, holder, "deploy", ServiceState::Asleep)
470 }
471
472 pub fn wake(self: &Arc<Self>, id: &str, holder: &str) -> Result<ServiceRecord, LifecycleError> {
477 self.start(id, holder, "wake", ServiceState::Held)
478 }
479
480 fn start(
481 self: &Arc<Self>,
482 id: &str,
483 holder: &str,
484 verb: &'static str,
485 from: ServiceState,
486 ) -> Result<ServiceRecord, LifecycleError> {
487 let mut registry = self.lock()?;
488 let record = registry
489 .records
490 .get(id)
491 .cloned()
492 .ok_or_else(|| LifecycleError::UnknownService(id.to_owned()))?;
493 let running = registry.running.contains_key(id);
494 if record.state != from || running || registry.stopping_all {
495 return Err(LifecycleError::NotNow {
496 verb,
497 state: record.state,
498 running,
499 });
500 }
501 let template = self
502 .templates
503 .get(&record.template)
504 .ok_or_else(|| LifecycleError::UnknownTemplate(record.template.clone()))?;
505 let (process, digest) = run::start(self, &record, template, holder)?;
506 registry.tokens.insert(
507 digest,
508 ProcessGrant {
509 service: record.id.clone(),
510 subjects: record.subjects.clone(),
511 },
512 );
513 registry.running.insert(record.id.clone(), process);
514 drop(registry);
515 Ok(record)
516 }
517
518 pub fn hold(
523 &self,
524 id: &str,
525 holder: &str,
526 now_ms: u64,
527 ) -> Result<ServiceRecord, LifecycleError> {
528 self.stop(id, holder, now_ms, "hold", ServiceState::Held)
529 }
530
531 pub fn retire(
536 &self,
537 id: &str,
538 holder: &str,
539 now_ms: u64,
540 ) -> Result<ServiceRecord, LifecycleError> {
541 self.stop(id, holder, now_ms, "retire", ServiceState::Retired)
542 }
543
544 fn stop(
545 &self,
546 id: &str,
547 holder: &str,
548 now_ms: u64,
549 verb: &'static str,
550 to: ServiceState,
551 ) -> Result<ServiceRecord, LifecycleError> {
552 let (state, stopping) = {
553 let mut registry = self.lock()?;
554 let state = registry
555 .records
556 .get(id)
557 .map(|record| record.state)
558 .ok_or_else(|| LifecycleError::UnknownService(id.to_owned()))?;
559 let running = registry.running.contains_key(id);
560 let applies = match to {
561 ServiceState::Held => state == ServiceState::Live || running,
562 _ => state != ServiceState::Retired,
563 };
564 if !applies {
565 return Err(LifecycleError::NotNow {
566 verb,
567 state,
568 running,
569 });
570 }
571 let process = revoke(&mut registry, id);
572 drop(registry);
573 (state, process)
574 };
575 if let Some(process) = stopping {
576 run::stop(&process, self.timeout_of(id)).map_err(LifecycleError::Process)?;
577 }
578 self.transition(id, state, to, holder, now_ms, None)
579 }
580
581 fn timeout_of(&self, id: &str) -> u64 {
582 self.lock()
583 .ok()
584 .and_then(|registry| {
585 registry
586 .records
587 .get(id)
588 .map(|record| record.template.clone())
589 })
590 .and_then(|template| self.templates.get(&template).map(|t| t.stop_timeout_ms))
591 .unwrap_or(1)
592 }
593
594 fn transition(
595 &self,
596 id: &str,
597 from: ServiceState,
598 to: ServiceState,
599 holder: &str,
600 now_ms: u64,
601 note: Option<String>,
602 ) -> Result<ServiceRecord, LifecycleError> {
603 let mut registry = self.lock()?;
604 let mut record = registry
605 .records
606 .get(id)
607 .cloned()
608 .ok_or_else(|| LifecycleError::UnknownService(id.to_owned()))?;
609 record.state = to;
610 record.transitions.push(Transition {
611 from,
612 to,
613 holder: holder.to_owned(),
614 at_ms: now_ms,
615 note,
616 });
617 self.keep(&record)?;
618 registry.records.insert(id.to_owned(), record.clone());
619 drop(registry);
620 Ok(record)
621 }
622
623 #[must_use]
625 pub fn admit_process(&self, token: &[u8]) -> Option<ProcessGrant> {
626 let digest = *blake3::hash(token).as_bytes();
627 self.lock().ok()?.tokens.get(&digest).cloned()
628 }
629
630 pub fn mark_live(&self, id: &str, now_ms: u64) -> Result<(), LifecycleError> {
636 let (state, starter) = {
637 let registry = self.lock()?;
638 let state = registry
639 .records
640 .get(id)
641 .map(|record| record.state)
642 .ok_or_else(|| LifecycleError::UnknownService(id.to_owned()))?;
643 let starter = registry
644 .running
645 .get(id)
646 .map_or_else(|| "process".to_owned(), |process| process.starter.clone());
647 drop(registry);
648 (state, starter)
649 };
650 if state == ServiceState::Live {
651 return Ok(());
652 }
653 self.transition(id, state, ServiceState::Live, &starter, now_ms, None)
654 .map(|_| ())
655 }
656
657 pub fn restart_live(self: &Arc<Self>, now_ms: u64) -> Vec<(String, LifecycleError)> {
661 let live: Vec<(String, String)> = self.lock().map_or_else(
662 |_| Vec::new(),
663 |registry| {
664 registry
665 .records
666 .values()
667 .filter(|record| record.state == ServiceState::Live)
668 .map(|record| {
669 let starter = record
670 .transitions
671 .last()
672 .map_or_else(|| record.created_by.clone(), |t| t.holder.clone());
673 (record.id.clone(), starter)
674 })
675 .collect()
676 },
677 );
678 let mut failed = Vec::new();
679 for (id, starter) in live {
680 if let Err(error) = self.start(&id, &starter, "restart", ServiceState::Live) {
681 let note = Some(format!("restart failed: {error}"));
682 let _kept = self.transition(
683 &id,
684 ServiceState::Live,
685 ServiceState::Asleep,
686 "process",
687 now_ms,
688 note,
689 );
690 failed.push((id, error));
691 }
692 }
693 failed
694 }
695
696 #[must_use]
700 pub fn placed_since(&self, subject: &str, since_ms: u64) -> bool {
701 self.lock().is_ok_and(|registry| {
702 registry.records.values().any(|record| {
703 record.state != ServiceState::Retired
704 && record.subjects.iter().any(|served| served == subject)
705 && record
706 .transitions
707 .first()
708 .is_some_and(|created| created.at_ms >= since_ms)
709 })
710 })
711 }
712
713 pub fn shutdown_begun(&self) {
718 if let Ok(mut registry) = self.lock() {
719 registry.stopping_all = true;
720 }
721 }
722
723 pub fn shutdown(&self) {
726 self.shutdown_begun();
727 let ids: Vec<String> = match self.lock() {
728 Ok(registry) => registry.running.keys().cloned().collect(),
729 Err(_) => return,
730 };
731 for id in ids {
732 let timeout = self.timeout_of(&id);
733 let process = match self.lock() {
734 Ok(mut registry) => revoke(&mut registry, &id),
735 Err(_) => return,
736 };
737 if let Some(process) = process
738 && let Err(error) = run::stop(&process, timeout)
739 {
740 tracing::warn!(service = %id, error, "a service process did not stop at shutdown");
741 }
742 }
743 }
744
745 fn exited(&self, id: &str, status: &str, now_ms: u64) {
749 let (asked, stopping) = match self.lock() {
750 Ok(mut registry) => {
751 let asked = registry.running.remove(id).is_none_or(|process| {
752 registry.tokens.remove(&process.digest);
753 false
754 });
755 (asked, registry.stopping_all)
756 }
757 Err(_) => return,
758 };
759 if asked {
760 return;
761 }
762 if stopping {
763 tracing::info!(
764 service = id,
765 status,
766 "a service process exited during shutdown; its state stays"
767 );
768 return;
769 }
770 let Ok(record) = self.record(id) else {
771 return;
772 };
773 let note = Some(format!("exited unasked: {status}"));
774 if let Err(error) = self.transition(
775 id,
776 record.state,
777 ServiceState::Asleep,
778 "process",
779 now_ms,
780 note,
781 ) {
782 tracing::warn!(service = id, %error, "a service exit could not be recorded");
783 }
784 }
785
786 fn keep(&self, record: &ServiceRecord) -> Result<(), LifecycleError> {
789 let path = self.dir.join(format!("{}.json", record.id));
790 let temporary = self.dir.join(format!("{}.json.new", record.id));
791 let bytes = serde_json::to_vec_pretty(record)
792 .map_err(|error| LifecycleError::Store(error.to_string()))?;
793 let mut file =
794 std::fs::File::create(&temporary).map_err(|error| store(&temporary, &error))?;
795 file.write_all(&bytes)
796 .and_then(|()| file.sync_all())
797 .map_err(|error| store(&temporary, &error))?;
798 std::fs::rename(&temporary, &path).map_err(|error| store(&path, &error))?;
799 std::fs::File::open(&self.dir)
800 .and_then(|dir| dir.sync_all())
801 .map_err(|error| store(&self.dir, &error))
802 }
803
804 fn log_path(&self, id: &str) -> PathBuf {
806 self.dir.join(format!("{id}.log"))
807 }
808}
809
810fn revoke(registry: &mut Registry, id: &str) -> Option<run::Running> {
813 let process = registry.running.remove(id)?;
814 registry.tokens.remove(&process.digest);
815 Some(process)
816}
817
818fn store(path: &Path, error: &std::io::Error) -> LifecycleError {
819 LifecycleError::Store(format!("{}: {}", path.display(), error.kind()))
820}
821
822fn digest(parts: &[&str]) -> String {
824 let mut hasher = blake3::Hasher::new();
825 for part in parts {
826 hasher.update(&u64::try_from(part.len()).unwrap_or(u64::MAX).to_be_bytes());
827 hasher.update(part.as_bytes());
828 }
829 hasher.finalize().to_hex()[..32].to_owned()
830}
831
832#[cfg(test)]
833#[path = "lifecycle_tests.rs"]
834mod tests;