poison_guard/lib.rs
1/*!
2Utilities for maintaining sane state in the presence of panics and other failures.
3
4This library contains [`Poison<T>`], which implements poisoning independently of locks or
5other mechanisms for sharing state.
6
7## What is poisoning?
8
9Poisoning is a general strategy for keeping state consistent by blocking direct access to
10state if a previous user did something unexpected with it.
11
12Rust implements poisoning in the standard library's `Mutex<T>` type. This library offers poisoning
13without assuming locks. The standard library's poisoning is only concerned with panics, because
14they don't have an in-band signal like `?` to suggest an early return from a block of code is possible.
15Code may not be written to expect panics. Poisoning offers a general solution for such code.
16
17The `Poison<T>` in this library also supports poisoning for other exceptional circumstances besides
18panics. Poisoning can be applied anywhere there's complex state management at play, but is particularly
19useful for cordoning off external resources, like files, that may become corrupted without panicking.
20
21## Detecting invalid state
22
23In simple cases, we can just access a value, and if a panic occurs the value will be poisoned.
24This example defines an `Account`, with an invariant that the total balance is always equal to
25the sum of its changes. We can protect this invariant using `Poison<T>`:
26
27```
28use poison_guard::Poison;
29
30struct Account(Poison<AccountState>);
31
32struct AccountState {
33 total: i64,
34 // Invariant: the total must be the sum of the changes
35 changes: Vec<i64>,
36}
37
38impl Account {
39 pub fn new() -> Self {
40 Account(Poison::new(AccountState { total: 0, changes: vec![] }))
41 }
42
43 pub fn push_change(&mut self, change: i64) {
44 // In order to access our `AccountState` we need to get a poison guard
45 let mut state = match Poison::on_unwind(&mut self.0) {
46 // If our state was not poisoned then we can work with it
47 Ok(state) => state,
48 // If our state was poisoned then try to restore our invariant
49 // After that we'll be able to use it again
50 Err(poisoned) => poisoned.recover_with(|state| {
51 state.total = state.changes.iter().sum();
52 })
53 };
54
55 // Make some updates to the state
56 state.changes.push(change);
57
58 // If we panic here then our state is invalid.
59 // The `Poison::on_unwind` call above will start
60 // to panic rather than letting us continue to access
61 // the broken state
62
63 state.total += change;
64
65 // At this point the guard falls out of scope and the
66 // state is considered valid. Future callers will
67 // succeed when they call `Poison::on_unwind`
68 }
69
70 pub fn total(&self) -> i64 {
71 self.0.get().unwrap().total
72 }
73}
74```
75
76More complex usecases may need to poison in other cases besides panics. Say we're writing data to a file.
77If an individual write fails we might not know exactly what state the file has been left in on-disk
78and need to recover it before accessing again:
79
80```
81use poison_guard::Poison;
82use anyhow::Error;
83use std::{io::{self, Write}, fs::File};
84
85struct Writer {
86 file: Poison<File>,
87}
88
89struct Data {
90 id: u64,
91 payload: Vec<u8>,
92}
93
94impl Writer {
95 pub fn write_data(&mut self, data: Data) -> anyhow::Result<()> {
96 // Acquire a guard for our state that will only be unpoisoned
97 // if we explicitly recover it
98 let mut file = Poison::unless_recovered(&mut self.file)
99 .or_else(|poisoned| {
100 // If the value was poisoned, we'll try recover it
101 // Maybe one of our previous writes partially failed?
102 poisoned.try_recover_with(|file| Writer::check_and_fix(file))
103 })
104 .map_err(|poisoned| poisoned.into_error())?;
105
106 // Now that we have access to the value, we can interact with it
107 Writer::write_data_header(&mut file, data.id, data.payload.len() as u64)?;
108 Writer::write_data_payload(&mut file, data.payload)?;
109
110 // Return the guard, unpoisoning the value
111 // If we early return through a panic or `?` before we get here
112 // then the guard will remain poisoned
113 Poison::recover(file);
114
115 Ok(())
116 }
117
118 fn check_and_fix(file: &mut File) -> anyhow::Result<()> {
119 // ..
120# let _ = file;
121# Ok(())
122 }
123
124 fn write_data_header(file: &mut File, id: u64, len: u64) -> anyhow::Result<()> {
125 // ..
126# let _ = (file, id, len);
127# Ok(())
128 }
129
130 fn write_data_payload(file: &mut File, payload: Vec<u8>) -> anyhow::Result<()> {
131 // ..
132# let _ = (file, payload);
133# Ok(())
134 }
135}
136```
137
138## Propagating errors and unwinds
139
140If a `Poison<T>` is poisoned, future attempts to access it may convert that into a panic or error:
141
142```should_panic
143use poison_guard::Poison;
144use std::{sync::Arc, thread};
145use parking_lot::Mutex;
146
147# fn main() -> Result<(), Box<dyn std::error::Error>> {
148let mutex = Arc::new(Mutex::new(Poison::new(String::from("a value!"))));
149
150// Access the value from another thread, but poison while working with it
151# let h = {
152# let mutex = mutex.clone();
153thread::spawn(move || {
154 let mut guard = Poison::on_unwind(mutex.lock()).unwrap();
155
156 guard.push_str("And some more!");
157
158 panic!("explicit panic");
159})
160# };
161# drop(h.join());
162
163// ..
164
165// Later, we try access the poison
166// If it was poisoned we'll get a guard that can be
167// recovered, unwrapped or converted into an error
168match Poison::on_unwind(mutex.lock()) {
169 Ok(guard) => {
170 println!("the value is: {}", &*guard);
171 }
172 Err(recover) => {
173 println!("{}", recover);
174
175 return Err(recover.into_error().into());
176 }
177}
178# Ok(())
179# }
180```
181
182The above example will output something like:
183
184```text
185poisoned by a panic (the poisoning guard was acquired at 'src/lib.rs:13:38')
186```
187*/
188
189mod poison;
190
191#[doc(inline)]
192pub use self::poison::*;
193
194#[cfg(test)]
195mod tests;