Skip to main content

hexeract_core/
context.rs

1use tokio_util::sync::CancellationToken;
2
3use crate::ids::{CorrelationId, MessageId};
4
5/// Contextual information injected into every handler invocation.
6///
7/// The context carries the identifiers of the in-flight message, a
8/// [`CancellationToken`] for cooperative cancellation, and the active
9/// [`tracing::Span`] for distributed tracing propagation.
10#[derive(Debug, Clone)]
11pub struct HandlerContext {
12    /// Unique identifier of this specific message instance.
13    pub message_id: MessageId,
14    /// Identifier linking all messages in the same causal chain.
15    pub correlation_id: CorrelationId,
16    /// Token that is cancelled when the dispatch is aborted or timed out.
17    pub cancellation: CancellationToken,
18    /// Active tracing span at the time of dispatch.
19    pub span: tracing::Span,
20}
21
22impl HandlerContext {
23    /// Creates a new context for the given message identifiers.
24    ///
25    /// The [`CancellationToken`] is fresh (not yet cancelled) and the span is
26    /// captured from the current tracing context.
27    #[must_use]
28    pub fn new(message_id: MessageId, correlation_id: CorrelationId) -> Self {
29        Self {
30            message_id,
31            correlation_id,
32            cancellation: CancellationToken::new(),
33            span: tracing::Span::current(),
34        }
35    }
36
37    /// Overrides the tracing span. Useful when the caller manages span
38    /// lifecycle explicitly.
39    #[must_use]
40    pub fn with_span(mut self, span: tracing::Span) -> Self {
41        self.span = span;
42        self
43    }
44
45    /// Overrides the cancellation token. Use this to share a parent token
46    /// with the dispatch so that cancelling the parent propagates here.
47    #[must_use]
48    pub fn with_cancellation(mut self, token: CancellationToken) -> Self {
49        self.cancellation = token;
50        self
51    }
52
53    /// Returns `true` if the cancellation token has been cancelled.
54    #[must_use]
55    pub fn is_cancelled(&self) -> bool {
56        self.cancellation.is_cancelled()
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn new_context_is_not_cancelled() {
66        let ctx = HandlerContext::new(MessageId::new(), CorrelationId::new());
67        assert!(!ctx.is_cancelled());
68    }
69
70    #[test]
71    fn cancellation_propagates_from_parent() {
72        let parent = CancellationToken::new();
73        let child = parent.child_token();
74        let ctx =
75            HandlerContext::new(MessageId::new(), CorrelationId::new()).with_cancellation(child);
76
77        assert!(!ctx.is_cancelled());
78        parent.cancel();
79        assert!(ctx.is_cancelled());
80    }
81
82    #[test]
83    fn context_is_clone() {
84        let ctx = HandlerContext::new(MessageId::new(), CorrelationId::new());
85        let cloned = ctx.clone();
86        assert_eq!(ctx.message_id, cloned.message_id);
87        assert_eq!(ctx.correlation_id, cloned.correlation_id);
88    }
89}