Skip to main content

es_entity/context/
with_event_context.rs

1use pin_project::pin_project;
2
3use std::{
4    cell::RefCell,
5    future::Future,
6    pin::Pin,
7    task::{Context, Poll},
8};
9
10use super::{ContextData, EventContext};
11
12/// Extension trait for propagating event context across async boundaries.
13///
14/// This trait is automatically implemented for all `Future` types and provides
15/// the `with_event_context` method to carry context data across async operations
16/// like `tokio::spawn`, `tokio::task::yield_now()`, and other async boundaries
17/// where thread-local storage is not preserved.
18///
19/// # Examples
20///
21/// ```rust
22/// use es_entity::context::{EventContext, WithEventContext};
23///
24/// async fn example() {
25///     let mut ctx = EventContext::current();
26///     ctx.insert("request_id", &"abc123").unwrap();
27///
28///     let data = ctx.data();
29///     tokio::spawn(async {
30///         // Context is available here
31///         let current = EventContext::current();
32///         // current now has the request_id from the parent
33///     }.with_event_context(data)).await.unwrap();
34/// }
35/// ```
36pub trait WithEventContext: Future {
37    /// Wraps this future with event context data.
38    ///
39    /// This method ensures that when the future is polled, the provided context
40    /// data will be available as the current event context. This is essential
41    /// for maintaining context across async boundaries where the original
42    /// thread-local context is not available.
43    ///
44    /// # Arguments
45    ///
46    /// * `context_data` - The context data to make available during future execution
47    ///
48    /// # Returns
49    ///
50    /// Returns an [`EventContextFuture`] that will poll the wrapped future with
51    /// the provided context active.
52    fn with_event_context(self, context_data: ContextData) -> EventContextFuture<Self>
53    where
54        Self: Sized,
55    {
56        EventContextFuture {
57            future: self,
58            context_data: Some(context_data),
59        }
60    }
61}
62
63impl<F: Future> WithEventContext for F {}
64
65/// A future wrapper that provides event context during polling.
66///
67/// This struct is created by the `with_event_context` method and should not
68/// be constructed directly. It ensures that the wrapped future has access
69/// to the specified event context data whenever it is polled.
70///
71/// The future maintains context isolation - the context is only active
72/// during the polling of the wrapped future and does not leak to other
73/// concurrent operations.
74///
75/// Seeding is lazy: on each poll the context data is parked on a
76/// thread-local pending-seed stack, and a real [`EventContext`] entry is
77/// only materialized if the inner future actually observes the context
78/// (via [`EventContext::current`] or [`EventContext::seed`]) during that
79/// poll. Polls that never touch the context — the overwhelming majority —
80/// pay neither an allocation nor a clone.
81///
82/// [`EventContext`]: super::EventContext
83/// [`EventContext::current`]: super::EventContext::current
84/// [`EventContext::seed`]: super::EventContext::seed
85#[pin_project]
86pub struct EventContextFuture<F> {
87    #[pin]
88    future: F,
89    /// `Some` between polls; taken for the duration of each poll while the
90    /// data is parked on the pending-seed stack.
91    context_data: Option<ContextData>,
92}
93
94impl<F: Future> Future for EventContextFuture<F> {
95    type Output = F::Output;
96
97    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
98        let this = self.project();
99        let data = this
100            .context_data
101            .take()
102            .expect("EventContextFuture must not be polled after a panic");
103        let guard = SeedGuard::enter(data);
104        let res = this.future.poll(cx);
105        *this.context_data = Some(guard.finish());
106        res
107    }
108}
109
110/// A context seed deferred by [`EventContextFuture`]: the wrapper's
111/// [`ContextData`] is parked here on poll entry instead of eagerly becoming
112/// a context-stack entry. A real entry is only materialized — see
113/// [`materialize_pending_seeds`] — if the inner future actually observes the
114/// context during that poll. Most polls of a request/job future never do, so
115/// the common case skips the entry allocation, the push/pop, and the clone
116/// entirely.
117struct PendingSeed {
118    /// `Some` until materialized; then moved into the stack entry.
119    data: Option<ContextData>,
120    /// Handle to the materialized entry, if any. Dropping it removes the
121    /// entry via the normal [`EventContext`] drop logic.
122    ctx: Option<EventContext>,
123}
124
125thread_local! {
126    static PENDING_SEEDS: RefCell<Vec<PendingSeed>> = const { RefCell::new(Vec::new()) };
127}
128
129/// Materializes every pending seed into a real context-stack entry, in push
130/// order.
131///
132/// [`EventContext`] calls this before any operation that observes or pushes
133/// onto the stack ([`EventContext::current`] / [`EventContext::seed`]). This
134/// preserves the invariant that pending seeds always sit logically *above*
135/// every stack entry that existed when they were parked: any new entry is
136/// pushed after the seeds have been materialized beneath it.
137pub(super) fn materialize_pending_seeds() {
138    PENDING_SEEDS.with(|p| {
139        let mut pending = p.borrow_mut();
140        for seed in pending.iter_mut() {
141            if seed.ctx.is_none() {
142                let data = seed.data.take().expect("unmaterialized seed retains data");
143                seed.ctx = Some(EventContext::push_entry(data));
144            }
145        }
146    })
147}
148
149/// RAII token for one [`EventContextFuture`] poll invocation.
150///
151/// [`enter`](Self::enter) parks the wrapper's data as a pending seed;
152/// [`finish`](Self::finish) (normal exit) pops it and returns the data to
153/// store back into the wrapper — either untouched (fast path: the poll never
154/// observed the context) or harvested from the materialized entry. If the
155/// inner poll panics, `Drop` pops the record and discards any materialized
156/// handle so the thread-local stacks stay balanced.
157#[must_use]
158struct SeedGuard(());
159
160impl SeedGuard {
161    fn enter(data: ContextData) -> Self {
162        PENDING_SEEDS.with(|p| {
163            p.borrow_mut().push(PendingSeed {
164                data: Some(data),
165                ctx: None,
166            })
167        });
168        SeedGuard(())
169    }
170
171    fn finish(self) -> ContextData {
172        let seed = PENDING_SEEDS
173            .with(|p| p.borrow_mut().pop())
174            .expect("SeedGuard::finish: pending-seed stack is empty");
175        std::mem::forget(self);
176        match seed {
177            // Materialized: harvest the (possibly mutated) data. Dropping the
178            // handle removes the entry unless the inner future kept its own
179            // handle across the poll — same lifecycle as an eager seed.
180            PendingSeed { ctx: Some(ctx), .. } => ctx.data(),
181            // Fast path: the poll never observed the context; hand the data
182            // back unchanged without ever having touched the context stack.
183            PendingSeed { data, .. } => data.expect("unmaterialized seed retains data"),
184        }
185    }
186}
187
188impl Drop for SeedGuard {
189    fn drop(&mut self) {
190        // Unwind path: the wrapped poll panicked. Pop the record; dropping a
191        // materialized handle removes its stack entry via EventContext::drop.
192        // Write-back is skipped, matching the eager-seeding behavior.
193        let _ = PENDING_SEEDS.with(|p| p.borrow_mut().pop());
194    }
195}
196
197/// Test-only visibility into the pending-seed stack for the context tests.
198#[cfg(test)]
199pub(super) fn pending_seed_depth() -> usize {
200    PENDING_SEEDS.with(|p| p.borrow().len())
201}