Skip to main content

rsactor/
dead_letter.rs

1// Copyright 2022 Jeff Kim <hiking90@gmail.com>
2// SPDX-License-Identifier: Apache-2.0
3
4//! Dead Letter Tracking Module
5//!
6//! This module provides infrastructure for tracking and recording dead letters—
7//! messages that could not be delivered to their intended recipients.
8//!
9//! # Background
10//!
11//! Dead letters occur when a message cannot be delivered to an actor, such as:
12//! - The actor's mailbox channel has closed (actor stopped)
13//! - A send or ask operation times out
14//! - The reply channel was dropped before responding
15//!
16//! # Observability
17//!
18//! Dead letters are always logged with structured fields via `tracing`:
19//!
20//! ```text
21//! WARN dead_letter: Dead letter: message could not be delivered
22//!   actor.id=42
23//!   actor.type_name="MyActor"
24//!   message.type_name="PingMessage"
25//!   dead_letter.reason="actor stopped"
26//!   dead_letter.operation="tell"
27//! ```
28//!
29//! # Performance Characteristics
30//!
31//! Dead letter recording is designed for minimal overhead:
32//!
33//! | Scenario | Overhead |
34//! |----------|----------|
35//! | Successful message delivery (hot path) | **Zero** - no code executes |
36//! | Dead letter, no tracing subscriber | ~5-50 ns (fast check + early return) |
37//! | Dead letter, subscriber active | ~1-10 μs (logging + serialization) |
38//!
39//! Key optimizations:
40//! - `#[cold]` attribute hints compiler to optimize hot path
41//! - `Ordering::Relaxed` for atomic counter (no memory barriers)
42//! - Static string references for operation names (no allocation)
43//! - `std::any::type_name::<M>()` is compile-time computed (zero runtime cost)
44//!
45//! # Testing Support
46//!
47//! When the `test-utils` feature is enabled (or in unit tests), a counter tracks
48//! the number of dead letters for verification purposes. Use `dead_letter_count()`
49//! and `reset_dead_letter_count()` to inspect and reset this counter.
50//!
51//! ```toml
52//! [dev-dependencies]
53//! rsactor = { version = "...", features = ["test-utils"] }
54//! ```
55//!
56//! # Example: Observing Dead Letters
57//!
58//! ```rust,ignore
59//! use rsactor::{spawn, Actor, ActorRef};
60//!
61//! #[tokio::main]
62//! async fn main() {
63//!     // Initialize tracing to see dead letter logs
64//!     tracing_subscriber::fmt()
65//!         .with_env_filter("rsactor=debug")
66//!         .init();
67//!
68//!     // Dead letters are automatically logged when message delivery fails
69//!     let (actor_ref, handle) = spawn::<MyActor>(MyActor);
70//!     actor_ref.stop().await.unwrap();
71//!     handle.await.unwrap();
72//!
73//!     // This will log a dead letter warning
74//!     let _ = actor_ref.tell(MyMessage).await;
75//! }
76//! ```
77//!
78//! # Security Warning
79//!
80//! **Never enable `test-utils` in production builds!** This feature exposes
81//! internal metrics that could be used to:
82//! - Monitor message delivery failure rates
83//! - Reset monitoring metrics to evade detection
84//!
85//! For production observability, rely on the structured `tracing::warn!` logs instead.
86
87use crate::Identity;
88
89#[cfg(any(test, feature = "test-utils"))]
90use std::sync::atomic::{AtomicU64, Ordering};
91
92#[cfg(any(test, feature = "test-utils"))]
93static DEAD_LETTER_COUNT: AtomicU64 = AtomicU64::new(0);
94
95/// Reason why a message became a dead letter.
96///
97/// This enum is marked `#[non_exhaustive]` to allow adding new variants
98/// in future versions without breaking existing code.
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100#[non_exhaustive]
101pub enum DeadLetterReason {
102    /// Actor's mailbox channel was closed.
103    ///
104    /// This occurs when attempting to send a message to an actor that is no longer
105    /// running. The actor may have stopped normally via [`ActorRef::stop`](crate::ActorRef::stop) or
106    /// terminated abnormally.
107    ActorStopped,
108
109    /// A send or ask operation exceeded its timeout.
110    ///
111    /// When using [`ActorRef::tell_with_timeout`](crate::ActorRef::tell_with_timeout) or
112    /// [`ActorRef::ask_with_timeout`](crate::ActorRef::ask_with_timeout),
113    /// if the message cannot be delivered within the specified duration,
114    /// it becomes a dead letter.
115    Timeout,
116
117    /// The reply channel was dropped before a response could be sent.
118    ///
119    /// When using [`ActorRef::ask`](crate::ActorRef::ask), the handler may fail or the message processing
120    /// may be interrupted before sending a reply.
121    ReplyDropped,
122}
123
124impl std::fmt::Display for DeadLetterReason {
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        match self {
127            DeadLetterReason::ActorStopped => write!(f, "actor stopped"),
128            DeadLetterReason::Timeout => write!(f, "timeout"),
129            DeadLetterReason::ReplyDropped => write!(f, "reply dropped"),
130        }
131    }
132}
133
134/// Records a dead letter event with structured logging.
135///
136/// This function is called automatically by `ActorRef` methods when a message
137/// cannot be delivered. It serves two purposes:
138///
139/// 1. **Observability**: Logs a warning-level event with structured fields
140///    for debugging and monitoring (always available via `tracing`).
141///
142/// 2. **Testing**: When `test-utils` feature is enabled, increments an atomic counter
143///    that can be queried via [`dead_letter_count()`] to verify dead letter behavior.
144///
145/// # Arguments
146///
147/// * `identity` - The identity of the actor that failed to receive the message
148/// * `reason` - Why the message became a dead letter
149/// * `operation` - The operation that failed ("tell", "ask", "blocking_tell", etc.)
150///
151/// # Type Parameters
152///
153/// * `M` - The message type (used for logging the type name)
154///
155/// # Why `#[cold]`?
156///
157/// Dead letters are exceptional paths - they occur when something goes wrong.
158/// The `#[cold]` attribute hints to the compiler that this function is rarely
159/// called, allowing better optimization of the hot path (successful message delivery).
160#[cold]
161pub(crate) fn record<M: 'static>(
162    identity: Identity,
163    reason: DeadLetterReason,
164    operation: &'static str,
165) {
166    #[cfg(any(test, feature = "test-utils"))]
167    DEAD_LETTER_COUNT.fetch_add(1, Ordering::Relaxed);
168
169    // Always available - tracing is now a required dependency
170    tracing::warn!(
171        actor.id = identity.id,
172        actor.type_name = identity.name(),
173        message.type_name = std::any::type_name::<M>(),
174        dead_letter.reason = %reason,
175        dead_letter.operation = operation,
176        "Dead letter: message could not be delivered"
177    );
178}
179
180/// Returns the total number of dead letters recorded.
181///
182/// This function is only available when the `test-utils` feature is enabled.
183#[cfg(any(test, feature = "test-utils"))]
184pub fn dead_letter_count() -> u64 {
185    DEAD_LETTER_COUNT.load(Ordering::Relaxed)
186}
187
188/// Resets the dead letter counter.
189///
190/// This function is only available when the `test-utils` feature is enabled.
191#[cfg(any(test, feature = "test-utils"))]
192pub fn reset_dead_letter_count() {
193    DEAD_LETTER_COUNT.store(0, Ordering::Relaxed);
194}