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}