use std::sync::Arc;
use std::sync::atomic::Ordering;
use crate::context::Context;
use crate::plugin::PreparedPlugin;
use crate::registry::PluginKey;
use super::{Fiber, FiberHandle, inertia::InitialOutcome};
#[cfg(test)]
#[derive(Clone, Copy, PartialEq, Eq)]
enum HandoffPhase {
BeforeRecheck,
AfterRecheck,
}
#[cfg(test)]
struct HandoffProbe {
phase: HandoffPhase,
contract: std::any::TypeId,
reached: Arc<tokio::sync::Notify>,
resume: Arc<tokio::sync::Notify>,
}
#[cfg(test)]
static HANDOFF_PROBE: parking_lot::Mutex<Option<Arc<HandoffProbe>>> = parking_lot::Mutex::new(None);
#[cfg(test)]
async fn probe_handoff(phase: HandoffPhase, contract: std::any::TypeId) {
let probe = HANDOFF_PROBE.lock().clone();
if let Some(probe) = probe.filter(|probe| probe.phase == phase && probe.contract == contract) {
probe.reached.notify_one();
probe.resume.notified().await;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PluginFailureKind {
ReturnedError,
Panic,
}
#[derive(Debug)]
pub struct PluginFailure {
kind: PluginFailureKind,
diagnostic: String,
}
impl PluginFailure {
pub(crate) fn returned(diagnostic: String) -> Self {
Self {
kind: PluginFailureKind::ReturnedError,
diagnostic,
}
}
pub(crate) fn panicked(payload: String) -> Self {
Self {
kind: PluginFailureKind::Panic,
diagnostic: payload,
}
}
pub fn kind(&self) -> PluginFailureKind {
self.kind
}
pub fn diagnostic(&self) -> &str {
&self.diagnostic
}
}
impl std::fmt::Display for PluginFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.kind {
PluginFailureKind::ReturnedError => {
write!(f, "returned an error: {}", self.diagnostic)
}
PluginFailureKind::Panic => write!(f, "panicked: {}", self.diagnostic),
}
}
}
impl std::error::Error for PluginFailure {}
pub(crate) fn into_owned(failure: Arc<PluginFailure>) -> PluginFailure {
match Arc::try_unwrap(failure) {
Ok(failure) => failure,
Err(shared) => clone_owned(&shared),
}
}
pub(crate) fn clone_owned(failure: &PluginFailure) -> PluginFailure {
PluginFailure {
kind: failure.kind,
diagnostic: failure.diagnostic.clone(),
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SpawnError {
#[error("the spawning context's fiber generation is closed to new fibers")]
InactiveContext,
#[error("the declared dependency on `{service}` does not match its provider's contract")]
DependencyContractMismatch {
service: String,
},
#[error("the configuration for dependency `{service}` is invalid: {diagnostic}")]
DependencyConfiguration {
service: String,
diagnostic: String,
},
#[error("the initial apply {0}")]
InitialApply(PluginFailure),
#[error("the creation was invalidated by the framework before its FiberHandle was delivered")]
Interrupted,
}
struct CreationGuard {
fiber: Option<Arc<Fiber>>,
}
impl CreationGuard {
fn armed(fiber: Arc<Fiber>) -> Self {
Self { fiber: Some(fiber) }
}
fn disarm(&mut self) {
self.fiber = None;
}
}
impl Drop for CreationGuard {
fn drop(&mut self) {
let Some(fiber) = self.fiber.take() else {
return;
};
let work = async move {
fiber.creation_interrupted_teardown().await;
};
crate::effect::detach(work);
}
}
pub(crate) async fn spawn_prepared(
ctx: &Context,
prepared: PreparedPlugin,
) -> std::result::Result<FiberHandle, SpawnError> {
spawn_prepared_inner(ctx, prepared, true).await
}
pub(super) async fn spawn_prepared_era_successor(
ctx: &Context,
prepared: PreparedPlugin,
) -> std::result::Result<FiberHandle, SpawnError> {
spawn_prepared_inner(ctx, prepared, false).await
}
async fn spawn_prepared_inner(
ctx: &Context,
prepared: PreparedPlugin,
require_origin_open: bool,
) -> std::result::Result<FiberHandle, SpawnError> {
if require_origin_open {
ctx.fiber()
.assert_can_register()
.map_err(|_| SpawnError::InactiveContext)?;
}
let PreparedPlugin {
plugin,
name,
inject,
contract,
} = prepared;
let plugin_key = PluginKey::Typed(contract);
let root = &ctx.root;
let dependency_edges = inject
.entries()
.map(|entry| {
crate::fiber::DependencyEdge::new(entry.name.clone(), ctx.isolate_key(&entry.name))
})
.collect::<Vec<_>>();
let fiber = Fiber::new_with_edges(name.clone(), dependency_edges);
fiber.creation_pending.store(true, Ordering::SeqCst);
root.registry.attach_fiber(plugin_key, fiber.clone());
let mut guard = CreationGuard::armed(fiber.clone());
let mut scope = ctx.with_child_scope();
for entry in inject.entries() {
if let Some(configured) = &entry.configured {
scope = scope.with_configured_intercept(entry.name.clone(), configured.clone());
}
}
fiber
.spawn_state
.install(plugin, name, inject, scope, plugin_key, ctx.clone())
.expect("a fresh fiber has no spawn state installed");
root.deps.register(&fiber);
root.observations
.publish(crate::observation::RuntimeObservation::FiberResidency {
change: crate::observation::ResidencyChange::Admitted,
fiber: crate::observation::fiber_snapshot(root, &fiber),
});
let fiber_handle = FiberHandle::new(fiber.clone());
match fiber.initial_settle(root).await {
InitialOutcome::Active | InitialOutcome::Pending => {
#[cfg(test)]
probe_handoff(HandoffPhase::BeforeRecheck, contract).await;
fiber.slot.claim().await;
let alive = fiber.is_alive();
fiber.slot.abandon();
if alive {
fiber.creation_pending.store(false, Ordering::SeqCst);
#[cfg(test)]
probe_handoff(HandoffPhase::AfterRecheck, contract).await;
guard.disarm();
Ok(fiber_handle)
} else {
fiber.force_unlink();
guard.disarm();
Err(SpawnError::Interrupted)
}
}
InitialOutcome::Failed(failure) => {
guard.disarm();
Err(SpawnError::InitialApply(failure))
}
InitialOutcome::Interrupted => {
guard.disarm();
Err(SpawnError::Interrupted)
}
}
}
impl Context {
pub async fn spawn(
&self,
prepared: PreparedPlugin,
) -> std::result::Result<FiberHandle, SpawnError> {
spawn_prepared(self, prepared).await
}
}
#[cfg(test)]
mod tests {
use super::{FiberHandle, HANDOFF_PROBE, HandoffPhase, HandoffProbe, spawn_prepared};
use crate::context::Context;
use crate::plugin::{Plugin, PreparedPlugin};
use std::convert::Infallible;
use std::sync::Arc;
struct ParkedApply {
entered: Arc<tokio::sync::Notify>,
release: Arc<tokio::sync::Notify>,
}
impl Plugin for ParkedApply {
type Config = ();
type Input = ();
type PrepareError = Infallible;
type ApplyError = Infallible;
fn prepare(&self, _config: ()) -> Result<(), Infallible> {
Ok(())
}
async fn apply(&self, _ctx: Context, _prepared: &()) -> Result<(), Infallible> {
self.entered.notify_one();
self.release.notified().await;
Ok(())
}
}
struct ProbeReset;
impl Drop for ProbeReset {
fn drop(&mut self) {
*HANDOFF_PROBE.lock() = None;
}
}
async fn handoff_race(phase: HandoffPhase) -> Result<FiberHandle, super::SpawnError> {
let ctx = Context::new();
let entered = Arc::new(tokio::sync::Notify::new());
let release_apply = Arc::new(tokio::sync::Notify::new());
let reached = Arc::new(tokio::sync::Notify::new());
let resume = Arc::new(tokio::sync::Notify::new());
*HANDOFF_PROBE.lock() = Some(Arc::new(HandoffProbe {
phase,
contract: std::any::TypeId::of::<ParkedApply>(),
reached: reached.clone(),
resume: resume.clone(),
}));
let _reset = ProbeReset;
let prepared = PreparedPlugin::from_input(
ParkedApply {
entered: entered.clone(),
release: release_apply.clone(),
},
(),
);
let spawn = {
let ctx = ctx.clone();
tokio::spawn(async move { spawn_prepared(&ctx, prepared).await })
};
entered.notified().await;
release_apply.notify_one();
reached.notified().await;
let remove_ctx = ctx.clone();
crate::deadline::bounded(2000, async move {
remove_ctx.remove_plugins::<ParkedApply>().await.unwrap();
})
.await
.expect("framework invalidation completed");
assert!(ctx.root.registry.snapshot_fibers().is_empty());
resume.notify_one();
crate::deadline::bounded(2000, spawn)
.await
.expect("spawn resolved")
.unwrap()
}
async fn cancel_at_handoff_barrier() {
let ctx = Context::new();
let entered = Arc::new(tokio::sync::Notify::new());
let release_apply = Arc::new(tokio::sync::Notify::new());
let reached = Arc::new(tokio::sync::Notify::new());
let resume = Arc::new(tokio::sync::Notify::new());
*HANDOFF_PROBE.lock() = Some(Arc::new(HandoffProbe {
phase: HandoffPhase::BeforeRecheck,
contract: std::any::TypeId::of::<ParkedApply>(),
reached: reached.clone(),
resume,
}));
let _reset = ProbeReset;
let prepared = PreparedPlugin::from_input(
ParkedApply {
entered: entered.clone(),
release: release_apply.clone(),
},
(),
);
let spawn = {
let ctx = ctx.clone();
tokio::spawn(async move { spawn_prepared(&ctx, prepared).await })
};
entered.notified().await;
release_apply.notify_one();
reached.notified().await;
spawn.abort();
assert!(spawn.await.unwrap_err().is_cancelled());
crate::deadline::bounded(2000, async {
while !ctx.root.registry.snapshot_fibers().is_empty() {
tokio::task::yield_now().await;
}
})
.await
.expect("caller cancellation completed disposal and unlink");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn handoff_barrier_discriminates_invalidation_from_caller_cancellation() {
assert!(matches!(
handoff_race(HandoffPhase::BeforeRecheck).await,
Err(super::SpawnError::Interrupted)
));
cancel_at_handoff_barrier().await;
let handed_off = handoff_race(HandoffPhase::AfterRecheck)
.await
.expect("invalidation after the commit cannot become Interrupted");
assert_eq!(handed_off.state(), crate::FiberState::Disposed);
era_framework_invalidation_is_successor_lost_after_complete_unlink().await;
}
struct EraHandoffProbe {
entered: Arc<tokio::sync::Notify>,
release: Arc<tokio::sync::Notify>,
cleaned: Arc<std::sync::atomic::AtomicBool>,
}
impl Plugin for EraHandoffProbe {
type Config = u8;
type Input = u8;
type PrepareError = Infallible;
type ApplyError = Infallible;
fn prepare(&self, config: u8) -> Result<u8, Infallible> {
Ok(config)
}
async fn apply(&self, ctx: Context, input: &u8) -> Result<(), Infallible> {
if *input == 2 {
let cleaned = self.cleaned.clone();
ctx.effect_sync(move || {
cleaned.store(true, std::sync::atomic::Ordering::SeqCst);
})
.unwrap();
self.entered.notify_one();
self.release.notified().await;
}
Ok(())
}
}
async fn era_framework_invalidation_is_successor_lost_after_complete_unlink() {
let ctx = Context::new();
let entered = Arc::new(tokio::sync::Notify::new());
let release_apply = Arc::new(tokio::sync::Notify::new());
let reached = Arc::new(tokio::sync::Notify::new());
let resume = Arc::new(tokio::sync::Notify::new());
let cleaned = Arc::new(std::sync::atomic::AtomicBool::new(false));
let source = ctx
.spawn(PreparedPlugin::from_input(
EraHandoffProbe {
entered: entered.clone(),
release: release_apply.clone(),
cleaned: cleaned.clone(),
},
1,
))
.await
.unwrap();
*HANDOFF_PROBE.lock() = Some(Arc::new(HandoffProbe {
phase: HandoffPhase::BeforeRecheck,
contract: std::any::TypeId::of::<EraHandoffProbe>(),
reached: reached.clone(),
resume: resume.clone(),
}));
let _reset = ProbeReset;
let swap = tokio::spawn(async move {
source
.era_swap(crate::PreparedChange::from_input::<EraHandoffProbe>(2))
.await
});
entered.notified().await;
release_apply.notify_one();
reached.notified().await;
crate::deadline::bounded(2000, ctx.remove_plugins::<EraHandoffProbe>())
.await
.expect("framework invalidation completed")
.unwrap();
assert!(cleaned.load(std::sync::atomic::Ordering::SeqCst));
assert!(ctx.root.registry.snapshot_fibers().is_empty());
resume.notify_one();
let error = crate::deadline::bounded(2000, swap)
.await
.expect("era replacement resolved")
.unwrap()
.unwrap_err();
assert!(matches!(
error,
crate::fiber::EraSwapError::Incomplete(crate::fiber::EraSwapFailure::SuccessorLost)
));
assert!(ctx.root.registry.snapshot_fibers().is_empty());
}
}