#![allow(clippy::pedantic)]
use std::sync::Arc;
use thiserror::Error;
use crate::health::HealthStatus;
use crate::runtime::agent::AgentRuntime;
use crate::runtime::component::{AnyComponent, Component};
use crate::runtime::extensions::Extensions;
use crate::runtime::lifecycle::ShutdownToken;
use crate::runtime::registry::{ComponentRegistry, RegistryError, TypedAnyComponent};
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ManagedError {
#[error("component `{0}` not found")]
ComponentNotFound(String),
#[error("registry error: {0}")]
Registry(#[from] RegistryError),
#[error("reload failed for component `{name}`: {message}")]
Reload {
name: String,
message: String,
},
}
pub struct ManagedRuntime {
runtime: AgentRuntime,
registry: ComponentRegistry,
shutdown: ShutdownToken,
}
impl ManagedRuntime {
#[must_use]
pub fn new(
runtime: AgentRuntime,
registry: ComponentRegistry,
shutdown: ShutdownToken,
) -> Self {
Self {
runtime,
registry,
shutdown,
}
}
#[must_use]
pub fn runtime(&self) -> &AgentRuntime {
&self.runtime
}
#[must_use]
pub fn registry(&self) -> &ComponentRegistry {
&self.registry
}
#[must_use]
pub fn shutdown_token(&self) -> ShutdownToken {
self.shutdown.clone()
}
#[must_use]
pub fn extensions(&self) -> Arc<Extensions> {
Arc::clone(self.runtime.extensions())
}
pub fn component<T: Component>(&self, name: &str) -> Result<Arc<T>, ManagedError> {
self.registry
.get::<T>(name)
.map_err(|_| ManagedError::ComponentNotFound(name.to_owned()))
}
pub async fn serve(&self) -> Result<(), ManagedError> {
self.shutdown.wait().await;
self.registry.stop_all().await?;
Ok(())
}
#[must_use]
pub async fn health(&self) -> std::collections::HashMap<String, HealthStatus> {
self.registry.health().await
}
#[must_use]
pub async fn is_healthy(&self) -> bool {
let map = self.health().await;
map.values().all(|s| s.is_healthy())
}
#[must_use]
pub async fn overall_health(&self) -> HealthStatus {
let map = self.health().await;
HealthStatus::aggregate(&map)
}
#[must_use]
pub async fn is_ready(&self) -> bool {
let map = self.health().await;
map.values().all(|s| s.is_operational())
}
#[must_use]
pub async fn healthz_json(&self) -> serde_json::Value {
let map = self.health().await;
HealthStatus::healthz_response(&map)
}
pub async fn reload<T: Component>(
&self,
name: &str,
new_instance: T,
) -> Result<Arc<T>, ManagedError> {
let boxed: Box<dyn AnyComponent> = Box::new(TypedAnyComponent::new(new_instance));
let old_any = self
.registry
.replace_instance(name, boxed)
.await
.map_err(|e| match e {
RegistryError::NotFound { name: n } => ManagedError::ComponentNotFound(n),
RegistryError::Reload { name: n, message } => {
ManagedError::Reload { name: n, message }
}
other => ManagedError::Registry(other),
})?;
let any_arc = old_any.as_any_arc();
any_arc.downcast::<T>().map_err(|_| ManagedError::Reload {
name: name.to_string(),
message: "old instance type mismatch after swap".to_string(),
})
}
pub async fn reload_raw(
&self,
name: &str,
new_instance: Box<dyn AnyComponent>,
) -> Result<Arc<dyn AnyComponent>, ManagedError> {
self.registry
.replace_instance(name, new_instance)
.await
.map_err(|e| match e {
RegistryError::NotFound { name: n } => ManagedError::ComponentNotFound(n),
RegistryError::Reload { name: n, message } => {
ManagedError::Reload { name: n, message }
}
other => ManagedError::Registry(other),
})
}
}
impl std::fmt::Debug for ManagedRuntime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ManagedRuntime")
.field("components", &self.registry.len())
.finish()
}
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
use crate::runtime::component::ComponentContext;
use crate::runtime::policy::RuntimePolicy;
use async_trait::async_trait;
use schemars::JsonSchema;
use serde::Deserialize;
use std::time::Duration;
#[derive(Debug, Clone, Deserialize, JsonSchema)]
struct TestConfig {
label: String,
}
struct TestComp {
label: String,
}
#[async_trait]
impl Component for TestComp {
const NAME: &'static str = "test.managed";
type Config = TestConfig;
type Error = std::io::Error;
async fn init(cfg: &Self::Config, _ctx: &ComponentContext) -> Result<Self, Self::Error> {
Ok(Self {
label: cfg.label.clone(),
})
}
}
fn test_runtime() -> ManagedRuntime {
let exts = Arc::new(Extensions::default());
let policy = RuntimePolicy::default();
let runtime = AgentRuntime::new(exts, policy);
let registry = ComponentRegistry::new();
let shutdown = ShutdownToken::new();
ManagedRuntime::new(runtime, registry, shutdown)
}
#[tokio::test]
async fn serve_returns_on_shutdown() {
let managed = test_runtime();
let token = managed.shutdown_token();
let handle = tokio::spawn(async move {
managed.serve().await.expect("serve should succeed");
});
tokio::time::sleep(Duration::from_millis(20)).await;
token.signal_shutdown();
handle.await.expect("task should complete");
}
#[tokio::test]
async fn component_lookup_returns_not_found_for_empty() {
let managed = test_runtime();
let result = managed.component::<TestComp>("missing");
assert!(result.is_err());
assert!(matches!(result, Err(ManagedError::ComponentNotFound(_))));
}
#[tokio::test]
async fn component_lookup_after_init() {
let exts = Arc::new(Extensions::default());
let policy = RuntimePolicy::default();
let runtime = AgentRuntime::new(exts, policy);
let registry = ComponentRegistry::new();
registry
.register_typed::<TestComp>("test", serde_json::json!({ "label": "hello" }))
.expect("register should succeed");
registry.init_all().await.expect("init should succeed");
registry.start_all().await.expect("start should succeed");
let managed = ManagedRuntime::new(runtime, registry, ShutdownToken::new());
let comp: Arc<TestComp> = managed
.component::<TestComp>("test")
.expect("lookup should succeed");
assert_eq!(comp.label, "hello");
}
#[tokio::test]
async fn health_empty_is_healthy() {
let managed = test_runtime();
let map = managed.health().await;
assert!(map.is_empty());
assert!(managed.is_healthy().await);
}
#[tokio::test]
async fn health_aggregates_registered_components() {
let exts = Arc::new(Extensions::default());
let policy = RuntimePolicy::default();
let runtime = AgentRuntime::new(exts, policy);
let registry = ComponentRegistry::new();
registry
.register_typed::<TestComp>("c1", serde_json::json!({ "label": "a" }))
.expect("register should succeed");
registry.init_all().await.expect("init should succeed");
registry.start_all().await.expect("start should succeed");
let managed = ManagedRuntime::new(runtime, registry, ShutdownToken::new());
let map = managed.health().await;
assert_eq!(map.len(), 1);
assert!(map.get("c1").map(|s| s.is_healthy()).unwrap_or(false));
}
#[test]
fn debug_format_shows_component_count() {
let managed = test_runtime();
let dbg = format!("{managed:?}");
assert!(dbg.contains("ManagedRuntime"));
assert!(dbg.contains("components"));
}
async fn running_runtime() -> ManagedRuntime {
let exts = Arc::new(Extensions::default());
let policy = RuntimePolicy::default();
let runtime = AgentRuntime::new(exts, policy);
let registry = ComponentRegistry::new();
registry
.register_typed::<TestComp>("c1", serde_json::json!({ "label": "old" }))
.expect("register should succeed");
registry.init_all().await.expect("init should succeed");
registry.start_all().await.expect("start should succeed");
ManagedRuntime::new(runtime, registry, ShutdownToken::new())
}
#[tokio::test]
async fn reload_swaps_component_and_returns_old() {
let managed = running_runtime().await;
let old: Arc<TestComp> = managed
.component::<TestComp>("c1")
.expect("lookup should succeed");
assert_eq!(old.label, "old");
let returned_old = managed
.reload::<TestComp>(
"c1",
TestComp {
label: "new".into(),
},
)
.await
.expect("reload should succeed");
assert_eq!(returned_old.label, "old");
let current: Arc<TestComp> = managed
.component::<TestComp>("c1")
.expect("lookup should succeed");
assert_eq!(current.label, "new");
}
#[tokio::test]
async fn reload_not_found_returns_error() {
let managed = test_runtime();
let result = managed
.reload::<TestComp>("missing", TestComp { label: "x".into() })
.await;
assert!(matches!(result, Err(ManagedError::ComponentNotFound(_))));
}
#[tokio::test]
async fn reload_raw_swaps_type_erased_instance() {
let managed = running_runtime().await;
let new_instance: Box<dyn AnyComponent> =
Box::new(TypedAnyComponent::<TestComp>::new(TestComp {
label: "raw-new".into(),
}));
managed
.reload_raw("c1", new_instance)
.await
.expect("reload_raw should succeed");
let current: Arc<TestComp> = managed
.component::<TestComp>("c1")
.expect("lookup should succeed");
assert_eq!(current.label, "raw-new");
}
}