Skip to main content

isolation/
isolation.rs

1//! What each isolation level actually gives you.
2//!
3//! Every scenario below interleaves two transactions by hand, so the outcomes
4//! are deterministic rather than timing-dependent.
5//!
6//!     cargo run --example isolation
7
8use mvcc::{Config, Database, Mvcc, ReadCommitted, Result, Serializable, Snapshot};
9
10#[derive(Mvcc, Clone, Debug)]
11#[mvcc(table = "doctors")]
12struct Doctor {
13    #[mvcc(primary_key)]
14    id: u64,
15    name: String,
16    on_call: bool,
17}
18
19#[derive(Mvcc, Clone, Debug)]
20#[mvcc(table = "counters")]
21struct Counter {
22    #[mvcc(primary_key)]
23    id: u64,
24    value: i64,
25}
26
27fn banner(title: &str) {
28    println!("\n\x1b[1m{title}\x1b[0m");
29    println!("{}", "─".repeat(title.len()));
30}
31
32fn main() -> Result<()> {
33    let db = Database::open(Config::in_memory())?;
34    db.register::<Doctor>()?;
35    db.register::<Counter>()?;
36
37    db.transaction(|tx| {
38        tx.insert(Doctor {
39            id: 1,
40            name: "ada".into(),
41            on_call: true,
42        })?;
43        tx.insert(Doctor {
44            id: 2,
45            name: "bob".into(),
46            on_call: true,
47        })?;
48        tx.insert(Counter { id: 1, value: 0 })
49    })?;
50
51    // ------------------------------------------------------------------
52    banner("1. Snapshot: a reader is unaffected by concurrent commits");
53    {
54        let mut reader = db.begin_with::<Snapshot>();
55        let before = reader.get::<Counter>(&1)?.unwrap().value;
56
57        // Someone else commits, start to finish, while `reader` is open.
58        db.transaction(|tx| tx.update::<Counter>(&1, |c| c.value = 42).map(|_| ()))?;
59
60        let after = reader.get::<Counter>(&1)?.unwrap().value;
61        println!("  reader saw {before} before, {after} after a concurrent commit");
62        assert_eq!(before, after, "snapshot isolation must be stable");
63        println!("  ✓ the snapshot held: readers never block and never change under you");
64    }
65
66    // ------------------------------------------------------------------
67    banner("2. ReadCommitted: each statement sees a fresh snapshot");
68    {
69        let mut reader = db.begin_with::<ReadCommitted>();
70        let before = reader.get::<Counter>(&1)?.unwrap().value;
71
72        db.transaction(|tx| tx.update::<Counter>(&1, |c| c.value = 99).map(|_| ()))?;
73
74        let after = reader.get::<Counter>(&1)?.unwrap().value;
75        println!("  reader saw {before} before, {after} after a concurrent commit");
76        assert_ne!(
77            before, after,
78            "read committed should observe the new commit"
79        );
80        println!("  ✓ non-repeatable read — the tradeoff this level makes for cheapness");
81    }
82
83    // ------------------------------------------------------------------
84    banner("3. Write-write conflict: first committer wins");
85    {
86        let mut first = db.begin_with::<Snapshot>();
87        let mut second = db.begin_with::<Snapshot>();
88
89        first.update::<Counter>(&1, |c| c.value += 1)?;
90        first.commit()?;
91
92        // `second` took its snapshot before `first` committed, so writing here
93        // would silently drop `first`'s update.
94        let result = second.update::<Counter>(&1, |c| c.value += 1);
95        match result {
96            Err(e) => println!(
97                "  second transaction: {e}  (retriable: {})",
98                e.is_retriable()
99            ),
100            Ok(_) => unreachable!("the stale write should have been rejected"),
101        }
102        println!("  ✓ no lost update");
103    }
104
105    // ------------------------------------------------------------------
106    banner("4. Write skew: allowed under Snapshot");
107    {
108        // The rule: at least one doctor must stay on call. Each transaction
109        // checks the rule, sees it satisfied, and takes a *different* doctor
110        // off call — so there is no write-write conflict to catch them.
111        db.transaction(|tx| {
112            tx.update::<Doctor>(&1, |d| d.on_call = true)?;
113            tx.update::<Doctor>(&2, |d| d.on_call = true).map(|_| ())
114        })?;
115
116        let mut t1 = db.begin_with::<Snapshot>();
117        let mut t2 = db.begin_with::<Snapshot>();
118
119        let t1_sees = t1.get::<Doctor>(&1)?.unwrap().on_call as u8
120            + t1.get::<Doctor>(&2)?.unwrap().on_call as u8;
121        let t2_sees = t2.get::<Doctor>(&1)?.unwrap().on_call as u8
122            + t2.get::<Doctor>(&2)?.unwrap().on_call as u8;
123        let names: Vec<String> = {
124            let mut tx = db.begin();
125            tx.scan::<Doctor>()?
126                .iter()
127                .map(|d| d.name.clone())
128                .collect()
129        };
130        println!("  on the rota: {}", names.join(", "));
131        println!("  t1 counts {t1_sees} on call, t2 counts {t2_sees} — both think it is safe");
132
133        t1.update::<Doctor>(&1, |d| d.on_call = false)?;
134        t2.update::<Doctor>(&2, |d| d.on_call = false)?;
135        t1.commit()?;
136        t2.commit()?;
137
138        let mut check = db.begin();
139        let remaining = check.get::<Doctor>(&1)?.unwrap().on_call as u8
140            + check.get::<Doctor>(&2)?.unwrap().on_call as u8;
141        println!("  ✗ {remaining} doctors on call — the invariant is broken");
142        println!("    Both transactions were individually legal. This is write skew,");
143        println!("    and it is why Snapshot is not Serializable.");
144    }
145
146    // ------------------------------------------------------------------
147    banner("5. Write skew: prevented under Serializable");
148    {
149        db.transaction(|tx| {
150            tx.update::<Doctor>(&1, |d| d.on_call = true)?;
151            tx.update::<Doctor>(&2, |d| d.on_call = true).map(|_| ())
152        })?;
153
154        let mut t1 = db.begin_with::<Serializable>();
155        let mut t2 = db.begin_with::<Serializable>();
156
157        // Both read both rows, exactly as before.
158        let _ = t1.get::<Doctor>(&1)?;
159        let _ = t1.get::<Doctor>(&2)?;
160        let _ = t2.get::<Doctor>(&1)?;
161        let _ = t2.get::<Doctor>(&2)?;
162
163        t1.update::<Doctor>(&1, |d| d.on_call = false)?;
164        t2.update::<Doctor>(&2, |d| d.on_call = false)?;
165
166        t1.commit()?;
167        match t2.commit() {
168            Err(e) => println!("  t2: {e}  (retriable: {})", e.is_retriable()),
169            Ok(()) => unreachable!("t2 read a row t1 changed; it must not commit"),
170        }
171
172        let mut check = db.begin();
173        let remaining = check.get::<Doctor>(&1)?.unwrap().on_call as u8
174            + check.get::<Doctor>(&2)?.unwrap().on_call as u8;
175        println!("  ✓ {remaining} doctor still on call — the invariant held");
176        println!("    t2 read doctor 1, which t1 changed, so t2 could not be serialized.");
177    }
178
179    // ------------------------------------------------------------------
180    banner("6. Retries are the normal way to use Serializable");
181    {
182        // `transaction_with` reruns the closure on a retriable failure, so the
183        // abort in scenario 5 becomes invisible to the caller.
184        let mut attempts = 0;
185        db.transaction_with::<Serializable, _, _>(|tx| {
186            attempts += 1;
187            let ada = tx.get::<Doctor>(&1)?.unwrap().on_call;
188            let bob = tx.get::<Doctor>(&2)?.unwrap().on_call;
189            if ada && bob {
190                tx.update::<Doctor>(&1, |d| d.on_call = false)?;
191            }
192            Ok(())
193        })?;
194        println!("  committed after {attempts} attempt(s)");
195        println!("  ✓ write your transaction as if it runs alone; let the engine retry it");
196    }
197
198    banner("Summary");
199    println!("  ReadCommitted   cheapest; non-repeatable reads and phantoms");
200    println!("  RepeatableRead  stable snapshot; write skew possible");
201    println!("  Snapshot        stable snapshot; write skew possible   ← default");
202    println!("  Serializable    no anomalies; expect retriable aborts");
203
204    Ok(())
205}