use std::any::{Any, TypeId};
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, RwLock};
use async_trait::async_trait;
pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug)]
pub enum Error {
DuplicateProvider {
type_name: &'static str,
},
Boot {
name: &'static str,
source: BoxError,
},
Validate {
name: &'static str,
source: BoxError,
},
Reload {
name: &'static str,
source: BoxError,
},
Finalize {
name: &'static str,
source: BoxError,
},
Run {
name: &'static str,
source: BoxError,
},
Recoverable {
name: &'static str,
source: BoxError,
},
Other(BoxError),
}
impl std::fmt::Display for Error {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
match self {
Error::DuplicateProvider { type_name } => {
write!(f, "provider type '{type_name}' is already registered")
}
Error::Boot { name, source } => {
write!(f, "provider '{name}' failed during boot: {source}")
}
Error::Validate { name, source } => {
write!(f, "provider '{name}' failed during validate: {source}")
}
Error::Reload { name, source } => {
write!(f, "reload of '{name}' failed: {source}")
}
Error::Finalize { name, source } => {
write!(f, "finalize of '{name}' failed: {source}")
}
Error::Run { name, source } => {
write!(f, "runnable '{name}' failed: {source}")
}
Error::Recoverable { name, source } => {
write!(f, "runnable '{name}' failed (recoverable): {source}")
}
Error::Other(e) => std::fmt::Display::fmt(e, f),
}
}
}
impl<E> From<E> for Error
where
E: std::error::Error + Send + Sync + 'static,
{
fn from(e: E) -> Self {
Error::Other(Box::new(e))
}
}
impl Error {
pub fn msg(s: impl Into<String>) -> Self {
#[derive(Debug)]
struct MsgErr(String);
impl std::fmt::Display for MsgErr {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
impl std::error::Error for MsgErr {}
Error::Other(Box::new(MsgErr(s.into())))
}
fn into_boot(
self,
name: &'static str,
) -> Self {
match self {
Error::Other(source) => Error::Boot { name, source },
other => other,
}
}
fn into_validate(
self,
name: &'static str,
) -> Self {
match self {
Error::Other(source) => Error::Validate { name, source },
other => other,
}
}
pub(crate) fn into_reload(
self,
name: &'static str,
) -> Self {
match self {
Error::Other(source) => Error::Reload { name, source },
other => other,
}
}
fn into_finalize(
self,
name: &'static str,
) -> Self {
match self {
Error::Other(source) => Error::Finalize { name, source },
other => other,
}
}
pub(crate) fn into_run(
self,
name: &'static str,
) -> Self {
match self {
Error::Other(source) => Error::Run { name, source },
Error::Recoverable { name: "", source } => Error::Recoverable { name, source },
other => other,
}
}
pub fn recoverable(s: impl Into<String>) -> Self {
#[derive(Debug)]
struct MsgErr(String);
impl std::fmt::Display for MsgErr {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
impl std::error::Error for MsgErr {}
Error::Recoverable { name: "", source: Box::new(MsgErr(s.into())) }
}
}
#[derive(Debug)]
pub struct ValidationFailure {
provider: &'static str,
error: Error,
}
impl ValidationFailure {
pub fn provider(&self) -> &'static str {
self.provider
}
pub fn error(&self) -> &Error {
&self.error
}
pub fn into_error(self) -> Error {
self.error
}
}
#[derive(Debug, Default)]
#[must_use = "provider validation failures are reported through ValidationOutcome"]
pub struct ValidationOutcome {
validated_count: usize,
failures: Vec<ValidationFailure>,
}
impl ValidationOutcome {
pub fn is_valid(&self) -> bool {
self.failures.is_empty()
}
pub fn validated_count(&self) -> usize {
self.validated_count
}
pub fn failed_count(&self) -> usize {
self.failures.len()
}
pub fn failures(&self) -> &[ValidationFailure] {
&self.failures
}
pub fn into_failures(self) -> Vec<ValidationFailure> {
self.failures
}
}
#[derive(Debug)]
pub struct ReloadFailure {
provider: &'static str,
error: Error,
}
impl ReloadFailure {
pub fn provider(&self) -> &'static str {
self.provider
}
pub fn error(&self) -> &Error {
&self.error
}
pub fn into_error(self) -> Error {
self.error
}
}
#[derive(Debug, Default)]
#[must_use = "provider reload failures are reported through ReloadOutcome"]
pub struct ReloadOutcome {
reloaded_count: usize,
failures: Vec<ReloadFailure>,
}
impl ReloadOutcome {
pub fn is_complete(&self) -> bool {
self.failures.is_empty()
}
pub fn reloaded_count(&self) -> usize {
self.reloaded_count
}
pub fn failed_count(&self) -> usize {
self.failures.len()
}
pub fn failures(&self) -> &[ReloadFailure] {
&self.failures
}
pub fn into_failures(self) -> Vec<ReloadFailure> {
self.failures
}
}
#[derive(Debug)]
pub struct FinalizeFailure {
provider: &'static str,
error: Error,
}
impl FinalizeFailure {
pub fn provider(&self) -> &'static str {
self.provider
}
pub fn error(&self) -> &Error {
&self.error
}
pub fn into_error(self) -> Error {
self.error
}
}
#[derive(Debug, Default)]
#[must_use = "provider finalization failures are reported through FinalizeOutcome"]
pub struct FinalizeOutcome {
finalized_count: usize,
failures: Vec<FinalizeFailure>,
}
impl FinalizeOutcome {
pub fn is_complete(&self) -> bool {
self.failures.is_empty()
}
pub fn finalized_count(&self) -> usize {
self.finalized_count
}
pub fn failed_count(&self) -> usize {
self.failures.len()
}
pub fn failures(&self) -> &[FinalizeFailure] {
&self.failures
}
pub fn into_failures(self) -> Vec<FinalizeFailure> {
self.failures
}
}
pub mod priority {
#[doc(hidden)]
pub const FIRST: u8 = 0;
pub const EARLY: u8 = 50;
pub const NORMAL: u8 = 100;
pub const LATE: u8 = 150;
#[doc(hidden)]
pub const LAST: u8 = u8::MAX;
}
#[derive(Clone, Debug, Default)]
pub struct ProviderOrder {
before: Vec<TypeId>,
after: Vec<TypeId>,
}
impl ProviderOrder {
pub fn new() -> Self {
Self::default()
}
pub fn before<T: 'static>(mut self) -> Self {
self.before.push(TypeId::of::<T>());
self
}
pub fn after<T: 'static>(mut self) -> Self {
self.after.push(TypeId::of::<T>());
self
}
pub fn before_types(&self) -> &[TypeId] {
&self.before
}
pub fn after_types(&self) -> &[TypeId] {
&self.after
}
}
#[async_trait]
pub trait ReloadState: Send + Sync + Sized + 'static {
async fn reload(&self) -> Result<()>;
}
#[async_trait]
pub trait Reloadable<S>: Provider<S> {
fn priority(&self) -> Option<u8> {
None
}
async fn reload(
&self,
state: &S,
) -> Result<()>;
}
#[async_trait]
pub trait Runnable<S>: Provider<S> {
async fn run(
self: Arc<Self>,
state: S,
context: crate::RunContext,
) -> Result<()>;
}
#[async_trait]
pub trait Finalizable<S>: Provider<S> {
async fn finalize(
&self,
state: &S,
) -> Result<()>;
}
#[async_trait]
pub trait Provider<S>: Any + Send + Sync + 'static {
fn name(&self) -> &'static str {
"provider"
}
fn boot_priority(&self) -> Option<u8> {
None
}
fn run_priority(&self) -> Option<u8> {
None
}
fn order(&self) -> ProviderOrder {
ProviderOrder::default()
}
async fn boot(
&self,
_state: &S,
) -> Result<()> {
Ok(())
}
fn validate(
&self,
_state: &S,
) -> Result<()> {
Ok(())
}
fn as_any(&self) -> &dyn Any
where
Self: Sized,
{
self
}
fn as_reloadable(&self) -> Option<&dyn Reloadable<S>> {
None
}
fn as_finalizable(&self) -> Option<&dyn Finalizable<S>> {
None
}
fn as_runnable(self: Arc<Self>) -> Option<Arc<dyn Runnable<S>>> {
None
}
}
pub struct Registry<S> {
providers: RwLock<HashMap<TypeId, Arc<dyn Provider<S>>>>,
by_type: RwLock<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>,
dyn_by_type: RwLock<HashMap<TypeId, Box<dyn Any + Send + Sync>>>,
registration_order: RwLock<Vec<TypeId>>,
lifecycle_order: RwLock<Option<Vec<TypeId>>>,
}
pub(crate) struct RunnableEntry<S> {
pub(crate) name: &'static str,
pub(crate) provider: Arc<dyn Provider<S>>,
pub(crate) runnable: Arc<dyn Runnable<S>>,
}
impl<S: 'static> Registry<S> {
pub fn new() -> Self {
Self {
providers: RwLock::new(HashMap::new()),
by_type: RwLock::new(HashMap::new()),
dyn_by_type: RwLock::new(HashMap::new()),
registration_order: RwLock::new(Vec::new()),
lifecycle_order: RwLock::new(None),
}
}
pub fn insert<P>(
&self,
item: Arc<P>,
) -> Result<&Self>
where
P: Provider<S> + 'static,
{
let type_id = TypeId::of::<P>();
let any: Arc<dyn Any + Send + Sync> = item.clone();
let mut by_type = self.by_type.write().expect("registry by_type lock poisoned");
if by_type.contains_key(&type_id) {
return Err(Error::DuplicateProvider { type_name: std::any::type_name::<P>() });
}
by_type.insert(type_id, any);
drop(by_type);
let it: Arc<dyn Provider<S>> = item;
self.providers.write().expect("registry providers lock poisoned").insert(type_id, it);
self.registration_order.write().expect("registry order lock poisoned").push(type_id);
*self.lifecycle_order.write().expect("registry lifecycle order lock poisoned") = None;
Ok(self)
}
pub fn bind_dyn<I, P>(
&self,
cast: impl FnOnce(Arc<P>) -> Arc<I>,
) -> Result<&Self>
where
I: ?Sized + Send + Sync + 'static,
P: Provider<S> + 'static,
{
let concrete = self.resolve::<P>().ok_or_else(|| {
Error::msg(format!(
"bind_dyn: provider type '{}' is not registered",
std::any::type_name::<P>()
))
})?;
let interface_id = TypeId::of::<I>();
let mut dyn_by_type = self.dyn_by_type.write().expect("registry dyn_by_type lock poisoned");
let erased: Box<dyn Any + Send + Sync> = Box::new(cast(concrete));
if dyn_by_type.insert(interface_id, erased).is_some() {
tracing::debug!(
interface = std::any::type_name::<I>(),
"replaced dynamic capability binding"
);
}
Ok(self)
}
pub fn with_typed<T, R>(
&self,
f: impl FnOnce(&T) -> R,
) -> Option<R>
where
T: Provider<S> + 'static,
{
let typed = self.resolve::<T>()?;
Some(f(typed.as_ref()))
}
pub fn resolve<T>(&self) -> Option<Arc<T>>
where
T: Provider<S> + 'static,
{
let any = self
.by_type
.read()
.expect("registry by_type lock poisoned")
.get(&TypeId::of::<T>())?
.clone();
Arc::downcast::<T>(any).ok()
}
pub fn resolve_dyn<I>(&self) -> Option<Arc<I>>
where
I: ?Sized + Send + Sync + 'static,
{
self.dyn_by_type
.read()
.expect("registry dyn_by_type lock poisoned")
.get(&TypeId::of::<I>())?
.downcast_ref::<Arc<I>>()
.cloned()
}
pub fn providers(&self) -> Vec<Arc<dyn Provider<S>>> {
self.providers.read().expect("registry providers lock poisoned").values().cloned().collect()
}
fn provider_entries_snapshot(&self) -> Vec<ProviderEntry<S>> {
let providers = self.providers.read().expect("registry providers lock poisoned");
self.registration_order
.read()
.expect("registry order lock poisoned")
.iter()
.enumerate()
.filter_map(|(index, type_id)| {
providers.get(type_id).cloned().map(|provider| ProviderEntry {
type_id: *type_id,
index,
provider,
})
})
.collect()
}
fn lifecycle_plan(&self) -> Result<Vec<Arc<dyn Provider<S>>>> {
if let Some(type_ids) = self
.lifecycle_order
.read()
.expect("registry lifecycle order lock poisoned")
.as_ref()
.cloned()
{
return Ok(self.providers_from_type_ids(&type_ids));
}
let ordered = order_provider_entries(self.provider_entries_snapshot())?;
let type_ids = ordered.iter().map(|entry| entry.type_id).collect::<Vec<_>>();
let providers = ordered.iter().map(|entry| entry.provider.clone()).collect::<Vec<_>>();
#[cfg(debug_assertions)]
tracing::debug!(
providers = ?providers.iter().map(|provider| provider.name()).collect::<Vec<_>>(),
"provider lifecycle order"
);
*self.lifecycle_order.write().expect("registry lifecycle order lock poisoned") =
Some(type_ids);
Ok(providers)
}
fn providers_from_type_ids(
&self,
type_ids: &[TypeId],
) -> Vec<Arc<dyn Provider<S>>> {
let providers = self.providers.read().expect("registry providers lock poisoned");
type_ids.iter().filter_map(|type_id| providers.get(type_id).cloned()).collect()
}
pub fn list_names(&self) -> Vec<&'static str> {
self.providers().iter().map(|c| c.name()).collect()
}
pub fn lifecycle_names(&self) -> Result<Vec<&'static str>> {
Ok(self.lifecycle_plan()?.iter().map(|provider| provider.name()).collect())
}
pub(crate) fn runnable_entries(&self) -> Vec<RunnableEntry<S>> {
let mut providers = self.providers();
providers.sort_by_key(|provider| {
(provider.run_priority().unwrap_or(priority::NORMAL), provider.name())
});
let mut runnables = Vec::new();
for provider in providers {
let Some(runnable) = provider.clone().as_runnable() else { continue };
runnables.push(RunnableEntry { name: provider.name(), provider, runnable });
}
runnables
}
pub fn validate_all(
&self,
state: &S,
) -> Result<ValidationOutcome> {
let mut outcome = ValidationOutcome::default();
for provider in self.lifecycle_plan()? {
let name = provider.name();
match provider.validate(state) {
Ok(()) => outcome.validated_count += 1,
Err(error) => outcome
.failures
.push(ValidationFailure { provider: name, error: error.into_validate(name) }),
}
}
Ok(outcome)
}
pub async fn boot_all(
&self,
state: &S,
) -> Result<()> {
for provider in self.lifecycle_plan()? {
let name = provider.name();
if let Err(e) = provider.boot(state).await {
return Err(e.into_boot(name));
}
}
Ok(())
}
pub async fn finalize_all(
&self,
state: &S,
) -> Result<FinalizeOutcome> {
let mut providers = self.lifecycle_plan()?;
providers.reverse();
let mut outcome = FinalizeOutcome::default();
for provider in providers {
let Some(finalizable) = provider.as_finalizable() else { continue };
let name = provider.name();
match finalizable.finalize(state).await {
Ok(()) => outcome.finalized_count += 1,
Err(error) => outcome
.failures
.push(FinalizeFailure { provider: name, error: error.into_finalize(name) }),
}
}
Ok(outcome)
}
pub async fn reload_one(
&self,
name: &str,
state: &S,
) -> Result<()> {
let Some(provider) = self.providers().into_iter().find(|provider| provider.name() == name)
else {
return Err(Error::msg(format!(
"reload_by_name: no provider registered with name '{}'",
name
)));
};
let Some(reloadable) = provider.as_reloadable() else {
return Err(Error::msg(format!(
"reload_by_name: provider '{}' is not reloadable",
name
)));
};
let provider_name = provider.name();
reloadable.reload(state).await.map_err(|error| error.into_reload(provider_name))
}
}
impl<S> Registry<S>
where
S: ReloadState + 'static,
{
pub async fn reload_all(
&self,
state: &S,
) -> Result<ReloadOutcome> {
state.reload().await?;
let mut outcome = ReloadOutcome::default();
for provider in self.lifecycle_plan()? {
let name = provider.name();
if let Some(reloadable) = provider.as_reloadable() {
match reloadable.reload(state).await {
Ok(()) => outcome.reloaded_count += 1,
Err(error) => outcome
.failures
.push(ReloadFailure { provider: name, error: error.into_reload(name) }),
}
}
}
Ok(outcome)
}
}
struct ProviderEntry<S> {
type_id: TypeId,
index: usize,
provider: Arc<dyn Provider<S>>,
}
impl<S> Clone for ProviderEntry<S> {
fn clone(&self) -> Self {
Self { type_id: self.type_id, index: self.index, provider: self.provider.clone() }
}
}
fn order_provider_entries<S: 'static>(
entries: Vec<ProviderEntry<S>>
) -> Result<Vec<ProviderEntry<S>>> {
let len = entries.len();
let positions: HashMap<TypeId, usize> =
entries.iter().enumerate().map(|(idx, entry)| (entry.type_id, idx)).collect();
let priorities: Vec<u8> =
entries.iter().map(|entry| lifecycle_priority(&entry.provider)).collect();
let mut outgoing: Vec<HashSet<usize>> = (0..len).map(|_| HashSet::new()).collect();
let mut indegree = vec![0usize; len];
let mut add_edge = |from: usize, to: usize| {
if from != to && outgoing[from].insert(to) {
indegree[to] += 1;
}
};
for (idx, entry) in entries.iter().enumerate() {
let order = entry.provider.order();
for target in order.before_types() {
if let Some(&target_idx) = positions.get(target) {
add_edge(idx, target_idx);
}
}
for target in order.after_types() {
if let Some(&target_idx) = positions.get(target) {
add_edge(target_idx, idx);
}
}
}
let mut ready: Vec<usize> = indegree
.iter()
.enumerate()
.filter_map(|(idx, degree)| (*degree == 0).then_some(idx))
.collect();
let mut ordered = Vec::with_capacity(len);
while !ready.is_empty() {
ready.sort_by_key(|idx| {
(priorities[*idx], entries[*idx].index, entries[*idx].provider.name())
});
let idx = ready.remove(0);
ordered.push(idx);
let next: Vec<_> = outgoing[idx].iter().copied().collect();
for target in next {
indegree[target] -= 1;
if indegree[target] == 0 {
ready.push(target);
}
}
}
if ordered.len() != len {
let blocked = indegree
.iter()
.enumerate()
.filter_map(|(idx, degree)| (*degree > 0).then_some(entries[idx].provider.name()))
.collect::<Vec<_>>()
.join(", ");
return Err(Error::msg(format!("provider lifecycle order cycle detected: {blocked}")));
}
Ok(ordered.into_iter().map(|idx| entries[idx].clone()).collect())
}
fn lifecycle_priority<S: 'static>(provider: &Arc<dyn Provider<S>>) -> u8 {
provider
.boot_priority()
.or_else(|| provider.as_reloadable().and_then(|reloadable| reloadable.priority()))
.unwrap_or(priority::NORMAL)
}
impl<S: 'static> Default for Registry<S> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use super::*;
#[derive(Clone, Default)]
struct TestState {
seen: Arc<Mutex<Vec<&'static str>>>,
}
#[async_trait]
impl ReloadState for TestState {
async fn reload(&self) -> Result<()> {
Ok(())
}
}
struct DbProvider;
struct CacheProvider;
struct ApiProvider;
struct MetricsProvider;
struct FailsValidation;
#[async_trait]
impl Provider<TestState> for DbProvider {
fn name(&self) -> &'static str {
"db"
}
fn validate(
&self,
state: &TestState,
) -> Result<()> {
state.seen.lock().expect("test log poisoned").push("db");
Ok(())
}
}
#[async_trait]
impl Provider<TestState> for CacheProvider {
fn name(&self) -> &'static str {
"cache"
}
fn order(&self) -> ProviderOrder {
ProviderOrder::new().after::<DbProvider>()
}
fn validate(
&self,
state: &TestState,
) -> Result<()> {
state.seen.lock().expect("test log poisoned").push("cache");
Ok(())
}
}
#[async_trait]
impl Provider<TestState> for ApiProvider {
fn name(&self) -> &'static str {
"api"
}
fn order(&self) -> ProviderOrder {
ProviderOrder::new().after::<CacheProvider>()
}
fn validate(
&self,
state: &TestState,
) -> Result<()> {
state.seen.lock().expect("test log poisoned").push("api");
Ok(())
}
}
#[async_trait]
impl Provider<TestState> for MetricsProvider {
fn name(&self) -> &'static str {
"metrics"
}
fn order(&self) -> ProviderOrder {
ProviderOrder::new().before::<ApiProvider>()
}
fn validate(
&self,
state: &TestState,
) -> Result<()> {
state.seen.lock().expect("test log poisoned").push("metrics");
Ok(())
}
}
#[async_trait]
impl Provider<TestState> for FailsValidation {
fn name(&self) -> &'static str {
"fails-validation"
}
fn validate(
&self,
_: &TestState,
) -> Result<()> {
Err(Error::msg("validation rejected"))
}
}
#[test]
fn lifecycle_order_uses_type_dependencies() {
let state = TestState::default();
let registry = Registry::<TestState>::new();
registry
.insert(Arc::new(ApiProvider))
.expect("api registration should succeed")
.insert(Arc::new(CacheProvider))
.expect("cache registration should succeed")
.insert(Arc::new(DbProvider))
.expect("database registration should succeed");
let outcome = registry.validate_all(&state).expect("validation should run");
assert!(outcome.is_valid());
assert_eq!(outcome.validated_count(), 3);
let seen = state.seen.lock().expect("test log poisoned").clone();
assert_eq!(seen, vec!["db", "cache", "api"]);
}
#[test]
fn duplicate_provider_registration_is_rejected_without_replacement() {
let registry = Registry::<TestState>::new();
let original = Arc::new(ApiProvider);
registry.insert(original.clone()).expect("first registration should succeed");
let error = match registry.insert(Arc::new(ApiProvider)) {
Err(error) => error,
Ok(_) => panic!("duplicate registration must be rejected"),
};
assert!(matches!(
error,
Error::DuplicateProvider { type_name }
if type_name == std::any::type_name::<ApiProvider>()
));
let resolved = registry.resolve::<ApiProvider>().expect("original provider should remain");
assert!(Arc::ptr_eq(&resolved, &original));
}
#[test]
fn lifecycle_order_supports_before_edges_and_diagnostics() {
let state = TestState::default();
let registry = Registry::<TestState>::new();
registry
.insert(Arc::new(ApiProvider))
.expect("api registration should succeed")
.insert(Arc::new(MetricsProvider))
.expect("metrics registration should succeed")
.insert(Arc::new(CacheProvider))
.expect("cache registration should succeed")
.insert(Arc::new(DbProvider))
.expect("database registration should succeed");
let names = registry.lifecycle_names().expect("plan should build");
assert_eq!(names, vec!["metrics", "db", "cache", "api"]);
let outcome = registry.validate_all(&state).expect("validation should run");
assert!(outcome.is_valid());
assert_eq!(outcome.validated_count(), 4);
let seen = state.seen.lock().expect("test log poisoned").clone();
assert_eq!(seen, vec!["metrics", "db", "cache", "api"]);
}
#[test]
fn validate_all_retains_provider_failures_and_continues() {
let state = TestState::default();
let registry = Registry::<TestState>::new();
registry
.insert(Arc::new(FailsValidation))
.expect("failing validator should register")
.insert(Arc::new(DbProvider))
.expect("successful validator should register");
let outcome = registry.validate_all(&state).expect("validation should complete");
assert!(!outcome.is_valid());
assert_eq!(outcome.validated_count(), 1);
assert_eq!(outcome.failed_count(), 1);
let failure = &outcome.failures()[0];
assert_eq!(failure.provider(), "fails-validation");
assert!(matches!(failure.error(), Error::Validate { name: "fails-validation", .. }));
assert_eq!(
failure.error().to_string(),
"provider 'fails-validation' failed during validate: validation rejected"
);
assert_eq!(state.seen.lock().expect("test log poisoned").as_slice(), &["db"]);
}
trait LogSink: Send + Sync {
fn line(&self) -> &'static str;
}
struct ConsoleLogger;
struct FileLogger;
impl LogSink for ConsoleLogger {
fn line(&self) -> &'static str {
"console"
}
}
impl LogSink for FileLogger {
fn line(&self) -> &'static str {
"file"
}
}
#[async_trait]
impl Provider<TestState> for ConsoleLogger {
fn name(&self) -> &'static str {
"console-logger"
}
async fn boot(
&self,
state: &TestState,
) -> Result<()> {
state.seen.lock().expect("test log poisoned").push("console-logger");
Ok(())
}
}
#[async_trait]
impl Provider<TestState> for FileLogger {
fn name(&self) -> &'static str {
"file-logger"
}
async fn boot(
&self,
state: &TestState,
) -> Result<()> {
state.seen.lock().expect("test log poisoned").push("file-logger");
Ok(())
}
}
#[tokio::test]
async fn bind_dyn_resolves_trait_object_and_allows_replacement() {
let state = TestState::default();
let registry = Registry::<TestState>::new();
registry.insert(Arc::new(ConsoleLogger)).expect("console logger should register");
registry
.bind_dyn::<dyn LogSink, ConsoleLogger>(|logger| logger)
.expect("dyn binding should succeed");
let sink = registry.resolve_dyn::<dyn LogSink>().expect("LogSink should be bound");
assert_eq!(sink.line(), "console");
registry.insert(Arc::new(FileLogger)).expect("file logger should register");
registry
.bind_dyn::<dyn LogSink, FileLogger>(|logger| logger)
.expect("dyn binding replacement should succeed");
let sink = registry.resolve_dyn::<dyn LogSink>().expect("LogSink should be rebound");
assert_eq!(sink.line(), "file");
registry.boot_all(&state).await.expect("boot should succeed");
let seen = state.seen.lock().expect("test log poisoned").clone();
assert_eq!(seen, vec!["console-logger", "file-logger"]);
}
struct BootRecorder;
struct BootDependency;
#[async_trait]
impl Provider<TestState> for BootRecorder {
fn name(&self) -> &'static str {
"boot-recorder"
}
fn order(&self) -> ProviderOrder {
ProviderOrder::new().after::<BootDependency>()
}
async fn boot(
&self,
state: &TestState,
) -> Result<()> {
state.seen.lock().expect("test log poisoned").push("boot-recorder");
Ok(())
}
fn as_finalizable(&self) -> Option<&dyn Finalizable<TestState>> {
Some(self)
}
}
#[async_trait]
impl Finalizable<TestState> for BootRecorder {
async fn finalize(
&self,
state: &TestState,
) -> Result<()> {
state.seen.lock().expect("test log poisoned").push("finalize-recorder");
Ok(())
}
}
#[async_trait]
impl Provider<TestState> for BootDependency {
fn name(&self) -> &'static str {
"boot-dependency"
}
async fn boot(
&self,
state: &TestState,
) -> Result<()> {
state.seen.lock().expect("test log poisoned").push("boot-dependency");
Ok(())
}
fn as_finalizable(&self) -> Option<&dyn Finalizable<TestState>> {
Some(self)
}
}
#[async_trait]
impl Finalizable<TestState> for BootDependency {
async fn finalize(
&self,
state: &TestState,
) -> Result<()> {
state.seen.lock().expect("test log poisoned").push("finalize-dependency");
Ok(())
}
}
#[tokio::test]
async fn boot_and_finalize_share_lifecycle_plan() {
let state = TestState::default();
let registry = Registry::<TestState>::new();
registry
.insert(Arc::new(BootRecorder))
.expect("boot recorder should register")
.insert(Arc::new(BootDependency))
.expect("boot dependency should register");
registry.boot_all(&state).await.expect("boot should succeed");
let outcome = registry.finalize_all(&state).await.expect("finalize should succeed");
assert!(outcome.is_complete());
assert_eq!(outcome.finalized_count(), 2);
let seen = state.seen.lock().expect("test log poisoned").clone();
assert_eq!(
seen,
vec!["boot-dependency", "boot-recorder", "finalize-recorder", "finalize-dependency",]
);
}
struct CycleA;
struct CycleB;
#[async_trait]
impl Provider<TestState> for CycleA {
fn name(&self) -> &'static str {
"cycle-a"
}
fn order(&self) -> ProviderOrder {
ProviderOrder::new().after::<CycleB>()
}
}
#[async_trait]
impl Provider<TestState> for CycleB {
fn name(&self) -> &'static str {
"cycle-b"
}
fn order(&self) -> ProviderOrder {
ProviderOrder::new().after::<CycleA>()
}
}
#[test]
fn lifecycle_order_rejects_cycles() {
let state = TestState::default();
let registry = Registry::<TestState>::new();
registry
.insert(Arc::new(CycleA))
.expect("cycle A should register")
.insert(Arc::new(CycleB))
.expect("cycle B should register");
let err = registry.validate_all(&state).expect_err("cycle must be rejected");
assert!(err.to_string().contains("provider lifecycle order cycle detected"));
}
struct Reloads;
struct FailsReload;
struct Finalizes;
struct FailsFinalize;
#[async_trait]
impl Provider<TestState> for Finalizes {
fn name(&self) -> &'static str {
"finalizes"
}
fn as_finalizable(&self) -> Option<&dyn Finalizable<TestState>> {
Some(self)
}
}
#[async_trait]
impl Finalizable<TestState> for Finalizes {
async fn finalize(
&self,
_: &TestState,
) -> Result<()> {
Ok(())
}
}
#[async_trait]
impl Provider<TestState> for FailsFinalize {
fn name(&self) -> &'static str {
"fails-finalize"
}
fn as_finalizable(&self) -> Option<&dyn Finalizable<TestState>> {
Some(self)
}
}
#[async_trait]
impl Finalizable<TestState> for FailsFinalize {
async fn finalize(
&self,
_: &TestState,
) -> Result<()> {
Err(Error::msg("finalize rejected"))
}
}
#[async_trait]
impl Provider<TestState> for Reloads {
fn name(&self) -> &'static str {
"reloads"
}
fn as_reloadable(&self) -> Option<&dyn Reloadable<TestState>> {
Some(self)
}
}
#[async_trait]
impl Reloadable<TestState> for Reloads {
async fn reload(
&self,
_: &TestState,
) -> Result<()> {
Ok(())
}
}
#[async_trait]
impl Provider<TestState> for FailsReload {
fn name(&self) -> &'static str {
"fails-reload"
}
fn as_reloadable(&self) -> Option<&dyn Reloadable<TestState>> {
Some(self)
}
}
#[async_trait]
impl Reloadable<TestState> for FailsReload {
async fn reload(
&self,
_: &TestState,
) -> Result<()> {
Err(Error::msg("reload rejected"))
}
}
#[tokio::test]
async fn reload_all_retains_provider_failures_and_continues() {
let state = TestState::default();
let registry = Registry::<TestState>::new();
registry
.insert(Arc::new(FailsReload))
.expect("failing reloader should register")
.insert(Arc::new(Reloads))
.expect("successful reloader should register");
let outcome = registry.reload_all(&state).await.expect("broadcast should complete");
assert!(!outcome.is_complete());
assert_eq!(outcome.reloaded_count(), 1);
assert_eq!(outcome.failed_count(), 1);
let failure = &outcome.failures()[0];
assert_eq!(failure.provider(), "fails-reload");
assert!(matches!(failure.error(), Error::Reload { name: "fails-reload", .. }));
assert_eq!(failure.error().to_string(), "reload of 'fails-reload' failed: reload rejected");
}
#[tokio::test]
async fn finalize_all_retains_provider_failures_and_continues() {
let state = TestState::default();
let registry = Registry::<TestState>::new();
registry
.insert(Arc::new(Finalizes))
.expect("successful finalizer should register")
.insert(Arc::new(FailsFinalize))
.expect("failing finalizer should register");
let outcome = registry.finalize_all(&state).await.expect("finalization should complete");
assert!(!outcome.is_complete());
assert_eq!(outcome.finalized_count(), 1);
assert_eq!(outcome.failed_count(), 1);
let failure = &outcome.failures()[0];
assert_eq!(failure.provider(), "fails-finalize");
assert!(matches!(failure.error(), Error::Finalize { name: "fails-finalize", .. }));
assert_eq!(
failure.error().to_string(),
"finalize of 'fails-finalize' failed: finalize rejected"
);
}
}