use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, RwLock};
use arc_swap::ArcSwapOption;
use crate::scene_tree::SceneTree;
pub struct ExecutionContext {
pub exec_id: u64,
pub stop: Arc<AtomicBool>,
pub observer: Option<Arc<dyn crate::observer::RunObserver>>,
pub scene_tree: ArcSwapOption<RwLock<SceneTree>>,
pub channel: Option<Arc<dyn crate::output_channel::OutputChannel>>,
}
impl ExecutionContext {
pub fn new() -> Arc<Self> {
Arc::new(Self {
exec_id: alloc_exec_id(),
stop: Arc::new(AtomicBool::new(false)),
observer: None,
scene_tree: ArcSwapOption::const_empty(),
channel: None,
})
}
pub fn with_observer(observer: Arc<dyn crate::observer::RunObserver>) -> Arc<Self> {
Arc::new(Self {
exec_id: alloc_exec_id(),
stop: Arc::new(AtomicBool::new(false)),
observer: Some(observer),
scene_tree: ArcSwapOption::const_empty(),
channel: None,
})
}
pub fn with_observer_and_channel(
observer: Arc<dyn crate::observer::RunObserver>,
channel: Arc<dyn crate::output_channel::OutputChannel>,
) -> Arc<Self> {
Arc::new(Self {
exec_id: alloc_exec_id(),
stop: Arc::new(AtomicBool::new(false)),
observer: Some(observer),
scene_tree: ArcSwapOption::const_empty(),
channel: Some(channel),
})
}
}
tokio::task_local! {
static EXEC_CTX: Arc<ExecutionContext>;
static CURRENT_PHASE: crate::scene_tree::SceneNodeId;
static CURRENT_PHASE_START_MS: u64;
}
static NEXT_EXEC_ID: AtomicU64 = AtomicU64::new(1);
pub fn alloc_exec_id() -> u64 {
NEXT_EXEC_ID.fetch_add(1, Ordering::Relaxed)
}
pub fn try_current() -> Option<Arc<ExecutionContext>> {
EXEC_CTX.try_with(|c| c.clone()).ok()
}
pub fn current_exec_id() -> u64 {
EXEC_CTX.try_with(|c| c.exec_id).unwrap_or(1)
}
pub fn current_stop() -> Option<Arc<AtomicBool>> {
EXEC_CTX.try_with(|c| c.stop.clone()).ok()
}
pub fn current_observer() -> Option<Arc<dyn crate::observer::RunObserver>> {
EXEC_CTX.try_with(|c| c.observer.clone()).ok().flatten()
}
pub fn current_channel() -> Option<Arc<dyn crate::output_channel::OutputChannel>> {
EXEC_CTX.try_with(|c| c.channel.clone()).ok().flatten()
}
pub fn current_scene_tree() -> Option<Arc<RwLock<SceneTree>>> {
EXEC_CTX
.try_with(|c| c.scene_tree.load_full())
.ok()
.flatten()
}
pub fn install_scene_tree(tree: Arc<RwLock<SceneTree>>) -> bool {
EXEC_CTX
.try_with(|c| c.scene_tree.store(Some(tree)))
.is_ok()
}
pub async fn scope<F: std::future::Future>(ctx: Arc<ExecutionContext>, fut: F) -> F::Output {
EXEC_CTX.scope(ctx, fut).await
}
pub async fn with_current_phase<F: std::future::Future>(
scene_node_id: crate::scene_tree::SceneNodeId,
phase_start_ms: u64,
fut: F,
) -> F::Output {
CURRENT_PHASE_START_MS
.scope(phase_start_ms, CURRENT_PHASE.scope(scene_node_id, fut))
.await
}
pub fn current_phase_start_ms() -> Option<u64> {
CURRENT_PHASE_START_MS.try_with(|ms| *ms).ok()
}
pub fn current_phase_node() -> Option<crate::scene_tree::SceneNodeId> {
CURRENT_PHASE.try_with(|id| *id).ok()
}
pub fn propagate<F>(fut: F) -> impl std::future::Future<Output = F::Output> + Send
where
F: std::future::Future + Send + 'static,
F::Output: Send,
{
let ctx = try_current();
let phase = current_phase_node();
let phase_start = current_phase_start_ms();
async move {
let inner = async move {
let with_phase = async move {
match phase {
Some(p) => CURRENT_PHASE.scope(p, fut).await,
None => fut.await,
}
};
match phase_start {
Some(ms) => CURRENT_PHASE_START_MS.scope(ms, with_phase).await,
None => with_phase.await,
}
};
match ctx {
Some(c) => scope(c, inner).await,
None => inner.await,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn phase_start_is_scoped_and_absent_outside_a_phase() {
assert_eq!(
current_phase_start_ms(),
None,
"outside a phase there is no origin to be relative to"
);
let inside = with_current_phase(1, 111, async { current_phase_start_ms() }).await;
assert_eq!(inside, Some(111));
let sibling = with_current_phase(2, 222, async { current_phase_start_ms() }).await;
assert_eq!(sibling, Some(222));
assert_eq!(
current_phase_start_ms(),
None,
"the scope must not leak past the phase body"
);
}
#[tokio::test]
async fn phase_start_survives_propagate_into_a_spawned_task() {
let got = with_current_phase(3, 333, async {
tokio::spawn(propagate(async { current_phase_start_ms() }))
.await
.expect("spawned task")
})
.await;
assert_eq!(
got,
Some(333),
"a propagated fiber must resolve its own phase's origin"
);
}
#[test]
fn alloc_exec_id_is_monotonic_and_unique() {
let a = alloc_exec_id();
let b = alloc_exec_id();
assert!(b > a, "exec_id must be monotonic: {a} then {b}");
}
#[tokio::test]
async fn outside_a_scope_defaults_to_legacy_single_execution() {
assert_eq!(current_exec_id(), 1);
assert!(current_stop().is_none());
assert!(try_current().is_none());
}
#[tokio::test]
async fn propagate_carries_context_across_spawn() {
let ctx = ExecutionContext::new();
let id = ctx.exec_id;
let (bare, wrapped) = scope(ctx, async move {
let bare = tokio::spawn(async { current_exec_id() }).await.unwrap();
let wrapped = tokio::spawn(propagate(async { current_exec_id() }))
.await
.unwrap();
(bare, wrapped)
})
.await;
assert_eq!(bare, 1, "a bare spawn loses the context (sees the default)");
assert_eq!(
wrapped, id,
"propagate carries the exec_id across the spawn"
);
}
#[tokio::test]
async fn current_phase_node_defaults_to_none_and_scopes() {
assert_eq!(current_phase_node(), None);
let inside = with_current_phase(7, 0, async { current_phase_node() }).await;
assert_eq!(inside, Some(7));
assert_eq!(current_phase_node(), None, "the scope reverts on exit");
}
#[tokio::test]
async fn propagate_carries_current_phase_across_spawn() {
let (bare, wrapped) = with_current_phase(42, 0, async {
let bare = tokio::spawn(async { current_phase_node() }).await.unwrap();
let wrapped = tokio::spawn(propagate(async { current_phase_node() }))
.await
.unwrap();
(bare, wrapped)
})
.await;
assert_eq!(bare, None, "a bare spawn loses the ambient phase");
assert_eq!(
wrapped,
Some(42),
"propagate carries the phase node across the spawn"
);
}
#[tokio::test]
async fn propagate_carries_both_exec_ctx_and_phase() {
let ctx = ExecutionContext::new();
let id = ctx.exec_id;
let (eid, phase) = scope(
ctx,
with_current_phase(9, 0, async {
tokio::spawn(propagate(async {
(current_exec_id(), current_phase_node())
}))
.await
.unwrap()
}),
)
.await;
assert_eq!(eid, id, "propagate carries exec_id");
assert_eq!(phase, Some(9), "propagate carries the phase node");
}
#[allow(clippy::await_holding_lock)]
#[tokio::test]
async fn per_execution_stop_is_isolated() {
let _guard = crate::session_signals::STOP_GLOBAL_TEST_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
crate::session_signals::clear_session_stop_for_test();
let a = ExecutionContext::new();
let b = ExecutionContext::new();
assert_ne!(
a.exec_id, b.exec_id,
"concurrent executions get distinct ids"
);
a.stop.store(true, Ordering::Relaxed);
let a_id = scope(a.clone(), async { current_exec_id() }).await;
let a_stopped = scope(a.clone(), async {
crate::session_signals::stop_requested()
})
.await;
let b_stopped = scope(b.clone(), async {
crate::session_signals::stop_requested()
})
.await;
assert_eq!(a_id, a.exec_id, "the scoped exec_id resolves to A's");
assert!(a_stopped, "A observes its own stop inside A's scope");
assert!(
!b_stopped,
"B must NOT see A's stop — executions are isolated"
);
}
}