1use std::sync::Arc;
10use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
11use std::thread;
12use std::time::{Duration, Instant};
13
14use mvcc::{Config, Database, Mvcc, Result, Serializable};
15
16#[derive(Mvcc, Clone, Debug)]
17#[mvcc(table = "accounts")]
18struct Account {
19 #[mvcc(primary_key)]
20 id: u64,
21 balance: i64,
22}
23
24const ACCOUNTS: u64 = 64;
25const TRANSFER_THREADS: usize = 8;
26const TRANSFERS_PER_THREAD: usize = 500;
27const STARTING_BALANCE: i64 = 1_000;
28
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 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 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 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 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 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 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 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}