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}