Skip to main content

Database

Struct Database 

pub struct Database { /* private fields */ }
Expand description

Owns every table, version chain and index, and hands out Transactions.

Every type must be passed to Database::register before a transaction touches it; operations on an unregistered type fail with Error::TableNotRegistered. Registration is the only setup there is — nothing is read from disk, and nothing is written to it.

Shared across threads behind an &, so an Arc<Database> is the usual way to hand it to several of them. Reads take no locks whatever else is running. Dropping it frees every version it ever held.

Database::compact is the exception to all of that: it takes &mut self, so no transaction may be live while it runs.

Implementations§

§

impl Database

pub fn open(config: Config) -> Result<Self>

Create an empty database.

Nothing is read from disk and nothing will be written to it — see the crate docs. Every type it will store must then be passed to Database::register before use.

use mvcc::{Config, Database};

let db = Database::open(Config::in_memory())?;
§Errors

Currently infallible; the Result is here so that configuration that can fail may be added without a breaking change.

Examples found in repository?
examples/basic.rs (line 25)
24fn main() -> Result<()> {
25    let db = Database::open(Config::in_memory())?;
26    db.register::<Account>()?;
27
28    // ---- insert -----------------------------------------------------------
29    // `transaction` runs the closure, commits it, and retries it if it hits a
30    // retriable conflict. Snapshot isolation by default.
31    db.transaction(|tx| {
32        tx.insert(Account {
33            id: 1,
34            owner: "ada".into(),
35            branch: 10,
36            balance: 500,
37        })?;
38        tx.insert(Account {
39            id: 2,
40            owner: "bob".into(),
41            branch: 10,
42            balance: 250,
43        })?;
44        tx.insert(Account {
45            id: 3,
46            owner: "cleo".into(),
47            branch: 20,
48            balance: 900,
49        })?;
50        Ok(())
51    })?;
52
53    // ---- read -------------------------------------------------------------
54    let mut tx = db.begin();
55    let ada = tx.get::<Account>(&1)?.expect("just inserted");
56    println!("ada: branch {}, balance {}", ada.branch, ada.balance);
57
58    // A read-only transaction has nothing to commit; dropping it rolls back,
59    // which for a reader means simply releasing its snapshot.
60    drop(tx);
61
62    // ---- update -----------------------------------------------------------
63    db.transaction(|tx| {
64        tx.update::<Account>(&1, |a| a.balance -= 100)?;
65        tx.update::<Account>(&2, |a| a.balance += 100)?;
66        Ok(())
67    })?;
68
69    // ---- scan by primary key ----------------------------------------------
70    let mut tx = db.begin();
71    println!("\nall accounts:");
72    for account in tx.scan::<Account>()? {
73        println!(
74            "  {:>4}  {:<6} branch {}  {:>5}",
75            account.id, account.owner, account.branch, account.balance
76        );
77    }
78
79    // ---- scan by secondary index ------------------------------------------
80    println!("\nbranch 10:");
81    for account in tx.scan_index(Account::BRANCH, 10u32..=10)? {
82        println!("  {} ({})", account.owner, account.balance);
83    }
84    drop(tx);
85
86    // ---- constraint violations --------------------------------------------
87    let mut tx = db.begin();
88    let duplicate = tx.insert(Account {
89        id: 99,
90        owner: "ada".into(),
91        branch: 30,
92        balance: 0,
93    });
94    println!("\nreusing owner 'ada': {}", duplicate.unwrap_err());
95    drop(tx);
96
97    // ---- delete -----------------------------------------------------------
98    db.transaction(|tx| {
99        let existed = tx.delete::<Account>(&3)?;
100        println!("deleted cleo: {existed}");
101        Ok(())
102    })?;
103
104    let mut tx = db.begin();
105    println!(
106        "cleo now: {:?}",
107        tx.get::<Account>(&3)?.map(|r| r.to_owned())
108    );
109
110    Ok(())
111}
More examples
Hide additional examples
examples/concurrent.rs (line 30)
29fn main() -> Result<()> {
30    let db = Arc::new(Database::open(Config::in_memory())?);
31    db.register::<Account>()?;
32
33    db.transaction(|tx| {
34        for id in 0..ACCOUNTS {
35            tx.insert(Account {
36                id,
37                balance: STARTING_BALANCE,
38            })?;
39        }
40        Ok(())
41    })?;
42
43    let total_before = total(&db)?;
44    println!("starting total: {total_before}");
45
46    let stop = Arc::new(AtomicBool::new(false));
47    let reads = Arc::new(AtomicU64::new(0));
48    let retries = Arc::new(AtomicU64::new(0));
49
50    // ---- a reader that must never observe a torn transfer -----------------
51    // Money moves between accounts constantly. Under snapshot isolation this
52    // thread's scans must always sum to the same total: it either sees both
53    // halves of a transfer or neither.
54    let auditor = {
55        let db = Arc::clone(&db);
56        let stop = Arc::clone(&stop);
57        let reads = Arc::clone(&reads);
58        thread::spawn(move || -> Result<()> {
59            while !stop.load(Ordering::Relaxed) {
60                let mut tx = db.begin();
61                let sum: i64 = tx.scan::<Account>()?.iter().map(|a| a.balance).sum();
62                assert_eq!(
63                    sum,
64                    ACCOUNTS as i64 * STARTING_BALANCE,
65                    "a reader observed a partially applied transfer"
66                );
67                reads.fetch_add(1, Ordering::Relaxed);
68            }
69            Ok(())
70        })
71    };
72
73    // ---- writers ----------------------------------------------------------
74    let start = Instant::now();
75    let mut writers = Vec::new();
76    for t in 0..TRANSFER_THREADS {
77        let db = Arc::clone(&db);
78        let retries = Arc::clone(&retries);
79        writers.push(thread::spawn(move || -> Result<()> {
80            // A cheap per-thread PRNG; no dependency needed for this.
81            let mut seed = 0x9e37_79b9_7f4a_7c15u64 ^ (t as u64 + 1);
82            let mut next = move || {
83                seed ^= seed << 13;
84                seed ^= seed >> 7;
85                seed ^= seed << 17;
86                seed
87            };
88
89            for _ in 0..TRANSFERS_PER_THREAD {
90                let from = next() % ACCOUNTS;
91                let to = next() % ACCOUNTS;
92                if from == to {
93                    continue;
94                }
95
96                let mut attempts = 0u64;
97                // Serializable, because the transfer's *write* depends on a
98                // balance it *read* — the write-skew shape. Snapshot isolation
99                // would let two concurrent transfers both pass the check.
100                db.transaction_with::<Serializable, _, _>(|tx| {
101                    attempts += 1;
102                    let balance = tx.get::<Account>(&from)?.map(|a| a.balance).unwrap_or(0);
103                    if balance < 10 {
104                        return Ok(());
105                    }
106                    tx.update::<Account>(&from, |a| a.balance -= 10)?;
107                    tx.update::<Account>(&to, |a| a.balance += 10)?;
108                    Ok(())
109                })?;
110                retries.fetch_add(attempts - 1, Ordering::Relaxed);
111            }
112            Ok(())
113        }));
114    }
115
116    for w in writers {
117        w.join().expect("writer panicked")?;
118    }
119    let elapsed = start.elapsed();
120
121    stop.store(true, Ordering::Relaxed);
122    auditor.join().expect("auditor panicked")?;
123
124    // ---- results ----------------------------------------------------------
125    let total_after = total(&db)?;
126    let committed = (TRANSFER_THREADS * TRANSFERS_PER_THREAD) as u64;
127
128    println!("final total:    {total_after}");
129    assert_eq!(total_before, total_after, "money was created or destroyed");
130
131    println!("\n{committed} transfers in {elapsed:.2?}");
132    println!(
133        "  {:.0} transfers/sec",
134        committed as f64 / elapsed.as_secs_f64()
135    );
136    println!(
137        "  {} concurrent audit scans completed, none of them blocked",
138        reads.load(Ordering::Relaxed)
139    );
140    println!(
141        "  {} retries ({:.1} per transfer)",
142        retries.load(Ordering::Relaxed),
143        retries.load(Ordering::Relaxed) as f64 / committed as f64
144    );
145
146    // The GC watermark should be advancing. If `active_transactions` climbs and
147    // the watermark stalls, some transaction is being leaked — see engine::gc.
148    let stats = db.stats();
149    println!(
150        "\ngc watermark {:?}, {} transactions still live",
151        stats.watermark, stats.active_transactions
152    );
153
154    println!("\n✓ no torn reads, no lost updates, conservation held");
155
156    // Slow readers are what pin the GC watermark. Left here as the shape of the
157    // thing to watch for, not as something this example demonstrates going wrong.
158    thread::sleep(Duration::from_millis(1));
159    Ok(())
160}
examples/isolation.rs (line 33)
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}
examples/game.rs (line 142)
141fn main() -> Result<()> {
142    let db = Arc::new(Database::open(Config::in_memory())?);
143    db.register::<Hero>()?;
144    db.register::<Item>()?;
145    db.register::<PartyMember>()?;
146
147    let (ada, bram, cleo) = (1u64, 2u64, 3u64);
148
149    // -----------------------------------------------------------------------
150    act(1, "The world is created");
151
152    db.transaction(|tx| {
153        tx.insert(Hero {
154            id: ada,
155            name: "Ada".into(),
156            gold: 120,
157            strength: 100,
158        })?;
159        tx.insert(Hero {
160            id: bram,
161            name: "Bram".into(),
162            gold: 80,
163            strength: 100,
164        })?;
165        tx.insert(Hero {
166            id: cleo,
167            name: "Cleo".into(),
168            gold: 200,
169            strength: 60,
170        })?;
171
172        let mut item = |id, owner, name: &str, weight, value, kind| {
173            tx.insert(Item {
174                id,
175                owner,
176                name: name.into(),
177                weight,
178                value,
179                kind,
180                flavour: HashMap::new(),
181            })
182        };
183        item(10, ada, "Rusty Sword", 30, 15, Kind::Weapon)?;
184        item(11, ada, "Health Potion", 5, 20, Kind::Potion)?;
185        item(12, bram, "Oak Shield", 40, 35, Kind::Weapon)?;
186        item(13, cleo, "Lucky Charm", 2, 90, Kind::Trinket)?;
187        // Loot lying in the dungeon, owned by nobody.
188        item(20, GROUND, "Flaming Greatsword", 55, 500, Kind::Weapon)?;
189        item(21, GROUND, "Elven Cloak", 20, 240, Kind::Trinket)?;
190        item(22, GROUND, "Iron Helm", 35, 60, Kind::Weapon)?;
191
192        // Arbitrary per-item state, in a database row, with no schema to
193        // declare and no serialisation format to agree on.
194        tx.update::<Item>(&20, |i| {
195            i.flavour.insert("enchantment".into(), "flame".into());
196            i.flavour.insert("forged_by".into(), "Durin".into());
197        })?;
198        Ok(())
199    })?;
200
201    // A whole transaction that fails partway leaves nothing behind. The unique
202    // index on `name` refuses the second Ada, and the gold change goes with it.
203    let before = gold_of(&mut db.begin(), ada)?;
204    let doomed = db.transaction(|tx| {
205        tx.update::<Hero>(&ada, |h| h.gold += 10_000)?;
206        tx.insert(Hero {
207            id: 99,
208            name: "Ada".into(),
209            gold: 0,
210            strength: 10,
211        })
212    });
213    println!("  a duplicate hero name: {}", doomed.unwrap_err());
214    println!(
215        "  Ada's gold is still {} — the whole transaction rolled back, not just the insert",
216        gold_of(&mut db.begin(), ada)?
217    );
218    assert_eq!(gold_of(&mut db.begin(), ada)?, before);
219
220    let mut tx = db.begin();
221    for hero in [ada, bram, cleo] {
222        let name = name_of(&mut tx, hero)?;
223        let carried = carried(&mut tx, hero)?;
224        let items = inventory(&mut tx, hero)?;
225        let names: Vec<_> = items.iter().map(|(_, n, _)| n.as_str()).collect();
226        println!("  {name:<5} carries {carried:>3} — {}", names.join(", "));
227    }
228    drop(tx);
229
230    // -----------------------------------------------------------------------
231    act(2, "Two heroes reach for the same greatsword");
232
233    // Both begin, both see the sword unclaimed. The first to write takes it;
234    // the second is told immediately rather than being made to wait.
235    let mut ada_grabs = db.begin_with::<Snapshot>();
236    let mut bram_grabs = db.begin_with::<Snapshot>();
237
238    ada_grabs.update::<Item>(&20, |i| i.owner = ada)?;
239
240    match bram_grabs.update::<Item>(&20, |i| i.owner = bram) {
241        Err(e @ Error::WriteConflict { .. }) => {
242            println!("  Bram: {e}");
243            println!(
244                "  ...retriable: {} — he can try for something else",
245                e.is_retriable()
246            );
247        }
248        other => unreachable!("expected a conflict, got {other:?}"),
249    }
250
251    // A failed write does not end the transaction. Bram takes the cloak instead.
252    bram_grabs.update::<Item>(&21, |i| i.owner = bram)?;
253    ada_grabs.commit()?;
254    bram_grabs.commit()?;
255    println!("  Ada takes the greatsword, Bram takes the cloak. Nobody blocked.");
256
257    // -----------------------------------------------------------------------
258    act(3, "Cleo overloads herself — write skew, and the fix");
259
260    // Cleo can carry 60 and is carrying 2. Either 35kg helm fits; both do not.
261    // Each transaction reads her inventory, checks the total, and picks up a
262    // *different* item — so the two writes never touch the same record, and
263    // first-updater-wins has nothing to catch.
264    db.transaction(|tx| {
265        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
266        tx.insert(Item {
267            id: 23,
268            owner: GROUND,
269            name: "Steel Helm".into(),
270            weight: 35,
271            value: 70,
272            kind: Kind::Weapon,
273            flavour: HashMap::new(),
274        })
275    })?;
276
277    let pick_up = |item: u64| {
278        move |tx: &mut mvcc::Transaction<'_, Serializable>| -> Result<bool> {
279            let hero = tx.get::<Hero>(&cleo)?.expect("Cleo exists");
280            let strength = hero.strength;
281            let load = carried(tx, cleo)?;
282            let weight = tx.get::<Item>(&item)?.map(|i| i.weight).unwrap_or(0);
283            if load + weight > strength {
284                return Ok(false);
285            }
286            tx.update::<Item>(&item, |i| i.owner = cleo)?;
287            Ok(true)
288        }
289    };
290
291    // Under Snapshot, both checks pass against a stale inventory.
292    {
293        let mut t1 = db.begin_with::<Snapshot>();
294        let mut t2 = db.begin_with::<Snapshot>();
295        let (load1, load2) = (carried(&mut t1, cleo)?, carried(&mut t2, cleo)?);
296        println!(
297            "  Snapshot:     both transactions see {load1}/{load2} carried, both think a 35kg helm fits"
298        );
299        t1.update::<Item>(&22, |i| i.owner = cleo)?;
300        t2.update::<Item>(&23, |i| i.owner = cleo)?;
301        t1.commit()?;
302        t2.commit()?;
303        let over = carried(&mut db.begin(), cleo)?;
304        println!(
305            "  ✗ Cleo now carries {over} of a possible 60. Two legal transactions, one broken rule."
306        );
307    }
308
309    // Put it back and try again at Serializable.
310    db.transaction(|tx| {
311        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
312        tx.update::<Item>(&23, |i| i.owner = GROUND).map(|_| ())
313    })?;
314
315    {
316        let mut t1 = db.begin_with::<Serializable>();
317        let mut t2 = db.begin_with::<Serializable>();
318        // Both read the inventory, as before.
319        let _ = carried(&mut t1, cleo)?;
320        let _ = carried(&mut t2, cleo)?;
321        t1.update::<Item>(&22, |i| i.owner = cleo)?;
322        t2.update::<Item>(&23, |i| i.owner = cleo)?;
323        t1.commit()?;
324        match t2.commit() {
325            Err(e @ Error::SerializationFailure) => println!("  Serializable: second pickup {e}"),
326            other => unreachable!("expected a serialization failure, got {other:?}"),
327        }
328        println!(
329            "  ✓ Cleo carries {} — the second pickup read an inventory the first changed.",
330            carried(&mut db.begin(), cleo)?
331        );
332    }
333
334    // In real code you would not hand-roll that. `transaction_with` retries,
335    // and the retry re-reads the inventory and correctly declines.
336    let took_it = db.transaction_with::<Serializable, _, _>(pick_up(23))?;
337    println!("  ...and on retry the second helm is refused on its merits: picked_up = {took_it}");
338
339    // -----------------------------------------------------------------------
340    act(4, "The party fills up — a phantom, not a conflict");
341
342    db.transaction(|tx| {
343        tx.insert(PartyMember {
344            hero: ada,
345            party: PARTY,
346            role: "Vanguard".into(),
347        })?;
348        tx.insert(PartyMember {
349            hero: bram,
350            party: PARTY,
351            role: "Shield".into(),
352        })?;
353        tx.insert(PartyMember {
354            hero: cleo,
355            party: PARTY,
356            role: "Scout".into(),
357        })
358    })?;
359
360    // Two newcomers apply at once, with three of four slots taken. Neither
361    // writes a row the other wrote — they insert *different* rows — so there is
362    // no write conflict to detect. What they collide on is a row that did not
363    // exist when either of them counted.
364    let (dara, finn) = (4u64, 5u64);
365    db.transaction(|tx| {
366        tx.insert(Hero {
367            id: dara,
368            name: "Dara".into(),
369            gold: 40,
370            strength: 80,
371        })?;
372        tx.insert(Hero {
373            id: finn,
374            name: "Finn".into(),
375            gold: 40,
376            strength: 80,
377        })
378    })?;
379
380    let mut d = db.begin_with::<Serializable>();
381    let mut f = db.begin_with::<Serializable>();
382    println!(
383        "  Dara counts {} members, Finn counts {} — both see a free slot",
384        party_size(&mut d)?,
385        party_size(&mut f)?
386    );
387    d.insert(PartyMember {
388        hero: dara,
389        party: PARTY,
390        role: "Healer".into(),
391    })?;
392    f.insert(PartyMember {
393        hero: finn,
394        party: PARTY,
395        role: "Healer".into(),
396    })?;
397    d.commit()?;
398    match f.commit() {
399        Err(e @ Error::SerializationFailure) => println!("  Finn: {e}"),
400        other => unreachable!("expected a serialization failure, got {other:?}"),
401    }
402    let mut tx = db.begin();
403    let mut roster: Vec<_> = tx
404        .scan_where::<PartyMember, _>(|m| m.party == PARTY)?
405        .iter()
406        .map(|m| (m.hero, m.role.clone()))
407        .collect();
408    drop(tx);
409    roster.sort();
410    let size = roster.len();
411    let mut tx = db.begin();
412    let listed: Vec<String> = roster
413        .iter()
414        .map(|(h, role)| Ok(format!("{} the {role}", name_of(&mut tx, *h)?)))
415        .collect::<Result<_>>()?;
416    drop(tx);
417    println!("  party: {}", listed.join(", "));
418    println!("  ✓ {size}/{PARTY_LIMIT} filled. Finn's insert became a phantom in Dara's count.");
419    println!("    (`scan_where` hands the engine the predicate, so it can re-check it at commit.)");
420    assert!(size <= PARTY_LIMIT);
421
422    // -----------------------------------------------------------------------
423    act(5, "A trade — two records, one atomic step");
424
425    let before = total_gold(&db)?;
426
427    let trade = |seller: u64, buyer: u64, item: u64, price: i64| {
428        let db = Arc::clone(&db);
429        move || -> Result<bool> {
430            db.transaction_with::<Serializable, _, _>(|tx| {
431                let buyer_gold = gold_of(tx, buyer)?;
432                if buyer_gold < price {
433                    return Ok(false);
434                }
435                let owned_by_seller = tx
436                    .get::<Item>(&item)?
437                    .map(|i| i.owner == seller)
438                    .unwrap_or(false);
439                if !owned_by_seller {
440                    return Ok(false);
441                }
442                tx.update::<Hero>(&buyer, |h| h.gold -= price)?;
443                tx.update::<Hero>(&seller, |h| h.gold += price)?;
444                tx.update::<Item>(&item, |i| i.owner = buyer)?;
445                Ok(true)
446            })
447        }
448    };
449
450    let sold = trade(bram, cleo, 21, 150)()?;
451    println!("  Bram sells the Elven Cloak to Cleo for 150g: {sold}");
452    let mut tx = db.begin();
453    let cloak_owner = tx.get::<Item>(&21)?.expect("cloak").owner;
454    println!(
455        "  Bram {}g, Cleo {}g, cloak owner is {}",
456        gold_of(&mut tx, bram)?,
457        gold_of(&mut tx, cleo)?,
458        name_of(&mut tx, cloak_owner)?
459    );
460    drop(tx);
461    assert_eq!(total_gold(&db)?, before, "gold must be conserved");
462    println!("  ✓ total gold unchanged at {before} — no step of that was separately visible");
463
464    // -----------------------------------------------------------------------
465    act(6, "The chronicler reads while the world moves");
466
467    // A long report scanning the whole world while combat rewrites it. It never
468    // blocks a writer, and its view never shifts underneath it.
469    let mut chronicler = db.begin_with::<Snapshot>();
470    let opening = total_gold(&db)?;
471    let seen_first = chronicler.scan::<Hero>()?.len();
472
473    for _ in 0..50 {
474        db.transaction(|tx| {
475            tx.update::<Hero>(&ada, |h| h.gold += 1)?;
476            tx.update::<Hero>(&bram, |h| h.gold -= 1)?;
477            Ok(())
478        })?;
479    }
480    db.transaction(|tx| {
481        tx.insert(Hero {
482            id: 6,
483            name: "Mira".into(),
484            gold: 0,
485            strength: 50,
486        })
487    })?;
488
489    let chron_gold: i64 = chronicler.scan::<Hero>()?.iter().map(|h| h.gold).sum();
490    let seen_after = chronicler.scan::<Hero>()?.len();
491    let in_world = db.begin().scan::<Hero>()?.len();
492    println!("  chronicler saw {seen_first} heroes at the start and still sees {seen_after};");
493    println!("  the world now holds {in_world}, after 50 commits and one new arrival");
494    println!(
495        "  chronicler totals {chron_gold}g; the world now totals {}g",
496        total_gold(&db)?
497    );
498    assert_eq!(chron_gold, opening, "a snapshot must not move");
499    chronicler.commit()?;
500    println!("  ✓ a reader that ran across 51 commits saw exactly one consistent world");
501
502    // -----------------------------------------------------------------------
503    act(7, "The raid — many adventurers at once");
504
505    let stop = Arc::new(AtomicBool::new(false));
506    let trades = Arc::new(AtomicU64::new(0));
507    let retries = Arc::new(AtomicU64::new(0));
508    let audits = Arc::new(AtomicU64::new(0));
509    let opening = total_gold(&db)?;
510    let heroes = [ada, bram, cleo, dara, finn];
511
512    // An auditor that must never catch a trade half-applied.
513    let auditor = {
514        let (db, stop, audits) = (Arc::clone(&db), Arc::clone(&stop), Arc::clone(&audits));
515        thread::spawn(move || -> Result<()> {
516            while !stop.load(Ordering::Relaxed) {
517                let mut tx = db.begin();
518                let total: i64 = tx.scan::<Hero>()?.iter().map(|h| h.gold).sum();
519                assert_eq!(total, opening, "auditor saw a half-finished trade");
520                audits.fetch_add(1, Ordering::Relaxed);
521            }
522            Ok(())
523        })
524    };
525
526    let start = Instant::now();
527    let raiders: Vec<_> = (0..4)
528        .map(|t| {
529            let (db, trades, retries) =
530                (Arc::clone(&db), Arc::clone(&trades), Arc::clone(&retries));
531            thread::spawn(move || -> Result<()> {
532                let mut seed = 0x9e37_79b9_7f4a_7c15u64 ^ (t + 1);
533                for _ in 0..400 {
534                    seed ^= seed << 13;
535                    seed ^= seed >> 7;
536                    seed ^= seed << 17;
537                    let from = heroes[(seed % heroes.len() as u64) as usize];
538                    let to = heroes[((seed >> 8) % heroes.len() as u64) as usize];
539                    if from == to {
540                        continue;
541                    }
542
543                    let mut attempts = 0u64;
544                    // Serializable, because the payment depends on a balance we
545                    // read — the same shape as Cleo's carry limit in Act 3.
546                    db.transaction_with::<Serializable, _, _>(|tx| {
547                        attempts += 1;
548                        let purse = gold_of(tx, from)?;
549                        if purse < 5 {
550                            return Ok(());
551                        }
552                        tx.update::<Hero>(&from, |h| h.gold -= 5)?;
553                        tx.update::<Hero>(&to, |h| h.gold += 5)?;
554                        Ok(())
555                    })?;
556                    trades.fetch_add(1, Ordering::Relaxed);
557                    retries.fetch_add(attempts - 1, Ordering::Relaxed);
558                }
559                Ok(())
560            })
561        })
562        .collect();
563
564    for r in raiders {
565        r.join().expect("raider panicked")?;
566    }
567    let elapsed = start.elapsed();
568    stop.store(true, Ordering::Relaxed);
569    auditor.join().expect("auditor panicked")?;
570
571    let done = trades.load(Ordering::Relaxed);
572    println!("  {done} trades across 4 threads in {elapsed:.2?}");
573    println!(
574        "  {} audit sweeps completed alongside them, none blocked, none torn",
575        audits.load(Ordering::Relaxed)
576    );
577    println!(
578        "  {} retries ({:.2} per trade) — contention on 5 heroes, handled by the engine",
579        retries.load(Ordering::Relaxed),
580        retries.load(Ordering::Relaxed) as f64 / done.max(1) as f64
581    );
582    assert_eq!(total_gold(&db)?, opening, "gold was created or destroyed");
583    println!("  ✓ total gold still {opening}");
584
585    // -----------------------------------------------------------------------
586    act(8, "Closing the ledger");
587
588    let mut tx = db.begin();
589    let mut roster: Vec<_> = tx
590        .scan::<Hero>()?
591        .iter()
592        .map(|h| (h.id, h.name.clone(), h.gold))
593        .collect();
594    roster.sort_by_key(|(id, _, _)| *id);
595    drop(tx);
596
597    println!(
598        "  {:<6} {:>6} {:>5} {:>7}  inventory",
599        "hero", "gold", "load", "worth"
600    );
601    let mut tx = db.begin();
602    for (id, name, gold) in &roster {
603        let held = tx.scan_index(Item::OWNER, *id..=*id)?;
604        let load: i32 = held.iter().map(|i| i.weight).sum();
605        let worth: i64 = held.iter().map(|i| i.value).sum();
606        let names: Vec<String> = held
607            .iter()
608            .map(|i| match i.kind {
609                Kind::Weapon => format!("[wpn] {}", i.name),
610                Kind::Potion => format!("[pot] {}", i.name),
611                Kind::Trinket => format!("[trk] {}", i.name),
612            })
613            .collect();
614        let names = if names.is_empty() {
615            "—".to_string()
616        } else {
617            names.join(", ")
618        };
619        println!("  {name:<6} {gold:>5}g {load:>4}kg {worth:>6}g  {names}");
620    }
621
622    // Potions anywhere in the world, found by predicate rather than by key —
623    // and `kind` is an ordinary Rust enum the engine knows nothing about.
624    let potions = tx.scan_where::<Item, _>(|i| i.kind == Kind::Potion)?.len();
625    let sword = tx.get::<Item>(&20)?.expect("greatsword");
626    let enchantment = sword
627        .flavour
628        .get("enchantment")
629        .cloned()
630        .unwrap_or_default();
631    let forged_by = sword.flavour.get("forged_by").cloned().unwrap_or_default();
632    drop(tx);
633    // The rule Act 3 was about, checked rather than asserted in prose.
634    let mut tx = db.begin();
635    for (id, name, _) in &roster {
636        let strength = tx.get::<Hero>(id)?.map(|h| h.strength).unwrap_or(0);
637        let load = carried(&mut tx, *id)?;
638        assert!(
639            load <= strength,
640            "{name} is over their carry limit: {load} > {strength}"
641        );
642    }
643    drop(tx);
644
645    println!("\n  potions in the world: {potions}");
646    println!("  the greatsword is {enchantment}-enchanted, forged by {forged_by}");
647
648    // Nothing is reclaimed while a transaction that could still reach it is
649    // alive. This is the number to watch in a long-lived process: a forgotten
650    // transaction pins it and version chains grow without limit.
651    let stats = db.stats();
652    println!(
653        "\n  gc watermark {:?}, {} transactions still live",
654        stats.watermark, stats.active_transactions
655    );
656    println!("  (a watermark that stops moving while writes continue is the leak to look for)");
657
658    Ok(())
659}

pub fn stats(&self) -> GcStats

Snapshot of engine statistics. See crate::stats for what to watch.

Examples found in repository?
examples/concurrent.rs (line 148)
29fn main() -> Result<()> {
30    let db = Arc::new(Database::open(Config::in_memory())?);
31    db.register::<Account>()?;
32
33    db.transaction(|tx| {
34        for id in 0..ACCOUNTS {
35            tx.insert(Account {
36                id,
37                balance: STARTING_BALANCE,
38            })?;
39        }
40        Ok(())
41    })?;
42
43    let total_before = total(&db)?;
44    println!("starting total: {total_before}");
45
46    let stop = Arc::new(AtomicBool::new(false));
47    let reads = Arc::new(AtomicU64::new(0));
48    let retries = Arc::new(AtomicU64::new(0));
49
50    // ---- a reader that must never observe a torn transfer -----------------
51    // Money moves between accounts constantly. Under snapshot isolation this
52    // thread's scans must always sum to the same total: it either sees both
53    // halves of a transfer or neither.
54    let auditor = {
55        let db = Arc::clone(&db);
56        let stop = Arc::clone(&stop);
57        let reads = Arc::clone(&reads);
58        thread::spawn(move || -> Result<()> {
59            while !stop.load(Ordering::Relaxed) {
60                let mut tx = db.begin();
61                let sum: i64 = tx.scan::<Account>()?.iter().map(|a| a.balance).sum();
62                assert_eq!(
63                    sum,
64                    ACCOUNTS as i64 * STARTING_BALANCE,
65                    "a reader observed a partially applied transfer"
66                );
67                reads.fetch_add(1, Ordering::Relaxed);
68            }
69            Ok(())
70        })
71    };
72
73    // ---- writers ----------------------------------------------------------
74    let start = Instant::now();
75    let mut writers = Vec::new();
76    for t in 0..TRANSFER_THREADS {
77        let db = Arc::clone(&db);
78        let retries = Arc::clone(&retries);
79        writers.push(thread::spawn(move || -> Result<()> {
80            // A cheap per-thread PRNG; no dependency needed for this.
81            let mut seed = 0x9e37_79b9_7f4a_7c15u64 ^ (t as u64 + 1);
82            let mut next = move || {
83                seed ^= seed << 13;
84                seed ^= seed >> 7;
85                seed ^= seed << 17;
86                seed
87            };
88
89            for _ in 0..TRANSFERS_PER_THREAD {
90                let from = next() % ACCOUNTS;
91                let to = next() % ACCOUNTS;
92                if from == to {
93                    continue;
94                }
95
96                let mut attempts = 0u64;
97                // Serializable, because the transfer's *write* depends on a
98                // balance it *read* — the write-skew shape. Snapshot isolation
99                // would let two concurrent transfers both pass the check.
100                db.transaction_with::<Serializable, _, _>(|tx| {
101                    attempts += 1;
102                    let balance = tx.get::<Account>(&from)?.map(|a| a.balance).unwrap_or(0);
103                    if balance < 10 {
104                        return Ok(());
105                    }
106                    tx.update::<Account>(&from, |a| a.balance -= 10)?;
107                    tx.update::<Account>(&to, |a| a.balance += 10)?;
108                    Ok(())
109                })?;
110                retries.fetch_add(attempts - 1, Ordering::Relaxed);
111            }
112            Ok(())
113        }));
114    }
115
116    for w in writers {
117        w.join().expect("writer panicked")?;
118    }
119    let elapsed = start.elapsed();
120
121    stop.store(true, Ordering::Relaxed);
122    auditor.join().expect("auditor panicked")?;
123
124    // ---- results ----------------------------------------------------------
125    let total_after = total(&db)?;
126    let committed = (TRANSFER_THREADS * TRANSFERS_PER_THREAD) as u64;
127
128    println!("final total:    {total_after}");
129    assert_eq!(total_before, total_after, "money was created or destroyed");
130
131    println!("\n{committed} transfers in {elapsed:.2?}");
132    println!(
133        "  {:.0} transfers/sec",
134        committed as f64 / elapsed.as_secs_f64()
135    );
136    println!(
137        "  {} concurrent audit scans completed, none of them blocked",
138        reads.load(Ordering::Relaxed)
139    );
140    println!(
141        "  {} retries ({:.1} per transfer)",
142        retries.load(Ordering::Relaxed),
143        retries.load(Ordering::Relaxed) as f64 / committed as f64
144    );
145
146    // The GC watermark should be advancing. If `active_transactions` climbs and
147    // the watermark stalls, some transaction is being leaked — see engine::gc.
148    let stats = db.stats();
149    println!(
150        "\ngc watermark {:?}, {} transactions still live",
151        stats.watermark, stats.active_transactions
152    );
153
154    println!("\n✓ no torn reads, no lost updates, conservation held");
155
156    // Slow readers are what pin the GC watermark. Left here as the shape of the
157    // thing to watch for, not as something this example demonstrates going wrong.
158    thread::sleep(Duration::from_millis(1));
159    Ok(())
160}
More examples
Hide additional examples
examples/game.rs (line 651)
141fn main() -> Result<()> {
142    let db = Arc::new(Database::open(Config::in_memory())?);
143    db.register::<Hero>()?;
144    db.register::<Item>()?;
145    db.register::<PartyMember>()?;
146
147    let (ada, bram, cleo) = (1u64, 2u64, 3u64);
148
149    // -----------------------------------------------------------------------
150    act(1, "The world is created");
151
152    db.transaction(|tx| {
153        tx.insert(Hero {
154            id: ada,
155            name: "Ada".into(),
156            gold: 120,
157            strength: 100,
158        })?;
159        tx.insert(Hero {
160            id: bram,
161            name: "Bram".into(),
162            gold: 80,
163            strength: 100,
164        })?;
165        tx.insert(Hero {
166            id: cleo,
167            name: "Cleo".into(),
168            gold: 200,
169            strength: 60,
170        })?;
171
172        let mut item = |id, owner, name: &str, weight, value, kind| {
173            tx.insert(Item {
174                id,
175                owner,
176                name: name.into(),
177                weight,
178                value,
179                kind,
180                flavour: HashMap::new(),
181            })
182        };
183        item(10, ada, "Rusty Sword", 30, 15, Kind::Weapon)?;
184        item(11, ada, "Health Potion", 5, 20, Kind::Potion)?;
185        item(12, bram, "Oak Shield", 40, 35, Kind::Weapon)?;
186        item(13, cleo, "Lucky Charm", 2, 90, Kind::Trinket)?;
187        // Loot lying in the dungeon, owned by nobody.
188        item(20, GROUND, "Flaming Greatsword", 55, 500, Kind::Weapon)?;
189        item(21, GROUND, "Elven Cloak", 20, 240, Kind::Trinket)?;
190        item(22, GROUND, "Iron Helm", 35, 60, Kind::Weapon)?;
191
192        // Arbitrary per-item state, in a database row, with no schema to
193        // declare and no serialisation format to agree on.
194        tx.update::<Item>(&20, |i| {
195            i.flavour.insert("enchantment".into(), "flame".into());
196            i.flavour.insert("forged_by".into(), "Durin".into());
197        })?;
198        Ok(())
199    })?;
200
201    // A whole transaction that fails partway leaves nothing behind. The unique
202    // index on `name` refuses the second Ada, and the gold change goes with it.
203    let before = gold_of(&mut db.begin(), ada)?;
204    let doomed = db.transaction(|tx| {
205        tx.update::<Hero>(&ada, |h| h.gold += 10_000)?;
206        tx.insert(Hero {
207            id: 99,
208            name: "Ada".into(),
209            gold: 0,
210            strength: 10,
211        })
212    });
213    println!("  a duplicate hero name: {}", doomed.unwrap_err());
214    println!(
215        "  Ada's gold is still {} — the whole transaction rolled back, not just the insert",
216        gold_of(&mut db.begin(), ada)?
217    );
218    assert_eq!(gold_of(&mut db.begin(), ada)?, before);
219
220    let mut tx = db.begin();
221    for hero in [ada, bram, cleo] {
222        let name = name_of(&mut tx, hero)?;
223        let carried = carried(&mut tx, hero)?;
224        let items = inventory(&mut tx, hero)?;
225        let names: Vec<_> = items.iter().map(|(_, n, _)| n.as_str()).collect();
226        println!("  {name:<5} carries {carried:>3} — {}", names.join(", "));
227    }
228    drop(tx);
229
230    // -----------------------------------------------------------------------
231    act(2, "Two heroes reach for the same greatsword");
232
233    // Both begin, both see the sword unclaimed. The first to write takes it;
234    // the second is told immediately rather than being made to wait.
235    let mut ada_grabs = db.begin_with::<Snapshot>();
236    let mut bram_grabs = db.begin_with::<Snapshot>();
237
238    ada_grabs.update::<Item>(&20, |i| i.owner = ada)?;
239
240    match bram_grabs.update::<Item>(&20, |i| i.owner = bram) {
241        Err(e @ Error::WriteConflict { .. }) => {
242            println!("  Bram: {e}");
243            println!(
244                "  ...retriable: {} — he can try for something else",
245                e.is_retriable()
246            );
247        }
248        other => unreachable!("expected a conflict, got {other:?}"),
249    }
250
251    // A failed write does not end the transaction. Bram takes the cloak instead.
252    bram_grabs.update::<Item>(&21, |i| i.owner = bram)?;
253    ada_grabs.commit()?;
254    bram_grabs.commit()?;
255    println!("  Ada takes the greatsword, Bram takes the cloak. Nobody blocked.");
256
257    // -----------------------------------------------------------------------
258    act(3, "Cleo overloads herself — write skew, and the fix");
259
260    // Cleo can carry 60 and is carrying 2. Either 35kg helm fits; both do not.
261    // Each transaction reads her inventory, checks the total, and picks up a
262    // *different* item — so the two writes never touch the same record, and
263    // first-updater-wins has nothing to catch.
264    db.transaction(|tx| {
265        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
266        tx.insert(Item {
267            id: 23,
268            owner: GROUND,
269            name: "Steel Helm".into(),
270            weight: 35,
271            value: 70,
272            kind: Kind::Weapon,
273            flavour: HashMap::new(),
274        })
275    })?;
276
277    let pick_up = |item: u64| {
278        move |tx: &mut mvcc::Transaction<'_, Serializable>| -> Result<bool> {
279            let hero = tx.get::<Hero>(&cleo)?.expect("Cleo exists");
280            let strength = hero.strength;
281            let load = carried(tx, cleo)?;
282            let weight = tx.get::<Item>(&item)?.map(|i| i.weight).unwrap_or(0);
283            if load + weight > strength {
284                return Ok(false);
285            }
286            tx.update::<Item>(&item, |i| i.owner = cleo)?;
287            Ok(true)
288        }
289    };
290
291    // Under Snapshot, both checks pass against a stale inventory.
292    {
293        let mut t1 = db.begin_with::<Snapshot>();
294        let mut t2 = db.begin_with::<Snapshot>();
295        let (load1, load2) = (carried(&mut t1, cleo)?, carried(&mut t2, cleo)?);
296        println!(
297            "  Snapshot:     both transactions see {load1}/{load2} carried, both think a 35kg helm fits"
298        );
299        t1.update::<Item>(&22, |i| i.owner = cleo)?;
300        t2.update::<Item>(&23, |i| i.owner = cleo)?;
301        t1.commit()?;
302        t2.commit()?;
303        let over = carried(&mut db.begin(), cleo)?;
304        println!(
305            "  ✗ Cleo now carries {over} of a possible 60. Two legal transactions, one broken rule."
306        );
307    }
308
309    // Put it back and try again at Serializable.
310    db.transaction(|tx| {
311        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
312        tx.update::<Item>(&23, |i| i.owner = GROUND).map(|_| ())
313    })?;
314
315    {
316        let mut t1 = db.begin_with::<Serializable>();
317        let mut t2 = db.begin_with::<Serializable>();
318        // Both read the inventory, as before.
319        let _ = carried(&mut t1, cleo)?;
320        let _ = carried(&mut t2, cleo)?;
321        t1.update::<Item>(&22, |i| i.owner = cleo)?;
322        t2.update::<Item>(&23, |i| i.owner = cleo)?;
323        t1.commit()?;
324        match t2.commit() {
325            Err(e @ Error::SerializationFailure) => println!("  Serializable: second pickup {e}"),
326            other => unreachable!("expected a serialization failure, got {other:?}"),
327        }
328        println!(
329            "  ✓ Cleo carries {} — the second pickup read an inventory the first changed.",
330            carried(&mut db.begin(), cleo)?
331        );
332    }
333
334    // In real code you would not hand-roll that. `transaction_with` retries,
335    // and the retry re-reads the inventory and correctly declines.
336    let took_it = db.transaction_with::<Serializable, _, _>(pick_up(23))?;
337    println!("  ...and on retry the second helm is refused on its merits: picked_up = {took_it}");
338
339    // -----------------------------------------------------------------------
340    act(4, "The party fills up — a phantom, not a conflict");
341
342    db.transaction(|tx| {
343        tx.insert(PartyMember {
344            hero: ada,
345            party: PARTY,
346            role: "Vanguard".into(),
347        })?;
348        tx.insert(PartyMember {
349            hero: bram,
350            party: PARTY,
351            role: "Shield".into(),
352        })?;
353        tx.insert(PartyMember {
354            hero: cleo,
355            party: PARTY,
356            role: "Scout".into(),
357        })
358    })?;
359
360    // Two newcomers apply at once, with three of four slots taken. Neither
361    // writes a row the other wrote — they insert *different* rows — so there is
362    // no write conflict to detect. What they collide on is a row that did not
363    // exist when either of them counted.
364    let (dara, finn) = (4u64, 5u64);
365    db.transaction(|tx| {
366        tx.insert(Hero {
367            id: dara,
368            name: "Dara".into(),
369            gold: 40,
370            strength: 80,
371        })?;
372        tx.insert(Hero {
373            id: finn,
374            name: "Finn".into(),
375            gold: 40,
376            strength: 80,
377        })
378    })?;
379
380    let mut d = db.begin_with::<Serializable>();
381    let mut f = db.begin_with::<Serializable>();
382    println!(
383        "  Dara counts {} members, Finn counts {} — both see a free slot",
384        party_size(&mut d)?,
385        party_size(&mut f)?
386    );
387    d.insert(PartyMember {
388        hero: dara,
389        party: PARTY,
390        role: "Healer".into(),
391    })?;
392    f.insert(PartyMember {
393        hero: finn,
394        party: PARTY,
395        role: "Healer".into(),
396    })?;
397    d.commit()?;
398    match f.commit() {
399        Err(e @ Error::SerializationFailure) => println!("  Finn: {e}"),
400        other => unreachable!("expected a serialization failure, got {other:?}"),
401    }
402    let mut tx = db.begin();
403    let mut roster: Vec<_> = tx
404        .scan_where::<PartyMember, _>(|m| m.party == PARTY)?
405        .iter()
406        .map(|m| (m.hero, m.role.clone()))
407        .collect();
408    drop(tx);
409    roster.sort();
410    let size = roster.len();
411    let mut tx = db.begin();
412    let listed: Vec<String> = roster
413        .iter()
414        .map(|(h, role)| Ok(format!("{} the {role}", name_of(&mut tx, *h)?)))
415        .collect::<Result<_>>()?;
416    drop(tx);
417    println!("  party: {}", listed.join(", "));
418    println!("  ✓ {size}/{PARTY_LIMIT} filled. Finn's insert became a phantom in Dara's count.");
419    println!("    (`scan_where` hands the engine the predicate, so it can re-check it at commit.)");
420    assert!(size <= PARTY_LIMIT);
421
422    // -----------------------------------------------------------------------
423    act(5, "A trade — two records, one atomic step");
424
425    let before = total_gold(&db)?;
426
427    let trade = |seller: u64, buyer: u64, item: u64, price: i64| {
428        let db = Arc::clone(&db);
429        move || -> Result<bool> {
430            db.transaction_with::<Serializable, _, _>(|tx| {
431                let buyer_gold = gold_of(tx, buyer)?;
432                if buyer_gold < price {
433                    return Ok(false);
434                }
435                let owned_by_seller = tx
436                    .get::<Item>(&item)?
437                    .map(|i| i.owner == seller)
438                    .unwrap_or(false);
439                if !owned_by_seller {
440                    return Ok(false);
441                }
442                tx.update::<Hero>(&buyer, |h| h.gold -= price)?;
443                tx.update::<Hero>(&seller, |h| h.gold += price)?;
444                tx.update::<Item>(&item, |i| i.owner = buyer)?;
445                Ok(true)
446            })
447        }
448    };
449
450    let sold = trade(bram, cleo, 21, 150)()?;
451    println!("  Bram sells the Elven Cloak to Cleo for 150g: {sold}");
452    let mut tx = db.begin();
453    let cloak_owner = tx.get::<Item>(&21)?.expect("cloak").owner;
454    println!(
455        "  Bram {}g, Cleo {}g, cloak owner is {}",
456        gold_of(&mut tx, bram)?,
457        gold_of(&mut tx, cleo)?,
458        name_of(&mut tx, cloak_owner)?
459    );
460    drop(tx);
461    assert_eq!(total_gold(&db)?, before, "gold must be conserved");
462    println!("  ✓ total gold unchanged at {before} — no step of that was separately visible");
463
464    // -----------------------------------------------------------------------
465    act(6, "The chronicler reads while the world moves");
466
467    // A long report scanning the whole world while combat rewrites it. It never
468    // blocks a writer, and its view never shifts underneath it.
469    let mut chronicler = db.begin_with::<Snapshot>();
470    let opening = total_gold(&db)?;
471    let seen_first = chronicler.scan::<Hero>()?.len();
472
473    for _ in 0..50 {
474        db.transaction(|tx| {
475            tx.update::<Hero>(&ada, |h| h.gold += 1)?;
476            tx.update::<Hero>(&bram, |h| h.gold -= 1)?;
477            Ok(())
478        })?;
479    }
480    db.transaction(|tx| {
481        tx.insert(Hero {
482            id: 6,
483            name: "Mira".into(),
484            gold: 0,
485            strength: 50,
486        })
487    })?;
488
489    let chron_gold: i64 = chronicler.scan::<Hero>()?.iter().map(|h| h.gold).sum();
490    let seen_after = chronicler.scan::<Hero>()?.len();
491    let in_world = db.begin().scan::<Hero>()?.len();
492    println!("  chronicler saw {seen_first} heroes at the start and still sees {seen_after};");
493    println!("  the world now holds {in_world}, after 50 commits and one new arrival");
494    println!(
495        "  chronicler totals {chron_gold}g; the world now totals {}g",
496        total_gold(&db)?
497    );
498    assert_eq!(chron_gold, opening, "a snapshot must not move");
499    chronicler.commit()?;
500    println!("  ✓ a reader that ran across 51 commits saw exactly one consistent world");
501
502    // -----------------------------------------------------------------------
503    act(7, "The raid — many adventurers at once");
504
505    let stop = Arc::new(AtomicBool::new(false));
506    let trades = Arc::new(AtomicU64::new(0));
507    let retries = Arc::new(AtomicU64::new(0));
508    let audits = Arc::new(AtomicU64::new(0));
509    let opening = total_gold(&db)?;
510    let heroes = [ada, bram, cleo, dara, finn];
511
512    // An auditor that must never catch a trade half-applied.
513    let auditor = {
514        let (db, stop, audits) = (Arc::clone(&db), Arc::clone(&stop), Arc::clone(&audits));
515        thread::spawn(move || -> Result<()> {
516            while !stop.load(Ordering::Relaxed) {
517                let mut tx = db.begin();
518                let total: i64 = tx.scan::<Hero>()?.iter().map(|h| h.gold).sum();
519                assert_eq!(total, opening, "auditor saw a half-finished trade");
520                audits.fetch_add(1, Ordering::Relaxed);
521            }
522            Ok(())
523        })
524    };
525
526    let start = Instant::now();
527    let raiders: Vec<_> = (0..4)
528        .map(|t| {
529            let (db, trades, retries) =
530                (Arc::clone(&db), Arc::clone(&trades), Arc::clone(&retries));
531            thread::spawn(move || -> Result<()> {
532                let mut seed = 0x9e37_79b9_7f4a_7c15u64 ^ (t + 1);
533                for _ in 0..400 {
534                    seed ^= seed << 13;
535                    seed ^= seed >> 7;
536                    seed ^= seed << 17;
537                    let from = heroes[(seed % heroes.len() as u64) as usize];
538                    let to = heroes[((seed >> 8) % heroes.len() as u64) as usize];
539                    if from == to {
540                        continue;
541                    }
542
543                    let mut attempts = 0u64;
544                    // Serializable, because the payment depends on a balance we
545                    // read — the same shape as Cleo's carry limit in Act 3.
546                    db.transaction_with::<Serializable, _, _>(|tx| {
547                        attempts += 1;
548                        let purse = gold_of(tx, from)?;
549                        if purse < 5 {
550                            return Ok(());
551                        }
552                        tx.update::<Hero>(&from, |h| h.gold -= 5)?;
553                        tx.update::<Hero>(&to, |h| h.gold += 5)?;
554                        Ok(())
555                    })?;
556                    trades.fetch_add(1, Ordering::Relaxed);
557                    retries.fetch_add(attempts - 1, Ordering::Relaxed);
558                }
559                Ok(())
560            })
561        })
562        .collect();
563
564    for r in raiders {
565        r.join().expect("raider panicked")?;
566    }
567    let elapsed = start.elapsed();
568    stop.store(true, Ordering::Relaxed);
569    auditor.join().expect("auditor panicked")?;
570
571    let done = trades.load(Ordering::Relaxed);
572    println!("  {done} trades across 4 threads in {elapsed:.2?}");
573    println!(
574        "  {} audit sweeps completed alongside them, none blocked, none torn",
575        audits.load(Ordering::Relaxed)
576    );
577    println!(
578        "  {} retries ({:.2} per trade) — contention on 5 heroes, handled by the engine",
579        retries.load(Ordering::Relaxed),
580        retries.load(Ordering::Relaxed) as f64 / done.max(1) as f64
581    );
582    assert_eq!(total_gold(&db)?, opening, "gold was created or destroyed");
583    println!("  ✓ total gold still {opening}");
584
585    // -----------------------------------------------------------------------
586    act(8, "Closing the ledger");
587
588    let mut tx = db.begin();
589    let mut roster: Vec<_> = tx
590        .scan::<Hero>()?
591        .iter()
592        .map(|h| (h.id, h.name.clone(), h.gold))
593        .collect();
594    roster.sort_by_key(|(id, _, _)| *id);
595    drop(tx);
596
597    println!(
598        "  {:<6} {:>6} {:>5} {:>7}  inventory",
599        "hero", "gold", "load", "worth"
600    );
601    let mut tx = db.begin();
602    for (id, name, gold) in &roster {
603        let held = tx.scan_index(Item::OWNER, *id..=*id)?;
604        let load: i32 = held.iter().map(|i| i.weight).sum();
605        let worth: i64 = held.iter().map(|i| i.value).sum();
606        let names: Vec<String> = held
607            .iter()
608            .map(|i| match i.kind {
609                Kind::Weapon => format!("[wpn] {}", i.name),
610                Kind::Potion => format!("[pot] {}", i.name),
611                Kind::Trinket => format!("[trk] {}", i.name),
612            })
613            .collect();
614        let names = if names.is_empty() {
615            "—".to_string()
616        } else {
617            names.join(", ")
618        };
619        println!("  {name:<6} {gold:>5}g {load:>4}kg {worth:>6}g  {names}");
620    }
621
622    // Potions anywhere in the world, found by predicate rather than by key —
623    // and `kind` is an ordinary Rust enum the engine knows nothing about.
624    let potions = tx.scan_where::<Item, _>(|i| i.kind == Kind::Potion)?.len();
625    let sword = tx.get::<Item>(&20)?.expect("greatsword");
626    let enchantment = sword
627        .flavour
628        .get("enchantment")
629        .cloned()
630        .unwrap_or_default();
631    let forged_by = sword.flavour.get("forged_by").cloned().unwrap_or_default();
632    drop(tx);
633    // The rule Act 3 was about, checked rather than asserted in prose.
634    let mut tx = db.begin();
635    for (id, name, _) in &roster {
636        let strength = tx.get::<Hero>(id)?.map(|h| h.strength).unwrap_or(0);
637        let load = carried(&mut tx, *id)?;
638        assert!(
639            load <= strength,
640            "{name} is over their carry limit: {load} > {strength}"
641        );
642    }
643    drop(tx);
644
645    println!("\n  potions in the world: {potions}");
646    println!("  the greatsword is {enchantment}-enchanted, forged by {forged_by}");
647
648    // Nothing is reclaimed while a transaction that could still reach it is
649    // alive. This is the number to watch in a long-lived process: a forgotten
650    // transaction pins it and version chains grow without limit.
651    let stats = db.stats();
652    println!(
653        "\n  gc watermark {:?}, {} transactions still live",
654        stats.watermark, stats.active_transactions
655    );
656    println!("  (a watermark that stops moving while writes continue is the leak to look for)");
657
658    Ok(())
659}

pub fn compact(&mut self) -> usize

Reclaim the memory of deleted records, returning how many were freed.

Ordinary reclamation frees a record’s versions but not its slot: about 180 bytes per key stay behind, because the map that resolves a key to a slot is append-only, and that is what lets a lookup take no locks and write nothing. This is the operation that gives those bytes back.

It takes &mut self, and that is the entire safety argument. A Transaction borrows the database, so an exclusive borrow is a compile-time proof that none exist — which is what makes it sound to free a slot some transaction might otherwise have already resolved.

This is deliberately a separate call rather than something the engine does for you, and the reason is speed. Reclaiming slots as it went would mean either revalidating every write against the key map or making transactions announce themselves to a lock — putting synchronisation back onto paths that currently have none. That append-only property is what lets a lookup take no locks and write nothing to shared memory, and it is worth 1.9x on uniform point reads and 9.8x on contended ones at four threads — the shipped before-and-after, not the throwaway lock-deletion experiment crate::engine::slotmap tabulates. Confining reclamation to a moment when nothing else is running keeps all of it, so call it during a quiet one.

let mut db = Database::open(Config::in_memory())?;
db.register::<Session>()?;

db.transaction(|tx| tx.insert(Session { id: 1, token: 42 }))?;
db.transaction(|tx| tx.delete::<Session>(&1))?;

// No transaction may be alive here — the borrow checker enforces it.
let freed = db.compact();

Only keys whose records are gone are reclaimed — deleted, and their tombstone already collected. A record that merely has not been touched in a while is untouched. If a delete is very recent its tombstone may still be live, in which case that key is reclaimed by a later call rather than this one.

It also drops secondary index entries that can no longer resolve. Those were never unsafe — an index holds primary keys, not slot pointers — but they are dead weight in memory and in every scan that walks them.

pub fn register<T: Versioned>(&self) -> Result<()>

Register a type. Assigns its [TableId] and builds its indexes.

Must happen before any transaction touches T. Registering twice is a no-op rather than an error, so a library that registers its own types defensively does not conflict with an application that did the same.

Examples found in repository?
examples/basic.rs (line 26)
24fn main() -> Result<()> {
25    let db = Database::open(Config::in_memory())?;
26    db.register::<Account>()?;
27
28    // ---- insert -----------------------------------------------------------
29    // `transaction` runs the closure, commits it, and retries it if it hits a
30    // retriable conflict. Snapshot isolation by default.
31    db.transaction(|tx| {
32        tx.insert(Account {
33            id: 1,
34            owner: "ada".into(),
35            branch: 10,
36            balance: 500,
37        })?;
38        tx.insert(Account {
39            id: 2,
40            owner: "bob".into(),
41            branch: 10,
42            balance: 250,
43        })?;
44        tx.insert(Account {
45            id: 3,
46            owner: "cleo".into(),
47            branch: 20,
48            balance: 900,
49        })?;
50        Ok(())
51    })?;
52
53    // ---- read -------------------------------------------------------------
54    let mut tx = db.begin();
55    let ada = tx.get::<Account>(&1)?.expect("just inserted");
56    println!("ada: branch {}, balance {}", ada.branch, ada.balance);
57
58    // A read-only transaction has nothing to commit; dropping it rolls back,
59    // which for a reader means simply releasing its snapshot.
60    drop(tx);
61
62    // ---- update -----------------------------------------------------------
63    db.transaction(|tx| {
64        tx.update::<Account>(&1, |a| a.balance -= 100)?;
65        tx.update::<Account>(&2, |a| a.balance += 100)?;
66        Ok(())
67    })?;
68
69    // ---- scan by primary key ----------------------------------------------
70    let mut tx = db.begin();
71    println!("\nall accounts:");
72    for account in tx.scan::<Account>()? {
73        println!(
74            "  {:>4}  {:<6} branch {}  {:>5}",
75            account.id, account.owner, account.branch, account.balance
76        );
77    }
78
79    // ---- scan by secondary index ------------------------------------------
80    println!("\nbranch 10:");
81    for account in tx.scan_index(Account::BRANCH, 10u32..=10)? {
82        println!("  {} ({})", account.owner, account.balance);
83    }
84    drop(tx);
85
86    // ---- constraint violations --------------------------------------------
87    let mut tx = db.begin();
88    let duplicate = tx.insert(Account {
89        id: 99,
90        owner: "ada".into(),
91        branch: 30,
92        balance: 0,
93    });
94    println!("\nreusing owner 'ada': {}", duplicate.unwrap_err());
95    drop(tx);
96
97    // ---- delete -----------------------------------------------------------
98    db.transaction(|tx| {
99        let existed = tx.delete::<Account>(&3)?;
100        println!("deleted cleo: {existed}");
101        Ok(())
102    })?;
103
104    let mut tx = db.begin();
105    println!(
106        "cleo now: {:?}",
107        tx.get::<Account>(&3)?.map(|r| r.to_owned())
108    );
109
110    Ok(())
111}
More examples
Hide additional examples
examples/concurrent.rs (line 31)
29fn main() -> Result<()> {
30    let db = Arc::new(Database::open(Config::in_memory())?);
31    db.register::<Account>()?;
32
33    db.transaction(|tx| {
34        for id in 0..ACCOUNTS {
35            tx.insert(Account {
36                id,
37                balance: STARTING_BALANCE,
38            })?;
39        }
40        Ok(())
41    })?;
42
43    let total_before = total(&db)?;
44    println!("starting total: {total_before}");
45
46    let stop = Arc::new(AtomicBool::new(false));
47    let reads = Arc::new(AtomicU64::new(0));
48    let retries = Arc::new(AtomicU64::new(0));
49
50    // ---- a reader that must never observe a torn transfer -----------------
51    // Money moves between accounts constantly. Under snapshot isolation this
52    // thread's scans must always sum to the same total: it either sees both
53    // halves of a transfer or neither.
54    let auditor = {
55        let db = Arc::clone(&db);
56        let stop = Arc::clone(&stop);
57        let reads = Arc::clone(&reads);
58        thread::spawn(move || -> Result<()> {
59            while !stop.load(Ordering::Relaxed) {
60                let mut tx = db.begin();
61                let sum: i64 = tx.scan::<Account>()?.iter().map(|a| a.balance).sum();
62                assert_eq!(
63                    sum,
64                    ACCOUNTS as i64 * STARTING_BALANCE,
65                    "a reader observed a partially applied transfer"
66                );
67                reads.fetch_add(1, Ordering::Relaxed);
68            }
69            Ok(())
70        })
71    };
72
73    // ---- writers ----------------------------------------------------------
74    let start = Instant::now();
75    let mut writers = Vec::new();
76    for t in 0..TRANSFER_THREADS {
77        let db = Arc::clone(&db);
78        let retries = Arc::clone(&retries);
79        writers.push(thread::spawn(move || -> Result<()> {
80            // A cheap per-thread PRNG; no dependency needed for this.
81            let mut seed = 0x9e37_79b9_7f4a_7c15u64 ^ (t as u64 + 1);
82            let mut next = move || {
83                seed ^= seed << 13;
84                seed ^= seed >> 7;
85                seed ^= seed << 17;
86                seed
87            };
88
89            for _ in 0..TRANSFERS_PER_THREAD {
90                let from = next() % ACCOUNTS;
91                let to = next() % ACCOUNTS;
92                if from == to {
93                    continue;
94                }
95
96                let mut attempts = 0u64;
97                // Serializable, because the transfer's *write* depends on a
98                // balance it *read* — the write-skew shape. Snapshot isolation
99                // would let two concurrent transfers both pass the check.
100                db.transaction_with::<Serializable, _, _>(|tx| {
101                    attempts += 1;
102                    let balance = tx.get::<Account>(&from)?.map(|a| a.balance).unwrap_or(0);
103                    if balance < 10 {
104                        return Ok(());
105                    }
106                    tx.update::<Account>(&from, |a| a.balance -= 10)?;
107                    tx.update::<Account>(&to, |a| a.balance += 10)?;
108                    Ok(())
109                })?;
110                retries.fetch_add(attempts - 1, Ordering::Relaxed);
111            }
112            Ok(())
113        }));
114    }
115
116    for w in writers {
117        w.join().expect("writer panicked")?;
118    }
119    let elapsed = start.elapsed();
120
121    stop.store(true, Ordering::Relaxed);
122    auditor.join().expect("auditor panicked")?;
123
124    // ---- results ----------------------------------------------------------
125    let total_after = total(&db)?;
126    let committed = (TRANSFER_THREADS * TRANSFERS_PER_THREAD) as u64;
127
128    println!("final total:    {total_after}");
129    assert_eq!(total_before, total_after, "money was created or destroyed");
130
131    println!("\n{committed} transfers in {elapsed:.2?}");
132    println!(
133        "  {:.0} transfers/sec",
134        committed as f64 / elapsed.as_secs_f64()
135    );
136    println!(
137        "  {} concurrent audit scans completed, none of them blocked",
138        reads.load(Ordering::Relaxed)
139    );
140    println!(
141        "  {} retries ({:.1} per transfer)",
142        retries.load(Ordering::Relaxed),
143        retries.load(Ordering::Relaxed) as f64 / committed as f64
144    );
145
146    // The GC watermark should be advancing. If `active_transactions` climbs and
147    // the watermark stalls, some transaction is being leaked — see engine::gc.
148    let stats = db.stats();
149    println!(
150        "\ngc watermark {:?}, {} transactions still live",
151        stats.watermark, stats.active_transactions
152    );
153
154    println!("\n✓ no torn reads, no lost updates, conservation held");
155
156    // Slow readers are what pin the GC watermark. Left here as the shape of the
157    // thing to watch for, not as something this example demonstrates going wrong.
158    thread::sleep(Duration::from_millis(1));
159    Ok(())
160}
examples/isolation.rs (line 34)
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}
examples/game.rs (line 143)
141fn main() -> Result<()> {
142    let db = Arc::new(Database::open(Config::in_memory())?);
143    db.register::<Hero>()?;
144    db.register::<Item>()?;
145    db.register::<PartyMember>()?;
146
147    let (ada, bram, cleo) = (1u64, 2u64, 3u64);
148
149    // -----------------------------------------------------------------------
150    act(1, "The world is created");
151
152    db.transaction(|tx| {
153        tx.insert(Hero {
154            id: ada,
155            name: "Ada".into(),
156            gold: 120,
157            strength: 100,
158        })?;
159        tx.insert(Hero {
160            id: bram,
161            name: "Bram".into(),
162            gold: 80,
163            strength: 100,
164        })?;
165        tx.insert(Hero {
166            id: cleo,
167            name: "Cleo".into(),
168            gold: 200,
169            strength: 60,
170        })?;
171
172        let mut item = |id, owner, name: &str, weight, value, kind| {
173            tx.insert(Item {
174                id,
175                owner,
176                name: name.into(),
177                weight,
178                value,
179                kind,
180                flavour: HashMap::new(),
181            })
182        };
183        item(10, ada, "Rusty Sword", 30, 15, Kind::Weapon)?;
184        item(11, ada, "Health Potion", 5, 20, Kind::Potion)?;
185        item(12, bram, "Oak Shield", 40, 35, Kind::Weapon)?;
186        item(13, cleo, "Lucky Charm", 2, 90, Kind::Trinket)?;
187        // Loot lying in the dungeon, owned by nobody.
188        item(20, GROUND, "Flaming Greatsword", 55, 500, Kind::Weapon)?;
189        item(21, GROUND, "Elven Cloak", 20, 240, Kind::Trinket)?;
190        item(22, GROUND, "Iron Helm", 35, 60, Kind::Weapon)?;
191
192        // Arbitrary per-item state, in a database row, with no schema to
193        // declare and no serialisation format to agree on.
194        tx.update::<Item>(&20, |i| {
195            i.flavour.insert("enchantment".into(), "flame".into());
196            i.flavour.insert("forged_by".into(), "Durin".into());
197        })?;
198        Ok(())
199    })?;
200
201    // A whole transaction that fails partway leaves nothing behind. The unique
202    // index on `name` refuses the second Ada, and the gold change goes with it.
203    let before = gold_of(&mut db.begin(), ada)?;
204    let doomed = db.transaction(|tx| {
205        tx.update::<Hero>(&ada, |h| h.gold += 10_000)?;
206        tx.insert(Hero {
207            id: 99,
208            name: "Ada".into(),
209            gold: 0,
210            strength: 10,
211        })
212    });
213    println!("  a duplicate hero name: {}", doomed.unwrap_err());
214    println!(
215        "  Ada's gold is still {} — the whole transaction rolled back, not just the insert",
216        gold_of(&mut db.begin(), ada)?
217    );
218    assert_eq!(gold_of(&mut db.begin(), ada)?, before);
219
220    let mut tx = db.begin();
221    for hero in [ada, bram, cleo] {
222        let name = name_of(&mut tx, hero)?;
223        let carried = carried(&mut tx, hero)?;
224        let items = inventory(&mut tx, hero)?;
225        let names: Vec<_> = items.iter().map(|(_, n, _)| n.as_str()).collect();
226        println!("  {name:<5} carries {carried:>3} — {}", names.join(", "));
227    }
228    drop(tx);
229
230    // -----------------------------------------------------------------------
231    act(2, "Two heroes reach for the same greatsword");
232
233    // Both begin, both see the sword unclaimed. The first to write takes it;
234    // the second is told immediately rather than being made to wait.
235    let mut ada_grabs = db.begin_with::<Snapshot>();
236    let mut bram_grabs = db.begin_with::<Snapshot>();
237
238    ada_grabs.update::<Item>(&20, |i| i.owner = ada)?;
239
240    match bram_grabs.update::<Item>(&20, |i| i.owner = bram) {
241        Err(e @ Error::WriteConflict { .. }) => {
242            println!("  Bram: {e}");
243            println!(
244                "  ...retriable: {} — he can try for something else",
245                e.is_retriable()
246            );
247        }
248        other => unreachable!("expected a conflict, got {other:?}"),
249    }
250
251    // A failed write does not end the transaction. Bram takes the cloak instead.
252    bram_grabs.update::<Item>(&21, |i| i.owner = bram)?;
253    ada_grabs.commit()?;
254    bram_grabs.commit()?;
255    println!("  Ada takes the greatsword, Bram takes the cloak. Nobody blocked.");
256
257    // -----------------------------------------------------------------------
258    act(3, "Cleo overloads herself — write skew, and the fix");
259
260    // Cleo can carry 60 and is carrying 2. Either 35kg helm fits; both do not.
261    // Each transaction reads her inventory, checks the total, and picks up a
262    // *different* item — so the two writes never touch the same record, and
263    // first-updater-wins has nothing to catch.
264    db.transaction(|tx| {
265        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
266        tx.insert(Item {
267            id: 23,
268            owner: GROUND,
269            name: "Steel Helm".into(),
270            weight: 35,
271            value: 70,
272            kind: Kind::Weapon,
273            flavour: HashMap::new(),
274        })
275    })?;
276
277    let pick_up = |item: u64| {
278        move |tx: &mut mvcc::Transaction<'_, Serializable>| -> Result<bool> {
279            let hero = tx.get::<Hero>(&cleo)?.expect("Cleo exists");
280            let strength = hero.strength;
281            let load = carried(tx, cleo)?;
282            let weight = tx.get::<Item>(&item)?.map(|i| i.weight).unwrap_or(0);
283            if load + weight > strength {
284                return Ok(false);
285            }
286            tx.update::<Item>(&item, |i| i.owner = cleo)?;
287            Ok(true)
288        }
289    };
290
291    // Under Snapshot, both checks pass against a stale inventory.
292    {
293        let mut t1 = db.begin_with::<Snapshot>();
294        let mut t2 = db.begin_with::<Snapshot>();
295        let (load1, load2) = (carried(&mut t1, cleo)?, carried(&mut t2, cleo)?);
296        println!(
297            "  Snapshot:     both transactions see {load1}/{load2} carried, both think a 35kg helm fits"
298        );
299        t1.update::<Item>(&22, |i| i.owner = cleo)?;
300        t2.update::<Item>(&23, |i| i.owner = cleo)?;
301        t1.commit()?;
302        t2.commit()?;
303        let over = carried(&mut db.begin(), cleo)?;
304        println!(
305            "  ✗ Cleo now carries {over} of a possible 60. Two legal transactions, one broken rule."
306        );
307    }
308
309    // Put it back and try again at Serializable.
310    db.transaction(|tx| {
311        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
312        tx.update::<Item>(&23, |i| i.owner = GROUND).map(|_| ())
313    })?;
314
315    {
316        let mut t1 = db.begin_with::<Serializable>();
317        let mut t2 = db.begin_with::<Serializable>();
318        // Both read the inventory, as before.
319        let _ = carried(&mut t1, cleo)?;
320        let _ = carried(&mut t2, cleo)?;
321        t1.update::<Item>(&22, |i| i.owner = cleo)?;
322        t2.update::<Item>(&23, |i| i.owner = cleo)?;
323        t1.commit()?;
324        match t2.commit() {
325            Err(e @ Error::SerializationFailure) => println!("  Serializable: second pickup {e}"),
326            other => unreachable!("expected a serialization failure, got {other:?}"),
327        }
328        println!(
329            "  ✓ Cleo carries {} — the second pickup read an inventory the first changed.",
330            carried(&mut db.begin(), cleo)?
331        );
332    }
333
334    // In real code you would not hand-roll that. `transaction_with` retries,
335    // and the retry re-reads the inventory and correctly declines.
336    let took_it = db.transaction_with::<Serializable, _, _>(pick_up(23))?;
337    println!("  ...and on retry the second helm is refused on its merits: picked_up = {took_it}");
338
339    // -----------------------------------------------------------------------
340    act(4, "The party fills up — a phantom, not a conflict");
341
342    db.transaction(|tx| {
343        tx.insert(PartyMember {
344            hero: ada,
345            party: PARTY,
346            role: "Vanguard".into(),
347        })?;
348        tx.insert(PartyMember {
349            hero: bram,
350            party: PARTY,
351            role: "Shield".into(),
352        })?;
353        tx.insert(PartyMember {
354            hero: cleo,
355            party: PARTY,
356            role: "Scout".into(),
357        })
358    })?;
359
360    // Two newcomers apply at once, with three of four slots taken. Neither
361    // writes a row the other wrote — they insert *different* rows — so there is
362    // no write conflict to detect. What they collide on is a row that did not
363    // exist when either of them counted.
364    let (dara, finn) = (4u64, 5u64);
365    db.transaction(|tx| {
366        tx.insert(Hero {
367            id: dara,
368            name: "Dara".into(),
369            gold: 40,
370            strength: 80,
371        })?;
372        tx.insert(Hero {
373            id: finn,
374            name: "Finn".into(),
375            gold: 40,
376            strength: 80,
377        })
378    })?;
379
380    let mut d = db.begin_with::<Serializable>();
381    let mut f = db.begin_with::<Serializable>();
382    println!(
383        "  Dara counts {} members, Finn counts {} — both see a free slot",
384        party_size(&mut d)?,
385        party_size(&mut f)?
386    );
387    d.insert(PartyMember {
388        hero: dara,
389        party: PARTY,
390        role: "Healer".into(),
391    })?;
392    f.insert(PartyMember {
393        hero: finn,
394        party: PARTY,
395        role: "Healer".into(),
396    })?;
397    d.commit()?;
398    match f.commit() {
399        Err(e @ Error::SerializationFailure) => println!("  Finn: {e}"),
400        other => unreachable!("expected a serialization failure, got {other:?}"),
401    }
402    let mut tx = db.begin();
403    let mut roster: Vec<_> = tx
404        .scan_where::<PartyMember, _>(|m| m.party == PARTY)?
405        .iter()
406        .map(|m| (m.hero, m.role.clone()))
407        .collect();
408    drop(tx);
409    roster.sort();
410    let size = roster.len();
411    let mut tx = db.begin();
412    let listed: Vec<String> = roster
413        .iter()
414        .map(|(h, role)| Ok(format!("{} the {role}", name_of(&mut tx, *h)?)))
415        .collect::<Result<_>>()?;
416    drop(tx);
417    println!("  party: {}", listed.join(", "));
418    println!("  ✓ {size}/{PARTY_LIMIT} filled. Finn's insert became a phantom in Dara's count.");
419    println!("    (`scan_where` hands the engine the predicate, so it can re-check it at commit.)");
420    assert!(size <= PARTY_LIMIT);
421
422    // -----------------------------------------------------------------------
423    act(5, "A trade — two records, one atomic step");
424
425    let before = total_gold(&db)?;
426
427    let trade = |seller: u64, buyer: u64, item: u64, price: i64| {
428        let db = Arc::clone(&db);
429        move || -> Result<bool> {
430            db.transaction_with::<Serializable, _, _>(|tx| {
431                let buyer_gold = gold_of(tx, buyer)?;
432                if buyer_gold < price {
433                    return Ok(false);
434                }
435                let owned_by_seller = tx
436                    .get::<Item>(&item)?
437                    .map(|i| i.owner == seller)
438                    .unwrap_or(false);
439                if !owned_by_seller {
440                    return Ok(false);
441                }
442                tx.update::<Hero>(&buyer, |h| h.gold -= price)?;
443                tx.update::<Hero>(&seller, |h| h.gold += price)?;
444                tx.update::<Item>(&item, |i| i.owner = buyer)?;
445                Ok(true)
446            })
447        }
448    };
449
450    let sold = trade(bram, cleo, 21, 150)()?;
451    println!("  Bram sells the Elven Cloak to Cleo for 150g: {sold}");
452    let mut tx = db.begin();
453    let cloak_owner = tx.get::<Item>(&21)?.expect("cloak").owner;
454    println!(
455        "  Bram {}g, Cleo {}g, cloak owner is {}",
456        gold_of(&mut tx, bram)?,
457        gold_of(&mut tx, cleo)?,
458        name_of(&mut tx, cloak_owner)?
459    );
460    drop(tx);
461    assert_eq!(total_gold(&db)?, before, "gold must be conserved");
462    println!("  ✓ total gold unchanged at {before} — no step of that was separately visible");
463
464    // -----------------------------------------------------------------------
465    act(6, "The chronicler reads while the world moves");
466
467    // A long report scanning the whole world while combat rewrites it. It never
468    // blocks a writer, and its view never shifts underneath it.
469    let mut chronicler = db.begin_with::<Snapshot>();
470    let opening = total_gold(&db)?;
471    let seen_first = chronicler.scan::<Hero>()?.len();
472
473    for _ in 0..50 {
474        db.transaction(|tx| {
475            tx.update::<Hero>(&ada, |h| h.gold += 1)?;
476            tx.update::<Hero>(&bram, |h| h.gold -= 1)?;
477            Ok(())
478        })?;
479    }
480    db.transaction(|tx| {
481        tx.insert(Hero {
482            id: 6,
483            name: "Mira".into(),
484            gold: 0,
485            strength: 50,
486        })
487    })?;
488
489    let chron_gold: i64 = chronicler.scan::<Hero>()?.iter().map(|h| h.gold).sum();
490    let seen_after = chronicler.scan::<Hero>()?.len();
491    let in_world = db.begin().scan::<Hero>()?.len();
492    println!("  chronicler saw {seen_first} heroes at the start and still sees {seen_after};");
493    println!("  the world now holds {in_world}, after 50 commits and one new arrival");
494    println!(
495        "  chronicler totals {chron_gold}g; the world now totals {}g",
496        total_gold(&db)?
497    );
498    assert_eq!(chron_gold, opening, "a snapshot must not move");
499    chronicler.commit()?;
500    println!("  ✓ a reader that ran across 51 commits saw exactly one consistent world");
501
502    // -----------------------------------------------------------------------
503    act(7, "The raid — many adventurers at once");
504
505    let stop = Arc::new(AtomicBool::new(false));
506    let trades = Arc::new(AtomicU64::new(0));
507    let retries = Arc::new(AtomicU64::new(0));
508    let audits = Arc::new(AtomicU64::new(0));
509    let opening = total_gold(&db)?;
510    let heroes = [ada, bram, cleo, dara, finn];
511
512    // An auditor that must never catch a trade half-applied.
513    let auditor = {
514        let (db, stop, audits) = (Arc::clone(&db), Arc::clone(&stop), Arc::clone(&audits));
515        thread::spawn(move || -> Result<()> {
516            while !stop.load(Ordering::Relaxed) {
517                let mut tx = db.begin();
518                let total: i64 = tx.scan::<Hero>()?.iter().map(|h| h.gold).sum();
519                assert_eq!(total, opening, "auditor saw a half-finished trade");
520                audits.fetch_add(1, Ordering::Relaxed);
521            }
522            Ok(())
523        })
524    };
525
526    let start = Instant::now();
527    let raiders: Vec<_> = (0..4)
528        .map(|t| {
529            let (db, trades, retries) =
530                (Arc::clone(&db), Arc::clone(&trades), Arc::clone(&retries));
531            thread::spawn(move || -> Result<()> {
532                let mut seed = 0x9e37_79b9_7f4a_7c15u64 ^ (t + 1);
533                for _ in 0..400 {
534                    seed ^= seed << 13;
535                    seed ^= seed >> 7;
536                    seed ^= seed << 17;
537                    let from = heroes[(seed % heroes.len() as u64) as usize];
538                    let to = heroes[((seed >> 8) % heroes.len() as u64) as usize];
539                    if from == to {
540                        continue;
541                    }
542
543                    let mut attempts = 0u64;
544                    // Serializable, because the payment depends on a balance we
545                    // read — the same shape as Cleo's carry limit in Act 3.
546                    db.transaction_with::<Serializable, _, _>(|tx| {
547                        attempts += 1;
548                        let purse = gold_of(tx, from)?;
549                        if purse < 5 {
550                            return Ok(());
551                        }
552                        tx.update::<Hero>(&from, |h| h.gold -= 5)?;
553                        tx.update::<Hero>(&to, |h| h.gold += 5)?;
554                        Ok(())
555                    })?;
556                    trades.fetch_add(1, Ordering::Relaxed);
557                    retries.fetch_add(attempts - 1, Ordering::Relaxed);
558                }
559                Ok(())
560            })
561        })
562        .collect();
563
564    for r in raiders {
565        r.join().expect("raider panicked")?;
566    }
567    let elapsed = start.elapsed();
568    stop.store(true, Ordering::Relaxed);
569    auditor.join().expect("auditor panicked")?;
570
571    let done = trades.load(Ordering::Relaxed);
572    println!("  {done} trades across 4 threads in {elapsed:.2?}");
573    println!(
574        "  {} audit sweeps completed alongside them, none blocked, none torn",
575        audits.load(Ordering::Relaxed)
576    );
577    println!(
578        "  {} retries ({:.2} per trade) — contention on 5 heroes, handled by the engine",
579        retries.load(Ordering::Relaxed),
580        retries.load(Ordering::Relaxed) as f64 / done.max(1) as f64
581    );
582    assert_eq!(total_gold(&db)?, opening, "gold was created or destroyed");
583    println!("  ✓ total gold still {opening}");
584
585    // -----------------------------------------------------------------------
586    act(8, "Closing the ledger");
587
588    let mut tx = db.begin();
589    let mut roster: Vec<_> = tx
590        .scan::<Hero>()?
591        .iter()
592        .map(|h| (h.id, h.name.clone(), h.gold))
593        .collect();
594    roster.sort_by_key(|(id, _, _)| *id);
595    drop(tx);
596
597    println!(
598        "  {:<6} {:>6} {:>5} {:>7}  inventory",
599        "hero", "gold", "load", "worth"
600    );
601    let mut tx = db.begin();
602    for (id, name, gold) in &roster {
603        let held = tx.scan_index(Item::OWNER, *id..=*id)?;
604        let load: i32 = held.iter().map(|i| i.weight).sum();
605        let worth: i64 = held.iter().map(|i| i.value).sum();
606        let names: Vec<String> = held
607            .iter()
608            .map(|i| match i.kind {
609                Kind::Weapon => format!("[wpn] {}", i.name),
610                Kind::Potion => format!("[pot] {}", i.name),
611                Kind::Trinket => format!("[trk] {}", i.name),
612            })
613            .collect();
614        let names = if names.is_empty() {
615            "—".to_string()
616        } else {
617            names.join(", ")
618        };
619        println!("  {name:<6} {gold:>5}g {load:>4}kg {worth:>6}g  {names}");
620    }
621
622    // Potions anywhere in the world, found by predicate rather than by key —
623    // and `kind` is an ordinary Rust enum the engine knows nothing about.
624    let potions = tx.scan_where::<Item, _>(|i| i.kind == Kind::Potion)?.len();
625    let sword = tx.get::<Item>(&20)?.expect("greatsword");
626    let enchantment = sword
627        .flavour
628        .get("enchantment")
629        .cloned()
630        .unwrap_or_default();
631    let forged_by = sword.flavour.get("forged_by").cloned().unwrap_or_default();
632    drop(tx);
633    // The rule Act 3 was about, checked rather than asserted in prose.
634    let mut tx = db.begin();
635    for (id, name, _) in &roster {
636        let strength = tx.get::<Hero>(id)?.map(|h| h.strength).unwrap_or(0);
637        let load = carried(&mut tx, *id)?;
638        assert!(
639            load <= strength,
640            "{name} is over their carry limit: {load} > {strength}"
641        );
642    }
643    drop(tx);
644
645    println!("\n  potions in the world: {potions}");
646    println!("  the greatsword is {enchantment}-enchanted, forged by {forged_by}");
647
648    // Nothing is reclaimed while a transaction that could still reach it is
649    // alive. This is the number to watch in a long-lived process: a forgotten
650    // transaction pins it and version chains grow without limit.
651    let stats = db.stats();
652    println!(
653        "\n  gc watermark {:?}, {} transactions still live",
654        stats.watermark, stats.active_transactions
655    );
656    println!("  (a watermark that stops moving while writes continue is the leak to look for)");
657
658    Ok(())
659}

pub fn begin(&self) -> Transaction<'_, SnapshotLevel>

Begin a transaction at the default isolation level (snapshot isolation).

Examples found in repository?
examples/game.rs (line 129)
128fn total_gold(db: &Database) -> Result<i64> {
129    let mut tx = db.begin();
130    Ok(tx.scan::<Hero>()?.iter().map(|h| h.gold).sum())
131}
132
133fn act(n: u32, title: &str) {
134    println!("\n\x1b[1m── Act {n}. {title}\x1b[0m");
135}
136
137// A narrative example: one linear script of acts, read top to bottom. Splitting
138// it into helpers to satisfy the length and complexity budgets would make it
139// harder to follow, which is the only thing this file is for.
140#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
141fn main() -> Result<()> {
142    let db = Arc::new(Database::open(Config::in_memory())?);
143    db.register::<Hero>()?;
144    db.register::<Item>()?;
145    db.register::<PartyMember>()?;
146
147    let (ada, bram, cleo) = (1u64, 2u64, 3u64);
148
149    // -----------------------------------------------------------------------
150    act(1, "The world is created");
151
152    db.transaction(|tx| {
153        tx.insert(Hero {
154            id: ada,
155            name: "Ada".into(),
156            gold: 120,
157            strength: 100,
158        })?;
159        tx.insert(Hero {
160            id: bram,
161            name: "Bram".into(),
162            gold: 80,
163            strength: 100,
164        })?;
165        tx.insert(Hero {
166            id: cleo,
167            name: "Cleo".into(),
168            gold: 200,
169            strength: 60,
170        })?;
171
172        let mut item = |id, owner, name: &str, weight, value, kind| {
173            tx.insert(Item {
174                id,
175                owner,
176                name: name.into(),
177                weight,
178                value,
179                kind,
180                flavour: HashMap::new(),
181            })
182        };
183        item(10, ada, "Rusty Sword", 30, 15, Kind::Weapon)?;
184        item(11, ada, "Health Potion", 5, 20, Kind::Potion)?;
185        item(12, bram, "Oak Shield", 40, 35, Kind::Weapon)?;
186        item(13, cleo, "Lucky Charm", 2, 90, Kind::Trinket)?;
187        // Loot lying in the dungeon, owned by nobody.
188        item(20, GROUND, "Flaming Greatsword", 55, 500, Kind::Weapon)?;
189        item(21, GROUND, "Elven Cloak", 20, 240, Kind::Trinket)?;
190        item(22, GROUND, "Iron Helm", 35, 60, Kind::Weapon)?;
191
192        // Arbitrary per-item state, in a database row, with no schema to
193        // declare and no serialisation format to agree on.
194        tx.update::<Item>(&20, |i| {
195            i.flavour.insert("enchantment".into(), "flame".into());
196            i.flavour.insert("forged_by".into(), "Durin".into());
197        })?;
198        Ok(())
199    })?;
200
201    // A whole transaction that fails partway leaves nothing behind. The unique
202    // index on `name` refuses the second Ada, and the gold change goes with it.
203    let before = gold_of(&mut db.begin(), ada)?;
204    let doomed = db.transaction(|tx| {
205        tx.update::<Hero>(&ada, |h| h.gold += 10_000)?;
206        tx.insert(Hero {
207            id: 99,
208            name: "Ada".into(),
209            gold: 0,
210            strength: 10,
211        })
212    });
213    println!("  a duplicate hero name: {}", doomed.unwrap_err());
214    println!(
215        "  Ada's gold is still {} — the whole transaction rolled back, not just the insert",
216        gold_of(&mut db.begin(), ada)?
217    );
218    assert_eq!(gold_of(&mut db.begin(), ada)?, before);
219
220    let mut tx = db.begin();
221    for hero in [ada, bram, cleo] {
222        let name = name_of(&mut tx, hero)?;
223        let carried = carried(&mut tx, hero)?;
224        let items = inventory(&mut tx, hero)?;
225        let names: Vec<_> = items.iter().map(|(_, n, _)| n.as_str()).collect();
226        println!("  {name:<5} carries {carried:>3} — {}", names.join(", "));
227    }
228    drop(tx);
229
230    // -----------------------------------------------------------------------
231    act(2, "Two heroes reach for the same greatsword");
232
233    // Both begin, both see the sword unclaimed. The first to write takes it;
234    // the second is told immediately rather than being made to wait.
235    let mut ada_grabs = db.begin_with::<Snapshot>();
236    let mut bram_grabs = db.begin_with::<Snapshot>();
237
238    ada_grabs.update::<Item>(&20, |i| i.owner = ada)?;
239
240    match bram_grabs.update::<Item>(&20, |i| i.owner = bram) {
241        Err(e @ Error::WriteConflict { .. }) => {
242            println!("  Bram: {e}");
243            println!(
244                "  ...retriable: {} — he can try for something else",
245                e.is_retriable()
246            );
247        }
248        other => unreachable!("expected a conflict, got {other:?}"),
249    }
250
251    // A failed write does not end the transaction. Bram takes the cloak instead.
252    bram_grabs.update::<Item>(&21, |i| i.owner = bram)?;
253    ada_grabs.commit()?;
254    bram_grabs.commit()?;
255    println!("  Ada takes the greatsword, Bram takes the cloak. Nobody blocked.");
256
257    // -----------------------------------------------------------------------
258    act(3, "Cleo overloads herself — write skew, and the fix");
259
260    // Cleo can carry 60 and is carrying 2. Either 35kg helm fits; both do not.
261    // Each transaction reads her inventory, checks the total, and picks up a
262    // *different* item — so the two writes never touch the same record, and
263    // first-updater-wins has nothing to catch.
264    db.transaction(|tx| {
265        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
266        tx.insert(Item {
267            id: 23,
268            owner: GROUND,
269            name: "Steel Helm".into(),
270            weight: 35,
271            value: 70,
272            kind: Kind::Weapon,
273            flavour: HashMap::new(),
274        })
275    })?;
276
277    let pick_up = |item: u64| {
278        move |tx: &mut mvcc::Transaction<'_, Serializable>| -> Result<bool> {
279            let hero = tx.get::<Hero>(&cleo)?.expect("Cleo exists");
280            let strength = hero.strength;
281            let load = carried(tx, cleo)?;
282            let weight = tx.get::<Item>(&item)?.map(|i| i.weight).unwrap_or(0);
283            if load + weight > strength {
284                return Ok(false);
285            }
286            tx.update::<Item>(&item, |i| i.owner = cleo)?;
287            Ok(true)
288        }
289    };
290
291    // Under Snapshot, both checks pass against a stale inventory.
292    {
293        let mut t1 = db.begin_with::<Snapshot>();
294        let mut t2 = db.begin_with::<Snapshot>();
295        let (load1, load2) = (carried(&mut t1, cleo)?, carried(&mut t2, cleo)?);
296        println!(
297            "  Snapshot:     both transactions see {load1}/{load2} carried, both think a 35kg helm fits"
298        );
299        t1.update::<Item>(&22, |i| i.owner = cleo)?;
300        t2.update::<Item>(&23, |i| i.owner = cleo)?;
301        t1.commit()?;
302        t2.commit()?;
303        let over = carried(&mut db.begin(), cleo)?;
304        println!(
305            "  ✗ Cleo now carries {over} of a possible 60. Two legal transactions, one broken rule."
306        );
307    }
308
309    // Put it back and try again at Serializable.
310    db.transaction(|tx| {
311        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
312        tx.update::<Item>(&23, |i| i.owner = GROUND).map(|_| ())
313    })?;
314
315    {
316        let mut t1 = db.begin_with::<Serializable>();
317        let mut t2 = db.begin_with::<Serializable>();
318        // Both read the inventory, as before.
319        let _ = carried(&mut t1, cleo)?;
320        let _ = carried(&mut t2, cleo)?;
321        t1.update::<Item>(&22, |i| i.owner = cleo)?;
322        t2.update::<Item>(&23, |i| i.owner = cleo)?;
323        t1.commit()?;
324        match t2.commit() {
325            Err(e @ Error::SerializationFailure) => println!("  Serializable: second pickup {e}"),
326            other => unreachable!("expected a serialization failure, got {other:?}"),
327        }
328        println!(
329            "  ✓ Cleo carries {} — the second pickup read an inventory the first changed.",
330            carried(&mut db.begin(), cleo)?
331        );
332    }
333
334    // In real code you would not hand-roll that. `transaction_with` retries,
335    // and the retry re-reads the inventory and correctly declines.
336    let took_it = db.transaction_with::<Serializable, _, _>(pick_up(23))?;
337    println!("  ...and on retry the second helm is refused on its merits: picked_up = {took_it}");
338
339    // -----------------------------------------------------------------------
340    act(4, "The party fills up — a phantom, not a conflict");
341
342    db.transaction(|tx| {
343        tx.insert(PartyMember {
344            hero: ada,
345            party: PARTY,
346            role: "Vanguard".into(),
347        })?;
348        tx.insert(PartyMember {
349            hero: bram,
350            party: PARTY,
351            role: "Shield".into(),
352        })?;
353        tx.insert(PartyMember {
354            hero: cleo,
355            party: PARTY,
356            role: "Scout".into(),
357        })
358    })?;
359
360    // Two newcomers apply at once, with three of four slots taken. Neither
361    // writes a row the other wrote — they insert *different* rows — so there is
362    // no write conflict to detect. What they collide on is a row that did not
363    // exist when either of them counted.
364    let (dara, finn) = (4u64, 5u64);
365    db.transaction(|tx| {
366        tx.insert(Hero {
367            id: dara,
368            name: "Dara".into(),
369            gold: 40,
370            strength: 80,
371        })?;
372        tx.insert(Hero {
373            id: finn,
374            name: "Finn".into(),
375            gold: 40,
376            strength: 80,
377        })
378    })?;
379
380    let mut d = db.begin_with::<Serializable>();
381    let mut f = db.begin_with::<Serializable>();
382    println!(
383        "  Dara counts {} members, Finn counts {} — both see a free slot",
384        party_size(&mut d)?,
385        party_size(&mut f)?
386    );
387    d.insert(PartyMember {
388        hero: dara,
389        party: PARTY,
390        role: "Healer".into(),
391    })?;
392    f.insert(PartyMember {
393        hero: finn,
394        party: PARTY,
395        role: "Healer".into(),
396    })?;
397    d.commit()?;
398    match f.commit() {
399        Err(e @ Error::SerializationFailure) => println!("  Finn: {e}"),
400        other => unreachable!("expected a serialization failure, got {other:?}"),
401    }
402    let mut tx = db.begin();
403    let mut roster: Vec<_> = tx
404        .scan_where::<PartyMember, _>(|m| m.party == PARTY)?
405        .iter()
406        .map(|m| (m.hero, m.role.clone()))
407        .collect();
408    drop(tx);
409    roster.sort();
410    let size = roster.len();
411    let mut tx = db.begin();
412    let listed: Vec<String> = roster
413        .iter()
414        .map(|(h, role)| Ok(format!("{} the {role}", name_of(&mut tx, *h)?)))
415        .collect::<Result<_>>()?;
416    drop(tx);
417    println!("  party: {}", listed.join(", "));
418    println!("  ✓ {size}/{PARTY_LIMIT} filled. Finn's insert became a phantom in Dara's count.");
419    println!("    (`scan_where` hands the engine the predicate, so it can re-check it at commit.)");
420    assert!(size <= PARTY_LIMIT);
421
422    // -----------------------------------------------------------------------
423    act(5, "A trade — two records, one atomic step");
424
425    let before = total_gold(&db)?;
426
427    let trade = |seller: u64, buyer: u64, item: u64, price: i64| {
428        let db = Arc::clone(&db);
429        move || -> Result<bool> {
430            db.transaction_with::<Serializable, _, _>(|tx| {
431                let buyer_gold = gold_of(tx, buyer)?;
432                if buyer_gold < price {
433                    return Ok(false);
434                }
435                let owned_by_seller = tx
436                    .get::<Item>(&item)?
437                    .map(|i| i.owner == seller)
438                    .unwrap_or(false);
439                if !owned_by_seller {
440                    return Ok(false);
441                }
442                tx.update::<Hero>(&buyer, |h| h.gold -= price)?;
443                tx.update::<Hero>(&seller, |h| h.gold += price)?;
444                tx.update::<Item>(&item, |i| i.owner = buyer)?;
445                Ok(true)
446            })
447        }
448    };
449
450    let sold = trade(bram, cleo, 21, 150)()?;
451    println!("  Bram sells the Elven Cloak to Cleo for 150g: {sold}");
452    let mut tx = db.begin();
453    let cloak_owner = tx.get::<Item>(&21)?.expect("cloak").owner;
454    println!(
455        "  Bram {}g, Cleo {}g, cloak owner is {}",
456        gold_of(&mut tx, bram)?,
457        gold_of(&mut tx, cleo)?,
458        name_of(&mut tx, cloak_owner)?
459    );
460    drop(tx);
461    assert_eq!(total_gold(&db)?, before, "gold must be conserved");
462    println!("  ✓ total gold unchanged at {before} — no step of that was separately visible");
463
464    // -----------------------------------------------------------------------
465    act(6, "The chronicler reads while the world moves");
466
467    // A long report scanning the whole world while combat rewrites it. It never
468    // blocks a writer, and its view never shifts underneath it.
469    let mut chronicler = db.begin_with::<Snapshot>();
470    let opening = total_gold(&db)?;
471    let seen_first = chronicler.scan::<Hero>()?.len();
472
473    for _ in 0..50 {
474        db.transaction(|tx| {
475            tx.update::<Hero>(&ada, |h| h.gold += 1)?;
476            tx.update::<Hero>(&bram, |h| h.gold -= 1)?;
477            Ok(())
478        })?;
479    }
480    db.transaction(|tx| {
481        tx.insert(Hero {
482            id: 6,
483            name: "Mira".into(),
484            gold: 0,
485            strength: 50,
486        })
487    })?;
488
489    let chron_gold: i64 = chronicler.scan::<Hero>()?.iter().map(|h| h.gold).sum();
490    let seen_after = chronicler.scan::<Hero>()?.len();
491    let in_world = db.begin().scan::<Hero>()?.len();
492    println!("  chronicler saw {seen_first} heroes at the start and still sees {seen_after};");
493    println!("  the world now holds {in_world}, after 50 commits and one new arrival");
494    println!(
495        "  chronicler totals {chron_gold}g; the world now totals {}g",
496        total_gold(&db)?
497    );
498    assert_eq!(chron_gold, opening, "a snapshot must not move");
499    chronicler.commit()?;
500    println!("  ✓ a reader that ran across 51 commits saw exactly one consistent world");
501
502    // -----------------------------------------------------------------------
503    act(7, "The raid — many adventurers at once");
504
505    let stop = Arc::new(AtomicBool::new(false));
506    let trades = Arc::new(AtomicU64::new(0));
507    let retries = Arc::new(AtomicU64::new(0));
508    let audits = Arc::new(AtomicU64::new(0));
509    let opening = total_gold(&db)?;
510    let heroes = [ada, bram, cleo, dara, finn];
511
512    // An auditor that must never catch a trade half-applied.
513    let auditor = {
514        let (db, stop, audits) = (Arc::clone(&db), Arc::clone(&stop), Arc::clone(&audits));
515        thread::spawn(move || -> Result<()> {
516            while !stop.load(Ordering::Relaxed) {
517                let mut tx = db.begin();
518                let total: i64 = tx.scan::<Hero>()?.iter().map(|h| h.gold).sum();
519                assert_eq!(total, opening, "auditor saw a half-finished trade");
520                audits.fetch_add(1, Ordering::Relaxed);
521            }
522            Ok(())
523        })
524    };
525
526    let start = Instant::now();
527    let raiders: Vec<_> = (0..4)
528        .map(|t| {
529            let (db, trades, retries) =
530                (Arc::clone(&db), Arc::clone(&trades), Arc::clone(&retries));
531            thread::spawn(move || -> Result<()> {
532                let mut seed = 0x9e37_79b9_7f4a_7c15u64 ^ (t + 1);
533                for _ in 0..400 {
534                    seed ^= seed << 13;
535                    seed ^= seed >> 7;
536                    seed ^= seed << 17;
537                    let from = heroes[(seed % heroes.len() as u64) as usize];
538                    let to = heroes[((seed >> 8) % heroes.len() as u64) as usize];
539                    if from == to {
540                        continue;
541                    }
542
543                    let mut attempts = 0u64;
544                    // Serializable, because the payment depends on a balance we
545                    // read — the same shape as Cleo's carry limit in Act 3.
546                    db.transaction_with::<Serializable, _, _>(|tx| {
547                        attempts += 1;
548                        let purse = gold_of(tx, from)?;
549                        if purse < 5 {
550                            return Ok(());
551                        }
552                        tx.update::<Hero>(&from, |h| h.gold -= 5)?;
553                        tx.update::<Hero>(&to, |h| h.gold += 5)?;
554                        Ok(())
555                    })?;
556                    trades.fetch_add(1, Ordering::Relaxed);
557                    retries.fetch_add(attempts - 1, Ordering::Relaxed);
558                }
559                Ok(())
560            })
561        })
562        .collect();
563
564    for r in raiders {
565        r.join().expect("raider panicked")?;
566    }
567    let elapsed = start.elapsed();
568    stop.store(true, Ordering::Relaxed);
569    auditor.join().expect("auditor panicked")?;
570
571    let done = trades.load(Ordering::Relaxed);
572    println!("  {done} trades across 4 threads in {elapsed:.2?}");
573    println!(
574        "  {} audit sweeps completed alongside them, none blocked, none torn",
575        audits.load(Ordering::Relaxed)
576    );
577    println!(
578        "  {} retries ({:.2} per trade) — contention on 5 heroes, handled by the engine",
579        retries.load(Ordering::Relaxed),
580        retries.load(Ordering::Relaxed) as f64 / done.max(1) as f64
581    );
582    assert_eq!(total_gold(&db)?, opening, "gold was created or destroyed");
583    println!("  ✓ total gold still {opening}");
584
585    // -----------------------------------------------------------------------
586    act(8, "Closing the ledger");
587
588    let mut tx = db.begin();
589    let mut roster: Vec<_> = tx
590        .scan::<Hero>()?
591        .iter()
592        .map(|h| (h.id, h.name.clone(), h.gold))
593        .collect();
594    roster.sort_by_key(|(id, _, _)| *id);
595    drop(tx);
596
597    println!(
598        "  {:<6} {:>6} {:>5} {:>7}  inventory",
599        "hero", "gold", "load", "worth"
600    );
601    let mut tx = db.begin();
602    for (id, name, gold) in &roster {
603        let held = tx.scan_index(Item::OWNER, *id..=*id)?;
604        let load: i32 = held.iter().map(|i| i.weight).sum();
605        let worth: i64 = held.iter().map(|i| i.value).sum();
606        let names: Vec<String> = held
607            .iter()
608            .map(|i| match i.kind {
609                Kind::Weapon => format!("[wpn] {}", i.name),
610                Kind::Potion => format!("[pot] {}", i.name),
611                Kind::Trinket => format!("[trk] {}", i.name),
612            })
613            .collect();
614        let names = if names.is_empty() {
615            "—".to_string()
616        } else {
617            names.join(", ")
618        };
619        println!("  {name:<6} {gold:>5}g {load:>4}kg {worth:>6}g  {names}");
620    }
621
622    // Potions anywhere in the world, found by predicate rather than by key —
623    // and `kind` is an ordinary Rust enum the engine knows nothing about.
624    let potions = tx.scan_where::<Item, _>(|i| i.kind == Kind::Potion)?.len();
625    let sword = tx.get::<Item>(&20)?.expect("greatsword");
626    let enchantment = sword
627        .flavour
628        .get("enchantment")
629        .cloned()
630        .unwrap_or_default();
631    let forged_by = sword.flavour.get("forged_by").cloned().unwrap_or_default();
632    drop(tx);
633    // The rule Act 3 was about, checked rather than asserted in prose.
634    let mut tx = db.begin();
635    for (id, name, _) in &roster {
636        let strength = tx.get::<Hero>(id)?.map(|h| h.strength).unwrap_or(0);
637        let load = carried(&mut tx, *id)?;
638        assert!(
639            load <= strength,
640            "{name} is over their carry limit: {load} > {strength}"
641        );
642    }
643    drop(tx);
644
645    println!("\n  potions in the world: {potions}");
646    println!("  the greatsword is {enchantment}-enchanted, forged by {forged_by}");
647
648    // Nothing is reclaimed while a transaction that could still reach it is
649    // alive. This is the number to watch in a long-lived process: a forgotten
650    // transaction pins it and version chains grow without limit.
651    let stats = db.stats();
652    println!(
653        "\n  gc watermark {:?}, {} transactions still live",
654        stats.watermark, stats.active_transactions
655    );
656    println!("  (a watermark that stops moving while writes continue is the leak to look for)");
657
658    Ok(())
659}
More examples
Hide additional examples
examples/basic.rs (line 54)
24fn main() -> Result<()> {
25    let db = Database::open(Config::in_memory())?;
26    db.register::<Account>()?;
27
28    // ---- insert -----------------------------------------------------------
29    // `transaction` runs the closure, commits it, and retries it if it hits a
30    // retriable conflict. Snapshot isolation by default.
31    db.transaction(|tx| {
32        tx.insert(Account {
33            id: 1,
34            owner: "ada".into(),
35            branch: 10,
36            balance: 500,
37        })?;
38        tx.insert(Account {
39            id: 2,
40            owner: "bob".into(),
41            branch: 10,
42            balance: 250,
43        })?;
44        tx.insert(Account {
45            id: 3,
46            owner: "cleo".into(),
47            branch: 20,
48            balance: 900,
49        })?;
50        Ok(())
51    })?;
52
53    // ---- read -------------------------------------------------------------
54    let mut tx = db.begin();
55    let ada = tx.get::<Account>(&1)?.expect("just inserted");
56    println!("ada: branch {}, balance {}", ada.branch, ada.balance);
57
58    // A read-only transaction has nothing to commit; dropping it rolls back,
59    // which for a reader means simply releasing its snapshot.
60    drop(tx);
61
62    // ---- update -----------------------------------------------------------
63    db.transaction(|tx| {
64        tx.update::<Account>(&1, |a| a.balance -= 100)?;
65        tx.update::<Account>(&2, |a| a.balance += 100)?;
66        Ok(())
67    })?;
68
69    // ---- scan by primary key ----------------------------------------------
70    let mut tx = db.begin();
71    println!("\nall accounts:");
72    for account in tx.scan::<Account>()? {
73        println!(
74            "  {:>4}  {:<6} branch {}  {:>5}",
75            account.id, account.owner, account.branch, account.balance
76        );
77    }
78
79    // ---- scan by secondary index ------------------------------------------
80    println!("\nbranch 10:");
81    for account in tx.scan_index(Account::BRANCH, 10u32..=10)? {
82        println!("  {} ({})", account.owner, account.balance);
83    }
84    drop(tx);
85
86    // ---- constraint violations --------------------------------------------
87    let mut tx = db.begin();
88    let duplicate = tx.insert(Account {
89        id: 99,
90        owner: "ada".into(),
91        branch: 30,
92        balance: 0,
93    });
94    println!("\nreusing owner 'ada': {}", duplicate.unwrap_err());
95    drop(tx);
96
97    // ---- delete -----------------------------------------------------------
98    db.transaction(|tx| {
99        let existed = tx.delete::<Account>(&3)?;
100        println!("deleted cleo: {existed}");
101        Ok(())
102    })?;
103
104    let mut tx = db.begin();
105    println!(
106        "cleo now: {:?}",
107        tx.get::<Account>(&3)?.map(|r| r.to_owned())
108    );
109
110    Ok(())
111}
examples/concurrent.rs (line 60)
29fn main() -> Result<()> {
30    let db = Arc::new(Database::open(Config::in_memory())?);
31    db.register::<Account>()?;
32
33    db.transaction(|tx| {
34        for id in 0..ACCOUNTS {
35            tx.insert(Account {
36                id,
37                balance: STARTING_BALANCE,
38            })?;
39        }
40        Ok(())
41    })?;
42
43    let total_before = total(&db)?;
44    println!("starting total: {total_before}");
45
46    let stop = Arc::new(AtomicBool::new(false));
47    let reads = Arc::new(AtomicU64::new(0));
48    let retries = Arc::new(AtomicU64::new(0));
49
50    // ---- a reader that must never observe a torn transfer -----------------
51    // Money moves between accounts constantly. Under snapshot isolation this
52    // thread's scans must always sum to the same total: it either sees both
53    // halves of a transfer or neither.
54    let auditor = {
55        let db = Arc::clone(&db);
56        let stop = Arc::clone(&stop);
57        let reads = Arc::clone(&reads);
58        thread::spawn(move || -> Result<()> {
59            while !stop.load(Ordering::Relaxed) {
60                let mut tx = db.begin();
61                let sum: i64 = tx.scan::<Account>()?.iter().map(|a| a.balance).sum();
62                assert_eq!(
63                    sum,
64                    ACCOUNTS as i64 * STARTING_BALANCE,
65                    "a reader observed a partially applied transfer"
66                );
67                reads.fetch_add(1, Ordering::Relaxed);
68            }
69            Ok(())
70        })
71    };
72
73    // ---- writers ----------------------------------------------------------
74    let start = Instant::now();
75    let mut writers = Vec::new();
76    for t in 0..TRANSFER_THREADS {
77        let db = Arc::clone(&db);
78        let retries = Arc::clone(&retries);
79        writers.push(thread::spawn(move || -> Result<()> {
80            // A cheap per-thread PRNG; no dependency needed for this.
81            let mut seed = 0x9e37_79b9_7f4a_7c15u64 ^ (t as u64 + 1);
82            let mut next = move || {
83                seed ^= seed << 13;
84                seed ^= seed >> 7;
85                seed ^= seed << 17;
86                seed
87            };
88
89            for _ in 0..TRANSFERS_PER_THREAD {
90                let from = next() % ACCOUNTS;
91                let to = next() % ACCOUNTS;
92                if from == to {
93                    continue;
94                }
95
96                let mut attempts = 0u64;
97                // Serializable, because the transfer's *write* depends on a
98                // balance it *read* — the write-skew shape. Snapshot isolation
99                // would let two concurrent transfers both pass the check.
100                db.transaction_with::<Serializable, _, _>(|tx| {
101                    attempts += 1;
102                    let balance = tx.get::<Account>(&from)?.map(|a| a.balance).unwrap_or(0);
103                    if balance < 10 {
104                        return Ok(());
105                    }
106                    tx.update::<Account>(&from, |a| a.balance -= 10)?;
107                    tx.update::<Account>(&to, |a| a.balance += 10)?;
108                    Ok(())
109                })?;
110                retries.fetch_add(attempts - 1, Ordering::Relaxed);
111            }
112            Ok(())
113        }));
114    }
115
116    for w in writers {
117        w.join().expect("writer panicked")?;
118    }
119    let elapsed = start.elapsed();
120
121    stop.store(true, Ordering::Relaxed);
122    auditor.join().expect("auditor panicked")?;
123
124    // ---- results ----------------------------------------------------------
125    let total_after = total(&db)?;
126    let committed = (TRANSFER_THREADS * TRANSFERS_PER_THREAD) as u64;
127
128    println!("final total:    {total_after}");
129    assert_eq!(total_before, total_after, "money was created or destroyed");
130
131    println!("\n{committed} transfers in {elapsed:.2?}");
132    println!(
133        "  {:.0} transfers/sec",
134        committed as f64 / elapsed.as_secs_f64()
135    );
136    println!(
137        "  {} concurrent audit scans completed, none of them blocked",
138        reads.load(Ordering::Relaxed)
139    );
140    println!(
141        "  {} retries ({:.1} per transfer)",
142        retries.load(Ordering::Relaxed),
143        retries.load(Ordering::Relaxed) as f64 / committed as f64
144    );
145
146    // The GC watermark should be advancing. If `active_transactions` climbs and
147    // the watermark stalls, some transaction is being leaked — see engine::gc.
148    let stats = db.stats();
149    println!(
150        "\ngc watermark {:?}, {} transactions still live",
151        stats.watermark, stats.active_transactions
152    );
153
154    println!("\n✓ no torn reads, no lost updates, conservation held");
155
156    // Slow readers are what pin the GC watermark. Left here as the shape of the
157    // thing to watch for, not as something this example demonstrates going wrong.
158    thread::sleep(Duration::from_millis(1));
159    Ok(())
160}
161
162fn total(db: &Database) -> Result<i64> {
163    let mut tx = db.begin();
164    Ok(tx.scan::<Account>()?.iter().map(|a| a.balance).sum())
165}
examples/isolation.rs (line 124)
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}

pub fn begin_with<I: IsolationLevel>(&self) -> Transaction<'_, I>

Begin a transaction at a chosen isolation level.

The level is a type parameter, so the cost of the strongest never leaks into the weakest — see IsolationLevel.

let strict = db.begin_with::<Serializable>();
let cheap = db.begin_with::<ReadCommitted>();

Prefer Database::transaction_with unless you need manual control: this hands back a transaction you must commit yourself, and dropping it without committing rolls it back.

Examples found in repository?
examples/isolation.rs (line 54)
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}
More examples
Hide additional examples
examples/game.rs (line 235)
141fn main() -> Result<()> {
142    let db = Arc::new(Database::open(Config::in_memory())?);
143    db.register::<Hero>()?;
144    db.register::<Item>()?;
145    db.register::<PartyMember>()?;
146
147    let (ada, bram, cleo) = (1u64, 2u64, 3u64);
148
149    // -----------------------------------------------------------------------
150    act(1, "The world is created");
151
152    db.transaction(|tx| {
153        tx.insert(Hero {
154            id: ada,
155            name: "Ada".into(),
156            gold: 120,
157            strength: 100,
158        })?;
159        tx.insert(Hero {
160            id: bram,
161            name: "Bram".into(),
162            gold: 80,
163            strength: 100,
164        })?;
165        tx.insert(Hero {
166            id: cleo,
167            name: "Cleo".into(),
168            gold: 200,
169            strength: 60,
170        })?;
171
172        let mut item = |id, owner, name: &str, weight, value, kind| {
173            tx.insert(Item {
174                id,
175                owner,
176                name: name.into(),
177                weight,
178                value,
179                kind,
180                flavour: HashMap::new(),
181            })
182        };
183        item(10, ada, "Rusty Sword", 30, 15, Kind::Weapon)?;
184        item(11, ada, "Health Potion", 5, 20, Kind::Potion)?;
185        item(12, bram, "Oak Shield", 40, 35, Kind::Weapon)?;
186        item(13, cleo, "Lucky Charm", 2, 90, Kind::Trinket)?;
187        // Loot lying in the dungeon, owned by nobody.
188        item(20, GROUND, "Flaming Greatsword", 55, 500, Kind::Weapon)?;
189        item(21, GROUND, "Elven Cloak", 20, 240, Kind::Trinket)?;
190        item(22, GROUND, "Iron Helm", 35, 60, Kind::Weapon)?;
191
192        // Arbitrary per-item state, in a database row, with no schema to
193        // declare and no serialisation format to agree on.
194        tx.update::<Item>(&20, |i| {
195            i.flavour.insert("enchantment".into(), "flame".into());
196            i.flavour.insert("forged_by".into(), "Durin".into());
197        })?;
198        Ok(())
199    })?;
200
201    // A whole transaction that fails partway leaves nothing behind. The unique
202    // index on `name` refuses the second Ada, and the gold change goes with it.
203    let before = gold_of(&mut db.begin(), ada)?;
204    let doomed = db.transaction(|tx| {
205        tx.update::<Hero>(&ada, |h| h.gold += 10_000)?;
206        tx.insert(Hero {
207            id: 99,
208            name: "Ada".into(),
209            gold: 0,
210            strength: 10,
211        })
212    });
213    println!("  a duplicate hero name: {}", doomed.unwrap_err());
214    println!(
215        "  Ada's gold is still {} — the whole transaction rolled back, not just the insert",
216        gold_of(&mut db.begin(), ada)?
217    );
218    assert_eq!(gold_of(&mut db.begin(), ada)?, before);
219
220    let mut tx = db.begin();
221    for hero in [ada, bram, cleo] {
222        let name = name_of(&mut tx, hero)?;
223        let carried = carried(&mut tx, hero)?;
224        let items = inventory(&mut tx, hero)?;
225        let names: Vec<_> = items.iter().map(|(_, n, _)| n.as_str()).collect();
226        println!("  {name:<5} carries {carried:>3} — {}", names.join(", "));
227    }
228    drop(tx);
229
230    // -----------------------------------------------------------------------
231    act(2, "Two heroes reach for the same greatsword");
232
233    // Both begin, both see the sword unclaimed. The first to write takes it;
234    // the second is told immediately rather than being made to wait.
235    let mut ada_grabs = db.begin_with::<Snapshot>();
236    let mut bram_grabs = db.begin_with::<Snapshot>();
237
238    ada_grabs.update::<Item>(&20, |i| i.owner = ada)?;
239
240    match bram_grabs.update::<Item>(&20, |i| i.owner = bram) {
241        Err(e @ Error::WriteConflict { .. }) => {
242            println!("  Bram: {e}");
243            println!(
244                "  ...retriable: {} — he can try for something else",
245                e.is_retriable()
246            );
247        }
248        other => unreachable!("expected a conflict, got {other:?}"),
249    }
250
251    // A failed write does not end the transaction. Bram takes the cloak instead.
252    bram_grabs.update::<Item>(&21, |i| i.owner = bram)?;
253    ada_grabs.commit()?;
254    bram_grabs.commit()?;
255    println!("  Ada takes the greatsword, Bram takes the cloak. Nobody blocked.");
256
257    // -----------------------------------------------------------------------
258    act(3, "Cleo overloads herself — write skew, and the fix");
259
260    // Cleo can carry 60 and is carrying 2. Either 35kg helm fits; both do not.
261    // Each transaction reads her inventory, checks the total, and picks up a
262    // *different* item — so the two writes never touch the same record, and
263    // first-updater-wins has nothing to catch.
264    db.transaction(|tx| {
265        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
266        tx.insert(Item {
267            id: 23,
268            owner: GROUND,
269            name: "Steel Helm".into(),
270            weight: 35,
271            value: 70,
272            kind: Kind::Weapon,
273            flavour: HashMap::new(),
274        })
275    })?;
276
277    let pick_up = |item: u64| {
278        move |tx: &mut mvcc::Transaction<'_, Serializable>| -> Result<bool> {
279            let hero = tx.get::<Hero>(&cleo)?.expect("Cleo exists");
280            let strength = hero.strength;
281            let load = carried(tx, cleo)?;
282            let weight = tx.get::<Item>(&item)?.map(|i| i.weight).unwrap_or(0);
283            if load + weight > strength {
284                return Ok(false);
285            }
286            tx.update::<Item>(&item, |i| i.owner = cleo)?;
287            Ok(true)
288        }
289    };
290
291    // Under Snapshot, both checks pass against a stale inventory.
292    {
293        let mut t1 = db.begin_with::<Snapshot>();
294        let mut t2 = db.begin_with::<Snapshot>();
295        let (load1, load2) = (carried(&mut t1, cleo)?, carried(&mut t2, cleo)?);
296        println!(
297            "  Snapshot:     both transactions see {load1}/{load2} carried, both think a 35kg helm fits"
298        );
299        t1.update::<Item>(&22, |i| i.owner = cleo)?;
300        t2.update::<Item>(&23, |i| i.owner = cleo)?;
301        t1.commit()?;
302        t2.commit()?;
303        let over = carried(&mut db.begin(), cleo)?;
304        println!(
305            "  ✗ Cleo now carries {over} of a possible 60. Two legal transactions, one broken rule."
306        );
307    }
308
309    // Put it back and try again at Serializable.
310    db.transaction(|tx| {
311        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
312        tx.update::<Item>(&23, |i| i.owner = GROUND).map(|_| ())
313    })?;
314
315    {
316        let mut t1 = db.begin_with::<Serializable>();
317        let mut t2 = db.begin_with::<Serializable>();
318        // Both read the inventory, as before.
319        let _ = carried(&mut t1, cleo)?;
320        let _ = carried(&mut t2, cleo)?;
321        t1.update::<Item>(&22, |i| i.owner = cleo)?;
322        t2.update::<Item>(&23, |i| i.owner = cleo)?;
323        t1.commit()?;
324        match t2.commit() {
325            Err(e @ Error::SerializationFailure) => println!("  Serializable: second pickup {e}"),
326            other => unreachable!("expected a serialization failure, got {other:?}"),
327        }
328        println!(
329            "  ✓ Cleo carries {} — the second pickup read an inventory the first changed.",
330            carried(&mut db.begin(), cleo)?
331        );
332    }
333
334    // In real code you would not hand-roll that. `transaction_with` retries,
335    // and the retry re-reads the inventory and correctly declines.
336    let took_it = db.transaction_with::<Serializable, _, _>(pick_up(23))?;
337    println!("  ...and on retry the second helm is refused on its merits: picked_up = {took_it}");
338
339    // -----------------------------------------------------------------------
340    act(4, "The party fills up — a phantom, not a conflict");
341
342    db.transaction(|tx| {
343        tx.insert(PartyMember {
344            hero: ada,
345            party: PARTY,
346            role: "Vanguard".into(),
347        })?;
348        tx.insert(PartyMember {
349            hero: bram,
350            party: PARTY,
351            role: "Shield".into(),
352        })?;
353        tx.insert(PartyMember {
354            hero: cleo,
355            party: PARTY,
356            role: "Scout".into(),
357        })
358    })?;
359
360    // Two newcomers apply at once, with three of four slots taken. Neither
361    // writes a row the other wrote — they insert *different* rows — so there is
362    // no write conflict to detect. What they collide on is a row that did not
363    // exist when either of them counted.
364    let (dara, finn) = (4u64, 5u64);
365    db.transaction(|tx| {
366        tx.insert(Hero {
367            id: dara,
368            name: "Dara".into(),
369            gold: 40,
370            strength: 80,
371        })?;
372        tx.insert(Hero {
373            id: finn,
374            name: "Finn".into(),
375            gold: 40,
376            strength: 80,
377        })
378    })?;
379
380    let mut d = db.begin_with::<Serializable>();
381    let mut f = db.begin_with::<Serializable>();
382    println!(
383        "  Dara counts {} members, Finn counts {} — both see a free slot",
384        party_size(&mut d)?,
385        party_size(&mut f)?
386    );
387    d.insert(PartyMember {
388        hero: dara,
389        party: PARTY,
390        role: "Healer".into(),
391    })?;
392    f.insert(PartyMember {
393        hero: finn,
394        party: PARTY,
395        role: "Healer".into(),
396    })?;
397    d.commit()?;
398    match f.commit() {
399        Err(e @ Error::SerializationFailure) => println!("  Finn: {e}"),
400        other => unreachable!("expected a serialization failure, got {other:?}"),
401    }
402    let mut tx = db.begin();
403    let mut roster: Vec<_> = tx
404        .scan_where::<PartyMember, _>(|m| m.party == PARTY)?
405        .iter()
406        .map(|m| (m.hero, m.role.clone()))
407        .collect();
408    drop(tx);
409    roster.sort();
410    let size = roster.len();
411    let mut tx = db.begin();
412    let listed: Vec<String> = roster
413        .iter()
414        .map(|(h, role)| Ok(format!("{} the {role}", name_of(&mut tx, *h)?)))
415        .collect::<Result<_>>()?;
416    drop(tx);
417    println!("  party: {}", listed.join(", "));
418    println!("  ✓ {size}/{PARTY_LIMIT} filled. Finn's insert became a phantom in Dara's count.");
419    println!("    (`scan_where` hands the engine the predicate, so it can re-check it at commit.)");
420    assert!(size <= PARTY_LIMIT);
421
422    // -----------------------------------------------------------------------
423    act(5, "A trade — two records, one atomic step");
424
425    let before = total_gold(&db)?;
426
427    let trade = |seller: u64, buyer: u64, item: u64, price: i64| {
428        let db = Arc::clone(&db);
429        move || -> Result<bool> {
430            db.transaction_with::<Serializable, _, _>(|tx| {
431                let buyer_gold = gold_of(tx, buyer)?;
432                if buyer_gold < price {
433                    return Ok(false);
434                }
435                let owned_by_seller = tx
436                    .get::<Item>(&item)?
437                    .map(|i| i.owner == seller)
438                    .unwrap_or(false);
439                if !owned_by_seller {
440                    return Ok(false);
441                }
442                tx.update::<Hero>(&buyer, |h| h.gold -= price)?;
443                tx.update::<Hero>(&seller, |h| h.gold += price)?;
444                tx.update::<Item>(&item, |i| i.owner = buyer)?;
445                Ok(true)
446            })
447        }
448    };
449
450    let sold = trade(bram, cleo, 21, 150)()?;
451    println!("  Bram sells the Elven Cloak to Cleo for 150g: {sold}");
452    let mut tx = db.begin();
453    let cloak_owner = tx.get::<Item>(&21)?.expect("cloak").owner;
454    println!(
455        "  Bram {}g, Cleo {}g, cloak owner is {}",
456        gold_of(&mut tx, bram)?,
457        gold_of(&mut tx, cleo)?,
458        name_of(&mut tx, cloak_owner)?
459    );
460    drop(tx);
461    assert_eq!(total_gold(&db)?, before, "gold must be conserved");
462    println!("  ✓ total gold unchanged at {before} — no step of that was separately visible");
463
464    // -----------------------------------------------------------------------
465    act(6, "The chronicler reads while the world moves");
466
467    // A long report scanning the whole world while combat rewrites it. It never
468    // blocks a writer, and its view never shifts underneath it.
469    let mut chronicler = db.begin_with::<Snapshot>();
470    let opening = total_gold(&db)?;
471    let seen_first = chronicler.scan::<Hero>()?.len();
472
473    for _ in 0..50 {
474        db.transaction(|tx| {
475            tx.update::<Hero>(&ada, |h| h.gold += 1)?;
476            tx.update::<Hero>(&bram, |h| h.gold -= 1)?;
477            Ok(())
478        })?;
479    }
480    db.transaction(|tx| {
481        tx.insert(Hero {
482            id: 6,
483            name: "Mira".into(),
484            gold: 0,
485            strength: 50,
486        })
487    })?;
488
489    let chron_gold: i64 = chronicler.scan::<Hero>()?.iter().map(|h| h.gold).sum();
490    let seen_after = chronicler.scan::<Hero>()?.len();
491    let in_world = db.begin().scan::<Hero>()?.len();
492    println!("  chronicler saw {seen_first} heroes at the start and still sees {seen_after};");
493    println!("  the world now holds {in_world}, after 50 commits and one new arrival");
494    println!(
495        "  chronicler totals {chron_gold}g; the world now totals {}g",
496        total_gold(&db)?
497    );
498    assert_eq!(chron_gold, opening, "a snapshot must not move");
499    chronicler.commit()?;
500    println!("  ✓ a reader that ran across 51 commits saw exactly one consistent world");
501
502    // -----------------------------------------------------------------------
503    act(7, "The raid — many adventurers at once");
504
505    let stop = Arc::new(AtomicBool::new(false));
506    let trades = Arc::new(AtomicU64::new(0));
507    let retries = Arc::new(AtomicU64::new(0));
508    let audits = Arc::new(AtomicU64::new(0));
509    let opening = total_gold(&db)?;
510    let heroes = [ada, bram, cleo, dara, finn];
511
512    // An auditor that must never catch a trade half-applied.
513    let auditor = {
514        let (db, stop, audits) = (Arc::clone(&db), Arc::clone(&stop), Arc::clone(&audits));
515        thread::spawn(move || -> Result<()> {
516            while !stop.load(Ordering::Relaxed) {
517                let mut tx = db.begin();
518                let total: i64 = tx.scan::<Hero>()?.iter().map(|h| h.gold).sum();
519                assert_eq!(total, opening, "auditor saw a half-finished trade");
520                audits.fetch_add(1, Ordering::Relaxed);
521            }
522            Ok(())
523        })
524    };
525
526    let start = Instant::now();
527    let raiders: Vec<_> = (0..4)
528        .map(|t| {
529            let (db, trades, retries) =
530                (Arc::clone(&db), Arc::clone(&trades), Arc::clone(&retries));
531            thread::spawn(move || -> Result<()> {
532                let mut seed = 0x9e37_79b9_7f4a_7c15u64 ^ (t + 1);
533                for _ in 0..400 {
534                    seed ^= seed << 13;
535                    seed ^= seed >> 7;
536                    seed ^= seed << 17;
537                    let from = heroes[(seed % heroes.len() as u64) as usize];
538                    let to = heroes[((seed >> 8) % heroes.len() as u64) as usize];
539                    if from == to {
540                        continue;
541                    }
542
543                    let mut attempts = 0u64;
544                    // Serializable, because the payment depends on a balance we
545                    // read — the same shape as Cleo's carry limit in Act 3.
546                    db.transaction_with::<Serializable, _, _>(|tx| {
547                        attempts += 1;
548                        let purse = gold_of(tx, from)?;
549                        if purse < 5 {
550                            return Ok(());
551                        }
552                        tx.update::<Hero>(&from, |h| h.gold -= 5)?;
553                        tx.update::<Hero>(&to, |h| h.gold += 5)?;
554                        Ok(())
555                    })?;
556                    trades.fetch_add(1, Ordering::Relaxed);
557                    retries.fetch_add(attempts - 1, Ordering::Relaxed);
558                }
559                Ok(())
560            })
561        })
562        .collect();
563
564    for r in raiders {
565        r.join().expect("raider panicked")?;
566    }
567    let elapsed = start.elapsed();
568    stop.store(true, Ordering::Relaxed);
569    auditor.join().expect("auditor panicked")?;
570
571    let done = trades.load(Ordering::Relaxed);
572    println!("  {done} trades across 4 threads in {elapsed:.2?}");
573    println!(
574        "  {} audit sweeps completed alongside them, none blocked, none torn",
575        audits.load(Ordering::Relaxed)
576    );
577    println!(
578        "  {} retries ({:.2} per trade) — contention on 5 heroes, handled by the engine",
579        retries.load(Ordering::Relaxed),
580        retries.load(Ordering::Relaxed) as f64 / done.max(1) as f64
581    );
582    assert_eq!(total_gold(&db)?, opening, "gold was created or destroyed");
583    println!("  ✓ total gold still {opening}");
584
585    // -----------------------------------------------------------------------
586    act(8, "Closing the ledger");
587
588    let mut tx = db.begin();
589    let mut roster: Vec<_> = tx
590        .scan::<Hero>()?
591        .iter()
592        .map(|h| (h.id, h.name.clone(), h.gold))
593        .collect();
594    roster.sort_by_key(|(id, _, _)| *id);
595    drop(tx);
596
597    println!(
598        "  {:<6} {:>6} {:>5} {:>7}  inventory",
599        "hero", "gold", "load", "worth"
600    );
601    let mut tx = db.begin();
602    for (id, name, gold) in &roster {
603        let held = tx.scan_index(Item::OWNER, *id..=*id)?;
604        let load: i32 = held.iter().map(|i| i.weight).sum();
605        let worth: i64 = held.iter().map(|i| i.value).sum();
606        let names: Vec<String> = held
607            .iter()
608            .map(|i| match i.kind {
609                Kind::Weapon => format!("[wpn] {}", i.name),
610                Kind::Potion => format!("[pot] {}", i.name),
611                Kind::Trinket => format!("[trk] {}", i.name),
612            })
613            .collect();
614        let names = if names.is_empty() {
615            "—".to_string()
616        } else {
617            names.join(", ")
618        };
619        println!("  {name:<6} {gold:>5}g {load:>4}kg {worth:>6}g  {names}");
620    }
621
622    // Potions anywhere in the world, found by predicate rather than by key —
623    // and `kind` is an ordinary Rust enum the engine knows nothing about.
624    let potions = tx.scan_where::<Item, _>(|i| i.kind == Kind::Potion)?.len();
625    let sword = tx.get::<Item>(&20)?.expect("greatsword");
626    let enchantment = sword
627        .flavour
628        .get("enchantment")
629        .cloned()
630        .unwrap_or_default();
631    let forged_by = sword.flavour.get("forged_by").cloned().unwrap_or_default();
632    drop(tx);
633    // The rule Act 3 was about, checked rather than asserted in prose.
634    let mut tx = db.begin();
635    for (id, name, _) in &roster {
636        let strength = tx.get::<Hero>(id)?.map(|h| h.strength).unwrap_or(0);
637        let load = carried(&mut tx, *id)?;
638        assert!(
639            load <= strength,
640            "{name} is over their carry limit: {load} > {strength}"
641        );
642    }
643    drop(tx);
644
645    println!("\n  potions in the world: {potions}");
646    println!("  the greatsword is {enchantment}-enchanted, forged by {forged_by}");
647
648    // Nothing is reclaimed while a transaction that could still reach it is
649    // alive. This is the number to watch in a long-lived process: a forgotten
650    // transaction pins it and version chains grow without limit.
651    let stats = db.stats();
652    println!(
653        "\n  gc watermark {:?}, {} transactions still live",
654        stats.watermark, stats.active_transactions
655    );
656    println!("  (a watermark that stops moving while writes continue is the leak to look for)");
657
658    Ok(())
659}

pub fn transaction<R, F>(&self, f: F) -> Result<R>
where F: FnMut(&mut Transaction<'_, SnapshotLevel>) -> Result<R>,

Run f in a transaction at snapshot isolation, retrying while it fails retriably, and commit.

This is the API most users should reach for. Under snapshot isolation, and especially serializable, an abort is a normal outcome rather than an error condition — every caller would otherwise write this loop.

f may run more than once, so it must not have side effects outside the transaction.

Split from Database::transaction_with rather than defaulting a type parameter, because Rust cannot infer a defaulted type parameter on a function — db.transaction(|tx| …) would not compile.

Examples found in repository?
examples/basic.rs (lines 31-51)
24fn main() -> Result<()> {
25    let db = Database::open(Config::in_memory())?;
26    db.register::<Account>()?;
27
28    // ---- insert -----------------------------------------------------------
29    // `transaction` runs the closure, commits it, and retries it if it hits a
30    // retriable conflict. Snapshot isolation by default.
31    db.transaction(|tx| {
32        tx.insert(Account {
33            id: 1,
34            owner: "ada".into(),
35            branch: 10,
36            balance: 500,
37        })?;
38        tx.insert(Account {
39            id: 2,
40            owner: "bob".into(),
41            branch: 10,
42            balance: 250,
43        })?;
44        tx.insert(Account {
45            id: 3,
46            owner: "cleo".into(),
47            branch: 20,
48            balance: 900,
49        })?;
50        Ok(())
51    })?;
52
53    // ---- read -------------------------------------------------------------
54    let mut tx = db.begin();
55    let ada = tx.get::<Account>(&1)?.expect("just inserted");
56    println!("ada: branch {}, balance {}", ada.branch, ada.balance);
57
58    // A read-only transaction has nothing to commit; dropping it rolls back,
59    // which for a reader means simply releasing its snapshot.
60    drop(tx);
61
62    // ---- update -----------------------------------------------------------
63    db.transaction(|tx| {
64        tx.update::<Account>(&1, |a| a.balance -= 100)?;
65        tx.update::<Account>(&2, |a| a.balance += 100)?;
66        Ok(())
67    })?;
68
69    // ---- scan by primary key ----------------------------------------------
70    let mut tx = db.begin();
71    println!("\nall accounts:");
72    for account in tx.scan::<Account>()? {
73        println!(
74            "  {:>4}  {:<6} branch {}  {:>5}",
75            account.id, account.owner, account.branch, account.balance
76        );
77    }
78
79    // ---- scan by secondary index ------------------------------------------
80    println!("\nbranch 10:");
81    for account in tx.scan_index(Account::BRANCH, 10u32..=10)? {
82        println!("  {} ({})", account.owner, account.balance);
83    }
84    drop(tx);
85
86    // ---- constraint violations --------------------------------------------
87    let mut tx = db.begin();
88    let duplicate = tx.insert(Account {
89        id: 99,
90        owner: "ada".into(),
91        branch: 30,
92        balance: 0,
93    });
94    println!("\nreusing owner 'ada': {}", duplicate.unwrap_err());
95    drop(tx);
96
97    // ---- delete -----------------------------------------------------------
98    db.transaction(|tx| {
99        let existed = tx.delete::<Account>(&3)?;
100        println!("deleted cleo: {existed}");
101        Ok(())
102    })?;
103
104    let mut tx = db.begin();
105    println!(
106        "cleo now: {:?}",
107        tx.get::<Account>(&3)?.map(|r| r.to_owned())
108    );
109
110    Ok(())
111}
More examples
Hide additional examples
examples/concurrent.rs (lines 33-41)
29fn main() -> Result<()> {
30    let db = Arc::new(Database::open(Config::in_memory())?);
31    db.register::<Account>()?;
32
33    db.transaction(|tx| {
34        for id in 0..ACCOUNTS {
35            tx.insert(Account {
36                id,
37                balance: STARTING_BALANCE,
38            })?;
39        }
40        Ok(())
41    })?;
42
43    let total_before = total(&db)?;
44    println!("starting total: {total_before}");
45
46    let stop = Arc::new(AtomicBool::new(false));
47    let reads = Arc::new(AtomicU64::new(0));
48    let retries = Arc::new(AtomicU64::new(0));
49
50    // ---- a reader that must never observe a torn transfer -----------------
51    // Money moves between accounts constantly. Under snapshot isolation this
52    // thread's scans must always sum to the same total: it either sees both
53    // halves of a transfer or neither.
54    let auditor = {
55        let db = Arc::clone(&db);
56        let stop = Arc::clone(&stop);
57        let reads = Arc::clone(&reads);
58        thread::spawn(move || -> Result<()> {
59            while !stop.load(Ordering::Relaxed) {
60                let mut tx = db.begin();
61                let sum: i64 = tx.scan::<Account>()?.iter().map(|a| a.balance).sum();
62                assert_eq!(
63                    sum,
64                    ACCOUNTS as i64 * STARTING_BALANCE,
65                    "a reader observed a partially applied transfer"
66                );
67                reads.fetch_add(1, Ordering::Relaxed);
68            }
69            Ok(())
70        })
71    };
72
73    // ---- writers ----------------------------------------------------------
74    let start = Instant::now();
75    let mut writers = Vec::new();
76    for t in 0..TRANSFER_THREADS {
77        let db = Arc::clone(&db);
78        let retries = Arc::clone(&retries);
79        writers.push(thread::spawn(move || -> Result<()> {
80            // A cheap per-thread PRNG; no dependency needed for this.
81            let mut seed = 0x9e37_79b9_7f4a_7c15u64 ^ (t as u64 + 1);
82            let mut next = move || {
83                seed ^= seed << 13;
84                seed ^= seed >> 7;
85                seed ^= seed << 17;
86                seed
87            };
88
89            for _ in 0..TRANSFERS_PER_THREAD {
90                let from = next() % ACCOUNTS;
91                let to = next() % ACCOUNTS;
92                if from == to {
93                    continue;
94                }
95
96                let mut attempts = 0u64;
97                // Serializable, because the transfer's *write* depends on a
98                // balance it *read* — the write-skew shape. Snapshot isolation
99                // would let two concurrent transfers both pass the check.
100                db.transaction_with::<Serializable, _, _>(|tx| {
101                    attempts += 1;
102                    let balance = tx.get::<Account>(&from)?.map(|a| a.balance).unwrap_or(0);
103                    if balance < 10 {
104                        return Ok(());
105                    }
106                    tx.update::<Account>(&from, |a| a.balance -= 10)?;
107                    tx.update::<Account>(&to, |a| a.balance += 10)?;
108                    Ok(())
109                })?;
110                retries.fetch_add(attempts - 1, Ordering::Relaxed);
111            }
112            Ok(())
113        }));
114    }
115
116    for w in writers {
117        w.join().expect("writer panicked")?;
118    }
119    let elapsed = start.elapsed();
120
121    stop.store(true, Ordering::Relaxed);
122    auditor.join().expect("auditor panicked")?;
123
124    // ---- results ----------------------------------------------------------
125    let total_after = total(&db)?;
126    let committed = (TRANSFER_THREADS * TRANSFERS_PER_THREAD) as u64;
127
128    println!("final total:    {total_after}");
129    assert_eq!(total_before, total_after, "money was created or destroyed");
130
131    println!("\n{committed} transfers in {elapsed:.2?}");
132    println!(
133        "  {:.0} transfers/sec",
134        committed as f64 / elapsed.as_secs_f64()
135    );
136    println!(
137        "  {} concurrent audit scans completed, none of them blocked",
138        reads.load(Ordering::Relaxed)
139    );
140    println!(
141        "  {} retries ({:.1} per transfer)",
142        retries.load(Ordering::Relaxed),
143        retries.load(Ordering::Relaxed) as f64 / committed as f64
144    );
145
146    // The GC watermark should be advancing. If `active_transactions` climbs and
147    // the watermark stalls, some transaction is being leaked — see engine::gc.
148    let stats = db.stats();
149    println!(
150        "\ngc watermark {:?}, {} transactions still live",
151        stats.watermark, stats.active_transactions
152    );
153
154    println!("\n✓ no torn reads, no lost updates, conservation held");
155
156    // Slow readers are what pin the GC watermark. Left here as the shape of the
157    // thing to watch for, not as something this example demonstrates going wrong.
158    thread::sleep(Duration::from_millis(1));
159    Ok(())
160}
examples/isolation.rs (lines 37-49)
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}
examples/game.rs (lines 152-199)
141fn main() -> Result<()> {
142    let db = Arc::new(Database::open(Config::in_memory())?);
143    db.register::<Hero>()?;
144    db.register::<Item>()?;
145    db.register::<PartyMember>()?;
146
147    let (ada, bram, cleo) = (1u64, 2u64, 3u64);
148
149    // -----------------------------------------------------------------------
150    act(1, "The world is created");
151
152    db.transaction(|tx| {
153        tx.insert(Hero {
154            id: ada,
155            name: "Ada".into(),
156            gold: 120,
157            strength: 100,
158        })?;
159        tx.insert(Hero {
160            id: bram,
161            name: "Bram".into(),
162            gold: 80,
163            strength: 100,
164        })?;
165        tx.insert(Hero {
166            id: cleo,
167            name: "Cleo".into(),
168            gold: 200,
169            strength: 60,
170        })?;
171
172        let mut item = |id, owner, name: &str, weight, value, kind| {
173            tx.insert(Item {
174                id,
175                owner,
176                name: name.into(),
177                weight,
178                value,
179                kind,
180                flavour: HashMap::new(),
181            })
182        };
183        item(10, ada, "Rusty Sword", 30, 15, Kind::Weapon)?;
184        item(11, ada, "Health Potion", 5, 20, Kind::Potion)?;
185        item(12, bram, "Oak Shield", 40, 35, Kind::Weapon)?;
186        item(13, cleo, "Lucky Charm", 2, 90, Kind::Trinket)?;
187        // Loot lying in the dungeon, owned by nobody.
188        item(20, GROUND, "Flaming Greatsword", 55, 500, Kind::Weapon)?;
189        item(21, GROUND, "Elven Cloak", 20, 240, Kind::Trinket)?;
190        item(22, GROUND, "Iron Helm", 35, 60, Kind::Weapon)?;
191
192        // Arbitrary per-item state, in a database row, with no schema to
193        // declare and no serialisation format to agree on.
194        tx.update::<Item>(&20, |i| {
195            i.flavour.insert("enchantment".into(), "flame".into());
196            i.flavour.insert("forged_by".into(), "Durin".into());
197        })?;
198        Ok(())
199    })?;
200
201    // A whole transaction that fails partway leaves nothing behind. The unique
202    // index on `name` refuses the second Ada, and the gold change goes with it.
203    let before = gold_of(&mut db.begin(), ada)?;
204    let doomed = db.transaction(|tx| {
205        tx.update::<Hero>(&ada, |h| h.gold += 10_000)?;
206        tx.insert(Hero {
207            id: 99,
208            name: "Ada".into(),
209            gold: 0,
210            strength: 10,
211        })
212    });
213    println!("  a duplicate hero name: {}", doomed.unwrap_err());
214    println!(
215        "  Ada's gold is still {} — the whole transaction rolled back, not just the insert",
216        gold_of(&mut db.begin(), ada)?
217    );
218    assert_eq!(gold_of(&mut db.begin(), ada)?, before);
219
220    let mut tx = db.begin();
221    for hero in [ada, bram, cleo] {
222        let name = name_of(&mut tx, hero)?;
223        let carried = carried(&mut tx, hero)?;
224        let items = inventory(&mut tx, hero)?;
225        let names: Vec<_> = items.iter().map(|(_, n, _)| n.as_str()).collect();
226        println!("  {name:<5} carries {carried:>3} — {}", names.join(", "));
227    }
228    drop(tx);
229
230    // -----------------------------------------------------------------------
231    act(2, "Two heroes reach for the same greatsword");
232
233    // Both begin, both see the sword unclaimed. The first to write takes it;
234    // the second is told immediately rather than being made to wait.
235    let mut ada_grabs = db.begin_with::<Snapshot>();
236    let mut bram_grabs = db.begin_with::<Snapshot>();
237
238    ada_grabs.update::<Item>(&20, |i| i.owner = ada)?;
239
240    match bram_grabs.update::<Item>(&20, |i| i.owner = bram) {
241        Err(e @ Error::WriteConflict { .. }) => {
242            println!("  Bram: {e}");
243            println!(
244                "  ...retriable: {} — he can try for something else",
245                e.is_retriable()
246            );
247        }
248        other => unreachable!("expected a conflict, got {other:?}"),
249    }
250
251    // A failed write does not end the transaction. Bram takes the cloak instead.
252    bram_grabs.update::<Item>(&21, |i| i.owner = bram)?;
253    ada_grabs.commit()?;
254    bram_grabs.commit()?;
255    println!("  Ada takes the greatsword, Bram takes the cloak. Nobody blocked.");
256
257    // -----------------------------------------------------------------------
258    act(3, "Cleo overloads herself — write skew, and the fix");
259
260    // Cleo can carry 60 and is carrying 2. Either 35kg helm fits; both do not.
261    // Each transaction reads her inventory, checks the total, and picks up a
262    // *different* item — so the two writes never touch the same record, and
263    // first-updater-wins has nothing to catch.
264    db.transaction(|tx| {
265        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
266        tx.insert(Item {
267            id: 23,
268            owner: GROUND,
269            name: "Steel Helm".into(),
270            weight: 35,
271            value: 70,
272            kind: Kind::Weapon,
273            flavour: HashMap::new(),
274        })
275    })?;
276
277    let pick_up = |item: u64| {
278        move |tx: &mut mvcc::Transaction<'_, Serializable>| -> Result<bool> {
279            let hero = tx.get::<Hero>(&cleo)?.expect("Cleo exists");
280            let strength = hero.strength;
281            let load = carried(tx, cleo)?;
282            let weight = tx.get::<Item>(&item)?.map(|i| i.weight).unwrap_or(0);
283            if load + weight > strength {
284                return Ok(false);
285            }
286            tx.update::<Item>(&item, |i| i.owner = cleo)?;
287            Ok(true)
288        }
289    };
290
291    // Under Snapshot, both checks pass against a stale inventory.
292    {
293        let mut t1 = db.begin_with::<Snapshot>();
294        let mut t2 = db.begin_with::<Snapshot>();
295        let (load1, load2) = (carried(&mut t1, cleo)?, carried(&mut t2, cleo)?);
296        println!(
297            "  Snapshot:     both transactions see {load1}/{load2} carried, both think a 35kg helm fits"
298        );
299        t1.update::<Item>(&22, |i| i.owner = cleo)?;
300        t2.update::<Item>(&23, |i| i.owner = cleo)?;
301        t1.commit()?;
302        t2.commit()?;
303        let over = carried(&mut db.begin(), cleo)?;
304        println!(
305            "  ✗ Cleo now carries {over} of a possible 60. Two legal transactions, one broken rule."
306        );
307    }
308
309    // Put it back and try again at Serializable.
310    db.transaction(|tx| {
311        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
312        tx.update::<Item>(&23, |i| i.owner = GROUND).map(|_| ())
313    })?;
314
315    {
316        let mut t1 = db.begin_with::<Serializable>();
317        let mut t2 = db.begin_with::<Serializable>();
318        // Both read the inventory, as before.
319        let _ = carried(&mut t1, cleo)?;
320        let _ = carried(&mut t2, cleo)?;
321        t1.update::<Item>(&22, |i| i.owner = cleo)?;
322        t2.update::<Item>(&23, |i| i.owner = cleo)?;
323        t1.commit()?;
324        match t2.commit() {
325            Err(e @ Error::SerializationFailure) => println!("  Serializable: second pickup {e}"),
326            other => unreachable!("expected a serialization failure, got {other:?}"),
327        }
328        println!(
329            "  ✓ Cleo carries {} — the second pickup read an inventory the first changed.",
330            carried(&mut db.begin(), cleo)?
331        );
332    }
333
334    // In real code you would not hand-roll that. `transaction_with` retries,
335    // and the retry re-reads the inventory and correctly declines.
336    let took_it = db.transaction_with::<Serializable, _, _>(pick_up(23))?;
337    println!("  ...and on retry the second helm is refused on its merits: picked_up = {took_it}");
338
339    // -----------------------------------------------------------------------
340    act(4, "The party fills up — a phantom, not a conflict");
341
342    db.transaction(|tx| {
343        tx.insert(PartyMember {
344            hero: ada,
345            party: PARTY,
346            role: "Vanguard".into(),
347        })?;
348        tx.insert(PartyMember {
349            hero: bram,
350            party: PARTY,
351            role: "Shield".into(),
352        })?;
353        tx.insert(PartyMember {
354            hero: cleo,
355            party: PARTY,
356            role: "Scout".into(),
357        })
358    })?;
359
360    // Two newcomers apply at once, with three of four slots taken. Neither
361    // writes a row the other wrote — they insert *different* rows — so there is
362    // no write conflict to detect. What they collide on is a row that did not
363    // exist when either of them counted.
364    let (dara, finn) = (4u64, 5u64);
365    db.transaction(|tx| {
366        tx.insert(Hero {
367            id: dara,
368            name: "Dara".into(),
369            gold: 40,
370            strength: 80,
371        })?;
372        tx.insert(Hero {
373            id: finn,
374            name: "Finn".into(),
375            gold: 40,
376            strength: 80,
377        })
378    })?;
379
380    let mut d = db.begin_with::<Serializable>();
381    let mut f = db.begin_with::<Serializable>();
382    println!(
383        "  Dara counts {} members, Finn counts {} — both see a free slot",
384        party_size(&mut d)?,
385        party_size(&mut f)?
386    );
387    d.insert(PartyMember {
388        hero: dara,
389        party: PARTY,
390        role: "Healer".into(),
391    })?;
392    f.insert(PartyMember {
393        hero: finn,
394        party: PARTY,
395        role: "Healer".into(),
396    })?;
397    d.commit()?;
398    match f.commit() {
399        Err(e @ Error::SerializationFailure) => println!("  Finn: {e}"),
400        other => unreachable!("expected a serialization failure, got {other:?}"),
401    }
402    let mut tx = db.begin();
403    let mut roster: Vec<_> = tx
404        .scan_where::<PartyMember, _>(|m| m.party == PARTY)?
405        .iter()
406        .map(|m| (m.hero, m.role.clone()))
407        .collect();
408    drop(tx);
409    roster.sort();
410    let size = roster.len();
411    let mut tx = db.begin();
412    let listed: Vec<String> = roster
413        .iter()
414        .map(|(h, role)| Ok(format!("{} the {role}", name_of(&mut tx, *h)?)))
415        .collect::<Result<_>>()?;
416    drop(tx);
417    println!("  party: {}", listed.join(", "));
418    println!("  ✓ {size}/{PARTY_LIMIT} filled. Finn's insert became a phantom in Dara's count.");
419    println!("    (`scan_where` hands the engine the predicate, so it can re-check it at commit.)");
420    assert!(size <= PARTY_LIMIT);
421
422    // -----------------------------------------------------------------------
423    act(5, "A trade — two records, one atomic step");
424
425    let before = total_gold(&db)?;
426
427    let trade = |seller: u64, buyer: u64, item: u64, price: i64| {
428        let db = Arc::clone(&db);
429        move || -> Result<bool> {
430            db.transaction_with::<Serializable, _, _>(|tx| {
431                let buyer_gold = gold_of(tx, buyer)?;
432                if buyer_gold < price {
433                    return Ok(false);
434                }
435                let owned_by_seller = tx
436                    .get::<Item>(&item)?
437                    .map(|i| i.owner == seller)
438                    .unwrap_or(false);
439                if !owned_by_seller {
440                    return Ok(false);
441                }
442                tx.update::<Hero>(&buyer, |h| h.gold -= price)?;
443                tx.update::<Hero>(&seller, |h| h.gold += price)?;
444                tx.update::<Item>(&item, |i| i.owner = buyer)?;
445                Ok(true)
446            })
447        }
448    };
449
450    let sold = trade(bram, cleo, 21, 150)()?;
451    println!("  Bram sells the Elven Cloak to Cleo for 150g: {sold}");
452    let mut tx = db.begin();
453    let cloak_owner = tx.get::<Item>(&21)?.expect("cloak").owner;
454    println!(
455        "  Bram {}g, Cleo {}g, cloak owner is {}",
456        gold_of(&mut tx, bram)?,
457        gold_of(&mut tx, cleo)?,
458        name_of(&mut tx, cloak_owner)?
459    );
460    drop(tx);
461    assert_eq!(total_gold(&db)?, before, "gold must be conserved");
462    println!("  ✓ total gold unchanged at {before} — no step of that was separately visible");
463
464    // -----------------------------------------------------------------------
465    act(6, "The chronicler reads while the world moves");
466
467    // A long report scanning the whole world while combat rewrites it. It never
468    // blocks a writer, and its view never shifts underneath it.
469    let mut chronicler = db.begin_with::<Snapshot>();
470    let opening = total_gold(&db)?;
471    let seen_first = chronicler.scan::<Hero>()?.len();
472
473    for _ in 0..50 {
474        db.transaction(|tx| {
475            tx.update::<Hero>(&ada, |h| h.gold += 1)?;
476            tx.update::<Hero>(&bram, |h| h.gold -= 1)?;
477            Ok(())
478        })?;
479    }
480    db.transaction(|tx| {
481        tx.insert(Hero {
482            id: 6,
483            name: "Mira".into(),
484            gold: 0,
485            strength: 50,
486        })
487    })?;
488
489    let chron_gold: i64 = chronicler.scan::<Hero>()?.iter().map(|h| h.gold).sum();
490    let seen_after = chronicler.scan::<Hero>()?.len();
491    let in_world = db.begin().scan::<Hero>()?.len();
492    println!("  chronicler saw {seen_first} heroes at the start and still sees {seen_after};");
493    println!("  the world now holds {in_world}, after 50 commits and one new arrival");
494    println!(
495        "  chronicler totals {chron_gold}g; the world now totals {}g",
496        total_gold(&db)?
497    );
498    assert_eq!(chron_gold, opening, "a snapshot must not move");
499    chronicler.commit()?;
500    println!("  ✓ a reader that ran across 51 commits saw exactly one consistent world");
501
502    // -----------------------------------------------------------------------
503    act(7, "The raid — many adventurers at once");
504
505    let stop = Arc::new(AtomicBool::new(false));
506    let trades = Arc::new(AtomicU64::new(0));
507    let retries = Arc::new(AtomicU64::new(0));
508    let audits = Arc::new(AtomicU64::new(0));
509    let opening = total_gold(&db)?;
510    let heroes = [ada, bram, cleo, dara, finn];
511
512    // An auditor that must never catch a trade half-applied.
513    let auditor = {
514        let (db, stop, audits) = (Arc::clone(&db), Arc::clone(&stop), Arc::clone(&audits));
515        thread::spawn(move || -> Result<()> {
516            while !stop.load(Ordering::Relaxed) {
517                let mut tx = db.begin();
518                let total: i64 = tx.scan::<Hero>()?.iter().map(|h| h.gold).sum();
519                assert_eq!(total, opening, "auditor saw a half-finished trade");
520                audits.fetch_add(1, Ordering::Relaxed);
521            }
522            Ok(())
523        })
524    };
525
526    let start = Instant::now();
527    let raiders: Vec<_> = (0..4)
528        .map(|t| {
529            let (db, trades, retries) =
530                (Arc::clone(&db), Arc::clone(&trades), Arc::clone(&retries));
531            thread::spawn(move || -> Result<()> {
532                let mut seed = 0x9e37_79b9_7f4a_7c15u64 ^ (t + 1);
533                for _ in 0..400 {
534                    seed ^= seed << 13;
535                    seed ^= seed >> 7;
536                    seed ^= seed << 17;
537                    let from = heroes[(seed % heroes.len() as u64) as usize];
538                    let to = heroes[((seed >> 8) % heroes.len() as u64) as usize];
539                    if from == to {
540                        continue;
541                    }
542
543                    let mut attempts = 0u64;
544                    // Serializable, because the payment depends on a balance we
545                    // read — the same shape as Cleo's carry limit in Act 3.
546                    db.transaction_with::<Serializable, _, _>(|tx| {
547                        attempts += 1;
548                        let purse = gold_of(tx, from)?;
549                        if purse < 5 {
550                            return Ok(());
551                        }
552                        tx.update::<Hero>(&from, |h| h.gold -= 5)?;
553                        tx.update::<Hero>(&to, |h| h.gold += 5)?;
554                        Ok(())
555                    })?;
556                    trades.fetch_add(1, Ordering::Relaxed);
557                    retries.fetch_add(attempts - 1, Ordering::Relaxed);
558                }
559                Ok(())
560            })
561        })
562        .collect();
563
564    for r in raiders {
565        r.join().expect("raider panicked")?;
566    }
567    let elapsed = start.elapsed();
568    stop.store(true, Ordering::Relaxed);
569    auditor.join().expect("auditor panicked")?;
570
571    let done = trades.load(Ordering::Relaxed);
572    println!("  {done} trades across 4 threads in {elapsed:.2?}");
573    println!(
574        "  {} audit sweeps completed alongside them, none blocked, none torn",
575        audits.load(Ordering::Relaxed)
576    );
577    println!(
578        "  {} retries ({:.2} per trade) — contention on 5 heroes, handled by the engine",
579        retries.load(Ordering::Relaxed),
580        retries.load(Ordering::Relaxed) as f64 / done.max(1) as f64
581    );
582    assert_eq!(total_gold(&db)?, opening, "gold was created or destroyed");
583    println!("  ✓ total gold still {opening}");
584
585    // -----------------------------------------------------------------------
586    act(8, "Closing the ledger");
587
588    let mut tx = db.begin();
589    let mut roster: Vec<_> = tx
590        .scan::<Hero>()?
591        .iter()
592        .map(|h| (h.id, h.name.clone(), h.gold))
593        .collect();
594    roster.sort_by_key(|(id, _, _)| *id);
595    drop(tx);
596
597    println!(
598        "  {:<6} {:>6} {:>5} {:>7}  inventory",
599        "hero", "gold", "load", "worth"
600    );
601    let mut tx = db.begin();
602    for (id, name, gold) in &roster {
603        let held = tx.scan_index(Item::OWNER, *id..=*id)?;
604        let load: i32 = held.iter().map(|i| i.weight).sum();
605        let worth: i64 = held.iter().map(|i| i.value).sum();
606        let names: Vec<String> = held
607            .iter()
608            .map(|i| match i.kind {
609                Kind::Weapon => format!("[wpn] {}", i.name),
610                Kind::Potion => format!("[pot] {}", i.name),
611                Kind::Trinket => format!("[trk] {}", i.name),
612            })
613            .collect();
614        let names = if names.is_empty() {
615            "—".to_string()
616        } else {
617            names.join(", ")
618        };
619        println!("  {name:<6} {gold:>5}g {load:>4}kg {worth:>6}g  {names}");
620    }
621
622    // Potions anywhere in the world, found by predicate rather than by key —
623    // and `kind` is an ordinary Rust enum the engine knows nothing about.
624    let potions = tx.scan_where::<Item, _>(|i| i.kind == Kind::Potion)?.len();
625    let sword = tx.get::<Item>(&20)?.expect("greatsword");
626    let enchantment = sword
627        .flavour
628        .get("enchantment")
629        .cloned()
630        .unwrap_or_default();
631    let forged_by = sword.flavour.get("forged_by").cloned().unwrap_or_default();
632    drop(tx);
633    // The rule Act 3 was about, checked rather than asserted in prose.
634    let mut tx = db.begin();
635    for (id, name, _) in &roster {
636        let strength = tx.get::<Hero>(id)?.map(|h| h.strength).unwrap_or(0);
637        let load = carried(&mut tx, *id)?;
638        assert!(
639            load <= strength,
640            "{name} is over their carry limit: {load} > {strength}"
641        );
642    }
643    drop(tx);
644
645    println!("\n  potions in the world: {potions}");
646    println!("  the greatsword is {enchantment}-enchanted, forged by {forged_by}");
647
648    // Nothing is reclaimed while a transaction that could still reach it is
649    // alive. This is the number to watch in a long-lived process: a forgotten
650    // transaction pins it and version chains grow without limit.
651    let stats = db.stats();
652    println!(
653        "\n  gc watermark {:?}, {} transactions still live",
654        stats.watermark, stats.active_transactions
655    );
656    println!("  (a watermark that stops moving while writes continue is the leak to look for)");
657
658    Ok(())
659}

pub fn transaction_with<I, R, F>(&self, f: F) -> Result<R>
where I: IsolationLevel, F: FnMut(&mut Transaction<'_, I>) -> Result<R>,

Run f in a transaction at isolation level I, retrying while it fails retriably, and commit.

Reach for Serializable here when a transaction’s write depends on a value it merely read — a balance check, a capacity limit. That is the write-skew shape, and snapshot isolation does not catch it.

// Withdraw only if the balance covers it. The check and the write must
// be serializable together, or two concurrent transfers each see a
// sufficient balance and both withdraw.
let moved = db.transaction_with::<Serializable, _, _>(|tx| {
    let balance = tx.get::<Account>(&1)?.map_or(0, |a| a.balance);
    if balance < 50 {
        return Ok(false);
    }
    tx.update::<Account>(&1, |a| a.balance -= 50)?;
    tx.update::<Account>(&2, |a| a.balance += 50)?;
    Ok(true)
})?;

assert!(moved);

As with Database::transaction, f may run more than once, so it must not have side effects outside the transaction. Return the value and let the caller act on the committed result, as above.

Examples found in repository?
examples/concurrent.rs (lines 100-109)
29fn main() -> Result<()> {
30    let db = Arc::new(Database::open(Config::in_memory())?);
31    db.register::<Account>()?;
32
33    db.transaction(|tx| {
34        for id in 0..ACCOUNTS {
35            tx.insert(Account {
36                id,
37                balance: STARTING_BALANCE,
38            })?;
39        }
40        Ok(())
41    })?;
42
43    let total_before = total(&db)?;
44    println!("starting total: {total_before}");
45
46    let stop = Arc::new(AtomicBool::new(false));
47    let reads = Arc::new(AtomicU64::new(0));
48    let retries = Arc::new(AtomicU64::new(0));
49
50    // ---- a reader that must never observe a torn transfer -----------------
51    // Money moves between accounts constantly. Under snapshot isolation this
52    // thread's scans must always sum to the same total: it either sees both
53    // halves of a transfer or neither.
54    let auditor = {
55        let db = Arc::clone(&db);
56        let stop = Arc::clone(&stop);
57        let reads = Arc::clone(&reads);
58        thread::spawn(move || -> Result<()> {
59            while !stop.load(Ordering::Relaxed) {
60                let mut tx = db.begin();
61                let sum: i64 = tx.scan::<Account>()?.iter().map(|a| a.balance).sum();
62                assert_eq!(
63                    sum,
64                    ACCOUNTS as i64 * STARTING_BALANCE,
65                    "a reader observed a partially applied transfer"
66                );
67                reads.fetch_add(1, Ordering::Relaxed);
68            }
69            Ok(())
70        })
71    };
72
73    // ---- writers ----------------------------------------------------------
74    let start = Instant::now();
75    let mut writers = Vec::new();
76    for t in 0..TRANSFER_THREADS {
77        let db = Arc::clone(&db);
78        let retries = Arc::clone(&retries);
79        writers.push(thread::spawn(move || -> Result<()> {
80            // A cheap per-thread PRNG; no dependency needed for this.
81            let mut seed = 0x9e37_79b9_7f4a_7c15u64 ^ (t as u64 + 1);
82            let mut next = move || {
83                seed ^= seed << 13;
84                seed ^= seed >> 7;
85                seed ^= seed << 17;
86                seed
87            };
88
89            for _ in 0..TRANSFERS_PER_THREAD {
90                let from = next() % ACCOUNTS;
91                let to = next() % ACCOUNTS;
92                if from == to {
93                    continue;
94                }
95
96                let mut attempts = 0u64;
97                // Serializable, because the transfer's *write* depends on a
98                // balance it *read* — the write-skew shape. Snapshot isolation
99                // would let two concurrent transfers both pass the check.
100                db.transaction_with::<Serializable, _, _>(|tx| {
101                    attempts += 1;
102                    let balance = tx.get::<Account>(&from)?.map(|a| a.balance).unwrap_or(0);
103                    if balance < 10 {
104                        return Ok(());
105                    }
106                    tx.update::<Account>(&from, |a| a.balance -= 10)?;
107                    tx.update::<Account>(&to, |a| a.balance += 10)?;
108                    Ok(())
109                })?;
110                retries.fetch_add(attempts - 1, Ordering::Relaxed);
111            }
112            Ok(())
113        }));
114    }
115
116    for w in writers {
117        w.join().expect("writer panicked")?;
118    }
119    let elapsed = start.elapsed();
120
121    stop.store(true, Ordering::Relaxed);
122    auditor.join().expect("auditor panicked")?;
123
124    // ---- results ----------------------------------------------------------
125    let total_after = total(&db)?;
126    let committed = (TRANSFER_THREADS * TRANSFERS_PER_THREAD) as u64;
127
128    println!("final total:    {total_after}");
129    assert_eq!(total_before, total_after, "money was created or destroyed");
130
131    println!("\n{committed} transfers in {elapsed:.2?}");
132    println!(
133        "  {:.0} transfers/sec",
134        committed as f64 / elapsed.as_secs_f64()
135    );
136    println!(
137        "  {} concurrent audit scans completed, none of them blocked",
138        reads.load(Ordering::Relaxed)
139    );
140    println!(
141        "  {} retries ({:.1} per transfer)",
142        retries.load(Ordering::Relaxed),
143        retries.load(Ordering::Relaxed) as f64 / committed as f64
144    );
145
146    // The GC watermark should be advancing. If `active_transactions` climbs and
147    // the watermark stalls, some transaction is being leaked — see engine::gc.
148    let stats = db.stats();
149    println!(
150        "\ngc watermark {:?}, {} transactions still live",
151        stats.watermark, stats.active_transactions
152    );
153
154    println!("\n✓ no torn reads, no lost updates, conservation held");
155
156    // Slow readers are what pin the GC watermark. Left here as the shape of the
157    // thing to watch for, not as something this example demonstrates going wrong.
158    thread::sleep(Duration::from_millis(1));
159    Ok(())
160}
More examples
Hide additional examples
examples/isolation.rs (lines 185-193)
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}
examples/game.rs (line 336)
141fn main() -> Result<()> {
142    let db = Arc::new(Database::open(Config::in_memory())?);
143    db.register::<Hero>()?;
144    db.register::<Item>()?;
145    db.register::<PartyMember>()?;
146
147    let (ada, bram, cleo) = (1u64, 2u64, 3u64);
148
149    // -----------------------------------------------------------------------
150    act(1, "The world is created");
151
152    db.transaction(|tx| {
153        tx.insert(Hero {
154            id: ada,
155            name: "Ada".into(),
156            gold: 120,
157            strength: 100,
158        })?;
159        tx.insert(Hero {
160            id: bram,
161            name: "Bram".into(),
162            gold: 80,
163            strength: 100,
164        })?;
165        tx.insert(Hero {
166            id: cleo,
167            name: "Cleo".into(),
168            gold: 200,
169            strength: 60,
170        })?;
171
172        let mut item = |id, owner, name: &str, weight, value, kind| {
173            tx.insert(Item {
174                id,
175                owner,
176                name: name.into(),
177                weight,
178                value,
179                kind,
180                flavour: HashMap::new(),
181            })
182        };
183        item(10, ada, "Rusty Sword", 30, 15, Kind::Weapon)?;
184        item(11, ada, "Health Potion", 5, 20, Kind::Potion)?;
185        item(12, bram, "Oak Shield", 40, 35, Kind::Weapon)?;
186        item(13, cleo, "Lucky Charm", 2, 90, Kind::Trinket)?;
187        // Loot lying in the dungeon, owned by nobody.
188        item(20, GROUND, "Flaming Greatsword", 55, 500, Kind::Weapon)?;
189        item(21, GROUND, "Elven Cloak", 20, 240, Kind::Trinket)?;
190        item(22, GROUND, "Iron Helm", 35, 60, Kind::Weapon)?;
191
192        // Arbitrary per-item state, in a database row, with no schema to
193        // declare and no serialisation format to agree on.
194        tx.update::<Item>(&20, |i| {
195            i.flavour.insert("enchantment".into(), "flame".into());
196            i.flavour.insert("forged_by".into(), "Durin".into());
197        })?;
198        Ok(())
199    })?;
200
201    // A whole transaction that fails partway leaves nothing behind. The unique
202    // index on `name` refuses the second Ada, and the gold change goes with it.
203    let before = gold_of(&mut db.begin(), ada)?;
204    let doomed = db.transaction(|tx| {
205        tx.update::<Hero>(&ada, |h| h.gold += 10_000)?;
206        tx.insert(Hero {
207            id: 99,
208            name: "Ada".into(),
209            gold: 0,
210            strength: 10,
211        })
212    });
213    println!("  a duplicate hero name: {}", doomed.unwrap_err());
214    println!(
215        "  Ada's gold is still {} — the whole transaction rolled back, not just the insert",
216        gold_of(&mut db.begin(), ada)?
217    );
218    assert_eq!(gold_of(&mut db.begin(), ada)?, before);
219
220    let mut tx = db.begin();
221    for hero in [ada, bram, cleo] {
222        let name = name_of(&mut tx, hero)?;
223        let carried = carried(&mut tx, hero)?;
224        let items = inventory(&mut tx, hero)?;
225        let names: Vec<_> = items.iter().map(|(_, n, _)| n.as_str()).collect();
226        println!("  {name:<5} carries {carried:>3} — {}", names.join(", "));
227    }
228    drop(tx);
229
230    // -----------------------------------------------------------------------
231    act(2, "Two heroes reach for the same greatsword");
232
233    // Both begin, both see the sword unclaimed. The first to write takes it;
234    // the second is told immediately rather than being made to wait.
235    let mut ada_grabs = db.begin_with::<Snapshot>();
236    let mut bram_grabs = db.begin_with::<Snapshot>();
237
238    ada_grabs.update::<Item>(&20, |i| i.owner = ada)?;
239
240    match bram_grabs.update::<Item>(&20, |i| i.owner = bram) {
241        Err(e @ Error::WriteConflict { .. }) => {
242            println!("  Bram: {e}");
243            println!(
244                "  ...retriable: {} — he can try for something else",
245                e.is_retriable()
246            );
247        }
248        other => unreachable!("expected a conflict, got {other:?}"),
249    }
250
251    // A failed write does not end the transaction. Bram takes the cloak instead.
252    bram_grabs.update::<Item>(&21, |i| i.owner = bram)?;
253    ada_grabs.commit()?;
254    bram_grabs.commit()?;
255    println!("  Ada takes the greatsword, Bram takes the cloak. Nobody blocked.");
256
257    // -----------------------------------------------------------------------
258    act(3, "Cleo overloads herself — write skew, and the fix");
259
260    // Cleo can carry 60 and is carrying 2. Either 35kg helm fits; both do not.
261    // Each transaction reads her inventory, checks the total, and picks up a
262    // *different* item — so the two writes never touch the same record, and
263    // first-updater-wins has nothing to catch.
264    db.transaction(|tx| {
265        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
266        tx.insert(Item {
267            id: 23,
268            owner: GROUND,
269            name: "Steel Helm".into(),
270            weight: 35,
271            value: 70,
272            kind: Kind::Weapon,
273            flavour: HashMap::new(),
274        })
275    })?;
276
277    let pick_up = |item: u64| {
278        move |tx: &mut mvcc::Transaction<'_, Serializable>| -> Result<bool> {
279            let hero = tx.get::<Hero>(&cleo)?.expect("Cleo exists");
280            let strength = hero.strength;
281            let load = carried(tx, cleo)?;
282            let weight = tx.get::<Item>(&item)?.map(|i| i.weight).unwrap_or(0);
283            if load + weight > strength {
284                return Ok(false);
285            }
286            tx.update::<Item>(&item, |i| i.owner = cleo)?;
287            Ok(true)
288        }
289    };
290
291    // Under Snapshot, both checks pass against a stale inventory.
292    {
293        let mut t1 = db.begin_with::<Snapshot>();
294        let mut t2 = db.begin_with::<Snapshot>();
295        let (load1, load2) = (carried(&mut t1, cleo)?, carried(&mut t2, cleo)?);
296        println!(
297            "  Snapshot:     both transactions see {load1}/{load2} carried, both think a 35kg helm fits"
298        );
299        t1.update::<Item>(&22, |i| i.owner = cleo)?;
300        t2.update::<Item>(&23, |i| i.owner = cleo)?;
301        t1.commit()?;
302        t2.commit()?;
303        let over = carried(&mut db.begin(), cleo)?;
304        println!(
305            "  ✗ Cleo now carries {over} of a possible 60. Two legal transactions, one broken rule."
306        );
307    }
308
309    // Put it back and try again at Serializable.
310    db.transaction(|tx| {
311        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
312        tx.update::<Item>(&23, |i| i.owner = GROUND).map(|_| ())
313    })?;
314
315    {
316        let mut t1 = db.begin_with::<Serializable>();
317        let mut t2 = db.begin_with::<Serializable>();
318        // Both read the inventory, as before.
319        let _ = carried(&mut t1, cleo)?;
320        let _ = carried(&mut t2, cleo)?;
321        t1.update::<Item>(&22, |i| i.owner = cleo)?;
322        t2.update::<Item>(&23, |i| i.owner = cleo)?;
323        t1.commit()?;
324        match t2.commit() {
325            Err(e @ Error::SerializationFailure) => println!("  Serializable: second pickup {e}"),
326            other => unreachable!("expected a serialization failure, got {other:?}"),
327        }
328        println!(
329            "  ✓ Cleo carries {} — the second pickup read an inventory the first changed.",
330            carried(&mut db.begin(), cleo)?
331        );
332    }
333
334    // In real code you would not hand-roll that. `transaction_with` retries,
335    // and the retry re-reads the inventory and correctly declines.
336    let took_it = db.transaction_with::<Serializable, _, _>(pick_up(23))?;
337    println!("  ...and on retry the second helm is refused on its merits: picked_up = {took_it}");
338
339    // -----------------------------------------------------------------------
340    act(4, "The party fills up — a phantom, not a conflict");
341
342    db.transaction(|tx| {
343        tx.insert(PartyMember {
344            hero: ada,
345            party: PARTY,
346            role: "Vanguard".into(),
347        })?;
348        tx.insert(PartyMember {
349            hero: bram,
350            party: PARTY,
351            role: "Shield".into(),
352        })?;
353        tx.insert(PartyMember {
354            hero: cleo,
355            party: PARTY,
356            role: "Scout".into(),
357        })
358    })?;
359
360    // Two newcomers apply at once, with three of four slots taken. Neither
361    // writes a row the other wrote — they insert *different* rows — so there is
362    // no write conflict to detect. What they collide on is a row that did not
363    // exist when either of them counted.
364    let (dara, finn) = (4u64, 5u64);
365    db.transaction(|tx| {
366        tx.insert(Hero {
367            id: dara,
368            name: "Dara".into(),
369            gold: 40,
370            strength: 80,
371        })?;
372        tx.insert(Hero {
373            id: finn,
374            name: "Finn".into(),
375            gold: 40,
376            strength: 80,
377        })
378    })?;
379
380    let mut d = db.begin_with::<Serializable>();
381    let mut f = db.begin_with::<Serializable>();
382    println!(
383        "  Dara counts {} members, Finn counts {} — both see a free slot",
384        party_size(&mut d)?,
385        party_size(&mut f)?
386    );
387    d.insert(PartyMember {
388        hero: dara,
389        party: PARTY,
390        role: "Healer".into(),
391    })?;
392    f.insert(PartyMember {
393        hero: finn,
394        party: PARTY,
395        role: "Healer".into(),
396    })?;
397    d.commit()?;
398    match f.commit() {
399        Err(e @ Error::SerializationFailure) => println!("  Finn: {e}"),
400        other => unreachable!("expected a serialization failure, got {other:?}"),
401    }
402    let mut tx = db.begin();
403    let mut roster: Vec<_> = tx
404        .scan_where::<PartyMember, _>(|m| m.party == PARTY)?
405        .iter()
406        .map(|m| (m.hero, m.role.clone()))
407        .collect();
408    drop(tx);
409    roster.sort();
410    let size = roster.len();
411    let mut tx = db.begin();
412    let listed: Vec<String> = roster
413        .iter()
414        .map(|(h, role)| Ok(format!("{} the {role}", name_of(&mut tx, *h)?)))
415        .collect::<Result<_>>()?;
416    drop(tx);
417    println!("  party: {}", listed.join(", "));
418    println!("  ✓ {size}/{PARTY_LIMIT} filled. Finn's insert became a phantom in Dara's count.");
419    println!("    (`scan_where` hands the engine the predicate, so it can re-check it at commit.)");
420    assert!(size <= PARTY_LIMIT);
421
422    // -----------------------------------------------------------------------
423    act(5, "A trade — two records, one atomic step");
424
425    let before = total_gold(&db)?;
426
427    let trade = |seller: u64, buyer: u64, item: u64, price: i64| {
428        let db = Arc::clone(&db);
429        move || -> Result<bool> {
430            db.transaction_with::<Serializable, _, _>(|tx| {
431                let buyer_gold = gold_of(tx, buyer)?;
432                if buyer_gold < price {
433                    return Ok(false);
434                }
435                let owned_by_seller = tx
436                    .get::<Item>(&item)?
437                    .map(|i| i.owner == seller)
438                    .unwrap_or(false);
439                if !owned_by_seller {
440                    return Ok(false);
441                }
442                tx.update::<Hero>(&buyer, |h| h.gold -= price)?;
443                tx.update::<Hero>(&seller, |h| h.gold += price)?;
444                tx.update::<Item>(&item, |i| i.owner = buyer)?;
445                Ok(true)
446            })
447        }
448    };
449
450    let sold = trade(bram, cleo, 21, 150)()?;
451    println!("  Bram sells the Elven Cloak to Cleo for 150g: {sold}");
452    let mut tx = db.begin();
453    let cloak_owner = tx.get::<Item>(&21)?.expect("cloak").owner;
454    println!(
455        "  Bram {}g, Cleo {}g, cloak owner is {}",
456        gold_of(&mut tx, bram)?,
457        gold_of(&mut tx, cleo)?,
458        name_of(&mut tx, cloak_owner)?
459    );
460    drop(tx);
461    assert_eq!(total_gold(&db)?, before, "gold must be conserved");
462    println!("  ✓ total gold unchanged at {before} — no step of that was separately visible");
463
464    // -----------------------------------------------------------------------
465    act(6, "The chronicler reads while the world moves");
466
467    // A long report scanning the whole world while combat rewrites it. It never
468    // blocks a writer, and its view never shifts underneath it.
469    let mut chronicler = db.begin_with::<Snapshot>();
470    let opening = total_gold(&db)?;
471    let seen_first = chronicler.scan::<Hero>()?.len();
472
473    for _ in 0..50 {
474        db.transaction(|tx| {
475            tx.update::<Hero>(&ada, |h| h.gold += 1)?;
476            tx.update::<Hero>(&bram, |h| h.gold -= 1)?;
477            Ok(())
478        })?;
479    }
480    db.transaction(|tx| {
481        tx.insert(Hero {
482            id: 6,
483            name: "Mira".into(),
484            gold: 0,
485            strength: 50,
486        })
487    })?;
488
489    let chron_gold: i64 = chronicler.scan::<Hero>()?.iter().map(|h| h.gold).sum();
490    let seen_after = chronicler.scan::<Hero>()?.len();
491    let in_world = db.begin().scan::<Hero>()?.len();
492    println!("  chronicler saw {seen_first} heroes at the start and still sees {seen_after};");
493    println!("  the world now holds {in_world}, after 50 commits and one new arrival");
494    println!(
495        "  chronicler totals {chron_gold}g; the world now totals {}g",
496        total_gold(&db)?
497    );
498    assert_eq!(chron_gold, opening, "a snapshot must not move");
499    chronicler.commit()?;
500    println!("  ✓ a reader that ran across 51 commits saw exactly one consistent world");
501
502    // -----------------------------------------------------------------------
503    act(7, "The raid — many adventurers at once");
504
505    let stop = Arc::new(AtomicBool::new(false));
506    let trades = Arc::new(AtomicU64::new(0));
507    let retries = Arc::new(AtomicU64::new(0));
508    let audits = Arc::new(AtomicU64::new(0));
509    let opening = total_gold(&db)?;
510    let heroes = [ada, bram, cleo, dara, finn];
511
512    // An auditor that must never catch a trade half-applied.
513    let auditor = {
514        let (db, stop, audits) = (Arc::clone(&db), Arc::clone(&stop), Arc::clone(&audits));
515        thread::spawn(move || -> Result<()> {
516            while !stop.load(Ordering::Relaxed) {
517                let mut tx = db.begin();
518                let total: i64 = tx.scan::<Hero>()?.iter().map(|h| h.gold).sum();
519                assert_eq!(total, opening, "auditor saw a half-finished trade");
520                audits.fetch_add(1, Ordering::Relaxed);
521            }
522            Ok(())
523        })
524    };
525
526    let start = Instant::now();
527    let raiders: Vec<_> = (0..4)
528        .map(|t| {
529            let (db, trades, retries) =
530                (Arc::clone(&db), Arc::clone(&trades), Arc::clone(&retries));
531            thread::spawn(move || -> Result<()> {
532                let mut seed = 0x9e37_79b9_7f4a_7c15u64 ^ (t + 1);
533                for _ in 0..400 {
534                    seed ^= seed << 13;
535                    seed ^= seed >> 7;
536                    seed ^= seed << 17;
537                    let from = heroes[(seed % heroes.len() as u64) as usize];
538                    let to = heroes[((seed >> 8) % heroes.len() as u64) as usize];
539                    if from == to {
540                        continue;
541                    }
542
543                    let mut attempts = 0u64;
544                    // Serializable, because the payment depends on a balance we
545                    // read — the same shape as Cleo's carry limit in Act 3.
546                    db.transaction_with::<Serializable, _, _>(|tx| {
547                        attempts += 1;
548                        let purse = gold_of(tx, from)?;
549                        if purse < 5 {
550                            return Ok(());
551                        }
552                        tx.update::<Hero>(&from, |h| h.gold -= 5)?;
553                        tx.update::<Hero>(&to, |h| h.gold += 5)?;
554                        Ok(())
555                    })?;
556                    trades.fetch_add(1, Ordering::Relaxed);
557                    retries.fetch_add(attempts - 1, Ordering::Relaxed);
558                }
559                Ok(())
560            })
561        })
562        .collect();
563
564    for r in raiders {
565        r.join().expect("raider panicked")?;
566    }
567    let elapsed = start.elapsed();
568    stop.store(true, Ordering::Relaxed);
569    auditor.join().expect("auditor panicked")?;
570
571    let done = trades.load(Ordering::Relaxed);
572    println!("  {done} trades across 4 threads in {elapsed:.2?}");
573    println!(
574        "  {} audit sweeps completed alongside them, none blocked, none torn",
575        audits.load(Ordering::Relaxed)
576    );
577    println!(
578        "  {} retries ({:.2} per trade) — contention on 5 heroes, handled by the engine",
579        retries.load(Ordering::Relaxed),
580        retries.load(Ordering::Relaxed) as f64 / done.max(1) as f64
581    );
582    assert_eq!(total_gold(&db)?, opening, "gold was created or destroyed");
583    println!("  ✓ total gold still {opening}");
584
585    // -----------------------------------------------------------------------
586    act(8, "Closing the ledger");
587
588    let mut tx = db.begin();
589    let mut roster: Vec<_> = tx
590        .scan::<Hero>()?
591        .iter()
592        .map(|h| (h.id, h.name.clone(), h.gold))
593        .collect();
594    roster.sort_by_key(|(id, _, _)| *id);
595    drop(tx);
596
597    println!(
598        "  {:<6} {:>6} {:>5} {:>7}  inventory",
599        "hero", "gold", "load", "worth"
600    );
601    let mut tx = db.begin();
602    for (id, name, gold) in &roster {
603        let held = tx.scan_index(Item::OWNER, *id..=*id)?;
604        let load: i32 = held.iter().map(|i| i.weight).sum();
605        let worth: i64 = held.iter().map(|i| i.value).sum();
606        let names: Vec<String> = held
607            .iter()
608            .map(|i| match i.kind {
609                Kind::Weapon => format!("[wpn] {}", i.name),
610                Kind::Potion => format!("[pot] {}", i.name),
611                Kind::Trinket => format!("[trk] {}", i.name),
612            })
613            .collect();
614        let names = if names.is_empty() {
615            "—".to_string()
616        } else {
617            names.join(", ")
618        };
619        println!("  {name:<6} {gold:>5}g {load:>4}kg {worth:>6}g  {names}");
620    }
621
622    // Potions anywhere in the world, found by predicate rather than by key —
623    // and `kind` is an ordinary Rust enum the engine knows nothing about.
624    let potions = tx.scan_where::<Item, _>(|i| i.kind == Kind::Potion)?.len();
625    let sword = tx.get::<Item>(&20)?.expect("greatsword");
626    let enchantment = sword
627        .flavour
628        .get("enchantment")
629        .cloned()
630        .unwrap_or_default();
631    let forged_by = sword.flavour.get("forged_by").cloned().unwrap_or_default();
632    drop(tx);
633    // The rule Act 3 was about, checked rather than asserted in prose.
634    let mut tx = db.begin();
635    for (id, name, _) in &roster {
636        let strength = tx.get::<Hero>(id)?.map(|h| h.strength).unwrap_or(0);
637        let load = carried(&mut tx, *id)?;
638        assert!(
639            load <= strength,
640            "{name} is over their carry limit: {load} > {strength}"
641        );
642    }
643    drop(tx);
644
645    println!("\n  potions in the world: {potions}");
646    println!("  the greatsword is {enchantment}-enchanted, forged by {forged_by}");
647
648    // Nothing is reclaimed while a transaction that could still reach it is
649    // alive. This is the number to watch in a long-lived process: a forgotten
650    // transaction pins it and version chains grow without limit.
651    let stats = db.stats();
652    println!(
653        "\n  gc watermark {:?}, {} transactions still live",
654        stats.watermark, stats.active_transactions
655    );
656    println!("  (a watermark that stops moving while writes continue is the leak to look for)");
657
658    Ok(())
659}

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.