Skip to main content

frame_core/
registry.rs

1//! Linearized component registration and lifecycle operations.
2
3use std::collections::{HashMap, HashSet};
4use std::num::NonZeroUsize;
5use std::sync::{Arc, Mutex};
6
7use beamr::atom::Atom;
8use beamr::namespace::NamespaceId;
9use beamr::scheduler::Scheduler;
10
11use crate::capability::{
12    Capability, CapabilityCheckError, CapabilityChecker, CapabilityMutationError, CapabilityTable,
13    CheckVerdict, HostCapabilityFacade,
14};
15use crate::component::{ComponentId, ComponentMeta};
16use crate::error::{FailureReason, RegistryError};
17use crate::event::{EventHub, LifecycleState, LifecycleSubscription};
18use crate::supervision::{LifecycleConfig, SharedStatus, StatusState, TreeHandle, start_tree};
19
20/// Result of an idempotent stop operation.
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum StopOutcome {
23    /// A live process tree completed the ordered drain.
24    Stopped,
25    /// No process tree was live, so no transition was needed.
26    AlreadyStopped,
27}
28
29/// Result of an idempotent remove operation.
30#[derive(Clone, Debug, Eq, PartialEq)]
31pub enum RemoveOutcome {
32    /// Code and registry state were removed after an orderly drain.
33    Removed,
34    /// Code and registry state were removed after host-forced failed-tree cleanup.
35    RemovedAfterForcedCleanup(ForcedCleanupReport),
36    /// No registration remained for the identity.
37    AlreadyRemoved,
38}
39
40/// Registry for component definitions and their native process trees.
41pub struct ComponentRegistry {
42    scheduler: Arc<Scheduler>,
43    config: LifecycleConfig,
44    records: Mutex<HashMap<ComponentId, Arc<ComponentRecord>>>,
45    capabilities: CapabilityTable,
46    events: Arc<EventHub>,
47    /// Per-IDENTITY monotonic incarnation history, surviving remove: every
48    /// committed entry into Running continues that identity's count, so a
49    /// remove + re-register cannot restart it and hand a replacement
50    /// incarnation an ordinal some stale handle already holds (F-2a's
51    /// held-vs-current rule needs the ordinals distinguishable — the F-5a
52    /// R2 red evidence pinned exactly that gap). Keyed per identity rather
53    /// than registry-wide so an identity's ordinals depend only on its OWN
54    /// lifecycle history — R6's determinism: permuting unrelated
55    /// components' start order never changes a published generation.
56    ///
57    /// Growth bound: one `u64` per distinct component identity EVER
58    /// registered in this registry's lifetime — configuration-scale (an
59    /// installation's component roster), never traffic-scale (no entry is
60    /// added by starts, stops, updates, or messages; re-registering a
61    /// known identity reuses its entry). Entries are deliberately never
62    /// pruned: dropping one on remove is exactly the reset that re-mints
63    /// a stale handle's ordinal, so pruning would reopen the fence.
64    incarnation_history: Mutex<HashMap<ComponentId, u64>>,
65}
66
67impl ComponentRegistry {
68    /// Creates a registry over an embedding-owned scheduler.
69    ///
70    /// The scheduler is never shut down by the registry. The caller must remove
71    /// or stop every component before shutting it down.
72    #[must_use]
73    pub fn new(scheduler: Arc<Scheduler>, config: LifecycleConfig) -> Self {
74        Self {
75            scheduler,
76            config,
77            records: Mutex::new(HashMap::new()),
78            capabilities: CapabilityTable::new(),
79            events: Arc::new(EventHub::default()),
80            incarnation_history: Mutex::new(HashMap::new()),
81        }
82    }
83
84    /// Adds a bounded lifecycle subscriber.
85    ///
86    /// Each subscriber drops its own oldest event on overflow and increments
87    /// its lag counter; publication never waits for a consumer.
88    ///
89    /// # Errors
90    ///
91    /// Returns a synchronization failure if a prior panic poisoned the stream.
92    pub fn subscribe(
93        &self,
94        capacity: NonZeroUsize,
95    ) -> Result<LifecycleSubscription, RegistryError> {
96        self.events
97            .subscribe(capacity)
98            .map_err(|_| RegistryError::SynchronizationPoisoned)
99    }
100
101    /// Borrows the embedding host's sole grant mutation and inspection facade.
102    ///
103    /// This Rust host value has no BEAM representation and is never passed to a
104    /// component entrypoint; component-facing code receives check-only paths.
105    #[must_use]
106    pub fn host_capabilities(&self) -> HostCapabilityFacade<'_> {
107        HostCapabilityFacade::new(&self.capabilities, Arc::clone(&self.events))
108    }
109
110    /// Creates a retained check-only path for one registered component.
111    ///
112    /// # Errors
113    ///
114    /// Refuses a missing component or poisoned capability table.
115    pub fn capability_checker(
116        &self,
117        component_id: ComponentId,
118    ) -> Result<CapabilityChecker, CapabilityMutationError> {
119        self.host_capabilities().checker(component_id)
120    }
121
122    /// Performs a fresh capability check for one host-mediated consuming act.
123    ///
124    /// Denials are returned as typed verdicts and emitted exactly once on the
125    /// lifecycle stream. No verdict or authority token is retained.
126    ///
127    /// # Errors
128    ///
129    /// Refuses a missing component or poisoned table/event synchronization.
130    pub fn check_capability(
131        &self,
132        component_id: ComponentId,
133        capability: &Capability,
134    ) -> Result<CheckVerdict, CapabilityCheckError> {
135        let checker = self
136            .capability_checker(component_id)
137            .map_err(|error| match error {
138                CapabilityMutationError::NotRegistered { component_id } => {
139                    CapabilityCheckError::NotRegistered { component_id }
140                }
141                CapabilityMutationError::UndeclaredNeed { .. }
142                | CapabilityMutationError::AlreadyGranted { .. }
143                | CapabilityMutationError::SynchronizationPoisoned => {
144                    CapabilityCheckError::SynchronizationPoisoned
145                }
146            })?;
147        checker.check(capability)
148    }
149
150    /// Validates and atomically registers metadata with its BEAM bytecode.
151    ///
152    /// # Errors
153    ///
154    /// Refuses duplicate identities, missing/cyclic dependency edges,
155    /// capability collisions, duplicate child keys, and absent/invalid policy.
156    pub fn register(&self, meta: ComponentMeta, bytecode: Vec<u8>) -> Result<(), RegistryError> {
157        let mut records = self
158            .records
159            .lock()
160            .map_err(|_| RegistryError::SynchronizationPoisoned)?;
161        validate_registration(&records, &meta, self.config.max_fragment_bytes)?;
162        let id = meta.id;
163        let needs = meta.needs.clone();
164        let record = Arc::new(ComponentRecord::new(meta, bytecode));
165        records.insert(id, record);
166        if self.capabilities.register_component(id, needs).is_err() {
167            records.remove(&id);
168            return Err(RegistryError::SynchronizationPoisoned);
169        }
170        if self
171            .events
172            .publish_transition(id, None, LifecycleState::Registered)
173            .is_err()
174        {
175            self.capabilities
176                .remove_component(id)
177                .map_err(|_| RegistryError::SynchronizationPoisoned)?;
178            records.remove(&id);
179            return Err(RegistryError::SynchronizationPoisoned);
180        }
181        Ok(())
182    }
183
184    /// Starts one component after checking every dependency is Running.
185    ///
186    /// Successful return means every child answered its declared mailbox probe.
187    ///
188    /// # Errors
189    ///
190    /// Returns typed state, dependency, loader, spawn, liveness, or protocol
191    /// failures. A runtime failure leaves an observable Failed registration.
192    pub fn start(&self, id: ComponentId) -> Result<(), RegistryError> {
193        let record = self.record(id)?;
194        claim_operation(&record, id, Operation::Start)?;
195        if let Err(error) = self.check_running_dependencies(&record.meta) {
196            clear_operation(&record)?;
197            return Err(error);
198        }
199        transition(&record, &self.events, id, LifecycleState::Starting, None)?;
200        let load_result = {
201            let bytecode = record
202                .bytecode
203                .lock()
204                .map_err(|_| RegistryError::SynchronizationPoisoned)?;
205            self.scheduler.hot_load_module(&bytecode)
206        };
207        let loaded = match load_result {
208            Ok(loaded) => loaded,
209            Err(error) => {
210                let reason = FailureReason {
211                    child: None,
212                    tombstone: None,
213                    detail: error.to_string(),
214                };
215                transition(
216                    &record,
217                    &self.events,
218                    id,
219                    LifecycleState::Failed,
220                    Some(reason),
221                )?;
222                clear_operation(&record)?;
223                return Err(RegistryError::ModuleLoad {
224                    id,
225                    detail: error.to_string(),
226                });
227            }
228        };
229        record
230            .modules
231            .lock()
232            .map_err(|_| RegistryError::SynchronizationPoisoned)?
233            .push(loaded.module_name);
234        if let Err(error) = self.refuse_deferred_bif_imports(id, loaded.module_name) {
235            transition(
236                &record,
237                &self.events,
238                id,
239                LifecycleState::Failed,
240                Some(failure_from_error(&error)),
241            )?;
242            clear_operation(&record)?;
243            return Err(error);
244        }
245        let policy = record
246            .meta
247            .supervision
248            .clone()
249            .ok_or(RegistryError::UndeclaredSupervision { id })?;
250        match start_tree(
251            id,
252            Arc::clone(&self.scheduler),
253            &record.meta.children,
254            policy,
255            self.config,
256            &record.status,
257            Arc::clone(&self.events),
258        ) {
259            Ok(tree) => {
260                *record
261                    .tree
262                    .lock()
263                    .map_err(|_| RegistryError::SynchronizationPoisoned)? = Some(tree);
264                self.prepare_running_incarnation(id, &record)?;
265                transition(&record, &self.events, id, LifecycleState::Running, None)?;
266                clear_operation(&record)
267            }
268            Err(error) => {
269                let reason = failure_from_error(&error);
270                transition(
271                    &record,
272                    &self.events,
273                    id,
274                    LifecycleState::Failed,
275                    Some(reason),
276                )?;
277                clear_operation(&record)?;
278                Err(error)
279            }
280        }
281    }
282
283    /// Stops a running component by draining children in declaration order and
284    /// then observing the host supervisor's Normal tombstone.
285    ///
286    /// # Errors
287    ///
288    /// Returns typed in-flight, protocol, timeout, or non-normal exit failures.
289    pub fn stop(&self, id: ComponentId) -> Result<StopOutcome, RegistryError> {
290        let record = self.record(id)?;
291        claim_operation(&record, id, Operation::Stop)?;
292        let current = state(&record)?;
293        if matches!(
294            current,
295            LifecycleState::Registered | LifecycleState::Stopped | LifecycleState::Failed
296        ) {
297            clear_operation(&record)?;
298            return Ok(StopOutcome::AlreadyStopped);
299        }
300        transition(&record, &self.events, id, LifecycleState::Stopping, None)?;
301        let tree = record
302            .tree
303            .lock()
304            .map_err(|_| RegistryError::SynchronizationPoisoned)?
305            .take()
306            .ok_or(RegistryError::MonitorTerminated { id })?;
307        match tree.stop(id) {
308            Ok(()) => {
309                clear_runtime(&record)?;
310                transition(&record, &self.events, id, LifecycleState::Stopped, None)?;
311                clear_operation(&record)?;
312                Ok(StopOutcome::Stopped)
313            }
314            Err(error) => {
315                transition(
316                    &record,
317                    &self.events,
318                    id,
319                    LifecycleState::Failed,
320                    Some(failure_from_error(&error)),
321                )?;
322                clear_operation(&record)?;
323                Err(error)
324            }
325        }
326    }
327
328    /// Stops if needed, unloads every loaded generation, and unregisters.
329    ///
330    /// # Errors
331    ///
332    /// Refuses concurrent operations, running dependents, abnormal drain, unsafe
333    /// purge, failed deletion, or post-delete lookup residue.
334    pub fn remove(&self, id: ComponentId) -> Result<RemoveOutcome, RegistryError> {
335        let Some(record) = self.optional_record(id)? else {
336            return Ok(RemoveOutcome::AlreadyRemoved);
337        };
338        claim_operation(&record, id, Operation::Remove)?;
339        if let Err(error) = self.check_running_dependents(id) {
340            clear_operation(&record)?;
341            return Err(error);
342        }
343        let current = state(&record)?;
344        let mut forced_cleanup = None;
345        if current == LifecycleState::Running {
346            transition(&record, &self.events, id, LifecycleState::Stopping, None)?;
347            let tree = record
348                .tree
349                .lock()
350                .map_err(|_| RegistryError::SynchronizationPoisoned)?
351                .take()
352                .ok_or(RegistryError::MonitorTerminated { id })?;
353            match tree.stop(id) {
354                Ok(()) => {
355                    clear_runtime(&record)?;
356                    transition(&record, &self.events, id, LifecycleState::Stopped, None)?;
357                }
358                Err(error) => {
359                    transition(
360                        &record,
361                        &self.events,
362                        id,
363                        LifecycleState::Failed,
364                        Some(failure_from_error(&error)),
365                    )?;
366                    clear_operation(&record)?;
367                    return Err(error);
368                }
369            }
370        } else if current == LifecycleState::Failed {
371            forced_cleanup = self.recover_failed_tree(id, &record)?;
372        }
373        self.unload_modules(id, &record)?;
374        let from = state(&record)?;
375        self.capabilities
376            .remove_component(id)
377            .map_err(|_| RegistryError::SynchronizationPoisoned)?;
378        self.events
379            .publish_transition(id, Some(from), LifecycleState::Removed)
380            .map_err(|_| RegistryError::SynchronizationPoisoned)?;
381        self.records
382            .lock()
383            .map_err(|_| RegistryError::SynchronizationPoisoned)?
384            .remove(&id);
385        Ok(forced_cleanup.map_or(
386            RemoveOutcome::Removed,
387            RemoveOutcome::RemovedAfterForcedCleanup,
388        ))
389    }
390
391    /// Replaces the registered bytecode a component's NEXT start hot-loads
392    /// — the F-7b hot-reload staging seam. The component identity, its
393    /// registered metadata, grants, and durable branch are untouched: the
394    /// branch outlives the bytes.
395    ///
396    /// Only legal while the component is Registered or Stopped: the
397    /// upgrade barrier drains and stops the old incarnation first, so new
398    /// bytes never stage under a live tree.
399    ///
400    /// Staging PURGES the retained old module generation (the C5 law: the
401    /// wall is retention-based, so drain alone never re-opens loading —
402    /// without this purge the third stop→stage→start lap of one
403    /// registration refuses `OldCodeStillRunning`). The purge is the SAFE
404    /// one: after an ordered stop nothing references old code, and a
405    /// `StillReferenced` refusal here is the typed drain-failure signal,
406    /// surfaced, never forced over.
407    ///
408    /// # Errors
409    ///
410    /// Refuses concurrent operations and non-stopped states; surfaces a
411    /// purge refusal as the drain failure it is. All typed.
412    pub fn upgrade_bytecode(
413        &self,
414        id: ComponentId,
415        bytecode: Vec<u8>,
416    ) -> Result<(), RegistryError> {
417        let record = self.record(id)?;
418        claim_operation(&record, id, Operation::Upgrade)?;
419        let current = state(&record)?;
420        if !matches!(
421            current,
422            LifecycleState::Registered | LifecycleState::Stopped
423        ) {
424            clear_operation(&record)?;
425            return Err(RegistryError::UpgradeRequiresStopped { id, state: current });
426        }
427        if let Err(error) = self.purge_retained_old(id, &record) {
428            clear_operation(&record)?;
429            return Err(error);
430        }
431        *record
432            .bytecode
433            .lock()
434            .map_err(|_| RegistryError::SynchronizationPoisoned)? = bytecode;
435        clear_operation(&record)
436    }
437
438    /// Purges the retained old generation of every module this component
439    /// has loaded, keeping the current generation live for rollback reads.
440    /// Distinct from [`Self::unload_modules`]: staging keeps the
441    /// registration and the current code; only the retained OLD version
442    /// goes.
443    fn purge_retained_old(
444        &self,
445        id: ComponentId,
446        record: &ComponentRecord,
447    ) -> Result<(), RegistryError> {
448        let modules = record
449            .modules
450            .lock()
451            .map_err(|_| RegistryError::SynchronizationPoisoned)?
452            .clone();
453        for module in unique_modules(modules) {
454            if self.scheduler.check_old_code(module) {
455                self.scheduler.purge_module(module).map_err(|error| {
456                    RegistryError::ModulePurge {
457                        id,
458                        module: self.module_name(module),
459                        detail: error.to_string(),
460                    }
461                })?;
462            }
463        }
464        Ok(())
465    }
466
467    /// The component's live incarnation ordinal — the per-identity mint,
468    /// strictly increasing across every entry into Running (the
469    /// registry-side freshness witness of the F-7b dev status report).
470    ///
471    /// # Errors
472    ///
473    /// Refuses unknown identities and non-running components, typed.
474    pub fn component_incarnation(&self, id: ComponentId) -> Result<u64, RegistryError> {
475        let record = self.record(id)?;
476        require_running(&record, id)?;
477        Ok(record
478            .incarnation
479            .load(std::sync::atomic::Ordering::Acquire))
480    }
481
482    /// The current VM code generation of the component's own module — the
483    /// VM-side freshness witness of the F-7b dev status report (beamr's
484    /// per-name monotonic counter, surfaced not interpreted).
485    ///
486    /// # Errors
487    ///
488    /// Refuses unknown identities, components that never loaded, and a
489    /// module absent from the VM, all typed.
490    pub fn component_module_generation(&self, id: ComponentId) -> Result<u64, RegistryError> {
491        let record = self.record(id)?;
492        let module = record
493            .modules
494            .lock()
495            .map_err(|_| RegistryError::SynchronizationPoisoned)?
496            .last()
497            .copied()
498            .ok_or_else(|| RegistryError::ModuleLoad {
499                id,
500                detail: "component has never loaded a module".to_owned(),
501            })?;
502        self.scheduler
503            .lookup_module_in(NamespaceId::DEFAULT, module)
504            .map(|loaded| loaded.generation())
505            .ok_or_else(|| RegistryError::ModuleLoad {
506                id,
507                detail: format!("module {} is absent from the VM", self.module_name(module)),
508            })
509    }
510
511    /// Sends an integer mailbox command to a named live child.
512    ///
513    /// # Errors
514    ///
515    /// Refuses non-running components, unknown children, and monitor failures.
516    pub fn send_child(
517        &self,
518        id: ComponentId,
519        child: impl Into<String>,
520        message: i64,
521    ) -> Result<(), RegistryError> {
522        let record = self.record(id)?;
523        require_running(&record, id)?;
524        record
525            .tree
526            .lock()
527            .map_err(|_| RegistryError::SynchronizationPoisoned)?
528            .as_ref()
529            .ok_or(RegistryError::MonitorTerminated { id })?
530            .send(id, child.into(), message)
531    }
532
533    /// Runs the child's declared liveness mailbox round-trip and returns its
534    /// integer state reply.
535    ///
536    /// # Errors
537    ///
538    /// Refuses non-running components, unknown children, timeout, or failure.
539    pub fn probe_child(
540        &self,
541        id: ComponentId,
542        child: impl Into<String>,
543    ) -> Result<i64, RegistryError> {
544        let record = self.record(id)?;
545        require_running(&record, id)?;
546        record
547            .tree
548            .lock()
549            .map_err(|_| RegistryError::SynchronizationPoisoned)?
550            .as_ref()
551            .ok_or(RegistryError::MonitorTerminated { id })?
552            .probe(id, child.into())
553    }
554
555    fn record(&self, id: ComponentId) -> Result<Arc<ComponentRecord>, RegistryError> {
556        self.optional_record(id)?
557            .ok_or(RegistryError::NotRegistered { id })
558    }
559
560    fn optional_record(
561        &self,
562        id: ComponentId,
563    ) -> Result<Option<Arc<ComponentRecord>>, RegistryError> {
564        self.records
565            .lock()
566            .map(|records| records.get(&id).cloned())
567            .map_err(|_| RegistryError::SynchronizationPoisoned)
568    }
569
570    fn check_running_dependencies(&self, meta: &ComponentMeta) -> Result<(), RegistryError> {
571        for required in &meta.requires {
572            let record = self.record(*required)?;
573            let required_state = state(&record)?;
574            if required_state != LifecycleState::Running {
575                return Err(RegistryError::DependencyNotRunning {
576                    component: meta.id,
577                    required: *required,
578                    state: required_state,
579                });
580            }
581        }
582        Ok(())
583    }
584
585    fn check_running_dependents(&self, id: ComponentId) -> Result<(), RegistryError> {
586        let records = self
587            .records
588            .lock()
589            .map_err(|_| RegistryError::SynchronizationPoisoned)?;
590        for (dependent, record) in records.iter() {
591            if record.meta.requires.contains(&id) && state(record)? == LifecycleState::Running {
592                return Err(RegistryError::RunningDependent {
593                    dependent: *dependent,
594                    required: id,
595                });
596            }
597        }
598        Ok(())
599    }
600
601    fn unload_modules(
602        &self,
603        id: ComponentId,
604        record: &ComponentRecord,
605    ) -> Result<(), RegistryError> {
606        let modules = std::mem::take(
607            &mut *record
608                .modules
609                .lock()
610                .map_err(|_| RegistryError::SynchronizationPoisoned)?,
611        );
612        for module in unique_modules(modules) {
613            let name = self.module_name(module);
614            if self.scheduler.check_old_code(module) {
615                self.scheduler.purge_module(module).map_err(|error| {
616                    RegistryError::ModulePurge {
617                        id,
618                        module: name.clone(),
619                        detail: error.to_string(),
620                    }
621                })?;
622            }
623            if !self.scheduler.delete_module(module) {
624                return Err(RegistryError::ModuleDelete { id, module: name });
625            }
626            if self
627                .scheduler
628                .lookup_module_in(NamespaceId::DEFAULT, module)
629                .is_some()
630            {
631                return Err(RegistryError::ModuleStillLoaded { id, module: name });
632            }
633        }
634        Ok(())
635    }
636
637    fn module_name(&self, module: Atom) -> String {
638        crate::composition::resolve_atom(&self.scheduler, module)
639    }
640
641    /// Refuses a freshly hot-loaded module whose committed import table still
642    /// defers any `erlang:*` entry (the scan itself, shared with the runtime
643    /// wrapper's support-module path, is
644    /// [`crate::composition::deferred_erlang_imports`] — see its docs for why
645    /// a deferred built-in is process-fatal at first dispatch).
646    fn refuse_deferred_bif_imports(
647        &self,
648        id: ComponentId,
649        module: Atom,
650    ) -> Result<(), RegistryError> {
651        let Some(deferred) = crate::composition::deferred_erlang_imports(&self.scheduler, module)
652        else {
653            return Err(RegistryError::ModuleLoad {
654                id,
655                detail: "committed module was absent immediately after hot load".to_owned(),
656            });
657        };
658        if deferred.is_empty() {
659            return Ok(());
660        }
661        Err(RegistryError::DeferredBifImports {
662            id,
663            module: self.module_name(module),
664            imports: deferred.join(", "),
665        })
666    }
667}
668
669mod fragments;
670mod query;
671mod recovery;
672mod support;
673
674pub use recovery::{ChildCleanup, ForcedCleanupReport, ProcessCleanup};
675use support::{
676    ComponentRecord, Operation, claim_operation, clear_operation, clear_runtime,
677    failure_from_error, require_running, state, transition, unique_modules, validate_registration,
678};