use crate::context::Context;
use crate::fiber::Fiber;
use std::collections::BTreeMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Weak};
pub(crate) type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
pub(crate) type Cleanup = Box<dyn FnOnce() -> BoxFuture<Result<(), EffectFailure>> + Send>;
pub(crate) fn sync_cleanup<F>(f: F) -> Cleanup
where
F: FnOnce() + Send + 'static,
{
Box::new(move || {
Box::pin(async move {
f();
Ok(())
})
})
}
pub(crate) fn fut_cleanup<F>(f: F) -> Cleanup
where
F: FnOnce() -> BoxFuture<()> + Send + 'static,
{
Box::new(move || {
Box::pin(async move {
f().await;
Ok(())
})
})
}
pub(crate) async fn execute_cleanup(cleanup: Cleanup) -> Option<EffectFailure> {
match crate::contained::catch_contained(async move { cleanup().await }).await {
Ok(Ok(())) => None,
Ok(Err(failure)) => Some(failure),
Err(payload) => Some(EffectFailure::panicked(crate::contained::payload_text(
&payload,
))),
}
}
pub(crate) fn report_cleanup_failure(
logger: Option<&crate::logger::Logger>,
failure: &EffectFailure,
) {
crate::contained::report_text(logger, format!("cordis: effect cleanup {failure}"));
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct DisposableToken(u64);
#[derive(Default)]
pub(crate) struct DisposableList {
sn: u64,
map: BTreeMap<u64, Cleanup>,
}
impl DisposableList {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn push(&mut self, cleanup: Cleanup) -> DisposableToken {
self.sn += 1;
let sn = self.sn;
self.map.insert(sn, cleanup);
DisposableToken(sn)
}
pub(crate) fn remove(&mut self, token: DisposableToken) -> Option<Cleanup> {
self.map.remove(&token.0)
}
pub(crate) fn tokens(&self) -> Vec<DisposableToken> {
self.map.keys().map(|k| DisposableToken(*k)).collect()
}
}
mod sealed {
pub trait Sealed {}
impl Sealed for () {}
impl<E: std::error::Error> Sealed for Result<(), E> {}
}
pub trait CleanupResult: sealed::Sealed {
#[doc(hidden)]
fn into_outcome(self) -> std::result::Result<(), EffectFailure>;
}
impl CleanupResult for () {
fn into_outcome(self) -> std::result::Result<(), EffectFailure> {
Ok(())
}
}
impl<E: std::error::Error> CleanupResult for Result<(), E> {
fn into_outcome(self) -> std::result::Result<(), EffectFailure> {
self.map_err(|e| EffectFailure::returned(e.to_string()))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EffectFailureKind {
ReturnedError,
Panic,
}
pub struct EffectFailure {
kind: EffectFailureKind,
diagnostic: String,
}
impl EffectFailure {
pub(crate) fn returned(diagnostic: String) -> Self {
Self {
kind: EffectFailureKind::ReturnedError,
diagnostic,
}
}
pub(crate) fn panicked(payload: String) -> Self {
Self {
kind: EffectFailureKind::Panic,
diagnostic: payload,
}
}
pub fn kind(&self) -> EffectFailureKind {
self.kind
}
pub fn diagnostic(&self) -> &str {
&self.diagnostic
}
}
impl std::fmt::Debug for EffectFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EffectFailure")
.field("kind", &self.kind)
.field("diagnostic", &self.diagnostic)
.finish()
}
}
impl std::fmt::Display for EffectFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.kind {
EffectFailureKind::ReturnedError => {
write!(f, "returned an error: {}", self.diagnostic)
}
EffectFailureKind::Panic => write!(f, "panicked: {}", self.diagnostic),
}
}
}
impl std::error::Error for EffectFailure {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum EffectRegistrationError {
#[error("the context's fiber generation is closed to new cleanup")]
InactiveContext,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum TaskRegistrationError {
#[error("the context's fiber generation is closed to new tasks")]
InactiveContext,
#[error("no async runtime is current")]
ExecutorUnavailable,
}
pub struct EffectRegistration {
token: DisposableToken,
owner: Weak<Fiber>,
}
impl EffectRegistration {
pub fn disarm(self) -> bool {
let Some(fiber) = self.owner.upgrade() else {
return false;
};
fiber.remove_disposable(self.token).is_some()
}
pub async fn dispose(self) -> std::result::Result<bool, EffectFailure> {
let Some(fiber) = self.owner.upgrade() else {
return Ok(false);
};
let Some(cleanup) = fiber.remove_disposable(self.token) else {
return Ok(false);
};
let logger = fiber.fiber_ctx().map(|ctx| ctx.logger());
let (tx, rx) = tokio::sync::oneshot::channel();
let work = async move {
let outcome = execute_cleanup(cleanup).await;
if let Err(outcome) = tx.send(outcome)
&& let Some(failure) = outcome
{
report_cleanup_failure(logger.as_ref(), &failure);
}
};
detach(work);
match rx.await {
Ok(None) => Ok(true),
Ok(Some(failure)) => Err(failure),
Err(_closed) => Ok(true),
}
}
}
impl Context {
#[doc(hidden)]
pub fn __generation_cleanup_admission(
&self,
) -> std::result::Result<(), EffectRegistrationError> {
self.fiber()
.assert_can_register()
.map_err(|_| EffectRegistrationError::InactiveContext)
}
pub fn effect<F, Fut, R>(
&self,
cleanup: F,
) -> std::result::Result<EffectRegistration, EffectRegistrationError>
where
F: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = R> + Send,
R: CleanupResult,
{
self.register_cleanup(Box::new(move || {
Box::pin(async move { cleanup().await.into_outcome() })
}))
}
pub fn effect_sync<F, R>(
&self,
cleanup: F,
) -> std::result::Result<EffectRegistration, EffectRegistrationError>
where
F: FnOnce() -> R + Send + 'static,
R: CleanupResult,
{
self.register_cleanup(Box::new(move || {
Box::pin(async move { cleanup().into_outcome() })
}))
}
fn register_cleanup(
&self,
cleanup: Cleanup,
) -> std::result::Result<EffectRegistration, EffectRegistrationError> {
let token = crate::gated::push_gated(self.fiber(), cleanup, &mut crate::gated::NoPublish)
.map_err(|_| EffectRegistrationError::InactiveContext)?;
Ok(EffectRegistration {
token,
owner: Arc::downgrade(self.fiber()),
})
}
}
pub(crate) fn detach(work: impl Future<Output = ()> + Send + 'static) {
match tokio::runtime::Handle::try_current() {
Ok(handle) => {
let _join = handle.spawn(work);
}
Err(_) => {
std::thread::spawn(move || {
match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(runtime) => runtime.block_on(work),
Err(_) => futures::executor::block_on(work),
}
});
}
}
}
#[cfg(test)]
mod tests {
use super::{DisposableList, EffectFailureKind, sync_cleanup};
#[test]
fn remove_claims_the_occurrence_exactly_once() {
let mut list = DisposableList::new();
let token = list.push(sync_cleanup(|| {}));
assert!(list.remove(token).is_some(), "first claim hands it out");
assert!(
list.remove(token).is_none(),
"a token resolves exactly once"
);
assert!(list.tokens().is_empty());
}
#[test]
fn tokens_snapshot_commit_order() {
let mut list = DisposableList::new();
let a = list.push(sync_cleanup(|| {}));
let b = list.push(sync_cleanup(|| {}));
assert_eq!(
list.tokens(),
vec![a, b],
"forward order — the drain reverses it"
);
list.remove(a);
assert_eq!(list.tokens(), vec![b]);
}
#[test]
fn cleanup_result_normalizes_once() {
use super::CleanupResult;
assert!(().into_outcome().is_ok());
#[derive(Debug, thiserror::Error)]
#[error("cleanup boom")]
struct Boom;
let failure = Err::<(), Boom>(Boom).into_outcome().unwrap_err();
assert_eq!(failure.kind(), EffectFailureKind::ReturnedError);
assert_eq!(failure.diagnostic(), "cleanup boom");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn cleanup_can_reenter_same_generation_registration() {
use crate::Context;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
let ctx = Context::new();
let reentered = Arc::new(AtomicBool::new(false));
let cleanup_ctx = ctx.clone();
let cleanup_reentered = reentered.clone();
let registration = ctx
.effect_sync(move || {
cleanup_ctx
.effect_sync(|| {})
.expect("cleanup may reenter the same generation journal");
cleanup_reentered.store(true, Ordering::SeqCst);
})
.unwrap();
let dispose = registration.dispose();
tokio::pin!(dispose);
let watchdog = crate::deadline::watchdog(Duration::from_secs(2));
tokio::pin!(watchdog);
tokio::select! {
result = &mut dispose => assert_eq!(result.unwrap(), true),
_ = &mut watchdog => panic!("cleanup reentry deadlocked generation bookkeeping"),
}
assert!(reentered.load(Ordering::SeqCst));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn cleanup_error_conversion_can_reenter_generation_bookkeeping() {
use crate::Context;
use std::fmt;
use std::time::Duration;
struct ReentrantError(Context);
impl fmt::Debug for ReentrantError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("ReentrantError")
}
}
impl fmt::Display for ReentrantError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0
.effect_sync(|| {})
.expect("cleanup error conversion may reenter the generation journal");
f.write_str("issue55 reentrant cleanup error")
}
}
impl std::error::Error for ReentrantError {}
let ctx = Context::new();
let conversion_ctx = ctx.clone();
let registration = ctx
.effect_sync(move || -> Result<(), ReentrantError> {
Err(ReentrantError(conversion_ctx))
})
.unwrap();
let dispose = registration.dispose();
tokio::pin!(dispose);
let watchdog = crate::deadline::watchdog(Duration::from_secs(2));
tokio::pin!(watchdog);
let failure = tokio::select! {
result = &mut dispose => result.expect_err("cleanup returns the probe error"),
_ = &mut watchdog => panic!("cleanup error conversion deadlocked generation bookkeeping"),
};
assert_eq!(failure.kind(), EffectFailureKind::ReturnedError);
assert_eq!(failure.diagnostic(), "issue55 reentrant cleanup error");
}
}