Skip to main content

es_entity/context/
mod.rs

1//! Thread-local system for adding context data to persisted events.
2//!
3//! This module provides a context propagation system for event sourcing that allows
4//! attaching metadata (like request IDs, user IDs, or audit information) to events
5//! as they are created and persisted to the database.
6//!
7//! # Core Components
8//!
9//! - [`EventContext`]: Thread-local context manager (`!Send`) that maintains a stack
10//!   of contexts within a single thread
11//! - [`ContextData`]: Immutable, thread-safe (`Send`) snapshot of context data that
12//!   can be passed across thread boundaries
13//! - [`WithEventContext`]: Extension trait for `Future` types to propagate context
14//!   across async boundaries
15//!
16//! # Usage Patterns
17//!
18//! ## Same Thread Context
19//! ```rust
20//! use es_entity::context::EventContext;
21//!
22//! let mut ctx = EventContext::current();
23//! ctx.insert("request_id", &"req-123").unwrap();
24//!
25//! // Fork for isolated scope
26//! {
27//!     let mut child = EventContext::fork();
28//!     child.insert("operation", &"update").unwrap();
29//!     // Both request_id and operation are available
30//! }
31//! // Only request_id remains in parent
32//! ```
33//!
34//! ## Async Task Context
35//! ```rust
36//! use es_entity::context::{EventContext, WithEventContext};
37//!
38//! async fn spawn_with_context() {
39//!     let mut ctx = EventContext::current();
40//!     ctx.insert("user_id", &"user-456").unwrap();
41//!
42//!     let data = ctx.data();
43//!     tokio::spawn(async move {
44//!         // Context is available in spawned task
45//!         let ctx = EventContext::current();
46//!         // Has user_id from parent
47//!     }.with_event_context(data)).await.unwrap();
48//! }
49//! ```
50//!
51//! ## Cross-Thread Context
52//! ```rust
53//! use es_entity::context::EventContext;
54//!
55//! let mut ctx = EventContext::current();
56//! ctx.insert("trace_id", &"trace-789").unwrap();
57//! let data = ctx.data();
58//!
59//! std::thread::spawn(move || {
60//!     let ctx = EventContext::seed(data);
61//!     // New thread has trace_id
62//! });
63//! ```
64//!
65//! # Database Integration
66//!
67//! When events are persisted using repositories with `event_context = true`, the current
68//! context is automatically serialized to JSON and stored in a `context` column
69//! alongside the event data, enabling comprehensive audit trails and debugging.
70
71mod sqlx;
72mod tracing;
73mod with_event_context;
74
75use serde::{Deserialize, Serialize};
76
77use std::{borrow::Cow, cell::RefCell, rc::Rc};
78
79pub use tracing::*;
80pub use with_event_context::*;
81
82/// Immutable context data that can be safely shared across thread boundaries.
83///
84/// This struct holds key-value pairs of context information that gets attached
85/// to events when they are persisted. It uses an immutable HashMap internally
86/// for efficient cloning and thread-safe sharing of data snapshots.
87///
88/// `ContextData` is `Send` and can be passed between threads, unlike [`EventContext`]
89/// which is thread-local. This makes it suitable for transferring context across
90/// async boundaries via the [`WithEventContext`] trait.
91#[derive(Debug, Clone, Serialize, Deserialize)]
92#[serde(transparent)]
93pub struct ContextData(im::HashMap<Cow<'static, str>, serde_json::Value>);
94
95impl ContextData {
96    fn new() -> Self {
97        Self(im::HashMap::new())
98    }
99
100    fn insert(&mut self, key: &'static str, value: serde_json::Value) {
101        self.0 = self.0.update(Cow::Borrowed(key), value);
102    }
103
104    #[cfg(feature = "tracing-context")]
105    pub(crate) fn with_tracing_info(mut self) -> Self {
106        let tracing = TracingContext::current();
107        self.insert(
108            "tracing",
109            serde_json::to_value(&tracing).expect("Could not inject tracing"),
110        );
111        self
112    }
113
114    pub fn lookup<T: serde::de::DeserializeOwned>(
115        &self,
116        key: &'static str,
117    ) -> Result<Option<T>, serde_json::Error> {
118        let Some(val) = self.0.get(key) else {
119            return Ok(None);
120        };
121        serde_json::from_value(val.clone()).map(Some)
122    }
123}
124
125struct StackEntry {
126    id: Rc<()>,
127    data: ContextData,
128}
129
130thread_local! {
131    static CONTEXT_STACK: RefCell<Vec<StackEntry>> = const { RefCell::new(Vec::new()) };
132}
133
134/// Thread-local event context for tracking metadata throughout event sourcing operations.
135///
136/// `EventContext` provides a way to attach contextual information (like request IDs, audit info,
137/// or operation metadata) to events as they are created and persisted. The context is managed
138/// as a thread-local stack, allowing for nested contexts within the same thread.
139///
140/// # Thread Safety
141///
142/// This struct is deliberately `!Send` to ensure thread-local safety. It uses `Rc` for reference
143/// counting which is not thread-safe. For propagating context across async boundaries or threads,
144/// use the [`WithEventContext`] trait which safely transfers context data.
145///
146/// # Usage Patterns
147///
148/// - **Same thread**: Use [`fork()`](Self::fork) to create isolated child contexts
149/// - **Async tasks**: Use [`with_event_context()`](WithEventContext::with_event_context) from the [`WithEventContext`] trait
150/// - **New threads**: Use [`seed()`](Self::seed) with data from [`data()`](Self::data) to transfer context
151///
152/// # Examples
153///
154/// ```rust
155/// use es_entity::context::EventContext;
156///
157/// // Create or get current context
158/// let mut ctx = EventContext::current();
159/// ctx.insert("user_id", &"123").unwrap();
160///
161/// // Fork for isolated scope
162/// {
163///     let mut child = EventContext::fork();
164///     child.insert("operation", &"update").unwrap();
165///     // Both user_id and operation are available here
166/// }
167/// // Only user_id remains in parent context
168/// ```
169pub struct EventContext {
170    id: Rc<()>,
171}
172
173impl Drop for EventContext {
174    fn drop(&mut self) {
175        // If strong_count is 2, it means this EventContext + one StackEntry reference
176        if Rc::strong_count(&self.id) == 2 {
177            CONTEXT_STACK.with(|c| {
178                let mut stack = c.borrow_mut();
179                for i in (0..stack.len()).rev() {
180                    if Rc::ptr_eq(&stack[i].id, &self.id) {
181                        stack.remove(i);
182                        break;
183                    }
184                }
185            });
186        }
187    }
188}
189
190impl EventContext {
191    /// Gets the current event context or creates a new one if none exists.
192    ///
193    /// This function is thread-local and will return a handle to the topmost context
194    /// on the current thread's context stack. If no context exists, it will create
195    /// a new empty context and push it onto the stack.
196    ///
197    /// # Examples
198    ///
199    /// ```rust
200    /// use es_entity::context::EventContext;
201    ///
202    /// let ctx = EventContext::current();
203    /// // Context is now available for the current thread
204    /// ```
205    pub fn current() -> Self {
206        with_event_context::materialize_pending_seeds();
207        CONTEXT_STACK.with(|c| {
208            let mut stack = c.borrow_mut();
209            if let Some(last) = stack.last() {
210                return EventContext {
211                    id: last.id.clone(),
212                };
213            }
214
215            let id = Rc::new(());
216            let data = ContextData::new();
217            stack.push(StackEntry {
218                id: id.clone(),
219                data,
220            });
221
222            EventContext { id }
223        })
224    }
225
226    /// Creates a new event context seeded with the provided data.
227    ///
228    /// This creates a completely new context stack entry with the given context data,
229    /// independent of any existing context. This is useful for starting fresh contexts
230    /// in new threads or async tasks.
231    ///
232    /// # Arguments
233    ///
234    /// * `data` - The initial context data for the new context
235    ///
236    /// # Examples
237    ///
238    /// ```rust
239    /// use es_entity::context::{EventContext, ContextData};
240    ///
241    /// let data = EventContext::current().data();
242    /// let new_ctx = EventContext::seed(data);
243    /// // new_ctx now has its own independent context stack
244    /// ```
245    pub fn seed(data: ContextData) -> Self {
246        with_event_context::materialize_pending_seeds();
247        Self::push_entry(data)
248    }
249
250    /// Pushes a new stack entry without materializing pending seeds first.
251    /// Only [`seed`](Self::seed) (after materializing) and
252    /// `with_event_context::materialize_pending_seeds` itself may call this.
253    fn push_entry(data: ContextData) -> Self {
254        CONTEXT_STACK.with(|c| {
255            let mut stack = c.borrow_mut();
256            let id = Rc::new(());
257            stack.push(StackEntry {
258                id: id.clone(),
259                data,
260            });
261
262            EventContext { id }
263        })
264    }
265
266    /// Creates a new isolated context that inherits data from the current context.
267    ///
268    /// This method creates a child context that starts with a copy of the current
269    /// context's data. Changes made to the forked context will not affect the parent
270    /// context, and when the forked context is dropped, the parent context remains
271    /// unchanged. This is useful for creating isolated scopes within the same thread.
272    ///
273    /// # Examples
274    ///
275    /// ```rust
276    /// use es_entity::context::EventContext;
277    ///
278    /// let mut parent = EventContext::current();
279    /// parent.insert("shared", &"value").unwrap();
280    ///
281    /// {
282    ///     let mut child = EventContext::fork();
283    ///     child.insert("child_only", &"data").unwrap();
284    ///     // child context has both "shared" and "child_only"
285    /// }
286    /// // parent context only has "shared" - "child_only" is gone
287    /// ```
288    pub fn fork() -> Self {
289        let current = Self::current();
290        let data = current.data();
291        Self::seed(data)
292    }
293
294    /// Inserts a key-value pair into the current context.
295    ///
296    /// The value will be serialized to JSON and stored in the context data.
297    /// This data will be available to all code running within this context
298    /// and any child contexts created via `fork()`.
299    ///
300    /// # Arguments
301    ///
302    /// * `key` - A static string key to identify the value
303    /// * `value` - Any serializable value to store in the context
304    ///
305    /// # Returns
306    ///
307    /// Returns `Ok(())` on success or a `serde_json::Error` if serialization fails.
308    ///
309    /// # Examples
310    ///
311    /// ```rust
312    /// use es_entity::context::EventContext;
313    ///
314    /// let mut ctx = EventContext::current();
315    /// ctx.insert("user_id", &"12345").unwrap();
316    /// ctx.insert("operation", &"transfer").unwrap();
317    /// ```
318    pub fn insert<T: Serialize>(
319        &mut self,
320        key: &'static str,
321        value: &T,
322    ) -> Result<(), serde_json::Error> {
323        let json_value = serde_json::to_value(value)?;
324
325        CONTEXT_STACK.with(|c| {
326            let mut stack = c.borrow_mut();
327            for entry in stack.iter_mut().rev() {
328                if Rc::ptr_eq(&entry.id, &self.id) {
329                    entry.data.insert(key, json_value);
330                    return;
331                }
332            }
333            panic!("EventContext missing on CONTEXT_STACK")
334        });
335
336        Ok(())
337    }
338
339    /// Returns a copy of the current context data.
340    ///
341    /// This method returns a snapshot of all key-value pairs stored in this context.
342    /// The returned [`ContextData`] can be used to seed new contexts or passed to
343    /// async tasks to maintain context across thread boundaries.
344    ///
345    /// # Examples
346    ///
347    /// ```rust
348    /// use es_entity::context::EventContext;
349    ///
350    /// let mut ctx = EventContext::current();
351    /// ctx.insert("request_id", &"abc123").unwrap();
352    ///
353    /// let data = ctx.data();
354    /// // data now contains a copy of the context with request_id
355    /// ```
356    pub fn data(&self) -> ContextData {
357        CONTEXT_STACK.with(|c| {
358            let stack = c.borrow();
359            for entry in stack.iter().rev() {
360                if Rc::ptr_eq(&entry.id, &self.id) {
361                    return entry.data.clone();
362                }
363            }
364            panic!("EventContext missing on CONTEXT_STACK")
365        })
366    }
367
368    #[allow(unused_mut)]
369    pub(crate) fn data_for_storing() -> ContextData {
370        let mut data = Self::current().data();
371        #[cfg(feature = "tracing-context")]
372        {
373            data = data.with_tracing_info();
374        }
375        data
376    }
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382
383    fn stack_depth() -> usize {
384        CONTEXT_STACK.with(|c| c.borrow().len())
385    }
386
387    fn current_json() -> serde_json::Value {
388        serde_json::to_value(EventContext::current().data()).unwrap()
389    }
390
391    #[test]
392    fn assert_stack_depth() {
393        fn assert_inner() {
394            let _ctx = EventContext::current();
395            assert_eq!(stack_depth(), 1);
396        }
397        assert_eq!(stack_depth(), 0);
398        {
399            let _ctx = EventContext::current();
400            assert_eq!(stack_depth(), 1);
401            assert_inner();
402        }
403        assert_eq!(stack_depth(), 0);
404    }
405
406    #[test]
407    fn insert() {
408        fn insert_inner(value: &serde_json::Value) {
409            let mut ctx = EventContext::current();
410            ctx.insert("new_data", &value).unwrap();
411            assert_eq!(
412                current_json(),
413                serde_json::json!({ "data": value, "new_data": value})
414            );
415        }
416
417        let mut ctx = EventContext::current();
418        assert_eq!(current_json(), serde_json::json!({}));
419        let value = serde_json::json!({ "hello": "world" });
420        ctx.insert("data", &value).unwrap();
421        assert_eq!(current_json(), serde_json::json!({ "data": value }));
422        insert_inner(&value);
423        assert_eq!(
424            current_json(),
425            serde_json::json!({ "data": value, "new_data": value})
426        );
427        let new_value = serde_json::json!({ "hello": "new_world" });
428        ctx.insert("data", &new_value).unwrap();
429        assert_eq!(
430            current_json(),
431            serde_json::json!({ "data": new_value, "new_data": value})
432        );
433    }
434
435    #[test]
436    fn thread_isolation() {
437        let mut ctx = EventContext::current();
438        let value = serde_json::json!({ "main": "thread" });
439        ctx.insert("data", &value).unwrap();
440        assert_eq!(stack_depth(), 1);
441
442        let ctx_data = ctx.data();
443        let handle = std::thread::spawn(move || {
444            assert_eq!(stack_depth(), 0);
445            let mut ctx = EventContext::seed(ctx_data);
446            assert_eq!(stack_depth(), 1);
447            ctx.insert("thread", &serde_json::json!("local")).unwrap();
448            assert_eq!(
449                current_json(),
450                serde_json::json!({ "data": { "main": "thread" }, "thread": "local" }),
451            );
452        });
453
454        handle.join().unwrap();
455        assert_eq!(current_json(), serde_json::json!({ "data": value }));
456    }
457
458    #[tokio::test]
459    async fn async_context() {
460        async fn inner_async() {
461            let mut ctx = EventContext::current();
462            ctx.insert("async_inner", &serde_json::json!("value"))
463                .unwrap();
464            assert_eq!(
465                current_json(),
466                serde_json::json!({ "async_data": { "test": "async" }, "async_inner": "value" })
467            );
468        }
469
470        let mut ctx = EventContext::current();
471        assert_eq!(current_json(), serde_json::json!({}));
472
473        let value = serde_json::json!({ "test": "async" });
474        ctx.insert("async_data", &value).unwrap();
475        assert_eq!(current_json(), serde_json::json!({ "async_data": value }));
476
477        inner_async().await;
478
479        assert_eq!(
480            current_json(),
481            serde_json::json!({ "async_data": value, "async_inner": "value" })
482        );
483    }
484
485    #[test]
486    fn fork() {
487        let mut ctx = EventContext::current();
488        ctx.insert("original", &serde_json::json!("value")).unwrap();
489        assert_eq!(stack_depth(), 1);
490        assert_eq!(current_json(), serde_json::json!({ "original": "value" }));
491
492        let mut forked = EventContext::fork();
493        assert_eq!(stack_depth(), 2);
494        assert_eq!(current_json(), serde_json::json!({ "original": "value" }));
495
496        forked.insert("forked", &serde_json::json!("data")).unwrap();
497        assert_eq!(
498            current_json(),
499            serde_json::json!({ "original": "value", "forked": "data" })
500        );
501
502        drop(forked);
503
504        assert_eq!(stack_depth(), 1);
505        assert_eq!(current_json(), serde_json::json!({ "original": "value" }));
506    }
507
508    #[tokio::test]
509    async fn with_event_context_spawned() {
510        let mut ctx = EventContext::current();
511        ctx.insert("parent", &serde_json::json!("context")).unwrap();
512
513        let handle = tokio::spawn(
514            async {
515                // Seeding is lazy: only the outer test context exists until
516                // the wrapped future observes the context.
517                assert_eq!(stack_depth(), 1);
518
519                EventContext::current()
520                    .insert("spawned", &serde_json::json!("value"))
521                    .unwrap();
522
523                // Observing the context materialized the wrapper's entry.
524                assert_eq!(stack_depth(), 2);
525
526                assert_eq!(
527                    current_json(),
528                    serde_json::json!({ "parent": "context", "spawned": "value" })
529                );
530                tokio::task::yield_now().await;
531                current_json()
532            }
533            .with_event_context(ctx.data()),
534        );
535
536        let result = handle.await.unwrap();
537        assert_eq!(
538            result,
539            serde_json::json!({ "parent": "context", "spawned": "value" })
540        );
541
542        assert_eq!(current_json(), serde_json::json!({ "parent": "context" }));
543    }
544
545    #[tokio::test(flavor = "multi_thread")]
546    async fn with_event_context_spawned_multi_thread() {
547        let mut ctx = EventContext::current();
548        ctx.insert("parent", &serde_json::json!("context")).unwrap();
549
550        let handle = tokio::spawn(
551            async {
552                // Seeding is lazy: the worker thread's stack stays empty
553                // until the wrapped future observes the context.
554                assert_eq!(stack_depth(), 0);
555
556                EventContext::current()
557                    .insert("spawned", &serde_json::json!("value"))
558                    .unwrap();
559
560                // Observing the context materialized the wrapper's entry.
561                assert_eq!(stack_depth(), 1);
562
563                assert_eq!(
564                    current_json(),
565                    serde_json::json!({ "parent": "context", "spawned": "value" })
566                );
567                let data = EventContext::current().data();
568                tokio::task::yield_now().with_event_context(data).await;
569                current_json()
570            }
571            .with_event_context(ctx.data()),
572        );
573
574        let result = handle.await.unwrap();
575        assert_eq!(
576            result,
577            serde_json::json!({ "parent": "context", "spawned": "value" })
578        );
579
580        assert_eq!(current_json(), serde_json::json!({ "parent": "context" }));
581    }
582
583    fn pending_depth() -> usize {
584        with_event_context::pending_seed_depth()
585    }
586
587    #[tokio::test]
588    async fn with_event_context_untouched_poll_leaves_stacks_alone() {
589        let mut ctx = EventContext::current();
590        ctx.insert("parent", &serde_json::json!("context")).unwrap();
591        let before = current_json();
592
593        async {
594            // The wrapper parked a pending seed but no stack entry exists.
595            assert_eq!(stack_depth(), 1);
596            assert_eq!(pending_depth(), 1);
597            tokio::task::yield_now().await;
598            assert_eq!(stack_depth(), 1);
599        }
600        .with_event_context(ctx.data())
601        .await;
602
603        assert_eq!(current_json(), before);
604        assert_eq!(stack_depth(), 1);
605        assert_eq!(pending_depth(), 0);
606    }
607
608    #[tokio::test]
609    async fn with_event_context_fork_materializes_wrapper_seed() {
610        let mut ctx = EventContext::current();
611        ctx.insert("parent", &serde_json::json!("context")).unwrap();
612
613        async {
614            // fork() = current() + seed(): materializes the wrapper's entry
615            // and pushes the fork above it.
616            let mut forked = EventContext::fork();
617            forked.insert("forked", &serde_json::json!("data")).unwrap();
618            assert_eq!(stack_depth(), 3);
619            assert_eq!(
620                current_json(),
621                serde_json::json!({ "parent": "context", "forked": "data" })
622            );
623            drop(forked);
624            assert_eq!(stack_depth(), 2);
625            // Fork isolation: the wrapper's entry never saw "forked".
626            assert_eq!(current_json(), serde_json::json!({ "parent": "context" }));
627        }
628        .with_event_context(ctx.data())
629        .await;
630
631        assert_eq!(current_json(), serde_json::json!({ "parent": "context" }));
632    }
633
634    #[tokio::test]
635    async fn with_event_context_panic_leaves_stacks_balanced() {
636        let ctx = EventContext::current();
637        let data = ctx.data();
638
639        let res = tokio::spawn(
640            async {
641                // Materialize the wrapper's entry, then panic mid-poll.
642                let _ = EventContext::current();
643                panic!("boom");
644            }
645            .with_event_context(data),
646        )
647        .await;
648
649        assert!(res.is_err());
650        // The guard's unwind path removed both the pending seed and the
651        // materialized entry (current-thread runtime: same thread).
652        assert_eq!(stack_depth(), 1);
653        assert_eq!(pending_depth(), 0);
654    }
655}