1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
use std::sync::Arc;
use atomo::{AtomoBuilder, DefaultSerdeBackend};
fn main() {
type Key = u64;
type Value = u64;
// Create a database with the provided configuration and setup.
// We should provide the different tables using the `with_table` method
// and provide the type for the key and value pair in that table.
//
// It is important to always use the same type for accessing the key and value on
// a table since Atomo perform runtime type confirmation to ensure that the same
// rust type is used on the table when accessing the table.
//
// Once we're done with providing the tables we call `build` to build and
// open the database.
//
// This ensure that only one update is happening at any given time.
let mut db = AtomoBuilder::<DefaultSerdeBackend>::new()
.with_table::<Key, Value>("name-of-table")
.build();
// Build returns an `Atomo<UpdatePerm, _>` which allows us to mutate the data
// there can only ever be one Atomo instance with update permission.
//
// Through Rust's borrow check logic we ensure that by explicitly making an
// `Atomo<UpdatePerm, _>: ~Clone`.
//
// But we can have as many `Atomo<QueryPerm, _>` as we want.
//
// Here we get access to a query runner instance.
let query_runner = db.query();
// It is generally a good idea to cache the table resolution. This removes the need to perform
// a table lookup and type check when accessing the table while running query/mutations.
//
// And since the type check happens during instantiating, you can avoid possible type
// mismatches and therefore is safer to do so.
let table_res = db.resolve::<Key, Value>("name-of-table");
// Insert `(0, 17)` to the table.
db.run(|ctx: &mut atomo::TableSelector<atomo::BincodeSerde>| {
let mut table_ref = table_res.get(ctx);
// Or if we didn't have a `ResolvedTableReference`.
// let mut table_ref = ctx.get_table::<Key, Value>("name-of-table");
table_ref.insert(0, 17);
});
// Here we demonstrate the consistent views that Atomo provides:
//
// We create a thread that will be responsible for running a query. The current thread will try
// to update a value in the middle of the execution of the query.
//
// We use two [`std::sync::Barrier`]s for synchronization.
//
// This is the linear log of the execution that we want to make happen:
//
// [Query Thread]: Enter the `run` closure. And print the value for key=0.
// [Main Thread]: Insert (key=0, value=12) to the table.
// [Query Thread]: Read the value for key=0 again.
//
// The value when read the second time MUST equal to the initial read (17) since
// the query is still running on the same snapshot of data.
let barrier_1 = Arc::new(std::sync::Barrier::new(2));
let barrier_2 = Arc::new(std::sync::Barrier::new(2));
let c_1 = barrier_1.clone();
let c_2 = barrier_2.clone();
let handle = std::thread::spawn(move || {
query_runner.run(|ctx: _| {
let table_ref = table_res.get(ctx);
println!("Query Started [get(0) == {:?}]", table_ref.get(0));
// Allow the main thread to continue.
c_1.wait();
// Waiting until the main thread updates the data.
c_2.wait();
println!("Query Finishing [get(0) == {:?}]", table_ref.get(0));
});
// Run a second query this should get the new data.
query_runner.run(|ctx: _| {
let table_ref = table_res.get(ctx);
println!("2nd Query Got [get(0) == {:?}]", table_ref.get(0));
});
});
// Wait for the query thread to 'start' running the query.
barrier_1.wait();
println!("Starting the update");
db.run(|ctx: _| {
let mut table_ref = table_res.get(ctx);
// Update the value and then allow the
table_ref.insert(0, 12);
println!("Value updated to 12");
});
// Allow the query thread to continue.
barrier_2.wait();
// Wait for the query thread to finish executing.
let _ = handle.join();
}