Skip to main content

monocoque_core/
poison.rs

1//! RAII guard for protecting against partial I/O corruption in async contexts.
2//!
3//! # The Problem
4//!
5//! In async Rust, when a Future is dropped (e.g., due to timeout), execution stops
6//! immediately. If this happens during a multi-step I/O operation like writing a
7//! multipart ZMTP message, the underlying stream is left in an undefined state
8//! with potentially half a frame written.
9//!
10//! # The Solution
11//!
12//! The `PoisonGuard` uses RAII to track whether a critical I/O section completed
13//! successfully:
14//!
15//! 1. `PoisonGuard::new()` sets a flag to `true` (assume failure)
16//! 2. If the Future is dropped before completion, the flag remains `true`
17//! 3. Only by calling `disarm()` after successful I/O does the flag reset to `false`
18//!
19//! # Example
20//!
21//! ```rust
22//! use monocoque_core::poison::PoisonGuard;
23//!
24//! struct MySocket {
25//!     is_poisoned: bool,
26//!     // ... other fields
27//! }
28//!
29//! impl MySocket {
30//!     async fn send(&mut self, data: &[u8]) -> std::io::Result<()> {
31//!         // Check health before attempting I/O
32//!         if self.is_poisoned {
33//!             return Err(std::io::Error::new(
34//!                 std::io::ErrorKind::BrokenPipe,
35//!                 "Socket poisoned by cancelled I/O"
36//!             ));
37//!         }
38//!
39//!         // Arm the guard - if dropped, socket remains poisoned
40//!         let guard = PoisonGuard::new(&mut self.is_poisoned);
41//!
42//!         // Critical section: if this is cancelled, guard drops
43//!         // and socket remains poisoned
44//!         // ... perform I/O operations ...
45//!
46//!         // Success! Disarm the guard
47//!         guard.disarm();
48//!         Ok(())
49//!     }
50//! }
51//! ```
52//!
53//! # When to Use
54//!
55//! Apply this to **every** function that performs non-atomic writes:
56//! - Writing multipart messages
57//! - Flushing buffered data
58//! - Any write operation larger than MTU
59//! - Sequential writes that form a logical unit
60//!
61//! # When NOT to Use
62//!
63//! Typically don't use for reads (they're usually idempotent), unless:
64//! - Reading multipart data where internal state changes
65//! - State transitions that can't be rolled back
66//!
67//! # Critical Rules
68//!
69//! 1. **Only disarm when the entire logical operation completes**
70//! 2. **Never manually reset `is_poisoned` after an error**
71//! 3. **Once poisoned, the connection must be dropped and reconnected**
72
73/// A RAII guard that marks a connection as poisoned if dropped before disarmed.
74///
75/// This is a structural guarantee that protects protocol integrity when async
76/// operations are cancelled (e.g., by timeouts).
77///
78/// # Safety
79///
80/// The guard must live across the entire critical section. Dropping it early
81/// or disarming before all I/O completes defeats its purpose.
82pub struct PoisonGuard<'a> {
83    flag: &'a mut bool,
84}
85
86impl<'a> PoisonGuard<'a> {
87    /// Create a new guard, immediately marking the connection as poisoned.
88    ///
89    /// The connection will remain poisoned unless `disarm()` is called.
90    #[inline]
91    pub fn new(flag: &'a mut bool) -> Self {
92        *flag = true;
93        Self { flag }
94    }
95
96    /// Disarm the guard, marking the connection as healthy.
97    ///
98    /// **Only call this when the entire I/O operation has completed successfully.**
99    ///
100    /// # Example
101    ///
102    /// ```rust
103    /// # use monocoque_core::poison::PoisonGuard;
104    /// # async fn example() -> std::io::Result<()> {
105    /// # let mut is_poisoned = false;
106    /// let guard = PoisonGuard::new(&mut is_poisoned);
107    ///
108    /// // Perform all I/O operations
109    /// // ...
110    ///
111    /// // Only disarm after everything succeeds
112    /// guard.disarm();
113    /// # Ok(())
114    /// # }
115    /// ```
116    #[inline]
117    pub fn disarm(self) {
118        *self.flag = false;
119        // self is dropped here, but since we updated the reference,
120        // the connection is now marked as healthy
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn test_poison_on_drop() {
130        let mut poisoned = false;
131        {
132            let _guard = PoisonGuard::new(&mut poisoned);
133            // Guard dropped without disarm
134        }
135        assert!(
136            poisoned,
137            "Connection should be poisoned when guard is dropped"
138        );
139    }
140
141    #[test]
142    fn test_disarm_clears_poison() {
143        let mut poisoned = false;
144        {
145            let guard = PoisonGuard::new(&mut poisoned);
146            // Can't check poisoned here - it's mutably borrowed by guard
147            guard.disarm();
148        }
149        assert!(!poisoned, "Connection should be healthy after disarm");
150    }
151
152    #[test]
153    fn test_disarm_at_end() {
154        let mut poisoned = false;
155        {
156            let guard = PoisonGuard::new(&mut poisoned);
157            // Simulate successful I/O
158            guard.disarm();
159            // Can only check after guard is dropped/disarmed
160        }
161        assert!(!poisoned);
162    }
163
164    #[test]
165    fn test_early_drop() {
166        let mut poisoned = false;
167        {
168            let _guard = PoisonGuard::new(&mut poisoned);
169            // Simulate a cancelled operation: the guard drops at the end of this
170            // scope without disarm, so the flag stays poisoned.
171        }
172        assert!(poisoned, "Should remain poisoned on early drop");
173    }
174}