use std::cell::RefCell;
use std::future::Future;
use camel_component_api::CamelError;
pub(crate) const CYCLE_ERROR_PREFIX: &str = "direct cycle detected";
pub(crate) const DEPTH_ERROR: &str = "direct inline dispatch depth limit (64) exceeded";
const MAX_INLINE_DEPTH: usize = 64;
tokio::task_local! {
static INLINE_STACK: RefCell<InlineStack>;
}
#[derive(Default)]
struct InlineStack {
active: Vec<Box<str>>,
}
pub(crate) struct InlineGuard {
name: Box<str>,
}
impl Drop for InlineGuard {
fn drop(&mut self) {
let _ = INLINE_STACK.try_with(|stack| {
stack
.borrow_mut()
.active
.retain(|active| *active != self.name);
});
}
}
pub(crate) fn enter(name: &str) -> Result<InlineGuard, CamelError> {
INLINE_STACK
.try_with(|stack| {
let mut stack = stack.borrow_mut();
if stack.active.iter().any(|active| &**active == name) {
return Err(CamelError::ProcessorError(format!(
"{CYCLE_ERROR_PREFIX} re-entering direct:{name}"
)));
}
if stack.active.len() >= MAX_INLINE_DEPTH {
return Err(CamelError::ProcessorError(DEPTH_ERROR.into()));
}
stack.active.push(name.into());
Ok(InlineGuard { name: name.into() })
})
.unwrap_or_else(|_| {
Err(CamelError::ProcessorError(
"direct inline dispatch entered outside a with_inline_stack scope".into(),
))
})
}
pub(crate) async fn with_inline_stack<R>(fut: impl Future<Output = R>) -> R {
if INLINE_STACK.try_with(|_| ()).is_ok() {
fut.await
} else {
INLINE_STACK
.scope(RefCell::new(InlineStack::default()), fut)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn guard_rejects_cycle_immediately() {
with_inline_stack(async {
let _guard = enter("a").unwrap();
let res = enter("a");
assert!(matches!(
res,
Err(CamelError::ProcessorError(ref msg))
if msg.starts_with(CYCLE_ERROR_PREFIX)
));
})
.await;
}
#[tokio::test]
async fn guard_rejects_depth_65() {
with_inline_stack(async {
let guards: Vec<InlineGuard> =
(0..64).map(|i| enter(&format!("n{i}")).unwrap()).collect();
let res = enter("n65");
assert!(matches!(
res,
Err(CamelError::ProcessorError(ref msg)) if msg == DEPTH_ERROR
));
drop(guards);
})
.await;
}
#[tokio::test]
async fn guard_allows_64_and_unwinds() {
with_inline_stack(async {
let guards: Vec<InlineGuard> =
(0..64).map(|i| enter(&format!("n{i}")).unwrap()).collect();
assert_eq!(guards.len(), 64);
for guard in guards.into_iter().rev() {
drop(guard);
}
assert!(enter("a").is_ok());
})
.await;
}
#[tokio::test]
async fn nested_calls_share_stack() {
with_inline_stack(async {
let _outer = enter("outer").unwrap();
with_inline_stack(async {
let res = enter("outer");
assert!(matches!(
res,
Err(CamelError::ProcessorError(ref msg))
if msg.starts_with(CYCLE_ERROR_PREFIX)
));
})
.await;
})
.await;
}
#[test]
fn with_inline_stack_future_is_send() {
fn assert_send<F: Future + Send>(_: &F) {}
let fut = with_inline_stack(async {});
assert_send(&fut);
}
}