nav.groups.core
ComponentRegistry
Dependency-aware orchestrator that drives the init → start → stop lifecycle for typed components.
ComponentRegistry
Dependency-aware orchestrator for the
Componentlifecycle.
ComponentRegistry is the lifecycle orchestrator. It owns a typed collection of Component instances, drives them through init → start → [serve] → stop in dependency order, exposes a typed lookup, and aggregates health. The trait provides the primitive contract; the registry handles the graph.
The full file is src/runtime/registry.rs.
Why a separate registry
A Component is a single instance. In practice, the runtime has many of them, and they depend on each other: the OpenAI adapter depends on the HTTP client; the context pipeline depends on the session store; the snapshot store depends on the S3 client. Bootstrapping them in the right order, recovering from partial failures, and downcasting Box<dyn AnyComponent> back to Arc<MyType> for callers who know what they want — all of this lives in the registry.
State machine
Each registered component moves through this state machine independently:
health() aggregates all states: healthy if every component is Running, degraded if any is Failed but others are Running, unhealthy if no component is Running.
API
use behest::runtime::registry::ComponentRegistry;
use behest::runtime::lifecycle::ShutdownToken;
let registry = ComponentRegistry::with_shutdown(ShutdownToken::new());
// Register a factory by name; the registry stores the factory, not the instance.
registry.register_factory("primary.openai", OpenAiChatComponent::NAME, factory)?;
// Drive the lifecycle.
registry.init_all().await?;
registry.start_all().await?;
// Typed lookup.
let openai: Arc<OpenAiChatComponent> = registry.get("primary.openai")?;
// Health.
let aggregate = registry.health().await; // healthy | degraded | unhealthy
// Graceful shutdown.
registry.stop_all().await?;
Errors
pub enum RegistryError {
AlreadyRegistered { name: String },
NotFound { name: String },
NotInitialized { name: String },
NotRunning { name: String },
InitFailed { name: String, source: Box<dyn Error + Send + Sync> },
StartFailed { name: String, source: Box<dyn Error + Send + Sync> },
DependencyCycle { cycle: Vec<String> },
DowncastFailed { name: String, type_name: &'static str },
}
The variants are #[non_exhaustive].
Dependency order
Components declare their dependencies via Component::depends_on(). The registry topologically sorts the registered names before driving init_all / start_all. Cycles return RegistryError::DependencyCycle { cycle } with the offending node sequence.
impl Component for ContextPipelineComponent {
fn depends_on() -> &'static [&'static str] {
&["store.session.memory", "store.embedding.memory"]
}
// ...
}
Typed downcast
registry.get::<MyComponent>("name") returns Result<Arc<MyComponent>, RegistryError>. The downcast uses the as_any_arc method on AnyComponent. The registry stores a side-table of concrete type names for diagnostics.
let store: Arc<MemorySessionStoreComponent> = registry.get("store.session.memory")?;
let inner: &MemorySessionStore = &store.inner;
TypedAnyComponent and TypedFactory are convenience wrappers that make the downcast ergonomic.
Hot-swap: replace_instance
replace_instance performs a drain-aware atomic swap of a running component:
let old = registry.replace_instance("db", new_instance).await?;
// old is Arc<dyn AnyComponent> — existing Arc clones keep it alive
The protocol:
- Validate — the component must be in
Runningstate. - Pre-replace hook —
pre_replaceis called on the old instance (signal it will stop accepting new traffic). - Start new —
startis called on the new instance. If it fails, the old instance remains in place. - Atomic swap — the registry slot is updated. New lookups return the new instance.
- Post-replace hook —
post_replaceis called on the old instance (best-effort; errors are logged but do not roll back).
Returns the old Arc<dyn AnyComponent>. Existing Arc clones held by other tasks keep the old instance alive until dropped (natural drain via reference counting).
Errors
| Condition | Variant |
|---|---|
| Name not registered | RegistryError::NotFound |
Component not in Running state | RegistryError::Reload |
pre_replace or start fails | RegistryError::Reload |
Worked example
use std::sync::Arc;
use behest::runtime::registry::ComponentRegistry;
use behest::runtime::factory_registry::FactoryRegistry;
use behest::runtime::lifecycle::ShutdownToken;
use behest::runtime::components::{
default_factory_registry, OpenAiChatComponent,
MemorySessionStoreComponent,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let registry = ComponentRegistry::with_shutdown(ShutdownToken::new());
let factories: FactoryRegistry = default_factory_registry();
// Register the same factory under two names.
let ctx = registry.context();
registry.register_factory(
"openai.primary",
OpenAiChatComponent::NAME,
factories.get("provider.openai.chat")?,
)?;
registry.register_factory(
"store.session.default",
MemorySessionStoreComponent::NAME,
factories.get("store.session.memory")?,
)?;
registry.init_all().await?;
registry.start_all().await?;
assert!(registry.health().await.is_healthy());
Ok(())
}
Edge cases
- Partial init failure — if component B's
initfails, the registry stops. Components that succeeded are kept in theInitializedstate; you can re-callinit_allafter fixing the underlying issue. The failed component is reported viahealth(). - Idempotent start — calling
start_alltwice in a row is a no-op. Callingstart_allafter astop_allis also a no-op (state machine already past the transition). - Stop on uninitialized component —
stop_allis a no-op for components that never reachedRunning. No error. - Cycle in
depends_on— returned asRegistryError::DependencyCycle. The cycle is the topological sort result, in the order that closes back to the start. - Missing dependency — if
Component::depends_onreturns["nonexistent"],init_allreturnsRegistryError::NotFound { name: "nonexistent" }. Dependencies must be registered.
Relationship to other components
- Component — the trait that components implement.
- FactoryRegistry — the kind → factory mapping used to populate the registry.
- Extensions — the runtime-facing facade that reads from the same underlying instances.
- ExtensionPoint — the storage primitive; the registry may use it for the registry of registered components.
See also
- Component — the lifecycle contract.
- FactoryRegistry — populating the registry.
- Extensions — the runtime-facing view.