Skip to main content

Error

Enum Error 

#[non_exhaustive]
pub enum Error { WriteConflict { table: &'static str, }, SerializationFailure, DuplicateKey { table: &'static str, index: &'static str, }, Aborted, TableNotRegistered { table: &'static str, }, PrimaryKeyChanged { table: &'static str, }, }
Expand description

Everything an engine operation can fail with.

The split that matters is Error::is_retriable: WriteConflict and SerializationFailure are the engine reporting that two transactions could not both happen, and re-running is the correct response. The rest are programming mistakes that will fail again identically.

Database::transaction already loops on the retriable ones, so most code never matches on this at all.

use mvcc::{Config, Database, Error, Mvcc};

#[derive(Mvcc, Clone)]
struct Account {
    #[mvcc(primary_key)] id: u64,
    balance: i64,
}

let db = Database::open(Config::in_memory())?;
db.register::<Account>()?;
db.transaction(|tx| tx.insert(Account { id: 1, balance: 0 }))?;

// Inserting the same primary key twice is a programming error, not a race:
// it is not retriable, and `transaction` returns it rather than looping.
let err = db
    .transaction(|tx| tx.insert(Account { id: 1, balance: 0 }))
    .unwrap_err();

assert!(matches!(err, Error::DuplicateKey { .. }));
assert!(!err.is_retriable());

#[non_exhaustive]: matching must include a _ arm, so that a new failure mode is not a breaking change.

Variants (Non-exhaustive)§

This enum is marked as non-exhaustive
Non-exhaustive enums could have additional variants added in future. Therefore, when matching against variants of non-exhaustive enums, an extra wildcard arm must be added to account for any future variants.
§

WriteConflict

Another transaction committed a conflicting write to the same key after our snapshot. Retriable: re-run the transaction.

Fields

§table: &'static str

The table whose record was contended.

§

SerializationFailure

Serializable validation found that something this transaction read has since changed. Retriable, and expected under contention — this does not indicate a bug.

§

DuplicateKey

A unique index, or the primary key, already holds this value.

Fields

§table: &'static str

The table the value would have been written to.

§index: &'static str

The index that already holds it, or the primary key.

§

Aborted

The transaction was aborted, explicitly or by being dropped, and can no longer be used.

§

TableNotRegistered

The type was never passed to Database::register.

Fields

§table: &'static str

The unregistered type’s table name.

§

PrimaryKeyChanged

An update tried to change the primary key, which would move the record to a different slot and leave the old one holding a value that no longer matches its key. Delete and re-insert instead.

Fields

§table: &'static str

The table whose primary key the update tried to change.

Implementations§

§

impl Error

pub fn is_retriable(&self) -> bool

Whether re-running the transaction from the top is a reasonable response. Database::transaction loops on exactly this.

Examples found in repository?
examples/isolation.rs (line 98)
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 245)
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}

Trait Implementations§

§

impl Debug for Error

§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
§

impl Display for Error

§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
§

impl Error for Error

1.30.0 · Source§

fn source(&self) -> Option<&(dyn Error + 'static)>

Returns the lower-level source of this error, if any. Read more
1.0.0 · Source§

fn description(&self) -> &str

👎Deprecated since 1.42.0:

use the Display impl or to_string()

1.0.0 · Source§

fn cause(&self) -> Option<&dyn Error>

👎Deprecated since 1.33.0:

replaced by Error::source, which can support downcasting

Source§

fn provide<'a>(&'a self, request: &mut Request<'a>)

🔬This is a nightly-only experimental API. (error_generic_member_access)
Provides type-based access to context intended for error reports. Read more

Auto Trait Implementations§

§

impl Freeze for Error

§

impl RefUnwindSafe for Error

§

impl Send for Error

§

impl Sync for Error

§

impl Unpin for Error

§

impl UnsafeUnpin for Error

§

impl UnwindSafe for Error

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> ToString for T
where T: Display + ?Sized,

Source§

fn to_string(&self) -> String

Converts the given value to a String. 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.