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
impl Database
pub fn open(config: Config) -> Result<Self>
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?
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
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}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}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
pub fn stats(&self) -> GcStats
Snapshot of engine statistics. See crate::stats for what to watch.
Examples found in repository?
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
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
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<()>
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?
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
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}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}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>
pub fn begin(&self) -> Transaction<'_, SnapshotLevel>
Begin a transaction at the default isolation level (snapshot isolation).
Examples found in repository?
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
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}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}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>
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?
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
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>
pub fn transaction<R, F>(&self, f: F) -> 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?
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
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}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}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>
pub fn transaction_with<I, R, F>(&self, f: F) -> 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?
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
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}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}