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