todc_utils/linearizability.rs
1//! Checking [linearizability](https://en.wikipedia.org/wiki/Linearizability) of a
2//! history of operations applied to a shared object.
3//!
4//! For more information, see the documentation of the [`WGLChecker`] and [`History`] structs.
5use std::collections::HashSet;
6use std::marker::PhantomData;
7
8use crate::linearizability::history::{Entry, History};
9use crate::specifications::Specification;
10
11pub mod history;
12
13/// A linearizability checker.
14///
15/// An implementation of the algorithm originally defined by Jeannette Wing and Chun Gong
16/// [\[WG93\]](https://www.cs.cmu.edu/~wing/publications/WingGong93.pdf), and
17/// extended by Gavin Lowe [\[L17\]](http://www.cs.ox.ac.uk/people/gavin.lowe/LinearizabiltyTesting/).
18/// This particular implementation is based on the description given by Alex Horn
19/// and Daniel Kroenig [\[HK15\]](https://arxiv.org/abs/1504.00204).
20///
21/// Given a history of operations, the algorithm works by linearizing each operation
22/// as soon as possible. When an operation cannot be linearized, it backtracks and
23/// proceeds with the next operation. Memoization occurs by caching each partial
24/// linearization, and preventing the algorithm from continuing its search when it
25/// is already known that the state of the object and remaining operations have no
26/// valid linearization.
27///
28///
29/// # Examples
30///
31/// Consider the following [`Specification`] of a register containing `u32` values.
32///
33/// ```
34/// use todc_utils::specifications::Specification;
35///
36/// #[derive(Copy, Clone, Debug)]
37/// enum RegisterOp {
38/// Read(Option<u32>),
39/// Write(u32),
40/// }
41///
42/// use RegisterOp::{Read, Write};
43///
44/// struct RegisterSpec;
45///
46/// impl Specification for RegisterSpec {
47/// type State = u32;
48/// type Operation = RegisterOp;
49///
50/// fn init() -> Self::State {
51/// 0
52/// }
53///
54/// fn apply(operation: &Self::Operation, state: &Self::State) -> (bool, Self::State) {
55/// match operation {
56/// // A read is valid if the value returned is equal to the
57/// // current state. Reads always leave the state unchanged.
58/// Read(value) => match value {
59/// Some(value) => (value == state, *state),
60/// None => (false, *state)
61/// },
62/// // Writes are always valid, and update the state to be
63/// // equal to the value being written.
64/// Write(value) => (true, *value),
65/// }
66/// }
67/// }
68/// ```
69///
70/// Using the [`Action::Call`](history::Action::Call) and
71/// [`Action::Response`](history::Action::Response) types, we can model read
72/// and write operations as follows:
73/// * The call of a read operation is modeled by `Call(Read(None))` and a
74/// response containing the value `x` is modeled by `Response(Read(Some(x)))`.
75/// We are required to use an [`Option`] because the value being read cannot
76/// be known until the register responds.
77/// * Similarily, the call of a write operation with the value `y` is modeled
78/// by `Call(Write(y))` and the response is modeled by `Response(Write(y))`.
79///
80/// Next, we can define a linearizability for this specification, and check some
81/// histories.
82/// ```
83/// # use todc_utils::specifications::Specification;
84/// # #[derive(Copy, Clone, Debug)]
85/// # enum RegisterOp {
86/// # Read(Option<u32>),
87/// # Write(u32),
88/// # }
89/// # use RegisterOp::{Read, Write};
90/// # struct RegisterSpec;
91/// # impl Specification for RegisterSpec {
92/// # type State = u32;
93/// # type Operation = RegisterOp;
94/// # fn init() -> Self::State {
95/// # 0
96/// # }
97/// # fn apply(operation: &Self::Operation, state: &Self::State) -> (bool, Self::State) {
98/// # match operation {
99/// # // A read is valid if the value returned is equal to the
100/// # // current state. Reads always leave the state unchanged.
101/// # Read(value) => match value {
102/// # Some(value) => (value == state, *state),
103/// # None => (false, *state)
104/// # },
105/// # // Writes are always valid, and update the state to be
106/// # // equal to the value being written.
107/// # Write(value) => (true, *value),
108/// # }
109/// # }
110/// # }
111/// use todc_utils::linearizability::{WGLChecker, history::{History, Action::{Call, Response}}};
112///
113/// type RegisterChecker = WGLChecker<RegisterSpec>;
114///
115/// // A history of sequantial operations is always linearizable.
116/// // PO |------| Write(0)
117/// // P1 |------| Read(Some(0))
118/// let history = History::from_actions(vec![
119/// (0, Call(Write(0))),
120/// (0, Response(Write(0))),
121/// (1, Call(Read(None))),
122/// (1, Response(Read(Some(0)))),
123/// ]);
124/// assert!(RegisterChecker::is_linearizable(history));
125///
126/// // Concurrent operations might not be linearized
127/// // in the order in which they are called.
128/// // PO |--------------| Write(0)
129/// // P1 |--------------| Write(1)
130/// // P2 |---| Read(Some(1))
131/// // P3 |---| Read(Some(0))
132/// let history = History::from_actions(vec![
133/// (0, Call(Write(0))),
134/// (1, Call(Write(1))),
135/// (2, Call(Read(None))),
136/// (2, Response(Read(Some(1)))),
137/// (3, Call(Read(None))),
138/// (3, Response(Read(Some(0)))),
139/// (0, Response(Write(0))),
140/// (1, Response(Write(1))),
141/// ]);
142/// assert!(RegisterChecker::is_linearizable(history));
143///
144/// // A sequentially consistent history is **not**
145/// // necessarily linearizable.
146/// // PO |---| Write(0)
147/// // P1 |---| Write(1)
148/// // P2 |---| Read(Some(1))
149/// // P3 |---| Read(Some(0))
150/// let history = History::from_actions(vec![
151/// (0, Call(Write(0))),
152/// (1, Call(Write(1))),
153/// (0, Response(Write(0))),
154/// (1, Response(Write(1))),
155/// (2, Call(Read(None))),
156/// (2, Response(Read(Some(1)))),
157/// (3, Call(Read(None))),
158/// (3, Response(Read(Some(0)))),
159/// ]);
160/// assert!(!RegisterChecker::is_linearizable(history));
161/// ```
162///
163/// For examples of using [`WGLChecker`] to check the linearizability of more
164/// complex histories, see
165/// [`todc-mem/tests/snapshot/common.rs`](https://github.com/kaymanb/todc/blob/main/todc-mem/tests/snapshot/common.rs)
166/// or
167/// [`todc-utils/tests/linearizability/etcd.rs`](https://github.com/kaymanb/todc/blob/main/todc-utils/tests/linearizability/etcd.rs).
168///
169/// # Implementations in Other Languages
170///
171/// For an implementation in C++, see [`linearizability-checker`](https://github.com/ahorn/linearizability-checker).
172/// For an implementation in Go, see [`porcupine`](https://github.com/anishathalye/porcupine).
173pub struct WGLChecker<S: Specification> {
174 data_type: PhantomData<S>,
175}
176
177type OperationEntry<S> = Entry<<S as Specification>::Operation>;
178type OperationCall<S> = (
179 (OperationEntry<S>, OperationEntry<S>),
180 <S as Specification>::State,
181);
182
183impl<S: Specification> WGLChecker<S> {
184 /// Returns whether the history of operations is linearizable with respect to the specification.
185 pub fn is_linearizable(mut history: History<S::Operation>) -> bool {
186 let mut state = S::init();
187 let mut linearized = vec![false; history.len()];
188 let mut calls: Vec<OperationCall<S>> = Vec::new();
189 let mut cache: HashSet<(Vec<bool>, S::State)> = HashSet::new();
190 let mut curr = 0;
191 loop {
192 if history.is_empty() {
193 return true;
194 }
195 match &history[curr] {
196 Entry::Call(call) => match &history[history.index_of_id(call.response)] {
197 Entry::Call(_) => panic!("Response cannot be a call entry"),
198 Entry::Response(response) => {
199 let (is_valid, new_state) = S::apply(&response.operation, &state);
200 let mut changed = false;
201 if is_valid {
202 let mut tmp_linearized = linearized.clone();
203 tmp_linearized[call.id] = true;
204 changed = cache.insert((tmp_linearized, new_state.clone()));
205 }
206 if changed {
207 linearized[call.id] = true;
208 let call = history.lift(curr);
209 calls.push((call, state));
210 state = new_state;
211 curr = 0;
212 } else {
213 curr += 1;
214 }
215 }
216 },
217 Entry::Response(_) => match calls.pop() {
218 None => return false,
219 Some(((call, response), old_state)) => {
220 state = old_state;
221 linearized[call.id()] = false;
222 let (call_index, _) = history.unlift(call, response);
223 curr = call_index + 1;
224 }
225 },
226 }
227 }
228 }
229}
230
231#[cfg(test)]
232mod test {
233 use super::*;
234 use history::Action::*;
235
236 #[derive(Copy, Clone, Debug)]
237 enum RegisterOperation {
238 Read(u32),
239 Write(u32),
240 }
241
242 use RegisterOperation::*;
243
244 struct IntegerRegisterSpec;
245
246 impl Specification for IntegerRegisterSpec {
247 type State = u32;
248 type Operation = RegisterOperation;
249
250 fn init() -> Self::State {
251 0
252 }
253
254 fn apply(operation: &Self::Operation, state: &Self::State) -> (bool, Self::State) {
255 match operation {
256 Read(value) => (value == state, *state),
257 Write(value) => (true, *value),
258 }
259 }
260 }
261
262 type RegisterChecker = WGLChecker<IntegerRegisterSpec>;
263
264 mod is_linearizable {
265 use super::*;
266
267 #[test]
268 fn accepts_sequential_read_and_write() {
269 let history = History::from_actions(vec![
270 (0, Call(Write(1))),
271 (0, Response(Write(1))),
272 (0, Call(Read(1))),
273 (0, Response(Read(1))),
274 ]);
275 assert!(RegisterChecker::is_linearizable(history));
276 }
277
278 #[test]
279 fn rejects_invalid_reads() {
280 let history = History::from_actions(vec![
281 (0, Call(Write(1))),
282 (0, Response(Write(1))),
283 (0, Call(Read(2))),
284 (0, Response(Read(2))),
285 ]);
286 assert!(!RegisterChecker::is_linearizable(history));
287 }
288
289 #[test]
290 fn accepts_writes_in_reverse_order() {
291 // Accepts the following history, in which processes
292 // P1, P2, and P3 must linearize their writes in the
293 // reverse order in which they are called.
294 // P0 |--------------------| Write(1)
295 // P1 |--------------------| Write(2)
296 // P2 |--------------------| Write(3)
297 // P3 |--| Read(3)
298 // P3 |--| Read(2)
299 // P3 |--| Read(1)
300 let history = History::from_actions(vec![
301 (0, Call(Write(1))),
302 (1, Call(Write(2))),
303 (2, Call(Write(3))),
304 (3, Call(Read(3))),
305 (3, Response(Read(3))),
306 (3, Call(Read(2))),
307 (3, Response(Read(2))),
308 (3, Call(Read(1))),
309 (3, Response(Read(1))),
310 (0, Response(Write(1))),
311 (1, Response(Write(2))),
312 (2, Response(Write(3))),
313 ]);
314 assert!(RegisterChecker::is_linearizable(history));
315 }
316
317 #[test]
318 fn rejects_sequentially_consistent_reads() {
319 // Rejects the following history, in which P1 and P2 read
320 // different values while overlapping with P0s write. Notice
321 // that this history is _sequentially consistent_, as P2s
322 // read could be re-ordered to complete prior to any other
323 // operation.
324 // P0 |-------------------| Write(1)
325 // P1 |--| Read(1)
326 // P2 |--| Read(0)
327 let history = History::from_actions(vec![
328 (0, Call(Write(1))),
329 (1, Call(Read(1))),
330 (1, Response(Read(1))),
331 (2, Call(Read(0))),
332 (2, Response(Read(0))),
333 (0, Response(Write(1))),
334 ]);
335 assert!(!RegisterChecker::is_linearizable(history));
336 }
337 }
338}