use std::any::{Any, TypeId};
use std::collections::HashSet;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, Weak};
use std::time::{Duration, Instant};
use tokio::sync::{mpsc, watch};
use tokio_util::sync::CancellationToken;
use crate::ctx::{Ctx, Shared};
use crate::diagnostics::ServiceAccess;
use crate::effect::{Effect, EffectMeta, EffectRecord};
use crate::error::{aggregate_arcs, panic_error, CordisError, ServiceReadFailure};
use crate::event::{CatchUnwind, Event};
use crate::key::{InstanceId, ScopeId, TypeKey};
use crate::{BoxFuture, Plugin, PluginFactory};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FiberState {
Pending,
Loading,
Active,
Failed,
Disposed,
Unloading,
}
#[derive(Debug, Clone)]
pub struct Snapshot {
pub generation: u64,
pub state: FiberState,
pub error: Option<Arc<CordisError>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct PluginId(pub u64);
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum DisposeWaitError {
#[error("fiber {plugin_id:?} generation {generation} still {state:?} after {elapsed:?}")]
TimedOut {
plugin_id: PluginId,
generation: u64,
state: FiberState,
elapsed: Duration,
},
#[error("fiber disposal failed: {0}")]
Failed(Arc<CordisError>),
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct FiberStatusChanged {
pub plugin_id: PluginId,
pub seq: u64,
pub generation: u64,
pub from: FiberState,
pub to: FiberState,
}
impl Event for FiberStatusChanged {
const NAME: &'static str = "rutis::FiberStatusChanged";
type Value = ();
}
pub(crate) enum TaskDone {
Running,
Done(Option<Arc<CordisError>>),
}
pub(crate) struct TransitionTask {
pub done_tx: watch::Sender<TaskDone>,
_keepalive: watch::Receiver<TaskDone>,
}
impl TransitionTask {
pub(crate) fn new() -> Arc<Self> {
let (done_tx, done_rx) = watch::channel(TaskDone::Running);
Arc::new(Self {
done_tx,
_keepalive: done_rx,
})
}
pub(crate) fn complete(&self, err: Option<Arc<CordisError>>) {
let _ = self.done_tx.send(TaskDone::Done(err));
}
}
pub(crate) enum Intent {
RefreshDeps,
RefreshDepsJoin(Arc<TransitionTask>),
Settle(Arc<TransitionTask>),
Restart(Arc<TransitionTask>),
Dispose,
Shutdown(Arc<TransitionTask>),
}
impl Intent {
fn task(&self) -> Option<&Arc<TransitionTask>> {
match self {
Intent::RefreshDepsJoin(task)
| Intent::Settle(task)
| Intent::Restart(task)
| Intent::Shutdown(task) => Some(task),
_ => None,
}
}
}
fn complete_intent(intent: &Intent, err: Option<Arc<CordisError>>) {
if let Some(task) = intent.task() {
task.complete(err);
}
}
enum NextState {
Pending,
Disposed,
}
pub(crate) struct Trans {
pub generation: u64,
pub state: FiberState,
pub error: Option<Arc<CordisError>>,
pub seq: u64,
pub status_queue: Vec<FiberStatusChanged>,
pub terminal_task: Option<Arc<TransitionTask>>,
}
pub(crate) struct FiberInner {
pub id: PluginId,
pub instance: InstanceId,
pub name: String,
pub is_root: bool,
pub plugin: Option<Arc<dyn Plugin>>,
pub factory: Option<Arc<dyn ErasedFactory>>,
pub config: Mutex<Option<Arc<dyn Any + Send + Sync>>>,
pub ctx: Ctx,
pub parent_fiber: Option<Weak<FiberInner>>,
pub children: Mutex<Vec<Weak<FiberInner>>>,
pub closing: Arc<AtomicBool>,
pub event_flights: Mutex<usize>,
pub event_flights_tx: watch::Sender<usize>,
_event_flights_rx: watch::Receiver<usize>,
pub shutdown_task: Mutex<Option<Arc<TransitionTask>>>,
pub shutdown_inner: Mutex<Option<Arc<TransitionTask>>>,
pub driver: Mutex<Option<tokio::task::JoinHandle<()>>>,
pub token: Mutex<CancellationToken>,
pub transition: Mutex<Trans>,
pub snapshot_tx: watch::Sender<Snapshot>,
pub snapshot_rx: watch::Receiver<Snapshot>,
pub effects: Mutex<Vec<Arc<EffectRecord>>>,
pub effect_index: Mutex<Vec<Weak<EffectRecord>>>,
pub drained_errors: Mutex<Vec<Arc<CordisError>>>,
pub mount: Mutex<Option<Arc<EffectRecord>>>,
pub declared_injects: Vec<TypeKey>,
pub intents_tx: mpsc::UnboundedSender<Intent>,
pub alive: AtomicBool,
pub provided: Mutex<Vec<(TypeKey, Option<crate::key::ScopeId>)>>,
pub last_deps: Mutex<Option<HashSet<(PluginId, u64, TypeKey, Option<crate::key::ScopeId>)>>>,
pub accesses: Mutex<Vec<ServiceAccess>>,
}
pub(crate) trait ErasedFactory: Send + Sync + 'static {
fn config_type_id(&self) -> TypeId;
fn name(&self) -> &str;
fn injects(&self) -> &[TypeKey];
fn validate_config_erased(&self, config: &dyn Any) -> Result<(), CordisError>;
fn build_erased(&self, config: &dyn Any) -> Result<Box<dyn Plugin>, CordisError>;
}
struct FactoryAdapter<F, C> {
factory: F,
_marker: std::marker::PhantomData<fn() -> C>,
}
impl<F, C> ErasedFactory for FactoryAdapter<F, C>
where
F: PluginFactory<C>,
C: Send + Sync + 'static,
{
fn config_type_id(&self) -> TypeId {
TypeId::of::<C>()
}
fn name(&self) -> &str {
self.factory.name()
}
fn injects(&self) -> &[TypeKey] {
self.factory.injects()
}
fn validate_config_erased(&self, config: &dyn Any) -> Result<(), CordisError> {
config
.downcast_ref::<C>()
.ok_or_else(|| CordisError::Validation {
issues: vec!["config type mismatch".into()],
})
.and_then(|c| self.factory.validate_config(c))
}
fn build_erased(&self, config: &dyn Any) -> Result<Box<dyn Plugin>, CordisError> {
config
.downcast_ref::<C>()
.ok_or_else(|| CordisError::Validation {
issues: vec!["config type mismatch".into()],
})
.and_then(|c| self.factory.build(c))
}
}
impl FiberInner {
pub(crate) fn push_effect(&self, record: Arc<EffectRecord>) {
self.effects.lock().unwrap().push(record.clone());
self.effect_index
.lock()
.unwrap()
.push(Arc::downgrade(&record));
}
fn current_plugin(&self) -> Result<Arc<dyn Plugin>, CordisError> {
if let Some(plugin) = &self.plugin {
return Ok(plugin.clone());
}
let factory = self
.factory
.as_ref()
.ok_or_else(|| CordisError::PluginFailed("fiber has no plugin or factory".into()))?;
let config = self
.config
.lock()
.unwrap()
.clone()
.ok_or_else(|| CordisError::PluginFailed("factory fiber has no config".into()))?;
let built = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
factory.build_erased(config.as_ref())
}))
.unwrap_or_else(|p| Err(panic_error(p)));
built.map(Arc::from)
}
pub(crate) fn current_token(&self) -> CancellationToken {
self.token.lock().unwrap().clone()
}
pub(crate) fn cancel_current(&self) {
self.token.lock().unwrap().cancel();
}
pub(crate) fn cancel_descendants(&self) {
let mut pending: Vec<_> = self
.children
.lock()
.unwrap()
.iter()
.filter_map(Weak::upgrade)
.collect();
while let Some(fiber) = pending.pop() {
fiber.cancel_current();
pending.extend(
fiber
.children
.lock()
.unwrap()
.iter()
.filter_map(Weak::upgrade),
);
}
}
pub(crate) fn begin_event(&self) {
let mut count = self.event_flights.lock().unwrap();
*count += 1;
let _ = self.event_flights_tx.send(*count);
}
pub(crate) fn finish_event(&self) {
let mut count = self.event_flights.lock().unwrap();
*count -= 1;
let _ = self.event_flights_tx.send(*count);
}
pub(crate) async fn wait_events(&self) {
let mut rx = self.event_flights_tx.subscribe();
loop {
if *rx.borrow_and_update() == 0 {
return;
}
if rx.changed().await.is_err() {
return;
}
}
}
fn new_generation_token(&self) -> CancellationToken {
let token = CancellationToken::new();
*self.token.lock().unwrap() = token.clone();
token
}
pub(crate) fn post(&self, intent: Intent) -> bool {
if !self.alive.load(Ordering::SeqCst) {
complete_intent(&intent, self.stopped_error());
return false;
}
match self.intents_tx.send(intent) {
Ok(()) => true,
Err(err) => {
complete_intent(&err.0, self.stopped_error());
false
}
}
}
fn stopped_error(&self) -> Option<Arc<CordisError>> {
self.transition.lock().unwrap().error.clone().or_else(|| {
self.ctx
.shared()
.closing
.load(Ordering::SeqCst)
.then(|| Arc::new(CordisError::Closed))
})
}
pub(crate) fn post_join(
&self,
task: Arc<TransitionTask>,
make: impl FnOnce(Arc<TransitionTask>) -> Intent,
) -> bool {
if !self.alive.load(Ordering::SeqCst) {
task.complete(self.stopped_error());
return false;
}
match self.intents_tx.send(make(task.clone())) {
Ok(()) => {
if !self.alive.load(Ordering::SeqCst) {
task.complete(self.stopped_error());
}
true
}
Err(_) => {
task.complete(self.stopped_error());
false
}
}
}
pub(crate) fn state(&self) -> FiberState {
self.transition.lock().unwrap().state
}
pub(crate) fn state_snapshot(&self) -> Snapshot {
let tr = self.transition.lock().unwrap();
Snapshot {
generation: tr.generation,
state: tr.state,
error: tr.error.clone(),
}
}
fn set_state(&self, tr: &mut Trans, new: FiberState) {
let old = tr.state;
if old == new {
return;
}
tr.state = new;
tr.seq += 1;
tr.status_queue.push(FiberStatusChanged {
plugin_id: self.id,
seq: tr.seq,
generation: tr.generation,
from: old,
to: new,
});
let _ = self.snapshot_tx.send(Snapshot {
generation: tr.generation,
state: new,
error: tr.error.clone(),
});
}
fn flush_status(&self) {
let queue: Vec<FiberStatusChanged> = {
let mut tr = self.transition.lock().unwrap();
std::mem::take(&mut tr.status_queue)
};
for event in queue {
if let Err(error) =
self.ctx
.shared()
.bus
.emit(&self.ctx, &crate::EventKey::of(), Arc::new(event))
{
self.ctx.error_sink()(Arc::new(error));
}
}
}
fn resolve_deps(
&self,
) -> (
HashSet<(PluginId, u64, TypeKey, Option<ScopeId>)>,
Vec<TypeKey>,
) {
let mut satisfied: HashSet<(PluginId, u64, TypeKey, Option<ScopeId>)> = HashSet::new();
let mut missing: Vec<TypeKey> = Vec::new();
let registry = &self.ctx.shared().registry;
for key in self.declared_injects.iter().cloned() {
if self.ctx.check_instance(&key).is_err() {
missing.push(key);
continue;
}
let scope = self.ctx.scope_for(&key);
match registry.resolve_dep(&key, scope.as_ref()) {
Some((provider_id, provider_gen)) => {
satisfied.insert((provider_id, provider_gen, key, scope));
}
None => missing.push(key),
}
}
(satisfied, missing)
}
fn read_a_lost_dependency(&self) -> bool {
self.accesses.lock().unwrap().iter().any(|access| {
access.strict
&& matches!(access.failure, Some(ServiceReadFailure::Unavailable(_)))
&& self.declared_injects.contains(&access.key)
})
}
async fn refresh_deps(this: &Arc<Self>) {
if this.closing.load(Ordering::SeqCst) || this.ctx.shared().closing.load(Ordering::SeqCst) {
return;
}
if this.plugin.is_none() && this.factory.is_none() {
return;
}
if this.state() == FiberState::Disposed {
return;
}
let (satisfied, missing) = this.resolve_deps();
let loaded = !matches!(this.state(), FiberState::Pending | FiberState::Disposed);
if !missing.is_empty() {
if matches!(this.state(), FiberState::Active | FiberState::Loading) {
Self::unload(this, NextState::Pending).await;
}
return;
}
let unchanged = {
let last = this.last_deps.lock().unwrap();
last.as_ref() == Some(&satisfied)
};
if unchanged {
return;
}
if loaded {
Self::unload(this, NextState::Pending).await;
}
Self::load(this, satisfied).await;
}
async fn load(this: &Arc<Self>, deps: HashSet<(PluginId, u64, TypeKey, Option<ScopeId>)>) {
let shared = this.ctx.shared().clone();
let _admission = shared.admission.lock().unwrap();
if this.closing.load(Ordering::SeqCst) || shared.closing.load(Ordering::SeqCst) {
return;
}
this.new_generation_token();
this.accesses.lock().unwrap().clear();
let generation = {
let mut tr = this.transition.lock().unwrap();
tr.generation += 1;
tr.error = None;
Self::set_state(this, &mut tr, FiberState::Loading);
tr.generation
};
this.flush_status();
drop(_admission);
let plugin = match this.current_plugin() {
Ok(p) => p,
Err(e) => {
Self::fail_load(this, e).await;
return;
}
};
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| plugin.validate())) {
Ok(Ok(())) => {}
Ok(Err(e)) => {
Self::fail_load(this, e).await;
return;
}
Err(p) => {
Self::fail_load(this, panic_error(p)).await;
return;
}
}
*this.last_deps.lock().unwrap() = Some(deps.clone());
let ctx = this.ctx.for_generation(generation);
let outcome = CatchUnwind::new(async { plugin.apply(&ctx).await }).await;
let result: Result<Effect, CordisError> = outcome.unwrap_or_else(|p| Err(panic_error(p)));
match result {
Ok(effect) => {
let record = EffectRecord::new(
effect,
format!("plugin apply: {}", this.name),
Arc::downgrade(this),
);
this.push_effect(record);
if this.closing.load(Ordering::SeqCst) {
Self::unload(this, NextState::Disposed).await;
return;
}
let (fresh, missing) = this.resolve_deps();
if missing.is_empty() {
*this.last_deps.lock().unwrap() = Some(fresh);
}
{
let mut tr = this.transition.lock().unwrap();
tr.error = None;
Self::set_state(this, &mut tr, FiberState::Active);
}
let provided: Vec<TypeKey> = this
.provided
.lock()
.unwrap()
.iter()
.map(|(k, _)| k.clone())
.collect();
for key in provided {
shared.registry.notify_key_changed(&key);
}
}
Err(CordisError::InjectUnsatisfied(_)) if this.read_a_lost_dependency() => {
Self::unload(this, NextState::Pending).await;
if this.resolve_deps().1.is_empty() {
this.post(Intent::RefreshDeps);
}
}
Err(e) => Self::fail_load(this, e).await,
}
this.flush_status();
}
async fn fail_load(this: &Arc<Self>, error: CordisError) {
{
let _admission = this.ctx.shared().admission.lock().unwrap();
let mut tr = this.transition.lock().unwrap();
Self::set_state(this, &mut tr, FiberState::Unloading);
}
this.flush_status();
this.cancel_current();
this.wait_events().await;
let cleanup_errors = Self::drain_effects(this).await;
let sink = this.ctx.error_sink();
for e in &cleanup_errors {
sink(e.clone());
}
let mut combined = vec![Arc::new(error)];
combined.extend(cleanup_errors);
let arc = aggregate_arcs(combined).expect("failure present");
{
let mut tr = this.transition.lock().unwrap();
tr.error = Some(arc);
Self::set_state(this, &mut tr, FiberState::Failed);
}
this.flush_status();
}
async fn drain_effects(this: &Arc<Self>) -> Vec<Arc<CordisError>> {
let handle = this.ctx.handle().clone();
let effects: Vec<Arc<EffectRecord>> = std::mem::take(&mut *this.effects.lock().unwrap());
let mut errors: Vec<Arc<CordisError>> = Vec::new();
for record in effects.into_iter().rev() {
if let Err(e) = record.drain(&handle).await {
errors.push(e);
}
}
errors.extend(std::mem::take(&mut *this.drained_errors.lock().unwrap()));
*this.last_deps.lock().unwrap() = None;
this.provided.lock().unwrap().clear();
errors
}
async fn unload(this: &Arc<Self>, next: NextState) {
{
let _admission = this.ctx.shared().admission.lock().unwrap();
let mut tr = this.transition.lock().unwrap();
Self::set_state(this, &mut tr, FiberState::Unloading);
}
this.flush_status();
this.cancel_current();
this.wait_events().await;
let errors = Self::drain_effects(this).await;
let err = aggregate_arcs(errors);
match next {
NextState::Pending => {
if let Some(e) = err {
(this.ctx.error_sink())(e);
}
let mut tr = this.transition.lock().unwrap();
Self::set_state(this, &mut tr, FiberState::Pending);
}
NextState::Disposed => {
let mut tr = this.transition.lock().unwrap();
tr.error = err;
Self::set_state(this, &mut tr, FiberState::Disposed);
}
}
this.flush_status();
}
}
pub(crate) async fn drive(this: Arc<FiberInner>, mut rx: mpsc::UnboundedReceiver<Intent>) {
while let Some(intent) = rx.recv().await {
let current_task = intent.task().cloned();
let terminal_dispose = matches!(intent, Intent::Dispose) && !this.is_root;
let shutdown_task = match &intent {
Intent::Shutdown(task) => Some(task.clone()),
_ => None,
};
let outcome = CatchUnwind::new(async {
match intent {
Intent::RefreshDeps => FiberInner::refresh_deps(&this).await,
Intent::RefreshDepsJoin(task) => {
FiberInner::refresh_deps(&this).await;
complete_task(&this, task);
}
Intent::Settle(task) => {
let err = {
let tr = this.transition.lock().unwrap();
(tr.state == FiberState::Failed)
.then(|| tr.error.clone())
.flatten()
};
task.complete(err);
}
Intent::Restart(task) => {
if this.closing.load(Ordering::SeqCst)
|| this.ctx.shared().closing.load(Ordering::SeqCst)
{
task.complete(Some(Arc::new(CordisError::Closed)));
return;
}
let state = this.state();
if state == FiberState::Disposed {
if this.is_root {
this.new_generation_token();
{
let mut tr = this.transition.lock().unwrap();
tr.error = None;
tr.terminal_task = None;
FiberInner::set_state(&this, &mut tr, FiberState::Active);
}
this.flush_status();
}
complete_task(&this, task);
return;
}
if !matches!(state, FiberState::Pending) {
FiberInner::unload(&this, NextState::Pending).await;
}
FiberInner::refresh_deps(&this).await;
complete_task(&this, task);
}
Intent::Dispose => {
if this.closing.load(Ordering::SeqCst) {
this.wait_events().await;
}
if this.state() != FiberState::Disposed {
FiberInner::unload(&this, NextState::Disposed).await;
}
let (task, err) = {
let tr = this.transition.lock().unwrap();
(tr.terminal_task.clone(), tr.error.clone())
};
if let Some(task) = task {
let _ = task.done_tx.send(TaskDone::Done(err));
}
}
Intent::Shutdown(_) => {
this.wait_events().await;
let prior = if !this.is_root && this.state() == FiberState::Failed {
this.transition.lock().unwrap().error.clone()
} else {
None
};
if this.state() != FiberState::Disposed {
FiberInner::unload(&this, NextState::Disposed).await;
}
if let Some(prior) = prior {
let mut tr = this.transition.lock().unwrap();
tr.error =
aggregate_arcs(std::iter::once(prior).chain(tr.error.take()).collect());
}
}
}
this.flush_status();
})
.await;
if let Err(panic) = outcome {
recover_driver_panic(&this, &mut rx, current_task, panic).await;
return;
}
if let Some(task) = shutdown_task {
this.alive.store(false, Ordering::SeqCst);
let (terminal, err) = {
let tr = this.transition.lock().unwrap();
(tr.terminal_task.clone(), tr.error.clone())
};
while let Ok(intent) = rx.try_recv() {
let completion = match &intent {
Intent::Settle(_) => None,
Intent::Shutdown(_) => err.clone(),
_ => Some(Arc::new(CordisError::Closed)),
};
complete_intent(&intent, completion);
}
if !this.is_root {
task.complete(err.clone());
}
if !this.is_root {
release_transient(&this).await;
}
if let Some(terminal) = terminal {
terminal.complete(err.clone());
}
drop(this);
task.complete(err);
return;
}
if terminal_dispose {
this.alive.store(false, Ordering::SeqCst);
let terminal_error = this.transition.lock().unwrap().error.clone();
while let Ok(intent) = rx.try_recv() {
let error = if matches!(intent, Intent::Settle(_)) {
None
} else {
terminal_error.clone()
};
complete_intent(&intent, error);
}
release_transient(&this).await;
return; }
}
}
async fn recover_driver_panic(
this: &Arc<FiberInner>,
rx: &mut mpsc::UnboundedReceiver<Intent>,
current_task: Option<Arc<TransitionTask>>,
panic: Box<dyn Any + Send>,
) {
this.cancel_current();
{
let _admission = this.ctx.shared().admission.lock().unwrap();
let mut tr = this.transition.lock().unwrap();
FiberInner::set_state(this, &mut tr, FiberState::Unloading);
}
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| this.flush_status()));
this.wait_events().await;
let mut errors = vec![Arc::new(panic_error(panic))];
let cleanup_errors = match CatchUnwind::new(FiberInner::drain_effects(this)).await {
Ok(cleanup_errors) => cleanup_errors,
Err(cleanup_panic) => vec![Arc::new(panic_error(cleanup_panic))],
};
let sink = this.ctx.error_sink();
for error in &cleanup_errors {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| sink(error.clone())));
}
errors.extend(cleanup_errors);
let error = aggregate_arcs(errors).expect("driver panic present");
let terminal_task = {
let mut tr = this.transition.lock().unwrap();
tr.error = Some(error.clone());
FiberInner::set_state(this, &mut tr, FiberState::Failed);
this.alive.store(false, Ordering::SeqCst);
tr.terminal_task.clone()
};
if let Some(task) = current_task {
task.complete(Some(error.clone()));
}
if let Some(task) = terminal_task {
task.complete(Some(error.clone()));
}
while let Ok(intent) = rx.try_recv() {
complete_intent(&intent, Some(error.clone()));
}
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| this.flush_status()));
if !this.is_root {
let _ = CatchUnwind::new(release_transient(this)).await;
}
}
fn complete_task(this: &Arc<FiberInner>, task: Arc<TransitionTask>) {
let err = {
let tr = this.transition.lock().unwrap();
tr.error.clone()
};
task.complete(err);
}
async fn release_transient(this: &Arc<FiberInner>) {
let shared = this.ctx.shared().clone();
shared
.registry
.unregister_injects(this, &this.declared_injects);
let record = this.mount.lock().unwrap().take();
if let Some(record) = record {
let handle = this.ctx.handle().clone();
if let Err(e) = record.drain(&handle).await {
let sink = this.ctx.error_sink();
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| sink(e)));
}
}
if let Some(parent) = this.parent_fiber.as_ref().and_then(Weak::upgrade) {
let self_weak = Arc::downgrade(this);
let mut children = parent.children.lock().unwrap();
children.retain(|child| !Weak::ptr_eq(child, &self_weak) && child.strong_count() > 0);
if children.capacity() > 64 && children.len() * 4 < children.capacity() {
children.shrink_to_fit();
}
}
}
pub(crate) fn spawn_fiber(
shared: &Arc<Shared>,
parent_ctx: Option<&Ctx>,
plugin: Option<Arc<dyn Plugin>>,
is_root: bool,
) -> Arc<FiberInner> {
spawn_fiber_inner(shared, parent_ctx, plugin, None, None, is_root)
}
pub(crate) fn spawn_factory_fiber<F, C>(
shared: &Arc<Shared>,
parent_ctx: Option<&Ctx>,
factory: F,
config: C,
is_root: bool,
) -> Arc<FiberInner>
where
F: PluginFactory<C>,
C: Send + Sync + 'static,
{
let erased: Arc<dyn ErasedFactory> = Arc::new(FactoryAdapter {
factory,
_marker: std::marker::PhantomData,
});
let boxed: Arc<dyn Any + Send + Sync> = Arc::new(config);
spawn_fiber_inner(shared, parent_ctx, None, Some(erased), Some(boxed), is_root)
}
fn spawn_fiber_inner(
shared: &Arc<Shared>,
parent_ctx: Option<&Ctx>,
plugin: Option<Arc<dyn Plugin>>,
factory: Option<Arc<dyn ErasedFactory>>,
config: Option<Arc<dyn Any + Send + Sync>>,
is_root: bool,
) -> Arc<FiberInner> {
let is_closed = || {
!is_root
&& (shared.closing.load(Ordering::SeqCst)
|| parent_ctx
.is_none_or(|p| p.subtree_closing() || p.weak_fiber().upgrade().is_none()))
};
let known_closed = is_closed();
let name = if known_closed {
"closed".to_string()
} else {
plugin
.as_ref()
.map(|p| p.name().to_string())
.or_else(|| factory.as_ref().map(|f| f.name().to_string()))
.unwrap_or_else(|| "root".to_string())
};
let declared_injects: Vec<TypeKey> = if known_closed {
Vec::new()
} else if let Some(plugin) = &plugin {
plugin.injects().to_vec()
} else if let Some(factory) = &factory {
factory.injects().to_vec()
} else {
Vec::new()
};
let _admission = shared.admission.lock().unwrap();
let closed = is_closed();
let (plugin, factory, config, declared_injects) = if closed {
(None, None, None, Vec::new())
} else {
(plugin, factory, config, declared_injects)
};
let id = PluginId(
shared
.next_plugin_id
.fetch_add(1, std::sync::atomic::Ordering::SeqCst),
);
let instance = InstanceId::allocate();
let initial = if closed {
FiberState::Disposed
} else if is_root {
FiberState::Active
} else {
FiberState::Pending
};
let closed_error = closed.then(|| Arc::new(CordisError::Closed));
let closed_task = closed.then(|| {
let task = TransitionTask::new();
task.complete(closed_error.clone());
task
});
let (snapshot_tx, snapshot_rx) = watch::channel(Snapshot {
generation: 0,
state: initial,
error: closed_error.clone(),
});
let (event_flights_tx, event_flights_rx) = watch::channel(0);
let (tx, rx) = mpsc::unbounded_channel();
let token = CancellationToken::new();
let closing = Arc::new(AtomicBool::new(closed));
let parent_fiber = parent_ctx.map(|p| p.weak_fiber());
let this = Arc::new_cyclic(|weak: &Weak<FiberInner>| {
let ctx = match parent_ctx {
Some(parent) => Ctx::new_child(
shared.clone(),
parent,
weak.clone(),
None,
instance,
id,
closing.clone(),
),
None => Ctx::new_root(shared.clone(), weak.clone(), instance, id, closing.clone()),
};
FiberInner {
id,
instance,
name: if closed { "closed".to_string() } else { name },
is_root,
plugin,
factory,
config: Mutex::new(config),
ctx,
parent_fiber,
children: Mutex::new(Vec::new()),
closing,
event_flights: Mutex::new(0),
event_flights_tx,
_event_flights_rx: event_flights_rx,
token: Mutex::new(token),
transition: Mutex::new(Trans {
generation: 0,
state: initial,
error: closed_error,
seq: 0,
status_queue: Vec::new(),
terminal_task: closed_task,
}),
snapshot_tx: snapshot_tx.clone(),
snapshot_rx,
effects: Mutex::new(Vec::new()),
effect_index: Mutex::new(Vec::new()),
drained_errors: Mutex::new(Vec::new()),
mount: Mutex::new(None),
declared_injects,
intents_tx: tx.clone(),
alive: AtomicBool::new(!closed),
provided: Mutex::new(Vec::new()),
last_deps: Mutex::new(None),
accesses: Mutex::new(Vec::new()),
shutdown_task: Mutex::new(None),
shutdown_inner: Mutex::new(None),
driver: Mutex::new(None),
}
});
if !closed {
if let Some(parent) = this.parent_fiber.as_ref().and_then(Weak::upgrade) {
parent.children.lock().unwrap().push(Arc::downgrade(&this));
}
}
if !closed {
for key in &this.declared_injects {
if this.ctx.in_instance_key(key) {
shared.registry.register_inject(key.clone(), &this);
}
}
let driver = shared.handle.spawn(drive(this.clone(), rx));
*this.driver.lock().unwrap() = Some(driver);
}
this
}
pub struct FiberView {
pub id: PluginId,
pub(crate) inner: Arc<FiberInner>,
}
impl Clone for FiberView {
fn clone(&self) -> Self {
Self {
id: self.id,
inner: self.inner.clone(),
}
}
}
impl FiberView {
pub(crate) fn from_inner(inner: Arc<FiberInner>) -> Self {
Self {
id: inner.id,
inner,
}
}
pub fn state(&self) -> Snapshot {
self.inner.state_snapshot()
}
pub fn effects(&self) -> Vec<EffectMeta> {
let records = self.inner.effect_index.lock().unwrap().clone();
records
.into_iter()
.filter_map(|entry| entry.upgrade())
.filter_map(|record| record.snapshot())
.collect()
}
pub fn name(&self) -> &str {
&self.inner.name
}
pub fn take_cleanup_errors(&self) -> Vec<Arc<CordisError>> {
std::mem::take(&mut *self.inner.drained_errors.lock().unwrap())
}
pub fn shutdown(&self) -> BoxFuture<'static, Result<(), Arc<CordisError>>> {
if self.inner.is_root {
return self.inner.ctx.shutdown();
}
let shared = self.inner.ctx.shared().clone();
let task = {
let _admission = shared.admission.lock().unwrap();
let mut pending = vec![self.inner.clone()];
while let Some(fiber) = pending.pop() {
fiber.closing.store(true, Ordering::SeqCst);
fiber.cancel_current();
pending.extend(
fiber
.children
.lock()
.unwrap()
.iter()
.filter_map(Weak::upgrade),
);
}
begin_subtree_shutdown_locked(&self.inner)
};
Box::pin(async move { join_task(&task).await })
}
pub fn watch(&self) -> watch::Receiver<Snapshot> {
self.inner.snapshot_rx.clone()
}
pub fn dispose(&self) -> BoxFuture<'static, Result<(), Arc<CordisError>>> {
if self.inner.is_root && self.inner.ctx.shared().closing.load(Ordering::SeqCst) {
let task = self
.inner
.ctx
.shared()
.shutdown_task
.lock()
.unwrap()
.clone();
if let Some(task) = task {
return Box::pin(async move { join_task(&task).await });
}
}
let task = match self.register_dispose(|_| Ok(())) {
Ok(task) => task,
Err(_) => unreachable!("an unconditional dispose is always registered"),
};
Box::pin(async move { join_task(&task).await })
}
pub(crate) fn register_dispose(
&self,
check: impl FnOnce(&Trans) -> Result<(), CordisError>,
) -> Result<Arc<TransitionTask>, CordisError> {
let (task, newly_registered) = {
let mut tr = self.inner.transition.lock().unwrap();
check(&tr)?;
match &tr.terminal_task {
Some(task) => (task.clone(), false),
None => {
self.inner.cancel_current();
let task = TransitionTask::new();
tr.terminal_task = Some(task.clone());
(task, true)
}
}
};
if newly_registered {
self.inner.post(Intent::Dispose);
if !self.inner.alive.load(Ordering::SeqCst) {
task.complete(self.inner.stopped_error());
}
}
Ok(task)
}
pub fn dispose_with_timeout(
&self,
limit: Duration,
) -> BoxFuture<'static, Result<(), DisposeWaitError>> {
let pending = self.dispose();
let inner = self.inner.clone();
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 = inner.state_snapshot();
Err(DisposeWaitError::TimedOut {
plugin_id: inner.id,
generation: snapshot.generation,
state: snapshot.state,
elapsed: started.elapsed(),
})
}
}
})
}
pub fn restart(&self) -> BoxFuture<'static, Result<(), Arc<CordisError>>> {
let this = self.inner.clone();
Box::pin(async move {
{
let tr = this.transition.lock().unwrap();
if this.ctx.shared().closing.load(Ordering::SeqCst)
|| this.closing.load(Ordering::SeqCst)
{
return Err(Arc::new(CordisError::Closed));
}
if !this.is_root
&& (tr.terminal_task.is_some() || !this.alive.load(Ordering::SeqCst))
{
return Err(Arc::new(CordisError::InactiveEffect));
}
}
this.cancel_current(); let task = TransitionTask::new();
this.post_join(task.clone(), Intent::Restart);
join_task(&task).await
})
}
pub fn update<C: Send + Sync + 'static>(
&self,
new_config: C,
) -> BoxFuture<'static, Result<(), Arc<CordisError>>> {
let this = self.inner.clone();
Box::pin(async move {
{
let tr = this.transition.lock().unwrap();
if this.ctx.shared().closing.load(Ordering::SeqCst)
|| this.closing.load(Ordering::SeqCst)
{
return Err(Arc::new(CordisError::Closed));
}
if !this.is_root
&& (tr.terminal_task.is_some() || !this.alive.load(Ordering::SeqCst))
{
return Err(Arc::new(CordisError::InactiveEffect));
}
}
let factory = this
.factory
.as_ref()
.and_then(|f| (f.config_type_id() == TypeId::of::<C>()).then(|| f.clone()));
let Some(factory) = factory else {
let reason = if this.factory.is_some() {
"config type mismatch"
} else {
"fiber has no factory (static plugin cannot update)"
};
return Err(Arc::new(CordisError::Validation {
issues: vec![reason.into()],
}));
};
let boxed: Arc<dyn Any + Send + Sync> = Arc::new(new_config);
let dry = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
factory.validate_config_erased(boxed.as_ref())?;
let instance = factory.build_erased(boxed.as_ref())?;
instance.validate()?;
Ok::<(), CordisError>(())
}));
match dry {
Ok(Ok(())) => {}
Ok(Err(e)) => return Err(Arc::new(e)),
Err(p) => return Err(Arc::new(panic_error(p))),
}
{
let tr = this.transition.lock().unwrap();
if this.ctx.shared().closing.load(Ordering::SeqCst)
|| this.closing.load(Ordering::SeqCst)
{
return Err(Arc::new(CordisError::Closed));
}
if !this.is_root
&& (tr.terminal_task.is_some() || !this.alive.load(Ordering::SeqCst))
{
return Err(Arc::new(CordisError::InactiveEffect));
}
}
*this.config.lock().unwrap() = Some(boxed);
this.cancel_current(); let task = TransitionTask::new();
this.post_join(task.clone(), Intent::Restart);
join_task(&task).await
})
}
pub fn current_config<C: Send + Sync + 'static>(&self) -> Option<Arc<C>> {
let boxed = self.inner.config.lock().unwrap().clone()?;
boxed.downcast::<C>().ok()
}
pub fn instance(&self) -> crate::InstanceId {
self.inner.instance
}
pub fn set_config<C: Send + Sync + 'static>(
&self,
new_config: C,
) -> Result<(), Arc<CordisError>> {
let this = &self.inner;
{
let tr = this.transition.lock().unwrap();
if this.ctx.shared().closing.load(Ordering::SeqCst)
|| this.closing.load(Ordering::SeqCst)
{
return Err(Arc::new(CordisError::Closed));
}
if !this.is_root && (tr.terminal_task.is_some() || !this.alive.load(Ordering::SeqCst)) {
return Err(Arc::new(CordisError::InactiveEffect));
}
}
let factory = this
.factory
.as_ref()
.filter(|f| f.config_type_id() == TypeId::of::<C>())
.ok_or_else(|| {
Arc::new(CordisError::Validation {
issues: vec![if this.factory.is_some() {
"config type mismatch".into()
} else {
"fiber has no factory (static plugin has no config)".into()
}],
})
})?;
let boxed: Arc<dyn Any + Send + Sync> = Arc::new(new_config);
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
factory.validate_config_erased(boxed.as_ref())
})) {
Ok(Ok(())) => {}
Ok(Err(e)) => return Err(Arc::new(e)),
Err(p) => return Err(Arc::new(panic_error(p))),
}
*this.config.lock().unwrap() = Some(boxed);
Ok(())
}
}
fn begin_subtree_shutdown_locked(fiber: &Arc<FiberInner>) -> Arc<TransitionTask> {
let mut slot = fiber.shutdown_task.lock().unwrap();
if let Some(task) = slot.as_ref() {
return task.clone();
}
let public = TransitionTask::new();
*slot = Some(public.clone());
drop(slot);
let children: Vec<Arc<FiberInner>> = fiber
.children
.lock()
.unwrap()
.iter()
.filter_map(Weak::upgrade)
.collect();
let child_tasks: Vec<Arc<TransitionTask>> =
children.iter().map(begin_subtree_shutdown_locked).collect();
let (inner, needs_post) = {
let mut tr = fiber.transition.lock().unwrap();
match tr.terminal_task.as_ref() {
Some(task) => (task.clone(), false),
None => {
let task = TransitionTask::new();
tr.terminal_task = Some(task.clone());
(task, true)
}
}
};
*fiber.shutdown_inner.lock().unwrap() = Some(inner.clone());
if needs_post {
fiber.post(Intent::Shutdown(inner.clone()));
}
let owner = fiber.clone();
let result = public.clone();
fiber.ctx.handle().spawn(async move {
let mut errors = Vec::new();
if let Err(error) = join_task(&inner).await {
errors.push(error);
}
let driver = { owner.driver.lock().unwrap().take() };
if let Some(driver) = driver {
if let Err(error) = driver.await {
errors.push(Arc::new(crate::error::join_panic_error(error)));
}
}
for child in child_tasks {
if let Err(error) = join_task(&child).await {
errors.push(error);
}
}
drop(owner);
result.complete(aggregate_arcs(errors));
});
public
}
pub(crate) async fn join_task(task: &Arc<TransitionTask>) -> Result<(), Arc<CordisError>> {
let mut rx = task.done_tx.subscribe();
loop {
match &*rx.borrow() {
TaskDone::Done(err) => return err.clone().map_or(Ok(()), Err),
TaskDone::Running => {}
}
if rx.changed().await.is_err() {
return Err(Arc::new(CordisError::PluginFailed(
"fiber dropped before transition completed".into(),
)));
}
}
}
fn settle(this: &FiberInner) -> BoxFuture<'static, Result<(), Arc<CordisError>>> {
let task = TransitionTask::new();
this.post_join(task.clone(), Intent::Settle);
Box::pin(async move { join_task(&task).await })
}
impl std::future::IntoFuture for FiberView {
type Output = Result<(), Arc<CordisError>>;
type IntoFuture = BoxFuture<'static, Result<(), Arc<CordisError>>>;
fn into_future(self) -> Self::IntoFuture {
settle(&self.inner)
}
}
impl std::future::IntoFuture for &FiberView {
type Output = Result<(), Arc<CordisError>>;
type IntoFuture = BoxFuture<'static, Result<(), Arc<CordisError>>>;
fn into_future(self) -> Self::IntoFuture {
settle(&self.inner)
}
}
#[cfg(test)]
mod transient_tests;