Skip to main content

behest_runtime/
registry.rs

1//! [`ComponentRegistry`]: ordered, dependency-aware, type-erased registry
2//! for every [`Component`] in the runtime.
3//!
4//! # Responsibilities
5//!
6//! - **Registration**: [`ComponentRegistry::register_factory`] queues a
7//!   component kind for initialization.
8//! - **Dependency resolution**: factory metadata lists `depends_on`
9//!   names; the registry topologically sorts the graph and refuses to
10//!   proceed if a cycle is present.
11//! - **Type erasure**: components are stored as `Arc<dyn AnyComponent>`
12//!   so the registry can hold heterogeneous instances. Typed access uses
13//!   [`ComponentRegistry::get`] to downcast back to a concrete
14//!   `Arc<C>`.
15//! - **Lifecycle orchestration**: [`ComponentRegistry::init_all`],
16//!   [`ComponentRegistry::start_all`], [`ComponentRegistry::stop_all`]
17//!   drive the four-phase component lifecycle in dependency order.
18//! - **Health aggregation**: [`ComponentRegistry::health`] fans out
19//!   `AnyComponent::health` calls concurrently and collects the results
20//!   into a single map for `/healthz` responses.
21//!
22//! # Lifecycle ordering
23//!
24//! ```text
25//!   register(...)        # queues factory
26//!       ↓
27//!   init_all()           # topo order, dep-first
28//!       ↓
29//!   start_all()          # topo order
30//!   ... serve ...
31//!       ↓
32//!   stop_all()           # reverse topo order
33//! ```
34//!
35//! On init error, the registry leaves the offending component in a
36//! [`ComponentState::Failed`] state and continues to attempt
37//! initialization of any components that do not depend on it. This
38//! maximizes operator visibility: a single broken optional backend
39//! should not prevent the rest of the system from coming up.
40
41#![allow(clippy::pedantic)]
42use std::any::{Any, TypeId};
43use std::collections::{HashMap, VecDeque};
44use std::sync::atomic::{AtomicBool, Ordering};
45use std::sync::{Arc, RwLock};
46
47use async_trait::async_trait;
48use futures_util::future::BoxFuture;
49use thiserror::Error;
50
51use super::component::{AnyComponent, AnyComponentError, Component, ComponentContext};
52use super::lifecycle::ShutdownToken;
53use behest_core::health::HealthStatus;
54
55/// Errors raised by [`ComponentRegistry`] operations.
56#[derive(Debug, Error)]
57#[non_exhaustive]
58pub enum RegistryError {
59    /// Tried to register a name that is already in use.
60    #[error("component `{name}` is already registered")]
61    AlreadyRegistered {
62        /// The conflicting name.
63        name: String,
64    },
65    /// Tried to unregister a name that is not present.
66    #[error("component `{name}` is not registered")]
67    NotFound {
68        /// The missing name.
69        name: String,
70    },
71    /// A factory listed in `depends_on` was not registered.
72    #[error("component `{name}` depends on missing component `{dep}`")]
73    MissingDependency {
74        /// The component declaring the dependency.
75        name: String,
76        /// The missing dependency name.
77        dep: String,
78    },
79    /// The dependency graph contains a cycle. The first cycle found is
80    /// reported as a list of node names.
81    #[error("cycle detected in component dependencies: {cycle:?}")]
82    Cycle {
83        /// The cycle, in the order it was discovered.
84        cycle: Vec<String>,
85    },
86    /// Init phase failed for a component.
87    #[error("init failed for component `{name}`: {message}")]
88    Init {
89        /// Name of the failing component.
90        name: String,
91        /// Human-readable error.
92        message: String,
93    },
94    /// Start phase failed for a component.
95    #[error("start failed for component `{name}`: {message}")]
96    Start {
97        /// Name of the failing component.
98        name: String,
99        /// Human-readable error.
100        message: String,
101    },
102    /// Stop phase failed for a component. Other components continue to
103    /// stop regardless.
104    #[error("stop failed for component `{name}`: {message}")]
105    Stop {
106        /// Name of the failing component.
107        name: String,
108        /// Human-readable error.
109        message: String,
110    },
111    /// A reload (hot-swap) operation failed.
112    #[error("reload failed for component `{name}`: {message}")]
113    Reload {
114        /// Name of the component being reloaded.
115        name: String,
116        /// Human-readable error.
117        message: String,
118    },
119    /// Internal lock acquisition failed.
120    #[error("component registry lock poisoned")]
121    LockPoisoned,
122    /// A typed downcast failed via [`ComponentRegistry::get`].
123    #[error("component `{name}` exists but is not of type `{type_id:?}`")]
124    TypeMismatch {
125        /// The name that was looked up.
126        name: String,
127        /// The actual `TypeId` of the stored instance.
128        type_id: TypeId,
129    },
130    /// Tried to `init_all` after a previous `init_all` already started
131    /// or completed. Use explicit unregister to re-init.
132    #[error("component `{name}` has already been initialized")]
133    AlreadyInitialized {
134        /// The already-initialized name.
135        name: String,
136    },
137}
138
139/// Initialization state of a registered component.
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141#[non_exhaustive]
142pub enum ComponentState {
143    /// Registered but not yet initialized.
144    Pending,
145    /// Currently running `init`.
146    Initializing,
147    /// Successfully initialized and ready to start.
148    Initialized,
149    /// `start` is in progress.
150    Starting,
151    /// `start` succeeded; component is serving.
152    Running,
153    /// `stop` is in progress.
154    Stopping,
155    /// `stop` completed; component is no longer serving but is still
156    /// initialized and could be re-`start`-ed.
157    Stopped,
158    /// Init failed. See [`RegistryError::Init`].
159    Failed,
160}
161
162/// Static descriptor of a component kind, used by the registry to plan
163/// initialization order without holding the actual factory.
164#[derive(Debug, Clone)]
165pub struct ComponentDescriptor {
166    /// The user-assigned instance name.
167    pub name: String,
168    /// Names of components this component depends on.
169    pub depends_on: Vec<String>,
170    /// Raw configuration value. The factory decides how to deserialize it.
171    pub config: serde_json::Value,
172}
173
174/// Type-erased factory for a concrete [`Component`] type. The registry
175/// holds factories as `Box<dyn ComponentFactory>` so it can drive
176/// heterogeneous types uniformly.
177#[async_trait]
178pub trait ComponentFactory: Send + Sync {
179    /// User-assigned instance name.
180    fn name(&self) -> &str;
181    /// Component kind identifier (e.g. `"provider.openai"`).
182    fn kind(&self) -> &'static str;
183    /// Names of components this component depends on.
184    fn depends_on(&self) -> Vec<String>;
185    /// Build an [`AnyComponent`] from the raw configuration value.
186    async fn build(
187        self: Box<Self>,
188        config: serde_json::Value,
189        ctx: &ComponentContext,
190    ) -> Result<Box<dyn AnyComponent>, RegistryError>;
191}
192
193/// Convenience wrapper that adapts a concrete [`Component`] into a
194/// [`ComponentFactory`].
195pub struct TypedFactory<C: Component> {
196    name: String,
197    extra_deps: Vec<String>,
198    _marker: std::marker::PhantomData<fn() -> C>,
199}
200
201impl<C: Component> TypedFactory<C> {
202    /// Construct a typed factory descriptor.
203    #[must_use]
204    pub fn new(name: impl Into<String>, extra_deps: Vec<String>) -> Self {
205        Self {
206            name: name.into(),
207            extra_deps,
208            _marker: std::marker::PhantomData,
209        }
210    }
211}
212
213#[async_trait]
214impl<C: Component> ComponentFactory for TypedFactory<C> {
215    fn name(&self) -> &str {
216        &self.name
217    }
218
219    fn kind(&self) -> &'static str {
220        C::NAME
221    }
222
223    fn depends_on(&self) -> Vec<String> {
224        let mut deps: Vec<String> = C::depends_on().iter().map(|s| (*s).to_string()).collect();
225        for d in &self.extra_deps {
226            if !deps.contains(d) {
227                deps.push(d.clone());
228            }
229        }
230        deps
231    }
232
233    async fn build(
234        self: Box<Self>,
235        config: serde_json::Value,
236        ctx: &ComponentContext,
237    ) -> Result<Box<dyn AnyComponent>, RegistryError> {
238        let name = self.name.clone();
239        let cfg: C::Config = serde_json::from_value(config).map_err(|e| RegistryError::Init {
240            name: name.clone(),
241            message: format!("config deserialize: {e}"),
242        })?;
243        let instance = C::init(&cfg, ctx).await.map_err(|e| RegistryError::Init {
244            name: name.clone(),
245            message: e.to_string(),
246        })?;
247        Ok(Box::new(TypedAnyComponent {
248            name,
249            kind: C::NAME,
250            instance: Arc::new(instance),
251        }))
252    }
253}
254
255/// Adapter from a typed [`Component`] to the type-erased [`AnyComponent`]
256/// trait. Created by [`TypedFactory::build`].
257pub struct TypedAnyComponent<C: Component> {
258    name: String,
259    kind: &'static str,
260    instance: Arc<C>,
261}
262
263impl<C: Component> TypedAnyComponent<C> {
264    /// Wraps a component instance into a type-erased [`AnyComponent`].
265    #[must_use]
266    pub fn new(instance: C) -> Self {
267        Self {
268            name: C::NAME.to_owned(),
269            kind: C::NAME,
270            instance: Arc::new(instance),
271        }
272    }
273}
274
275#[async_trait]
276impl<C: Component> AnyComponent for TypedAnyComponent<C> {
277    fn name(&self) -> &'static str {
278        self.kind
279    }
280
281    fn as_any_arc(&self) -> Arc<dyn Any + Send + Sync> {
282        self.instance.clone()
283    }
284
285    fn start(&self) -> BoxFuture<'_, Result<(), AnyComponentError>> {
286        let name = self.name.clone();
287        let instance = self.instance.clone();
288        Box::pin(async move {
289            instance
290                .start()
291                .await
292                .map_err(|e| AnyComponentError::Component {
293                    name,
294                    message: e.to_string(),
295                })
296        })
297    }
298
299    fn stop(&self) -> BoxFuture<'_, Result<(), AnyComponentError>> {
300        let name = self.name.clone();
301        let instance = self.instance.clone();
302        Box::pin(async move {
303            instance
304                .stop()
305                .await
306                .map_err(|e| AnyComponentError::Component {
307                    name,
308                    message: e.to_string(),
309                })
310        })
311    }
312
313    fn health(&self) -> BoxFuture<'_, HealthStatus> {
314        let instance = self.instance.clone();
315        Box::pin(async move { instance.health().await })
316    }
317
318    fn pre_replace(&self) -> BoxFuture<'_, Result<(), AnyComponentError>> {
319        let name = self.name.clone();
320        let instance = self.instance.clone();
321        Box::pin(async move {
322            instance
323                .pre_replace_hook()
324                .await
325                .map_err(|e| AnyComponentError::Component {
326                    name,
327                    message: e.to_string(),
328                })
329        })
330    }
331
332    fn post_replace(&self) -> BoxFuture<'_, Result<(), AnyComponentError>> {
333        let name = self.name.clone();
334        let instance = self.instance.clone();
335        Box::pin(async move {
336            instance
337                .post_replace_hook()
338                .await
339                .map_err(|e| AnyComponentError::Component {
340                    name,
341                    message: e.to_string(),
342                })
343        })
344    }
345}
346
347/// The central component registry.
348///
349/// Cloning a `ComponentRegistry` is cheap: it is backed by [`Arc`]-shared
350/// inner state. All clones observe the same set of registered components.
351pub struct ComponentRegistry {
352    inner: Arc<RegistryInner>,
353}
354
355struct RegistryInner {
356    /// Registered factories awaiting initialization. Cleared on init.
357    factories: RwLock<HashMap<String, Box<dyn ComponentFactory>>>,
358    /// Permanent descriptors; retained so that topo can be rebuilt
359    /// after `init_all` clears the factory map.
360    descriptors: RwLock<HashMap<String, ComponentDescriptor>>,
361    /// Initialized components keyed by name.
362    instances: RwLock<HashMap<String, Arc<dyn AnyComponent>>>,
363    /// Current state of every registered component.
364    states: RwLock<HashMap<String, ComponentState>>,
365    /// Cached topological init order. `Some(vec)` once computed, `None`
366    /// when invalid.
367    topo: RwLock<Option<Vec<String>>>,
368    /// Cooperative shutdown token handed to every component on init.
369    shutdown: ShutdownToken,
370    /// `true` once `init_all` has been called at least once. Used to
371    /// detect double-init.
372    init_started: AtomicBool,
373}
374
375impl Default for ComponentRegistry {
376    fn default() -> Self {
377        Self::new()
378    }
379}
380
381impl ComponentRegistry {
382    /// Construct an empty registry with a fresh shutdown token.
383    #[must_use]
384    pub fn new() -> Self {
385        Self::with_shutdown(ShutdownToken::new())
386    }
387
388    /// Construct a registry that hands `shutdown` to every component on
389    /// init. Used by the future `ManagedRuntime` to wire a single root
390    /// shutdown into every component.
391    #[must_use]
392    pub fn with_shutdown(shutdown: ShutdownToken) -> Self {
393        Self {
394            inner: Arc::new(RegistryInner {
395                factories: RwLock::new(HashMap::new()),
396                descriptors: RwLock::new(HashMap::new()),
397                instances: RwLock::new(HashMap::new()),
398                states: RwLock::new(HashMap::new()),
399                topo: RwLock::new(None),
400                shutdown,
401                init_started: AtomicBool::new(false),
402            }),
403        }
404    }
405
406    /// Borrow the root shutdown token.
407    #[must_use]
408    pub fn shutdown(&self) -> ShutdownToken {
409        self.inner.shutdown.clone()
410    }
411
412    /// Register a factory. Invalidates the cached topological order.
413    ///
414    /// # Errors
415    /// - [`RegistryError::AlreadyRegistered`] if the name is taken.
416    pub fn register_factory(
417        &self,
418        descriptor: ComponentDescriptor,
419        factory: Box<dyn ComponentFactory>,
420    ) -> Result<(), RegistryError> {
421        let name = descriptor.name.clone();
422        {
423            let mut factories = self
424                .inner
425                .factories
426                .write()
427                .map_err(|_| RegistryError::LockPoisoned)?;
428            if factories.contains_key(&name) {
429                return Err(RegistryError::AlreadyRegistered { name });
430            }
431            factories.insert(name.clone(), factory);
432        }
433        {
434            let mut descriptors = self
435                .inner
436                .descriptors
437                .write()
438                .map_err(|_| RegistryError::LockPoisoned)?;
439            descriptors.insert(name.clone(), descriptor);
440        }
441        {
442            let mut states = self
443                .inner
444                .states
445                .write()
446                .map_err(|_| RegistryError::LockPoisoned)?;
447            states.insert(name, ComponentState::Pending);
448        }
449        self.invalidate_topo();
450        Ok(())
451    }
452
453    /// Register a typed component using its [`Component::NAME`] constant
454    /// and [`Component::depends_on`] metadata.
455    pub fn register_typed<C: Component>(
456        &self,
457        instance_name: impl Into<String>,
458        config: serde_json::Value,
459    ) -> Result<(), RegistryError> {
460        let name = instance_name.into();
461        let factory = TypedFactory::<C>::new(name.clone(), Vec::new());
462        let descriptor = ComponentDescriptor {
463            name: name.clone(),
464            depends_on: factory.depends_on(),
465            config,
466        };
467        self.register_factory(descriptor, Box::new(factory))
468    }
469
470    /// Unregister a component. Returns the previous descriptor, or
471    /// [`RegistryError::NotFound`] if the name was not registered.
472    pub fn unregister(&self, name: &str) -> Result<ComponentDescriptor, RegistryError> {
473        {
474            let mut factories = self
475                .inner
476                .factories
477                .write()
478                .map_err(|_| RegistryError::LockPoisoned)?;
479            factories.remove(name);
480        }
481        let descriptor = {
482            let mut descriptors = self
483                .inner
484                .descriptors
485                .write()
486                .map_err(|_| RegistryError::LockPoisoned)?;
487            descriptors
488                .remove(name)
489                .ok_or_else(|| RegistryError::NotFound {
490                    name: name.to_string(),
491                })?
492        };
493        {
494            let mut states = self
495                .inner
496                .states
497                .write()
498                .map_err(|_| RegistryError::LockPoisoned)?;
499            states.remove(name);
500        }
501        self.invalidate_topo();
502        Ok(descriptor)
503    }
504
505    /// Number of registered components.
506    #[must_use]
507    pub fn len(&self) -> usize {
508        self.inner
509            .descriptors
510            .read()
511            .map(|m| m.len())
512            .unwrap_or_default()
513    }
514
515    /// Returns `true` if no components are registered.
516    #[must_use]
517    pub fn is_empty(&self) -> bool {
518        self.len() == 0
519    }
520
521    /// Returns the user-assigned names of all registered components,
522    /// in topological init order.
523    #[must_use]
524    pub fn names(&self) -> Vec<String> {
525        self.recompute_topo().unwrap_or_default()
526    }
527
528    /// Look up an initialized component by name, downcasting to a
529    /// concrete `Arc<C>`.
530    ///
531    /// # Errors
532    /// - [`RegistryError::NotFound`] if the name is not registered.
533    /// - [`RegistryError::TypeMismatch`] if the stored instance is not
534    ///   of type `C`.
535    /// - [`RegistryError::AlreadyInitialized`] is not raised here;
536    ///   a registered component without an instance simply returns
537    ///   `NotFound`.
538    pub fn get<C: Component>(&self, name: &str) -> Result<Arc<C>, RegistryError> {
539        let instances = self
540            .inner
541            .instances
542            .read()
543            .map_err(|_| RegistryError::LockPoisoned)?;
544        let instance = instances.get(name).ok_or_else(|| RegistryError::NotFound {
545            name: name.to_string(),
546        })?;
547        let any = instance.as_any_arc();
548        let type_id = any.type_id();
549        any.downcast::<C>()
550            .map_err(|_| RegistryError::TypeMismatch {
551                name: name.to_string(),
552                type_id,
553            })
554    }
555
556    /// Returns `true` if a component with the given name has been
557    /// successfully initialized.
558    #[must_use]
559    pub fn is_initialized(&self, name: &str) -> bool {
560        self.inner
561            .states
562            .read()
563            .ok()
564            .and_then(|m| m.get(name).copied())
565            .is_some_and(|s| {
566                matches!(
567                    s,
568                    ComponentState::Initialized
569                        | ComponentState::Running
570                        | ComponentState::Starting
571                        | ComponentState::Stopping
572                        | ComponentState::Stopped
573                )
574            })
575    }
576
577    /// Snapshot of the current state of a component, or `None` if the
578    /// name is not registered.
579    #[must_use]
580    pub fn state_of(&self, name: &str) -> Option<ComponentState> {
581        self.inner.states.read().ok()?.get(name).copied()
582    }
583
584    /// Initialize all registered components in topological order.
585    ///
586    /// On error from any single component, the registry records the
587    /// failure and continues with components that do not depend on the
588    /// failed one. The returned error is the first error encountered
589    /// (the rest are recorded in [`ComponentState::Failed`]).
590    ///
591    /// After a successful `init_all`, subsequent `init_all` calls are
592    /// no-ops for already-initialized components. To re-init, unregister
593    /// the component and re-register it.
594    pub async fn init_all(&self) -> Result<(), RegistryError> {
595        self.inner.init_started.store(true, Ordering::SeqCst);
596        let order = self.recompute_topo()?;
597        let ctx = ComponentContext::new(self.inner.shutdown.child());
598        let mut first_error: Option<RegistryError> = None;
599        for name in order {
600            let state = self.state_of(&name);
601            if matches!(
602                state,
603                Some(
604                    ComponentState::Initialized
605                        | ComponentState::Running
606                        | ComponentState::Starting
607                        | ComponentState::Stopping
608                        | ComponentState::Stopped,
609                )
610            ) {
611                continue;
612            }
613            let factory_box = {
614                let mut factories = self
615                    .inner
616                    .factories
617                    .write()
618                    .map_err(|_| RegistryError::LockPoisoned)?;
619                factories.remove(&name)
620            };
621            let Some(factory_box) = factory_box else {
622                continue;
623            };
624            let config = {
625                let descriptors = self
626                    .inner
627                    .descriptors
628                    .read()
629                    .map_err(|_| RegistryError::LockPoisoned)?;
630                descriptors
631                    .get(&name)
632                    .map(|d| d.config.clone())
633                    .unwrap_or_default()
634            };
635            self.set_state(&name, ComponentState::Initializing);
636            match factory_box.build(config, &ctx).await {
637                Ok(any) => {
638                    let arc: Arc<dyn AnyComponent> = any.into();
639                    self.inner
640                        .instances
641                        .write()
642                        .map_err(|_| RegistryError::LockPoisoned)?
643                        .insert(name.clone(), arc);
644                    self.set_state(&name, ComponentState::Initialized);
645                }
646                Err(e) => {
647                    self.set_state(&name, ComponentState::Failed);
648                    if first_error.is_none() {
649                        first_error = Some(e);
650                    }
651                }
652            }
653        }
654        first_error.map_or(Ok(()), Err)
655    }
656
657    /// Start all initialized components in topo order. Components that
658    /// are not in [`ComponentState::Initialized`] (e.g. failed, already
659    /// running) are skipped.
660    pub async fn start_all(&self) -> Result<(), RegistryError> {
661        let order = self.recompute_topo()?;
662        let mut first_error: Option<RegistryError> = None;
663        for name in order {
664            let state = self.state_of(&name);
665            if !matches!(state, Some(ComponentState::Initialized)) {
666                continue;
667            }
668            self.set_state(&name, ComponentState::Starting);
669            let instance = {
670                let instances = self
671                    .inner
672                    .instances
673                    .read()
674                    .map_err(|_| RegistryError::LockPoisoned)?;
675                instances.get(&name).cloned()
676            };
677            let Some(instance) = instance else {
678                continue;
679            };
680            match instance.start().await {
681                Ok(()) => {
682                    self.set_state(&name, ComponentState::Running);
683                }
684                Err(e) => {
685                    self.set_state(&name, ComponentState::Failed);
686                    if first_error.is_none() {
687                        first_error = Some(RegistryError::Start {
688                            name: name.clone(),
689                            message: e.to_string(),
690                        });
691                    }
692                }
693            }
694        }
695        first_error.map_or(Ok(()), Err)
696    }
697
698    /// Stop all running components in reverse topo order. Continues
699    /// even on per-component failure so a stuck component does not
700    /// prevent the rest of the system from draining.
701    pub async fn stop_all(&self) -> Result<(), RegistryError> {
702        let order = self.recompute_topo()?;
703        let mut first_error: Option<RegistryError> = None;
704        for name in order.into_iter().rev() {
705            let state = self.state_of(&name);
706            if !matches!(
707                state,
708                Some(ComponentState::Running | ComponentState::Initialized)
709            ) {
710                continue;
711            }
712            self.set_state(&name, ComponentState::Stopping);
713            let instance = {
714                let instances = self
715                    .inner
716                    .instances
717                    .read()
718                    .map_err(|_| RegistryError::LockPoisoned)?;
719                instances.get(&name).cloned()
720            };
721            let Some(instance) = instance else {
722                continue;
723            };
724            if let Err(e) = instance.stop().await
725                && first_error.is_none()
726            {
727                first_error = Some(RegistryError::Stop {
728                    name: name.clone(),
729                    message: e.to_string(),
730                });
731            }
732            self.set_state(&name, ComponentState::Stopped);
733        }
734        first_error.map_or(Ok(()), Err)
735    }
736
737    /// Atomically replace a running component with a new instance,
738    /// following the drain-aware hot-swap protocol.
739    ///
740    /// The protocol proceeds in five steps:
741    ///
742    /// 1. Verify the named component is in [`ComponentState::Running`].
743    /// 2. Call [`AnyComponent::pre_replace`] on the old instance, giving
744    ///    it a chance to reject new traffic or flush buffers.
745    /// 3. Call [`AnyComponent::start`] on the new instance. If this
746    ///    fails, the old instance remains in place.
747    /// 4. Swap the instance in the registry map. Existing `Arc` clones
748    ///    held by other tasks remain valid and keep the old instance
749    ///    alive until they are dropped (natural drain).
750    /// 5. Call [`AnyComponent::post_replace`] on the old instance
751    ///    (best-effort; errors are reported but do not roll back).
752    ///
753    /// Returns the old [`AnyComponent`] so the caller may await
754    /// explicit drain or call `stop` when appropriate.
755    ///
756    /// # Errors
757    /// - [`RegistryError::NotFound`] if no component with `name`
758    ///   exists.
759    /// - [`RegistryError::Reload`] if the component is not in
760    ///   `Running` state, or if `pre_replace` / `start` fails.
761    pub async fn replace_instance(
762        &self,
763        name: &str,
764        new_instance: Box<dyn AnyComponent>,
765    ) -> Result<Arc<dyn AnyComponent>, RegistryError> {
766        let old_instance = {
767            let instances = self
768                .inner
769                .instances
770                .read()
771                .map_err(|_| RegistryError::LockPoisoned)?;
772            instances
773                .get(name)
774                .cloned()
775                .ok_or_else(|| RegistryError::NotFound {
776                    name: name.to_string(),
777                })?
778        };
779
780        if !matches!(self.state_of(name), Some(ComponentState::Running)) {
781            return Err(RegistryError::Reload {
782                name: name.to_string(),
783                message: "component is not in Running state".to_string(),
784            });
785        }
786
787        if let Err(e) = old_instance.pre_replace().await {
788            return Err(RegistryError::Reload {
789                name: name.to_string(),
790                message: format!("pre_replace hook failed: {e}"),
791            });
792        }
793
794        if let Err(e) = new_instance.start().await {
795            return Err(RegistryError::Reload {
796                name: name.to_string(),
797                message: format!("new instance start failed: {e}"),
798            });
799        }
800
801        let new_arc: Arc<dyn AnyComponent> = new_instance.into();
802        {
803            let mut instances = self
804                .inner
805                .instances
806                .write()
807                .map_err(|_| RegistryError::LockPoisoned)?;
808            instances.insert(name.to_string(), new_arc);
809        }
810        self.set_state(name, ComponentState::Running);
811
812        if let Err(e) = old_instance.post_replace().await {
813            tracing::warn!(
814                component = name,
815                error = %e,
816                "post_replace hook failed (best-effort; continuing)"
817            );
818        }
819
820        Ok(old_instance)
821    }
822
823    /// Aggregate health of every initialized component. Calls
824    /// `AnyComponent::health` for each in topo order and collects the
825    /// results.
826    #[must_use]
827    pub async fn health(&self) -> HashMap<String, HealthStatus> {
828        let order = self.recompute_topo().unwrap_or_default();
829        let mut out = HashMap::new();
830        for name in order {
831            let instance = self
832                .inner
833                .instances
834                .read()
835                .ok()
836                .and_then(|m| m.get(&name).cloned());
837            if let Some(instance) = instance {
838                let h = instance.health().await;
839                out.insert(name, h);
840            } else {
841                out.insert(name, HealthStatus::unhealthy("not initialized"));
842            }
843        }
844        out
845    }
846
847    fn set_state(&self, name: &str, state: ComponentState) {
848        if let Ok(mut states) = self.inner.states.write() {
849            states.insert(name.to_string(), state);
850        }
851    }
852
853    fn invalidate_topo(&self) {
854        if let Ok(mut topo) = self.inner.topo.write() {
855            *topo = None;
856        }
857    }
858
859    /// Recompute the topological init order from the registered
860    /// descriptors. Caches the result until the next registration.
861    pub fn recompute_topo(&self) -> Result<Vec<String>, RegistryError> {
862        if let Ok(topo) = self.inner.topo.read()
863            && let Some(order) = topo.as_ref()
864        {
865            return Ok(order.clone());
866        }
867        let descriptors = self
868            .inner
869            .descriptors
870            .read()
871            .map_err(|_| RegistryError::LockPoisoned)?;
872        if descriptors.is_empty() {
873            if let Ok(mut cached) = self.inner.topo.write() {
874                *cached = Some(Vec::new());
875            }
876            return Ok(Vec::new());
877        }
878        let mut graph: HashMap<String, Vec<String>> = HashMap::new();
879        let mut in_degree: HashMap<String, usize> = HashMap::new();
880        for (name, desc) in descriptors.iter() {
881            graph.entry(name.clone()).or_default();
882            in_degree.entry(name.clone()).or_insert(0);
883            for dep in &desc.depends_on {
884                if !descriptors.contains_key(dep) {
885                    return Err(RegistryError::MissingDependency {
886                        name: name.clone(),
887                        dep: dep.clone(),
888                    });
889                }
890                graph.entry(dep.clone()).or_default().push(name.clone());
891                *in_degree.entry(name.clone()).or_insert(0) += 1;
892            }
893        }
894
895        // Kahn's algorithm with stable ordering for determinism.
896        let mut queue: VecDeque<String> = {
897            #[allow(clippy::filter_map_bool_then)]
898            in_degree
899                .iter()
900                .filter_map(|(n, d)| (*d == 0).then(|| n.clone()))
901                .collect()
902        };
903        let mut initial: Vec<String> = queue.drain(..).collect();
904        initial.sort_unstable();
905        for n in initial {
906            queue.push_back(n);
907        }
908
909        let mut order: Vec<String> = Vec::with_capacity(in_degree.len());
910        while let Some(n) = queue.pop_front() {
911            order.push(n.clone());
912            if let Some(deps) = graph.get(&n) {
913                for m in deps {
914                    if let Some(d) = in_degree.get_mut(m) {
915                        *d -= 1;
916                        if *d == 0 {
917                            queue.push_back(m.clone());
918                        }
919                    }
920                }
921            }
922        }
923
924        if order.len() != in_degree.len() {
925            let leftover: Vec<String> = {
926                #[allow(clippy::filter_map_bool_then)]
927                in_degree
928                    .iter()
929                    .filter_map(|(n, d)| (*d > 0).then(|| n.clone()))
930                    .collect()
931            };
932            return Err(RegistryError::Cycle { cycle: leftover });
933        }
934
935        if let Ok(mut cached) = self.inner.topo.write() {
936            *cached = Some(order.clone());
937        }
938        Ok(order)
939    }
940}
941
942#[cfg(test)]
943mod tests {
944    use super::*;
945    use async_trait::async_trait;
946    use schemars::JsonSchema;
947    use serde::{Deserialize, Serialize};
948
949    #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
950    struct EchoConfig {
951        label: String,
952    }
953
954    struct EchoComponent {
955        label: String,
956    }
957
958    #[async_trait]
959    impl Component for EchoComponent {
960        const NAME: &'static str = "test.echo";
961        type Config = EchoConfig;
962        type Error = std::io::Error;
963
964        async fn init(cfg: &Self::Config, _ctx: &ComponentContext) -> Result<Self, Self::Error> {
965            Ok(Self {
966                label: cfg.label.clone(),
967            })
968        }
969    }
970
971    fn cfg(label: &str) -> serde_json::Value {
972        serde_json::json!({ "label": label })
973    }
974
975    #[tokio::test]
976    async fn register_init_start_stop_lifecycle() {
977        let reg = ComponentRegistry::new();
978        reg.register_typed::<EchoComponent>("a", cfg("a-label"))
979            .unwrap_or_else(|e| panic!("{e}"));
980        reg.register_typed::<EchoComponent>("b", cfg("b-label"))
981            .unwrap_or_else(|e| panic!("{e}"));
982
983        reg.init_all().await.unwrap_or_else(|e| panic!("{e}"));
984        assert!(reg.is_initialized("a"));
985        assert!(reg.is_initialized("b"));
986
987        reg.start_all().await.unwrap_or_else(|e| panic!("{e}"));
988        assert_eq!(reg.state_of("a"), Some(ComponentState::Running));
989
990        reg.stop_all().await.unwrap_or_else(|e| panic!("{e}"));
991        assert_eq!(reg.state_of("a"), Some(ComponentState::Stopped));
992    }
993
994    #[tokio::test]
995    async fn topo_respects_dependencies() {
996        struct Dep;
997        #[async_trait]
998        impl Component for Dep {
999            const NAME: &'static str = "test.dep";
1000            type Config = serde_json::Value;
1001            type Error = std::io::Error;
1002            async fn init(
1003                _cfg: &Self::Config,
1004                _ctx: &ComponentContext,
1005            ) -> Result<Self, Self::Error> {
1006                Ok(Dep)
1007            }
1008        }
1009        struct User;
1010        #[async_trait]
1011        impl Component for User {
1012            const NAME: &'static str = "test.user";
1013            type Config = serde_json::Value;
1014            type Error = std::io::Error;
1015            async fn init(
1016                _cfg: &Self::Config,
1017                _ctx: &ComponentContext,
1018            ) -> Result<Self, Self::Error> {
1019                Ok(User)
1020            }
1021        }
1022        let reg = ComponentRegistry::new();
1023        // Register `user` with an explicit extra-dep on the instance
1024        // name "dep". The registry resolves dependencies against user
1025        // assigned instance names, not against `Component::NAME`.
1026        reg.register_typed::<User>("user", serde_json::json!({}))
1027            .unwrap_or_else(|e| panic!("{e}"));
1028        reg.register_typed::<Dep>("dep", serde_json::json!({}))
1029            .unwrap_or_else(|e| panic!("{e}"));
1030        // Re-register `user` with the extra-dep, since the first
1031        // registration did not carry one.
1032        reg.unregister("user").unwrap_or_else(|e| panic!("{e}"));
1033        reg.register_factory(
1034            ComponentDescriptor {
1035                name: "user".into(),
1036                depends_on: vec!["dep".into()],
1037                config: serde_json::json!({}),
1038            },
1039            Box::new(TypedFactory::<User>::new("user", vec!["dep".into()])),
1040        )
1041        .unwrap_or_else(|e| panic!("{e}"));
1042        let order = reg.recompute_topo().unwrap_or_else(|e| panic!("{e}"));
1043        assert_eq!(order, vec!["dep".to_string(), "user".to_string()]);
1044    }
1045
1046    #[tokio::test]
1047    async fn duplicate_registration_rejected() {
1048        let reg = ComponentRegistry::new();
1049        reg.register_typed::<EchoComponent>("a", cfg("a"))
1050            .unwrap_or_else(|e| panic!("{e}"));
1051        let err = match reg.register_typed::<EchoComponent>("a", cfg("b")) {
1052            Ok(_) => panic!("expected Err, got Ok"),
1053            Err(e) => e,
1054        };
1055        assert!(matches!(err, RegistryError::AlreadyRegistered { .. }));
1056    }
1057
1058    #[tokio::test]
1059    async fn missing_dependency_detected() {
1060        let reg = ComponentRegistry::new();
1061        let factory = TypedFactory::<EchoComponent>::new("a", vec!["missing".to_string()]);
1062        reg.register_factory(
1063            ComponentDescriptor {
1064                name: "a".into(),
1065                depends_on: vec!["missing".into()],
1066                config: cfg("a"),
1067            },
1068            Box::new(factory),
1069        )
1070        .unwrap_or_else(|e| panic!("{e}"));
1071        let err = match reg.init_all().await {
1072            Ok(_) => panic!("expected Err, got Ok"),
1073            Err(e) => e,
1074        };
1075        assert!(matches!(err, RegistryError::MissingDependency { .. }));
1076    }
1077
1078    #[tokio::test]
1079    async fn cycle_detected() {
1080        let reg = ComponentRegistry::new();
1081        let factory_a = TypedFactory::<EchoComponent>::new("a", vec!["b".to_string()]);
1082        let factory_b = TypedFactory::<EchoComponent>::new("b", vec!["a".to_string()]);
1083        reg.register_factory(
1084            ComponentDescriptor {
1085                name: "a".into(),
1086                depends_on: vec!["b".into()],
1087                config: cfg("a"),
1088            },
1089            Box::new(factory_a),
1090        )
1091        .unwrap_or_else(|e| panic!("{e}"));
1092        reg.register_factory(
1093            ComponentDescriptor {
1094                name: "b".into(),
1095                depends_on: vec!["a".into()],
1096                config: cfg("b"),
1097            },
1098            Box::new(factory_b),
1099        )
1100        .unwrap_or_else(|e| panic!("{e}"));
1101        let err = match reg.init_all().await {
1102            Ok(_) => panic!("expected Err, got Ok"),
1103            Err(e) => e,
1104        };
1105        assert!(matches!(err, RegistryError::Cycle { .. }));
1106    }
1107
1108    #[tokio::test]
1109    async fn health_aggregates_initialized_components() {
1110        let reg = ComponentRegistry::new();
1111        reg.register_typed::<EchoComponent>("a", cfg("a"))
1112            .unwrap_or_else(|e| panic!("{e}"));
1113        reg.init_all().await.unwrap_or_else(|e| panic!("{e}"));
1114        reg.start_all().await.unwrap_or_else(|e| panic!("{e}"));
1115        let h = reg.health().await;
1116        assert!(h.get("a").map(|s| s.is_healthy()).unwrap_or(false));
1117    }
1118
1119    #[tokio::test]
1120    async fn get_downcasts_to_concrete_type() {
1121        let reg = ComponentRegistry::new();
1122        reg.register_typed::<EchoComponent>("a", cfg("hello"))
1123            .unwrap_or_else(|e| panic!("{e}"));
1124        reg.init_all().await.unwrap_or_else(|e| panic!("{e}"));
1125        let c: Arc<EchoComponent> = reg.get("a").unwrap_or_else(|e| panic!("{e}"));
1126        assert_eq!(c.label, "hello");
1127    }
1128
1129    #[tokio::test]
1130    async fn get_returns_type_mismatch_on_wrong_type() {
1131        #[derive(Debug)]
1132        struct Other;
1133        #[async_trait]
1134        impl Component for Other {
1135            const NAME: &'static str = "test.other";
1136            type Config = serde_json::Value;
1137            type Error = std::io::Error;
1138            async fn init(
1139                _cfg: &Self::Config,
1140                _ctx: &ComponentContext,
1141            ) -> Result<Self, Self::Error> {
1142                Ok(Other)
1143            }
1144        }
1145        let reg = ComponentRegistry::new();
1146        reg.register_typed::<EchoComponent>("a", cfg("a"))
1147            .unwrap_or_else(|e| panic!("{e}"));
1148        reg.init_all().await.unwrap_or_else(|e| panic!("{e}"));
1149        let err = match reg.get::<Other>("a") {
1150            Ok(_) => panic!("expected Err, got Ok"),
1151            Err(e) => e,
1152        };
1153        assert!(matches!(err, RegistryError::TypeMismatch { .. }));
1154    }
1155
1156    #[tokio::test]
1157    async fn unregister_removes_state() {
1158        let reg = ComponentRegistry::new();
1159        reg.register_typed::<EchoComponent>("a", cfg("a"))
1160            .unwrap_or_else(|e| panic!("{e}"));
1161        reg.unregister("a").unwrap_or_else(|e| panic!("{e}"));
1162        assert!(!reg.is_initialized("a"));
1163        assert_eq!(reg.state_of("a"), None);
1164    }
1165
1166    #[tokio::test]
1167    async fn names_returns_topo_order() {
1168        let reg = ComponentRegistry::new();
1169        reg.register_typed::<EchoComponent>("zzz", cfg("z"))
1170            .unwrap_or_else(|e| panic!("{e}"));
1171        reg.register_typed::<EchoComponent>("aaa", cfg("a"))
1172            .unwrap_or_else(|e| panic!("{e}"));
1173        let names = reg.names();
1174        // With no dependencies, the topo sort returns names in
1175        // stable alphabetical order for determinism.
1176        assert_eq!(names.first().map(String::as_str), Some("aaa"));
1177        assert_eq!(names.last().map(String::as_str), Some("zzz"));
1178    }
1179}