use std::future::Future;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, Weak};
use std::time::{Duration, Instant};
use tokio::runtime::Handle;
use tokio_util::sync::CancellationToken;
use crate::bus::EventBus;
use crate::diagnostics::{
DependencyDiagnostics, DependencyStatus, PluginDiagnostics, ResolvedDependency,
RuntimeDiagnostics, ServiceAccess,
};
use crate::effect::{Disposer, Effect, EffectRecord};
use crate::error::{default_sink, CordisError, ErrorSink};
use crate::fiber::{
join_task, spawn_fiber, DisposeWaitError, FiberInner, FiberState, FiberView, Intent,
TransitionTask,
};
use crate::key::{InstanceId, ScopeId, TypeKey};
use crate::registry::{Binding, CheckFn, Registry, StoredValue};
use crate::Plugin;
use crate::PluginFactory;
pub(crate) struct Shared {
pub admission: Mutex<()>,
pub handle: Handle,
pub bus: EventBus,
pub registry: Registry,
pub error_sink: ErrorSink,
pub next_plugin_id: AtomicU64,
pub closing: AtomicBool,
pub shutdown_task: Mutex<Option<Arc<TransitionTask>>>,
}
pub(crate) struct CtxInner {
pub(crate) shared: Arc<Shared>,
pub(crate) parent: Option<Ctx>,
pub(crate) fiber: Weak<FiberInner>,
pub(crate) isolate: Option<(TypeKey, ScopeId)>,
pub(crate) instance: InstanceId,
}
#[derive(Clone)]
pub struct Ctx(Arc<CtxInner>);
struct EffectFactoryLease(Arc<FiberInner>);
impl Drop for EffectFactoryLease {
fn drop(&mut self) {
self.0.finish_event();
}
}
impl Ctx {
pub(crate) fn new_child(
shared: Arc<Shared>,
parent: &Ctx,
fiber: Weak<FiberInner>,
isolate: Option<(TypeKey, ScopeId)>,
instance: InstanceId,
) -> Self {
Self(Arc::new(CtxInner {
shared,
parent: Some(parent.clone()),
fiber,
isolate,
instance,
}))
}
pub(crate) fn new_root(
shared: Arc<Shared>,
fiber: Weak<FiberInner>,
instance: InstanceId,
) -> Self {
Self(Arc::new(CtxInner {
shared,
parent: None,
fiber,
isolate: None,
instance,
}))
}
pub(crate) fn weak_fiber(&self) -> Weak<FiberInner> {
self.0.fiber.clone()
}
pub(crate) fn shared(&self) -> &Arc<Shared> {
&self.0.shared
}
pub fn instance(&self) -> InstanceId {
self.0.instance
}
pub(crate) fn in_instance(&self, id: InstanceId) -> bool {
let mut current = self.0.fiber.upgrade();
while let Some(fiber) = current {
if fiber.instance == id {
return true;
}
current = fiber.parent_fiber.as_ref().and_then(Weak::upgrade);
}
false
}
pub(crate) fn instance_owner(&self, id: InstanceId) -> Option<Arc<FiberInner>> {
let mut current = self.0.fiber.upgrade();
while let Some(fiber) = current {
if fiber.instance == id {
return Some(fiber);
}
current = fiber.parent_fiber.as_ref().and_then(Weak::upgrade);
}
None
}
pub(crate) fn check_instance(&self, key: &TypeKey) -> Result<(), CordisError> {
if let Some(id) = key.instance_id() {
if !self.in_instance(id) {
return Err(CordisError::InstanceOutOfScope { instance: id });
}
}
Ok(())
}
pub(crate) fn in_instance_key(&self, key: &TypeKey) -> bool {
key.instance_id().is_none_or(|id| self.in_instance(id))
}
pub(crate) fn subtree_closing(&self) -> bool {
let mut current = self.0.fiber.upgrade();
while let Some(fiber) = current {
if fiber.closing.load(Ordering::SeqCst) {
return true;
}
current = fiber.parent_fiber.as_ref().and_then(Weak::upgrade);
}
false
}
pub(crate) fn registration_open(&self) -> Result<(), CordisError> {
if self.0.shared.closing.load(Ordering::SeqCst) || self.subtree_closing() {
Err(CordisError::Closed)
} else {
Ok(())
}
}
pub fn handle(&self) -> &Handle {
&self.0.shared.handle
}
pub fn error_sink(&self) -> ErrorSink {
self.0.shared.error_sink.clone()
}
pub fn root() -> Result<Ctx, CordisError> {
let handle = Handle::try_current().map_err(|_| {
CordisError::PluginFailed(
"no tokio runtime in scope; construct inside #[tokio::test]/runtime, or use Ctx::root_with(handle)".into(),
)
})?;
Ok(Self::root_with_sink(handle, default_sink()))
}
pub fn root_with(handle: Handle) -> Ctx {
Self::root_with_sink(handle, default_sink())
}
pub fn root_with_sink(handle: Handle, sink: ErrorSink) -> Ctx {
let shared = Arc::new(Shared {
admission: Mutex::new(()),
handle,
bus: EventBus::new(),
registry: Registry::new(),
error_sink: sink,
next_plugin_id: AtomicU64::new(1),
closing: AtomicBool::new(false),
shutdown_task: Mutex::new(None),
});
let root_fiber = spawn_fiber(&shared, None, None, true);
root_fiber.ctx.clone()
}
pub fn root_view(&self) -> Option<FiberView> {
let mut current = self.clone();
while let Some(parent) = current.0.parent.clone() {
current = parent;
}
current.0.fiber.upgrade().map(FiberView::from_inner)
}
pub fn diagnostics(&self) -> RuntimeDiagnostics {
let mut plugins = Vec::new();
if let Some(root) = self.root_view() {
let mut pending = vec![root.inner];
while let Some(fiber) = pending.pop() {
pending.extend(
fiber
.children
.lock()
.unwrap()
.iter()
.filter_map(Weak::upgrade),
);
let snapshot = fiber.state_snapshot();
let injects = fiber
.declared_injects
.iter()
.map(|key| {
let scope = fiber.ctx.scope_for(key);
let status = if !fiber.ctx.in_instance_key(key) {
DependencyStatus::OutOfScope
} else {
self.0
.shared
.registry
.dependency_status(key, scope.as_ref())
};
DependencyDiagnostics {
key: key.clone(),
scope: scope.as_ref().map(|s| s.to_string()),
status,
}
})
.collect();
let resolved_dependencies = fiber
.last_deps
.lock()
.unwrap()
.as_ref()
.map(|deps| {
deps.iter()
.map(|(provider, generation, key, scope)| ResolvedDependency {
key: key.clone(),
scope: scope.as_ref().map(|s| s.to_string()),
provider: *provider,
generation: *generation,
})
.collect()
})
.unwrap_or_default();
plugins.push(PluginDiagnostics {
id: fiber.id,
instance: fiber.instance,
parent: fiber
.parent_fiber
.as_ref()
.and_then(Weak::upgrade)
.map(|p| p.id),
name: fiber.name.clone(),
state: snapshot.state,
generation: snapshot.generation,
error: snapshot.error,
injects,
resolved_dependencies,
accesses: fiber.accesses.lock().unwrap().clone(),
});
}
}
RuntimeDiagnostics {
shutting_down: self.0.shared.closing.load(Ordering::SeqCst),
plugins,
bindings: self.0.shared.registry.bindings_snapshot(),
}
}
pub fn shutdown(&self) -> crate::BoxFuture<'static, Result<(), Arc<CordisError>>> {
let _admission = self.0.shared.admission.lock().unwrap();
let task = {
let mut slot = self.0.shared.shutdown_task.lock().unwrap();
if let Some(task) = slot.as_ref() {
task.clone()
} else {
let task = TransitionTask::new();
*slot = Some(task.clone());
self.0.shared.closing.store(true, Ordering::SeqCst);
if let Some(root) = self.root_view() {
root.inner.cancel_current();
root.inner.post(Intent::Shutdown(task.clone()));
} else {
task.complete(Some(Arc::new(CordisError::Closed)));
}
task
}
};
Box::pin(async move { join_task(&task).await })
}
pub fn shutdown_with_timeout(
&self,
limit: Duration,
) -> crate::BoxFuture<'static, Result<(), DisposeWaitError>> {
let root = self.root_view();
let pending = self.shutdown();
let Some(root) = root else {
return Box::pin(async move { pending.await.map_err(DisposeWaitError::Failed) });
};
Box::pin(async move {
let started = Instant::now();
match tokio::time::timeout(limit, pending).await {
Ok(Ok(())) => Ok(()),
Ok(Err(error)) => Err(DisposeWaitError::Failed(error)),
Err(_) => {
let snapshot = root.state();
Err(DisposeWaitError::TimedOut {
plugin_id: root.id,
generation: snapshot.generation,
state: snapshot.state,
elapsed: started.elapsed(),
})
}
}
})
}
pub fn events(&self) -> &EventBus {
&self.0.shared.bus
}
pub(crate) fn scope_for(&self, key: &TypeKey) -> Option<ScopeId> {
let mut current = Some(self.clone());
while let Some(ctx) = current {
if let Some((k, scope)) = &ctx.0.isolate {
if k == key {
return Some(scope.clone());
}
}
current = ctx.0.parent.clone();
}
None
}
pub fn isolate(&self, key: impl Into<TypeKey>, label: &str) -> Ctx {
Ctx::new_child(
self.0.shared.clone(),
self,
self.0.fiber.clone(),
Some((key.into(), Arc::from(label))),
self.instance(),
)
}
pub fn get<T: Send + Sync + 'static>(&self) -> Option<Arc<T>> {
self.get_as::<T>(TypeKey::of::<T>())
}
pub fn get_as<T: ?Sized + Send + Sync + 'static>(
&self,
key: impl Into<TypeKey>,
) -> Option<Arc<T>> {
let key = key.into();
let scope = self.scope_for(&key);
let out_of_scope = !self.in_instance_key(&key);
let mut found = None;
let result = (|| {
if out_of_scope {
return None;
}
let binding = self.0.shared.registry.lookup(&key, scope.as_ref())?;
let provider = binding.provider.upgrade()?;
let self_access = self.in_subtree_of(&provider);
found = Some((binding.provider_id, binding.provider_gen, !self_access));
if !self_access {
match self.0.fiber.upgrade() {
None => return None,
Some(accessor)
if matches!(
accessor.state(),
FiberState::Unloading | FiberState::Disposed
) =>
{
return None
}
_ => {}
}
let visible = provider.state() == FiberState::Active
&& !binding.removing.load(std::sync::atomic::Ordering::SeqCst);
if !visible {
return None;
}
}
binding.value.downcast::<T>()
})();
if let Some(fiber) = self.0.fiber.upgrade() {
if fiber.state() == FiberState::Loading {
let (provider, generation, external) = found
.map(|(p, g, x)| (Some(p), Some(g), x))
.unwrap_or((None, None, false));
let access = ServiceAccess {
key: key.clone(),
scope: scope.as_ref().map(|s| s.to_string()),
provider,
generation,
declared: fiber.declared_injects.contains(&key),
external,
out_of_scope,
};
let mut accesses = fiber.accesses.lock().unwrap();
if !accesses.contains(&access) {
accesses.push(access);
}
}
}
result
}
fn in_subtree_of(&self, other: &Arc<FiberInner>) -> bool {
let mut current = self.0.fiber.upgrade();
while let Some(fiber) = current {
if Arc::ptr_eq(&fiber, other) {
return true;
}
current = fiber.parent_fiber.as_ref().and_then(|w| w.upgrade());
}
false
}
pub fn provide<T: Send + Sync + 'static>(&self, value: T) -> Result<Disposer, CordisError> {
self.provide_as::<T>(TypeKey::of::<T>(), Arc::new(value))
}
pub fn provide_as<T: ?Sized + Send + Sync + 'static>(
&self,
key: impl Into<TypeKey>,
value: Arc<T>,
) -> Result<Disposer, CordisError> {
self.provide_inner(key.into(), value, None)
}
pub fn provide_as_with_check<T: ?Sized + Send + Sync + 'static>(
&self,
key: impl Into<TypeKey>,
value: Arc<T>,
check: impl Fn() -> bool + Send + Sync + 'static,
) -> Result<Disposer, CordisError> {
self.provide_inner(key.into(), value, Some(Arc::new(check)))
}
fn provide_inner<T: ?Sized + Send + Sync + 'static>(
&self,
key: TypeKey,
value: Arc<T>,
check: Option<CheckFn>,
) -> Result<Disposer, CordisError> {
self.check_instance(&key)?;
if !key.has_type::<T>() {
return Err(CordisError::Validation {
issues: vec![format!(
"service key {} expects value of type {}, got {}",
key.describe(),
key.type_name(),
std::any::type_name::<T>()
)],
});
}
let scope = self.scope_for(&key);
let shared = self.0.shared.clone();
self.register_internal_effect(move |fiber, provider_gen, state| {
shared.registry.insert_binding(
key.clone(),
scope.clone(),
Binding {
value: StoredValue::new(value),
provider: Arc::downgrade(fiber),
provider_id: fiber.id,
provider_gen,
check,
check_status: Mutex::new(None),
removing: AtomicBool::new(false),
},
)?;
fiber
.provided
.lock()
.unwrap()
.push((key.clone(), scope.clone()));
let cleanup_shared = shared.clone();
let provider = Arc::downgrade(fiber);
let pid = fiber.id;
let evict_scope = scope.clone();
let evict_key = key.clone();
if state == FiberState::Active {
shared.registry.notify_key_changed(&key);
}
Ok(Effect::AsyncDisposer(Box::new(move || {
let shared = cleanup_shared.clone();
let provider = provider.clone();
let scope = evict_scope.clone();
Box::pin(async move {
evict_and_finalize(&shared, provider, pid, provider_gen, evict_key, scope).await
})
})))
})
}
pub fn effect(&self, f: impl FnOnce() -> Effect) -> Result<Disposer, CordisError> {
let record = self.register_effect(f)?;
let handle = self.handle().clone();
Ok(Disposer::new(Box::new(move || {
let record = record.clone();
let handle = handle.clone();
Box::pin(async move { record.drain(&handle).await })
})))
}
pub(crate) fn register_internal_effect(
&self,
f: impl FnOnce(&Arc<FiberInner>, u64, FiberState) -> Result<Effect, CordisError>,
) -> Result<Disposer, CordisError> {
let _admission = self.0.shared.admission.lock().unwrap();
self.registration_open()?;
let fiber = self.0.fiber.upgrade().ok_or(CordisError::InactiveEffect)?;
let tr = fiber.transition.lock().unwrap();
if matches!(tr.state, FiberState::Unloading | FiberState::Disposed) {
return Err(CordisError::InactiveEffect);
}
let effect = f(&fiber, tr.generation, tr.state)?;
let record = EffectRecord::new(effect, self.0.fiber.clone());
fiber.effects.lock().unwrap().push(record.clone());
drop(tr);
let handle = self.handle().clone();
Ok(Disposer::new(Box::new(move || {
let record = record.clone();
let handle = handle.clone();
Box::pin(async move { record.drain(&handle).await })
})))
}
fn register_mount_effect(
&self,
child: &Arc<FiberInner>,
make: impl FnOnce() -> Effect,
) -> Result<Arc<EffectRecord>, CordisError> {
let _admission = self.0.shared.admission.lock().unwrap();
self.registration_open()?;
if child.closing.load(Ordering::SeqCst) || !child.alive.load(Ordering::SeqCst) {
return Err(CordisError::Closed);
}
let parent = self.0.fiber.upgrade().ok_or(CordisError::InactiveEffect)?;
let tr = parent.transition.lock().unwrap();
if matches!(tr.state, FiberState::Unloading | FiberState::Disposed) {
return Err(CordisError::InactiveEffect);
}
let record = EffectRecord::new(make(), self.0.fiber.clone());
parent.effects.lock().unwrap().push(record.clone());
*child.mount.lock().unwrap() = Some(record.clone());
Ok(record)
}
pub(crate) fn register_effect(
&self,
f: impl FnOnce() -> Effect,
) -> Result<Arc<EffectRecord>, CordisError> {
let fiber = self.0.fiber.upgrade().ok_or(CordisError::InactiveEffect)?;
let lease = {
let _admission = self.0.shared.admission.lock().unwrap();
let tr = fiber.transition.lock().unwrap();
if self.0.shared.closing.load(Ordering::SeqCst)
|| (self.subtree_closing() && tr.state != FiberState::Loading)
{
return Err(CordisError::Closed);
}
if matches!(tr.state, FiberState::Unloading | FiberState::Disposed) {
return Err(CordisError::InactiveEffect);
}
fiber.begin_event();
EffectFactoryLease(fiber.clone())
};
let record = EffectRecord::new(f(), self.0.fiber.clone());
let handle = self.handle().clone();
{
let tr = fiber.transition.lock().unwrap();
if matches!(tr.state, FiberState::Unloading | FiberState::Disposed) {
drop(tr);
let sink = self.error_sink();
let drain_handle = handle.clone();
handle.spawn(async move {
if let Err(e) = record.drain(&drain_handle).await {
sink(e);
}
drop(lease);
});
return Err(CordisError::InactiveEffect);
}
fiber.effects.lock().unwrap().push(record.clone());
}
drop(lease);
Ok(record)
}
pub fn plugin(&self, p: impl Plugin) -> FiberView {
let fiber = spawn_fiber(&self.0.shared, Some(self), Some(Arc::new(p)), false);
self.mount_fiber(fiber)
}
pub fn plugin_with<C: Send + Sync + 'static>(
&self,
factory: impl PluginFactory<C>,
config: C,
) -> FiberView {
let fiber =
crate::fiber::spawn_factory_fiber(&self.0.shared, Some(self), factory, config, false);
self.mount_fiber(fiber)
}
pub fn plugin_from<C: Send + Sync + 'static>(
&self,
build: impl Fn(&C) -> Result<Box<dyn Plugin>, CordisError> + Send + Sync + 'static,
config: C,
) -> FiberView {
struct ClosureFactory<F> {
build: F,
}
impl<C, F> PluginFactory<C> for ClosureFactory<F>
where
C: Send + Sync + 'static,
F: Fn(&C) -> Result<Box<dyn Plugin>, CordisError> + Send + Sync + 'static,
{
fn build(&self, config: &C) -> Result<Box<dyn Plugin>, CordisError> {
(self.build)(config)
}
}
self.plugin_with(ClosureFactory { build }, config)
}
fn mount_fiber(&self, fiber: std::sync::Arc<crate::fiber::FiberInner>) -> FiberView {
let view = FiberView::from_inner(fiber.clone());
if !fiber.alive.load(Ordering::SeqCst) {
return view;
}
let child = view.clone();
let sink = self.error_sink();
let registered = self.register_mount_effect(&fiber, move || {
Effect::AsyncDisposer(Box::new(move || {
let child = child.clone();
let sink = sink.clone();
Box::pin(async move {
let closing_task = {
let _admission = child.inner.ctx.shared().admission.lock().unwrap();
child.inner.shutdown_inner.lock().unwrap().clone()
};
if let Some(task) = closing_task {
let _ = join_task(&task).await;
return Ok(());
}
let delivered = matches!(child.state().state, FiberState::Disposed);
if let Err(e) = child.dispose().await {
if !delivered {
sink(e);
}
}
Ok(())
})
}))
});
match registered {
Ok(record) => {
drop(record);
fiber.post(Intent::RefreshDeps);
}
Err(_) => {
if fiber.closing.load(Ordering::SeqCst) {
} else if self.0.shared.closing.load(Ordering::SeqCst) {
drop(view.shutdown());
} else {
fiber.post(Intent::Dispose);
}
}
}
view
}
pub fn cancellation_token(&self) -> CancellationToken {
match self.0.fiber.upgrade() {
Some(fiber) => fiber.current_token(),
None => {
let token = CancellationToken::new();
token.cancel();
token
}
}
}
pub fn cancelled(&self) -> impl Future<Output = ()> + Send + 'static {
let token = self.cancellation_token();
async move { token.cancelled().await }
}
pub fn refresh(&self) {
self.0.shared.registry.refresh_all();
}
}
async fn evict_and_finalize(
shared: &Arc<Shared>,
provider: Weak<FiberInner>,
pid: crate::PluginId,
provider_gen: u64,
key: TypeKey,
scope: Option<crate::key::ScopeId>,
) -> Result<(), CordisError> {
let old = shared
.registry
.lookup(&key, scope.as_ref())
.filter(|b| b.provider_id == pid && b.provider_gen == provider_gen);
if let Some(binding) = &old {
binding
.removing
.store(true, std::sync::atomic::Ordering::SeqCst);
}
let consumers: Vec<Arc<FiberInner>> = shared
.registry
.consumers_of(&key, (pid, provider_gen, key.clone(), scope.clone()));
let mut tasks = Vec::new();
for fiber in &consumers {
if fiber.id == pid {
continue;
}
fiber.cancel_current();
if fiber.closing.load(Ordering::SeqCst) {
let task = {
let _admission = shared.admission.lock().unwrap();
fiber.shutdown_inner.lock().unwrap().clone()
};
if let Some(task) = task {
tasks.push(task);
}
continue;
}
let task = TransitionTask::new();
fiber.post_join(task.clone(), Intent::RefreshDepsJoin);
tasks.push(task);
}
shared.registry.notify_key_changed(&key);
for task in tasks {
let _ = join_task(&task).await;
}
if let Some(binding) = old {
shared
.registry
.finalize_binding_if(key.clone(), scope.clone(), &binding);
}
if let Some(fiber) = provider.upgrade() {
let mut provided = fiber.provided.lock().unwrap();
if let Some(pos) = provided.iter().position(|(k, s)| *k == key && *s == scope) {
provided.swap_remove(pos);
}
if provided.capacity() > 64 && provided.len() * 4 < provided.capacity() {
provided.shrink_to_fit();
}
}
Ok(())
}