From 7a173a8ae2f000235f40c0e748b9c33339dba87e Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 29 Mar 2023 09:03:53 +0300 Subject: [PATCH 001/128] Initial commit --- core/mvcc/.gitignore | 1 + core/mvcc/Cargo.lock | 515 ++++++++++++++++++++++++++++++++ core/mvcc/Cargo.toml | 5 + core/mvcc/LICENSE.md | 20 ++ core/mvcc/README.md | 7 + core/mvcc/database/Cargo.toml | 8 + core/mvcc/database/src/lib.rs | 540 ++++++++++++++++++++++++++++++++++ 7 files changed, 1096 insertions(+) create mode 100644 core/mvcc/.gitignore create mode 100644 core/mvcc/Cargo.lock create mode 100644 core/mvcc/Cargo.toml create mode 100644 core/mvcc/LICENSE.md create mode 100644 core/mvcc/README.md create mode 100644 core/mvcc/database/Cargo.toml create mode 100644 core/mvcc/database/src/lib.rs diff --git a/core/mvcc/.gitignore b/core/mvcc/.gitignore new file mode 100644 index 000000000..2f7896d1d --- /dev/null +++ b/core/mvcc/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/core/mvcc/Cargo.lock b/core/mvcc/Cargo.lock new file mode 100644 index 000000000..373c791b7 --- /dev/null +++ b/core/mvcc/Cargo.lock @@ -0,0 +1,515 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clipboard-win" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" +dependencies = [ + "error-code", + "str-buf", + "winapi", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "errno" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.45.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "error-code" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" +dependencies = [ + "libc", + "str-buf", +] + +[[package]] +name = "fd-lock" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9799aefb4a2e4a01cc47610b1dd47c18ab13d991f27bbcaed9296f5a53d5cbad" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys 0.45.0", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "io-lifetimes" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "libc" +version = "0.2.141" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" + +[[package]] +name = "linux-raw-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mvcc-rs" +version = "0.0.0" +dependencies = [ + "anyhow", + "rustyline", +] + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +dependencies = [ + "bitflags", + "cfg-if", + "libc", + "static_assertions", +] + +[[package]] +name = "proc-macro2" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + +[[package]] +name = "rustix" +version = "0.37.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aae838e49b3d63e9274e1c01833cc8139d3fec468c3b84688c628f44b1ae11d" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.45.0", +] + +[[package]] +name = "rustyline" +version = "11.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfc8644681285d1fb67a467fb3021bfea306b99b4146b166a1fe3ada965eece" +dependencies = [ + "bitflags", + "cfg-if", + "clipboard-win", + "dirs-next", + "fd-lock", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "scopeguard", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "winapi", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "str-buf" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" + +[[package]] +name = "syn" +version = "2.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c9da457c5285ac1f936ebd076af6dac17a61cfe7826f2076b4d015cf47bc8ec" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" diff --git a/core/mvcc/Cargo.toml b/core/mvcc/Cargo.toml new file mode 100644 index 000000000..04401335b --- /dev/null +++ b/core/mvcc/Cargo.toml @@ -0,0 +1,5 @@ +[workspace] +resolver = "2" +members = [ + "database", +] diff --git a/core/mvcc/LICENSE.md b/core/mvcc/LICENSE.md new file mode 100644 index 000000000..0c99a0831 --- /dev/null +++ b/core/mvcc/LICENSE.md @@ -0,0 +1,20 @@ +MIT License + +Copyright 2023 Pekka Enberg + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/core/mvcc/README.md b/core/mvcc/README.md new file mode 100644 index 000000000..de059824b --- /dev/null +++ b/core/mvcc/README.md @@ -0,0 +1,7 @@ +# MVCC for Rust + +This is a _work-in-progress_ Rust implementation of the Hekaton optimistic multiversion concurrency control algorithm. + +## References + +Larson et al. [High-Performance Concurrency Control Mechanisms for Main-Memory Databases](https://vldb.org/pvldb/vol5/p298_per-akelarson_vldb2012.pdf). VLDB '11 diff --git a/core/mvcc/database/Cargo.toml b/core/mvcc/database/Cargo.toml new file mode 100644 index 000000000..9789f8733 --- /dev/null +++ b/core/mvcc/database/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "mvcc-rs" +version = "0.0.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.70" +rustyline = "11.0.0" diff --git a/core/mvcc/database/src/lib.rs b/core/mvcc/database/src/lib.rs new file mode 100644 index 000000000..ab1b33c7d --- /dev/null +++ b/core/mvcc/database/src/lib.rs @@ -0,0 +1,540 @@ +//! Multiversion concurrency control (MVCC) for Rust. +//! +//! This module implements the main memory MVCC method outlined in the paper +//! "High-Performance Concurrency Control Mechanisms for Main-Memory Databases" +//! by Per-Åke Larson et al (VLDB, 2011). +//! +//! ## Data anomalies +//! +//! * A *dirty write* occurs when transaction T_m updates a value that is written by +//! transaction T_n but not yet committed. The MVCC algorithm prevents dirty +//! writes by validating that a row version is visible to transaction T_m before +//! allowing update to it. +//! +//! * A *dirty read* occurs when transaction T_m reads a value that was written by +//! transaction T_n but not yet committed. The MVCC algorithm prevents dirty +//! reads by validating that a row version is visible to transaction T_m. +//! +//! * A *fuzzy read* (non-repetable read) occurs when transaction T_m reads a +//! different value in the course of the transaction because another +//! transaction T_n has updated the value. +//! +//! TODO: phantom reads, lost updates, cursor lost updates, read skew, write skew. +//! +//! ## TODO +//! +//! * Optimistic reads and writes +//! * Garbage collection + +use std::cell::RefCell; +use std::collections::{HashMap, HashSet}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Mutex}; + +#[derive(Clone, Debug, PartialEq)] +pub struct Row { + pub id: u64, + pub data: String, +} + +/// A row version. +#[derive(Clone, Debug)] +struct RowVersion { + begin: TxTimestampOrID, + end: Option, + row: Row, +} + +/// A transaction timestamp or ID. +/// +/// Versions either track a timestamp or a transaction ID, depending on the +/// phase of the transaction. During the active phase, new versions track the +/// transaction ID in the `begin` and `end` fields. After a transaction commits, +/// versions switch to tracking timestamps. +#[derive(Clone, Debug, PartialEq)] +enum TxTimestampOrID { + Timestamp(u64), + TxID(u64), +} + +/// Transaction +#[derive(Debug, Clone)] +pub struct Transaction { + /// The state of the transaction. + state: TransactionState, + /// The transaction ID. + tx_id: u64, + /// The transaction begin timestamp. + begin_ts: u64, + /// The transaction write set. + write_set: HashSet, + /// The transaction read set. + read_set: RefCell>, +} + +impl Transaction { + fn new(tx_id: u64, begin_ts: u64) -> Transaction { + Transaction { + state: TransactionState::Active, + tx_id, + begin_ts, + write_set: HashSet::new(), + read_set: RefCell::new(HashSet::new()), + } + } + + fn insert_to_read_set(&self, id: u64) { + let mut read_set = self.read_set.borrow_mut(); + read_set.insert(id); + } + + fn insert_to_write_set(&mut self, id: u64) { + self.write_set.insert(id); + } +} + +/// Transaction state. +#[derive(Debug, Clone)] +enum TransactionState { + Active, + Preparing, + Committed, + Aborted, + Terminated, +} + +/// A database with MVCC. +#[derive(Debug)] +pub struct Database { + inner: Arc>>, +} + +type TxID = u64; + +/// Logical clock. +pub trait LogicalClock { + fn get_timestamp(&self) -> u64; +} + +/// A node-local clock backed by an atomic counter. +#[derive(Debug)] +pub struct LocalClock { + ts_sequence: AtomicU64, +} + +impl LocalClock { + pub fn new() -> Self { + Self { + ts_sequence: AtomicU64::new(0), + } + } +} + +impl LogicalClock for LocalClock { + fn get_timestamp(&self) -> u64 { + self.ts_sequence.fetch_add(1, Ordering::SeqCst) + } +} + +impl Default for LocalClock { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug)] +pub struct DatabaseInner { + rows: RefCell>>, + txs: RefCell>, + tx_ids: AtomicU64, + clock: Clock, +} + +impl Database { + /// Creates a new database. + pub fn new(clock: Clock) -> Self { + let inner = DatabaseInner { + rows: RefCell::new(HashMap::new()), + txs: RefCell::new(HashMap::new()), + tx_ids: AtomicU64::new(0), + clock, + }; + Self { + inner: Arc::new(Mutex::new(inner)), + } + } + + /// Inserts a new row into the database. + /// + /// This function inserts a new `row` into the database within the context + /// of the transaction `tx_id`. + /// + /// # Arguments + /// + /// * `tx_id` - the ID of the transaction in which to insert the new row. + /// * `row` - the row object containing the values to be inserted. + /// + pub fn insert(&self, tx_id: TxID, row: Row) { + let inner = self.inner.lock().unwrap(); + let mut txs = inner.txs.borrow_mut(); + let tx = txs.get_mut(&tx_id).unwrap(); + let id = row.id; + let row_version = RowVersion { + begin: TxTimestampOrID::TxID(tx.tx_id), + end: None, + row, + }; + let mut rows = inner.rows.borrow_mut(); + rows.entry(id).or_insert_with(Vec::new).push(row_version); + tx.insert_to_write_set(id); + } + + /// Updates a row in the database with new values. + /// + /// This function updates an existing row in the database within the + /// context of the transaction `tx_id`. The `row` argument identifies the + /// row to be updated as `id` and contains the new values to be inserted. + /// + /// If the row identified by the `id` does not exist, this function does + /// nothing and returns `false`. Otherwise, the function updates the row + /// with the new values and returns `true`. + /// + /// # Arguments + /// + /// * `tx_id` - the ID of the transaction in which to update the new row. + /// * `row` - the row object containing the values to be updated. + /// + /// # Returns + /// + /// Returns `true` if the row was successfully updated, and `false` otherwise. + pub fn update(&self, tx_id: TxID, row: Row) -> bool { + if !self.delete(tx_id, row.id) { + return false; + } + self.insert(tx_id, row); + true + } + + /// Deletes a row from the table with the given `id`. + /// + /// This function deletes an existing row `id` in the database within the + /// context of the transaction `tx_id`. + /// + /// # Arguments + /// + /// * `tx_id` - the ID of the transaction in which to delete the new row. + /// * `id` - the ID of the row to delete. + /// + /// # Returns + /// + /// Returns `true` if the row was successfully deleted, and `false` otherwise. + /// + pub fn delete(&self, tx: TxID, id: u64) -> bool { + let inner = self.inner.lock().unwrap(); + let mut rows = inner.rows.borrow_mut(); + let mut txs = inner.txs.borrow_mut(); + let row_versions = rows.get_mut(&id).unwrap(); + match row_versions.last_mut() { + Some(v) => { + let tx = txs.get(&tx).unwrap(); + if is_version_visible(&txs, tx, v) { + v.end = Some(TxTimestampOrID::TxID(tx.tx_id)); + } else { + return false; + } + } + None => unreachable!("no versions for row {}", id), + } + let tx = txs.get_mut(&tx).unwrap(); + tx.insert_to_write_set(id); + true + } + + /// Retrieves a row from the table with the given `id`. + /// + /// This operation is performed within the scope of the transaction identified + /// by `tx_id`. + /// + /// # Arguments + /// + /// * `tx_id` - The ID of the transaction to perform the read operation in. + /// * `id` - The ID of the row to retrieve. + /// + /// # Returns + /// + /// Returns `Some(row)` with the row data if the row with the given `id` exists, + /// and `None` otherwise. + pub fn read(&self, tx_id: TxID, id: u64) -> Option { + let inner = self.inner.lock().unwrap(); + let txs = inner.txs.borrow_mut(); + let tx = txs.get(&tx_id).unwrap(); + let rows = inner.rows.borrow(); + if let Some(row_versions) = rows.get(&id) { + for rv in row_versions.iter().rev() { + if is_version_visible(&txs, tx, rv) { + tx.insert_to_read_set(id); + return Some(rv.row.clone()); + } + } + } + None + } + + /// Begins a new transaction in the database. + /// + /// This function starts a new transaction in the database and returns a `TxID` value + /// that you can use to perform operations within the transaction. All changes made within the + /// transaction are isolated from other transactions until you commit the transaction. + pub fn begin_tx(&self) -> TxID { + let mut inner = self.inner.lock().unwrap(); + let tx_id = get_tx_id(&mut inner); + let begin_ts = get_timestamp(&mut inner); + let tx = Transaction::new(tx_id, begin_ts); + let mut txs = inner.txs.borrow_mut(); + txs.insert(tx_id, tx); + tx_id + } + + /// Commits a transaction with the specified transaction ID. + /// + /// This function commits the changes made within the specified transaction and finalizes the + /// transaction. Once a transaction has been committed, all changes made within the transaction + /// are visible to other transactions that access the same data. + /// + /// # Arguments + /// + /// * `tx_id` - The ID of the transaction to commit. + pub fn commit_tx(&self, tx_id: TxID) { + let mut inner = self.inner.lock().unwrap(); + let end_ts = get_timestamp(&mut inner); + let mut txs = inner.txs.borrow_mut(); + let mut tx = txs.get_mut(&tx_id).unwrap(); + let mut rows = inner.rows.borrow_mut(); + tx.state = TransactionState::Preparing; + for id in &tx.write_set { + if let Some(row_versions) = rows.get_mut(id) { + for row_version in row_versions.iter_mut() { + if let TxTimestampOrID::TxID(id) = row_version.begin { + if id == tx_id { + row_version.begin = TxTimestampOrID::Timestamp(tx.begin_ts); + } + } + if let Some(TxTimestampOrID::TxID(id)) = row_version.end { + if id == tx_id { + row_version.end = Some(TxTimestampOrID::Timestamp(end_ts)); + } + } + } + } + } + tx.state = TransactionState::Committed; + } + + /// Rolls back a transaction with the specified ID. + /// + /// This function rolls back a transaction with the specified `tx_id` by + /// discarding any changes made by the transaction. + /// + /// # Arguments + /// + /// * `tx_id` - The ID of the transaction to abort. + pub fn rollback_tx(&self, tx_id: TxID) { + let inner = self.inner.lock().unwrap(); + let mut txs = inner.txs.borrow_mut(); + let mut tx = txs.get_mut(&tx_id).unwrap(); + tx.state = TransactionState::Aborted; + let mut rows = inner.rows.borrow_mut(); + for id in &tx.write_set { + if let Some(row_versions) = rows.get_mut(id) { + row_versions.retain(|rv| rv.begin != TxTimestampOrID::TxID(tx_id)); + if row_versions.is_empty() { + rows.remove(id); + } + } + } + tx.state = TransactionState::Terminated; + } +} + +fn is_version_visible(txs: &HashMap, tx: &Transaction, rv: &RowVersion) -> bool { + is_begin_visible(txs, tx, rv) && is_end_visible(txs, tx, rv) +} + +fn is_begin_visible(txs: &HashMap, tx: &Transaction, rv: &RowVersion) -> bool { + match rv.begin { + TxTimestampOrID::Timestamp(rv_begin_ts) => tx.begin_ts >= rv_begin_ts, + TxTimestampOrID::TxID(rv_begin) => { + let tb = txs.get(&rv_begin).unwrap(); + match tb.state { + TransactionState::Active => tx.tx_id == tb.tx_id && rv.end.is_none(), + TransactionState::Preparing => todo!(), + TransactionState::Committed => todo!(), + TransactionState::Aborted => todo!(), + TransactionState::Terminated => todo!(), + } + } + } +} + +fn is_end_visible(txs: &HashMap, tx: &Transaction, rv: &RowVersion) -> bool { + match rv.end { + Some(TxTimestampOrID::Timestamp(rv_end_ts)) => tx.begin_ts < rv_end_ts, + Some(TxTimestampOrID::TxID(rv_end)) => { + let te = txs.get(&rv_end).unwrap(); + match te.state { + TransactionState::Active => tx.tx_id == te.tx_id && rv.end.is_none(), + TransactionState::Preparing => todo!(), + TransactionState::Committed => todo!(), + TransactionState::Aborted => todo!(), + TransactionState::Terminated => todo!(), + } + } + None => true, + } +} + +fn get_tx_id(inner: &mut DatabaseInner) -> u64 { + inner.tx_ids.fetch_add(1, Ordering::SeqCst) +} + +fn get_timestamp(inner: &mut DatabaseInner) -> u64 { + inner.clock.get_timestamp() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_commit() { + let clock = LocalClock::new(); + let db = Database::new(clock); + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: 1, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()); + let row = db.read(tx1, 1).unwrap(); + assert_eq!(tx1_row, row); + let tx1_updated_row = Row { + id: 1, + data: "World".to_string(), + }; + db.update(tx1, tx1_updated_row.clone()); + let row = db.read(tx1, 1).unwrap(); + assert_eq!(tx1_updated_row, row); + db.commit_tx(tx1); + + let tx2 = db.begin_tx(); + let row = db.read(tx2, 1).unwrap(); + db.commit_tx(tx2); + assert_eq!(tx1_updated_row, row); + } + + #[test] + fn test_rollback() { + let clock = LocalClock::new(); + let db = Database::new(clock); + let tx1 = db.begin_tx(); + let row1 = Row { + id: 1, + data: "Hello".to_string(), + }; + db.insert(tx1.clone(), row1.clone()); + let row2 = db.read(tx1.clone(), 1).unwrap(); + assert_eq!(row1, row2); + let row3 = Row { + id: 1, + data: "World".to_string(), + }; + db.update(tx1.clone(), row3.clone()); + let row4 = db.read(tx1.clone(), 1).unwrap(); + assert_eq!(row3, row4); + db.rollback_tx(tx1); + let tx2 = db.begin_tx(); + let row5 = db.read(tx2.clone(), 1); + assert_eq!(row5, None); + } + + #[test] + fn test_dirty_write() { + let clock = LocalClock::new(); + let db = Database::new(clock); + + // T1 inserts a row with ID 1, but does not commit. + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: 1, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()); + let row = db.read(tx1.clone(), 1).unwrap(); + assert_eq!(tx1_row, row); + + // T2 attempts to delete row with ID 1, but fails because T1 has not committed. + let tx2 = db.begin_tx(); + let tx2_row = Row { + id: 1, + data: "World".to_string(), + }; + assert_eq!(false, db.update(tx2, tx2_row.clone())); + + let row = db.read(tx1, 1).unwrap(); + assert_eq!(tx1_row, row); + } + + #[test] + fn test_dirty_read() { + let clock = LocalClock::new(); + let db = Database::new(clock); + + // T1 inserts a row with ID 1, but does not commit. + let tx1 = db.begin_tx(); + let row1 = Row { + id: 1, + data: "Hello".to_string(), + }; + db.insert(tx1, row1.clone()); + + // T2 attempts to read row with ID 1, but doesn't see one because T1 has not committed. + let tx2 = db.begin_tx(); + let row2 = db.read(tx2, 1); + assert_eq!(row2, None); + } + + #[test] + fn test_fuzzy_read() { + let clock = LocalClock::new(); + let db = Database::new(clock); + + // T1 inserts a row with ID 1 and commits. + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: 1, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()); + let row = db.read(tx1.clone(), 1).unwrap(); + assert_eq!(tx1_row, row); + db.commit_tx(tx1); + + // T2 reads the row with ID 1 within an active transaction. + let tx2 = db.begin_tx(); + let row = db.read(tx2, 1).unwrap(); + assert_eq!(tx1_row, row); + + // T3 updates the row and commits. + let tx3 = db.begin_tx(); + let tx3_row = Row { + id: 1, + data: "World".to_string(), + }; + db.update(tx3, tx3_row.clone()); + db.commit_tx(tx3); + + // T2 still reads the same version of the row as before. + let row = db.read(tx2, 1).unwrap(); + assert_eq!(tx1_row, row); + } +} From 29fca23417b4a40d73f984b363c4edbd3d279a7e Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Sat, 8 Apr 2023 16:34:16 +0300 Subject: [PATCH 002/128] Add test case for lost updates We currently never fail commit() operations so the test case is incomplete. But let's add it as a place-holder. --- core/mvcc/database/src/lib.rs | 44 ++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/core/mvcc/database/src/lib.rs b/core/mvcc/database/src/lib.rs index ab1b33c7d..cb9463a73 100644 --- a/core/mvcc/database/src/lib.rs +++ b/core/mvcc/database/src/lib.rs @@ -19,7 +19,12 @@ //! different value in the course of the transaction because another //! transaction T_n has updated the value. //! -//! TODO: phantom reads, lost updates, cursor lost updates, read skew, write skew. +//! * A *lost update* occurs when transactions T_m and T_n both attempt to update +//! the same value, resulting in one of the updates being lost. The MVCC algorithm +//! prevents lost updates by detecting the write-write conflict and letting the +//! first-writer win by aborting the later transaction. +//! +//! TODO: phantom reads, cursor lost updates, read skew, write skew. //! //! ## TODO //! @@ -537,4 +542,41 @@ mod tests { let row = db.read(tx2, 1).unwrap(); assert_eq!(tx1_row, row); } + + #[ignore] + #[test] + fn test_lost_update() { + let clock = LocalClock::new(); + let db = Database::new(clock); + + // T1 inserts a row with ID 1 and commits. + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: 1, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()); + let row = db.read(tx1.clone(), 1).unwrap(); + assert_eq!(tx1_row, row); + db.commit_tx(tx1); + + // T2 attempts to update row ID 1 within an active transaction. + let tx2 = db.begin_tx(); + let tx2_row = Row { + id: 1, + data: "World".to_string(), + }; + db.update(tx2, tx2_row.clone()); + + // T3 also attempts to update row ID 1 within an active transaction. + let tx3 = db.begin_tx(); + let tx3_row = Row { + id: 1, + data: "Hello, world!".to_string(), + }; + db.update(tx3, tx3_row.clone()); + + db.commit_tx(tx2); + db.commit_tx(tx3); // TODO: this should fail + } } From 7a2085c02f099a83ca8fd12fa27cc5a96232b3c3 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Sat, 8 Apr 2023 16:42:32 +0300 Subject: [PATCH 003/128] Improve lost update test case Let's verify that first-writer wins. We still need to fix the second writer commit() to fail. --- core/mvcc/database/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/mvcc/database/src/lib.rs b/core/mvcc/database/src/lib.rs index cb9463a73..bc29f0cc5 100644 --- a/core/mvcc/database/src/lib.rs +++ b/core/mvcc/database/src/lib.rs @@ -578,5 +578,9 @@ mod tests { db.commit_tx(tx2); db.commit_tx(tx3); // TODO: this should fail + + let tx4 = db.begin_tx(); + let row = db.read(tx4, 1).unwrap(); + assert_eq!(tx2_row, row); } } From 957949a49d14efbaa1bd890b526265bc5f383ee5 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Sat, 8 Apr 2023 18:03:41 +0300 Subject: [PATCH 004/128] Fix delete() on non-existent ID --- core/mvcc/database/src/lib.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/core/mvcc/database/src/lib.rs b/core/mvcc/database/src/lib.rs index bc29f0cc5..88584c1b3 100644 --- a/core/mvcc/database/src/lib.rs +++ b/core/mvcc/database/src/lib.rs @@ -238,17 +238,19 @@ impl Database { let inner = self.inner.lock().unwrap(); let mut rows = inner.rows.borrow_mut(); let mut txs = inner.txs.borrow_mut(); - let row_versions = rows.get_mut(&id).unwrap(); - match row_versions.last_mut() { - Some(v) => { - let tx = txs.get(&tx).unwrap(); - if is_version_visible(&txs, tx, v) { - v.end = Some(TxTimestampOrID::TxID(tx.tx_id)); - } else { - return false; + match rows.get_mut(&id) { + Some(row_versions) => match row_versions.last_mut() { + Some(v) => { + let tx = txs.get(&tx).unwrap(); + if is_version_visible(&txs, tx, v) { + v.end = Some(TxTimestampOrID::TxID(tx.tx_id)); + } else { + return false; + } } - } - None => unreachable!("no versions for row {}", id), + None => unreachable!("no versions for row {}", id), + }, + None => return false, } let tx = txs.get_mut(&tx).unwrap(); tx.insert_to_write_set(id); From fb60ccd04d17bc24abe45662832bbeb42c2d13f0 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Sat, 8 Apr 2023 18:03:49 +0300 Subject: [PATCH 005/128] Improve test suite --- core/mvcc/database/src/lib.rs | 60 +++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/core/mvcc/database/src/lib.rs b/core/mvcc/database/src/lib.rs index 88584c1b3..d6877bad1 100644 --- a/core/mvcc/database/src/lib.rs +++ b/core/mvcc/database/src/lib.rs @@ -412,6 +412,66 @@ fn get_timestamp(inner: &mut DatabaseInner) -> u64 { mod tests { use super::*; + #[test] + fn test_insert_read() { + let clock = LocalClock::new(); + let db = Database::new(clock); + + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: 1, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()); + let row = db.read(tx1, 1).unwrap(); + assert_eq!(tx1_row, row); + db.commit_tx(tx1); + + let tx2 = db.begin_tx(); + let row = db.read(tx2, 1).unwrap(); + assert_eq!(tx1_row, row); + } + + #[test] + fn test_read_nonexistent() { + let clock = LocalClock::new(); + let db = Database::new(clock); + let tx = db.begin_tx(); + let row = db.read(tx, 1); + assert!(row.is_none()); + } + + #[test] + fn test_delete() { + let clock = LocalClock::new(); + let db = Database::new(clock); + + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: 1, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()); + let row = db.read(tx1, 1).unwrap(); + assert_eq!(tx1_row, row); + db.delete(tx1, 1); + let row = db.read(tx1, 1); + assert!(row.is_none()); + db.commit_tx(tx1); + + let tx2 = db.begin_tx(); + let row = db.read(tx2, 1); + assert!(row.is_none()); + } + + #[test] + fn test_delete_nonexistent() { + let clock = LocalClock::new(); + let db = Database::new(clock); + let tx = db.begin_tx(); + assert_eq!(false, db.delete(tx, 1)); + } + #[test] fn test_commit() { let clock = LocalClock::new(); From df5500e0dfcfaa8fb9a27e4b4e1fd20b0be54b48 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Sat, 8 Apr 2023 18:35:45 +0300 Subject: [PATCH 006/128] Add test case for dirty read on delete The test fails, btw. --- core/mvcc/database/src/lib.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/core/mvcc/database/src/lib.rs b/core/mvcc/database/src/lib.rs index d6877bad1..97a9b62d5 100644 --- a/core/mvcc/database/src/lib.rs +++ b/core/mvcc/database/src/lib.rs @@ -570,6 +570,31 @@ mod tests { assert_eq!(row2, None); } + #[ignore] + #[test] + fn test_dirty_read_deleted() { + let clock = LocalClock::new(); + let db = Database::new(clock); + + // T1 inserts a row with ID 1 and commits. + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: 1, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()); + db.commit_tx(tx1); + + // T2 deletes row with ID 1, but does not commit. + let tx2 = db.begin_tx(); + assert_eq!(true, db.delete(tx2, 1)); + + // T3 reads row with ID 1, but doesn't see the delete because T2 hasn't committed. + let tx3 = db.begin_tx(); + let row = db.read(tx3, 1).unwrap(); + assert_eq!(tx1_row, row); + } + #[test] fn test_fuzzy_read() { let clock = LocalClock::new(); From df0cadc02e1875c0a9a2f8d3e97ea80825f457e5 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Sat, 8 Apr 2023 18:37:23 +0300 Subject: [PATCH 007/128] Clean up LocalClock default trait --- core/mvcc/database/src/lib.rs | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/core/mvcc/database/src/lib.rs b/core/mvcc/database/src/lib.rs index 97a9b62d5..40c17cd91 100644 --- a/core/mvcc/database/src/lib.rs +++ b/core/mvcc/database/src/lib.rs @@ -122,7 +122,7 @@ pub trait LogicalClock { } /// A node-local clock backed by an atomic counter. -#[derive(Debug)] +#[derive(Debug, Default)] pub struct LocalClock { ts_sequence: AtomicU64, } @@ -141,12 +141,6 @@ impl LogicalClock for LocalClock { } } -impl Default for LocalClock { - fn default() -> Self { - Self::new() - } -} - #[derive(Debug)] pub struct DatabaseInner { rows: RefCell>>, @@ -414,7 +408,7 @@ mod tests { #[test] fn test_insert_read() { - let clock = LocalClock::new(); + let clock = LocalClock::default(); let db = Database::new(clock); let tx1 = db.begin_tx(); @@ -434,7 +428,7 @@ mod tests { #[test] fn test_read_nonexistent() { - let clock = LocalClock::new(); + let clock = LocalClock::default(); let db = Database::new(clock); let tx = db.begin_tx(); let row = db.read(tx, 1); @@ -443,7 +437,7 @@ mod tests { #[test] fn test_delete() { - let clock = LocalClock::new(); + let clock = LocalClock::default(); let db = Database::new(clock); let tx1 = db.begin_tx(); @@ -466,7 +460,7 @@ mod tests { #[test] fn test_delete_nonexistent() { - let clock = LocalClock::new(); + let clock = LocalClock::default(); let db = Database::new(clock); let tx = db.begin_tx(); assert_eq!(false, db.delete(tx, 1)); @@ -474,7 +468,7 @@ mod tests { #[test] fn test_commit() { - let clock = LocalClock::new(); + let clock = LocalClock::default(); let db = Database::new(clock); let tx1 = db.begin_tx(); let tx1_row = Row { @@ -501,7 +495,7 @@ mod tests { #[test] fn test_rollback() { - let clock = LocalClock::new(); + let clock = LocalClock::default(); let db = Database::new(clock); let tx1 = db.begin_tx(); let row1 = Row { @@ -526,7 +520,7 @@ mod tests { #[test] fn test_dirty_write() { - let clock = LocalClock::new(); + let clock = LocalClock::default(); let db = Database::new(clock); // T1 inserts a row with ID 1, but does not commit. @@ -553,7 +547,7 @@ mod tests { #[test] fn test_dirty_read() { - let clock = LocalClock::new(); + let clock = LocalClock::default(); let db = Database::new(clock); // T1 inserts a row with ID 1, but does not commit. @@ -573,7 +567,7 @@ mod tests { #[ignore] #[test] fn test_dirty_read_deleted() { - let clock = LocalClock::new(); + let clock = LocalClock::default(); let db = Database::new(clock); // T1 inserts a row with ID 1 and commits. @@ -597,7 +591,7 @@ mod tests { #[test] fn test_fuzzy_read() { - let clock = LocalClock::new(); + let clock = LocalClock::default(); let db = Database::new(clock); // T1 inserts a row with ID 1 and commits. @@ -633,7 +627,7 @@ mod tests { #[ignore] #[test] fn test_lost_update() { - let clock = LocalClock::new(); + let clock = LocalClock::default(); let db = Database::new(clock); // T1 inserts a row with ID 1 and commits. From 8f30c2021569ed39c8818b933b3833aefdf2cb30 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Sun, 9 Apr 2023 08:43:00 +0300 Subject: [PATCH 008/128] Replace unwrap() with NoSuchTransactionID error --- core/mvcc/Cargo.lock | 1 + core/mvcc/database/Cargo.toml | 1 + core/mvcc/database/src/errors.rs | 7 ++ core/mvcc/database/src/lib.rs | 120 +++++++++++++++++-------------- 4 files changed, 74 insertions(+), 55 deletions(-) create mode 100644 core/mvcc/database/src/errors.rs diff --git a/core/mvcc/Cargo.lock b/core/mvcc/Cargo.lock index 373c791b7..0f4712b90 100644 --- a/core/mvcc/Cargo.lock +++ b/core/mvcc/Cargo.lock @@ -167,6 +167,7 @@ version = "0.0.0" dependencies = [ "anyhow", "rustyline", + "thiserror", ] [[package]] diff --git a/core/mvcc/database/Cargo.toml b/core/mvcc/database/Cargo.toml index 9789f8733..7029f5419 100644 --- a/core/mvcc/database/Cargo.toml +++ b/core/mvcc/database/Cargo.toml @@ -6,3 +6,4 @@ edition = "2021" [dependencies] anyhow = "1.0.70" rustyline = "11.0.0" +thiserror = "1.0.40" diff --git a/core/mvcc/database/src/errors.rs b/core/mvcc/database/src/errors.rs new file mode 100644 index 000000000..95901137b --- /dev/null +++ b/core/mvcc/database/src/errors.rs @@ -0,0 +1,7 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum DatabaseError { + #[error("no such transaction ID: `{0}`")] + NoSuchTransactionID(u64), +} diff --git a/core/mvcc/database/src/lib.rs b/core/mvcc/database/src/lib.rs index 40c17cd91..550b91d22 100644 --- a/core/mvcc/database/src/lib.rs +++ b/core/mvcc/database/src/lib.rs @@ -31,11 +31,16 @@ //! * Optimistic reads and writes //! * Garbage collection +pub mod errors; + +use crate::errors::DatabaseError; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; +type Result = std::result::Result; + #[derive(Clone, Debug, PartialEq)] pub struct Row { pub id: u64, @@ -173,10 +178,12 @@ impl Database { /// * `tx_id` - the ID of the transaction in which to insert the new row. /// * `row` - the row object containing the values to be inserted. /// - pub fn insert(&self, tx_id: TxID, row: Row) { + pub fn insert(&self, tx_id: TxID, row: Row) -> Result<()> { let inner = self.inner.lock().unwrap(); let mut txs = inner.txs.borrow_mut(); - let tx = txs.get_mut(&tx_id).unwrap(); + let tx = txs + .get_mut(&tx_id) + .ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; let id = row.id; let row_version = RowVersion { begin: TxTimestampOrID::TxID(tx.tx_id), @@ -186,6 +193,7 @@ impl Database { let mut rows = inner.rows.borrow_mut(); rows.entry(id).or_insert_with(Vec::new).push(row_version); tx.insert_to_write_set(id); + Ok(()) } /// Updates a row in the database with new values. @@ -206,12 +214,12 @@ impl Database { /// # Returns /// /// Returns `true` if the row was successfully updated, and `false` otherwise. - pub fn update(&self, tx_id: TxID, row: Row) -> bool { - if !self.delete(tx_id, row.id) { - return false; + pub fn update(&self, tx_id: TxID, row: Row) -> Result { + if !self.delete(tx_id, row.id)? { + return Ok(false); } - self.insert(tx_id, row); - true + self.insert(tx_id, row)?; + Ok(true) } /// Deletes a row from the table with the given `id`. @@ -228,27 +236,29 @@ impl Database { /// /// Returns `true` if the row was successfully deleted, and `false` otherwise. /// - pub fn delete(&self, tx: TxID, id: u64) -> bool { + pub fn delete(&self, tx: TxID, id: u64) -> Result { let inner = self.inner.lock().unwrap(); let mut rows = inner.rows.borrow_mut(); let mut txs = inner.txs.borrow_mut(); match rows.get_mut(&id) { Some(row_versions) => match row_versions.last_mut() { Some(v) => { - let tx = txs.get(&tx).unwrap(); + let tx = txs.get(&tx).ok_or(DatabaseError::NoSuchTransactionID(tx))?; if is_version_visible(&txs, tx, v) { v.end = Some(TxTimestampOrID::TxID(tx.tx_id)); } else { - return false; + return Ok(false); } } None => unreachable!("no versions for row {}", id), }, - None => return false, + None => return Ok(false), } - let tx = txs.get_mut(&tx).unwrap(); + let tx = txs + .get_mut(&tx) + .ok_or(DatabaseError::NoSuchTransactionID(tx))?; tx.insert_to_write_set(id); - true + Ok(true) } /// Retrieves a row from the table with the given `id`. @@ -265,7 +275,7 @@ impl Database { /// /// Returns `Some(row)` with the row data if the row with the given `id` exists, /// and `None` otherwise. - pub fn read(&self, tx_id: TxID, id: u64) -> Option { + pub fn read(&self, tx_id: TxID, id: u64) -> Result> { let inner = self.inner.lock().unwrap(); let txs = inner.txs.borrow_mut(); let tx = txs.get(&tx_id).unwrap(); @@ -274,11 +284,11 @@ impl Database { for rv in row_versions.iter().rev() { if is_version_visible(&txs, tx, rv) { tx.insert_to_read_set(id); - return Some(rv.row.clone()); + return Ok(Some(rv.row.clone())); } } } - None + Ok(None) } /// Begins a new transaction in the database. @@ -416,13 +426,13 @@ mod tests { id: 1, data: "Hello".to_string(), }; - db.insert(tx1, tx1_row.clone()); - let row = db.read(tx1, 1).unwrap(); + db.insert(tx1, tx1_row.clone()).unwrap(); + let row = db.read(tx1, 1).unwrap().unwrap(); assert_eq!(tx1_row, row); db.commit_tx(tx1); let tx2 = db.begin_tx(); - let row = db.read(tx2, 1).unwrap(); + let row = db.read(tx2, 1).unwrap().unwrap(); assert_eq!(tx1_row, row); } @@ -432,7 +442,7 @@ mod tests { let db = Database::new(clock); let tx = db.begin_tx(); let row = db.read(tx, 1); - assert!(row.is_none()); + assert!(row.unwrap().is_none()); } #[test] @@ -445,16 +455,16 @@ mod tests { id: 1, data: "Hello".to_string(), }; - db.insert(tx1, tx1_row.clone()); - let row = db.read(tx1, 1).unwrap(); + db.insert(tx1, tx1_row.clone()).unwrap(); + let row = db.read(tx1, 1).unwrap().unwrap(); assert_eq!(tx1_row, row); - db.delete(tx1, 1); - let row = db.read(tx1, 1); + db.delete(tx1, 1).unwrap(); + let row = db.read(tx1, 1).unwrap(); assert!(row.is_none()); db.commit_tx(tx1); let tx2 = db.begin_tx(); - let row = db.read(tx2, 1); + let row = db.read(tx2, 1).unwrap(); assert!(row.is_none()); } @@ -463,7 +473,7 @@ mod tests { let clock = LocalClock::default(); let db = Database::new(clock); let tx = db.begin_tx(); - assert_eq!(false, db.delete(tx, 1)); + assert_eq!(false, db.delete(tx, 1).unwrap()); } #[test] @@ -475,20 +485,20 @@ mod tests { id: 1, data: "Hello".to_string(), }; - db.insert(tx1, tx1_row.clone()); - let row = db.read(tx1, 1).unwrap(); + db.insert(tx1, tx1_row.clone()).unwrap(); + let row = db.read(tx1, 1).unwrap().unwrap(); assert_eq!(tx1_row, row); let tx1_updated_row = Row { id: 1, data: "World".to_string(), }; - db.update(tx1, tx1_updated_row.clone()); - let row = db.read(tx1, 1).unwrap(); + db.update(tx1, tx1_updated_row.clone()).unwrap(); + let row = db.read(tx1, 1).unwrap().unwrap(); assert_eq!(tx1_updated_row, row); db.commit_tx(tx1); let tx2 = db.begin_tx(); - let row = db.read(tx2, 1).unwrap(); + let row = db.read(tx2, 1).unwrap().unwrap(); db.commit_tx(tx2); assert_eq!(tx1_updated_row, row); } @@ -502,19 +512,19 @@ mod tests { id: 1, data: "Hello".to_string(), }; - db.insert(tx1.clone(), row1.clone()); - let row2 = db.read(tx1.clone(), 1).unwrap(); + db.insert(tx1.clone(), row1.clone()).unwrap(); + let row2 = db.read(tx1.clone(), 1).unwrap().unwrap(); assert_eq!(row1, row2); let row3 = Row { id: 1, data: "World".to_string(), }; - db.update(tx1.clone(), row3.clone()); - let row4 = db.read(tx1.clone(), 1).unwrap(); + db.update(tx1.clone(), row3.clone()).unwrap(); + let row4 = db.read(tx1.clone(), 1).unwrap().unwrap(); assert_eq!(row3, row4); db.rollback_tx(tx1); let tx2 = db.begin_tx(); - let row5 = db.read(tx2.clone(), 1); + let row5 = db.read(tx2.clone(), 1).unwrap(); assert_eq!(row5, None); } @@ -529,8 +539,8 @@ mod tests { id: 1, data: "Hello".to_string(), }; - db.insert(tx1, tx1_row.clone()); - let row = db.read(tx1.clone(), 1).unwrap(); + db.insert(tx1, tx1_row.clone()).unwrap(); + let row = db.read(tx1.clone(), 1).unwrap().unwrap(); assert_eq!(tx1_row, row); // T2 attempts to delete row with ID 1, but fails because T1 has not committed. @@ -539,9 +549,9 @@ mod tests { id: 1, data: "World".to_string(), }; - assert_eq!(false, db.update(tx2, tx2_row.clone())); + assert_eq!(false, db.update(tx2, tx2_row.clone()).unwrap()); - let row = db.read(tx1, 1).unwrap(); + let row = db.read(tx1, 1).unwrap().unwrap(); assert_eq!(tx1_row, row); } @@ -556,11 +566,11 @@ mod tests { id: 1, data: "Hello".to_string(), }; - db.insert(tx1, row1.clone()); + db.insert(tx1, row1.clone()).unwrap(); // T2 attempts to read row with ID 1, but doesn't see one because T1 has not committed. let tx2 = db.begin_tx(); - let row2 = db.read(tx2, 1); + let row2 = db.read(tx2, 1).unwrap(); assert_eq!(row2, None); } @@ -576,16 +586,16 @@ mod tests { id: 1, data: "Hello".to_string(), }; - db.insert(tx1, tx1_row.clone()); + db.insert(tx1, tx1_row.clone()).unwrap(); db.commit_tx(tx1); // T2 deletes row with ID 1, but does not commit. let tx2 = db.begin_tx(); - assert_eq!(true, db.delete(tx2, 1)); + assert_eq!(true, db.delete(tx2, 1).unwrap()); // T3 reads row with ID 1, but doesn't see the delete because T2 hasn't committed. let tx3 = db.begin_tx(); - let row = db.read(tx3, 1).unwrap(); + let row = db.read(tx3, 1).unwrap().unwrap(); assert_eq!(tx1_row, row); } @@ -600,14 +610,14 @@ mod tests { id: 1, data: "Hello".to_string(), }; - db.insert(tx1, tx1_row.clone()); - let row = db.read(tx1.clone(), 1).unwrap(); + db.insert(tx1, tx1_row.clone()).unwrap(); + let row = db.read(tx1.clone(), 1).unwrap().unwrap(); assert_eq!(tx1_row, row); db.commit_tx(tx1); // T2 reads the row with ID 1 within an active transaction. let tx2 = db.begin_tx(); - let row = db.read(tx2, 1).unwrap(); + let row = db.read(tx2, 1).unwrap().unwrap(); assert_eq!(tx1_row, row); // T3 updates the row and commits. @@ -616,11 +626,11 @@ mod tests { id: 1, data: "World".to_string(), }; - db.update(tx3, tx3_row.clone()); + db.update(tx3, tx3_row.clone()).unwrap(); db.commit_tx(tx3); // T2 still reads the same version of the row as before. - let row = db.read(tx2, 1).unwrap(); + let row = db.read(tx2, 1).unwrap().unwrap(); assert_eq!(tx1_row, row); } @@ -636,8 +646,8 @@ mod tests { id: 1, data: "Hello".to_string(), }; - db.insert(tx1, tx1_row.clone()); - let row = db.read(tx1.clone(), 1).unwrap(); + db.insert(tx1, tx1_row.clone()).unwrap(); + let row = db.read(tx1.clone(), 1).unwrap().unwrap(); assert_eq!(tx1_row, row); db.commit_tx(tx1); @@ -647,7 +657,7 @@ mod tests { id: 1, data: "World".to_string(), }; - db.update(tx2, tx2_row.clone()); + db.update(tx2, tx2_row.clone()).unwrap(); // T3 also attempts to update row ID 1 within an active transaction. let tx3 = db.begin_tx(); @@ -655,13 +665,13 @@ mod tests { id: 1, data: "Hello, world!".to_string(), }; - db.update(tx3, tx3_row.clone()); + db.update(tx3, tx3_row.clone()).unwrap(); db.commit_tx(tx2); db.commit_tx(tx3); // TODO: this should fail let tx4 = db.begin_tx(); - let row = db.read(tx4, 1).unwrap(); + let row = db.read(tx4, 1).unwrap().unwrap(); assert_eq!(tx2_row, row); } } From 02f40c05686751465132eec4ce1de4dd55e34e91 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Sun, 9 Apr 2023 08:55:06 +0300 Subject: [PATCH 009/128] Move MVCC to database.rs Let's keep lib.rs small and tidy. --- core/mvcc/database/src/database.rs | 642 ++++++++++++++++++++++++++++ core/mvcc/database/src/lib.rs | 644 +---------------------------- 2 files changed, 643 insertions(+), 643 deletions(-) create mode 100644 core/mvcc/database/src/database.rs diff --git a/core/mvcc/database/src/database.rs b/core/mvcc/database/src/database.rs new file mode 100644 index 000000000..cdebed823 --- /dev/null +++ b/core/mvcc/database/src/database.rs @@ -0,0 +1,642 @@ +use crate::errors::DatabaseError; +use std::cell::RefCell; +use std::collections::{HashMap, HashSet}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Mutex}; + +type Result = std::result::Result; + +#[derive(Clone, Debug, PartialEq)] +pub struct Row { + pub id: u64, + pub data: String, +} + +/// A row version. +#[derive(Clone, Debug)] +struct RowVersion { + begin: TxTimestampOrID, + end: Option, + row: Row, +} + +/// A transaction timestamp or ID. +/// +/// Versions either track a timestamp or a transaction ID, depending on the +/// phase of the transaction. During the active phase, new versions track the +/// transaction ID in the `begin` and `end` fields. After a transaction commits, +/// versions switch to tracking timestamps. +#[derive(Clone, Debug, PartialEq)] +enum TxTimestampOrID { + Timestamp(u64), + TxID(u64), +} + +/// Transaction +#[derive(Debug, Clone)] +pub struct Transaction { + /// The state of the transaction. + state: TransactionState, + /// The transaction ID. + tx_id: u64, + /// The transaction begin timestamp. + begin_ts: u64, + /// The transaction write set. + write_set: HashSet, + /// The transaction read set. + read_set: RefCell>, +} + +impl Transaction { + fn new(tx_id: u64, begin_ts: u64) -> Transaction { + Transaction { + state: TransactionState::Active, + tx_id, + begin_ts, + write_set: HashSet::new(), + read_set: RefCell::new(HashSet::new()), + } + } + + fn insert_to_read_set(&self, id: u64) { + let mut read_set = self.read_set.borrow_mut(); + read_set.insert(id); + } + + fn insert_to_write_set(&mut self, id: u64) { + self.write_set.insert(id); + } +} + +/// Transaction state. +#[derive(Debug, Clone)] +enum TransactionState { + Active, + Preparing, + Committed, + Aborted, + Terminated, +} + +/// A database with MVCC. +#[derive(Debug)] +pub struct Database { + inner: Arc>>, +} + +type TxID = u64; + +/// Logical clock. +pub trait LogicalClock { + fn get_timestamp(&self) -> u64; +} + +/// A node-local clock backed by an atomic counter. +#[derive(Debug, Default)] +pub struct LocalClock { + ts_sequence: AtomicU64, +} + +impl LocalClock { + pub fn new() -> Self { + Self { + ts_sequence: AtomicU64::new(0), + } + } +} + +impl LogicalClock for LocalClock { + fn get_timestamp(&self) -> u64 { + self.ts_sequence.fetch_add(1, Ordering::SeqCst) + } +} + +#[derive(Debug)] +pub struct DatabaseInner { + rows: RefCell>>, + txs: RefCell>, + tx_ids: AtomicU64, + clock: Clock, +} + +impl Database { + /// Creates a new database. + pub fn new(clock: Clock) -> Self { + let inner = DatabaseInner { + rows: RefCell::new(HashMap::new()), + txs: RefCell::new(HashMap::new()), + tx_ids: AtomicU64::new(0), + clock, + }; + Self { + inner: Arc::new(Mutex::new(inner)), + } + } + + /// Inserts a new row into the database. + /// + /// This function inserts a new `row` into the database within the context + /// of the transaction `tx_id`. + /// + /// # Arguments + /// + /// * `tx_id` - the ID of the transaction in which to insert the new row. + /// * `row` - the row object containing the values to be inserted. + /// + pub fn insert(&self, tx_id: TxID, row: Row) -> Result<()> { + let inner = self.inner.lock().unwrap(); + let mut txs = inner.txs.borrow_mut(); + let tx = txs + .get_mut(&tx_id) + .ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; + let id = row.id; + let row_version = RowVersion { + begin: TxTimestampOrID::TxID(tx.tx_id), + end: None, + row, + }; + let mut rows = inner.rows.borrow_mut(); + rows.entry(id).or_insert_with(Vec::new).push(row_version); + tx.insert_to_write_set(id); + Ok(()) + } + + /// Updates a row in the database with new values. + /// + /// This function updates an existing row in the database within the + /// context of the transaction `tx_id`. The `row` argument identifies the + /// row to be updated as `id` and contains the new values to be inserted. + /// + /// If the row identified by the `id` does not exist, this function does + /// nothing and returns `false`. Otherwise, the function updates the row + /// with the new values and returns `true`. + /// + /// # Arguments + /// + /// * `tx_id` - the ID of the transaction in which to update the new row. + /// * `row` - the row object containing the values to be updated. + /// + /// # Returns + /// + /// Returns `true` if the row was successfully updated, and `false` otherwise. + pub fn update(&self, tx_id: TxID, row: Row) -> Result { + if !self.delete(tx_id, row.id)? { + return Ok(false); + } + self.insert(tx_id, row)?; + Ok(true) + } + + /// Deletes a row from the table with the given `id`. + /// + /// This function deletes an existing row `id` in the database within the + /// context of the transaction `tx_id`. + /// + /// # Arguments + /// + /// * `tx_id` - the ID of the transaction in which to delete the new row. + /// * `id` - the ID of the row to delete. + /// + /// # Returns + /// + /// Returns `true` if the row was successfully deleted, and `false` otherwise. + /// + pub fn delete(&self, tx: TxID, id: u64) -> Result { + let inner = self.inner.lock().unwrap(); + let mut rows = inner.rows.borrow_mut(); + let mut txs = inner.txs.borrow_mut(); + match rows.get_mut(&id) { + Some(row_versions) => match row_versions.last_mut() { + Some(v) => { + let tx = txs.get(&tx).ok_or(DatabaseError::NoSuchTransactionID(tx))?; + if is_version_visible(&txs, tx, v) { + v.end = Some(TxTimestampOrID::TxID(tx.tx_id)); + } else { + return Ok(false); + } + } + None => unreachable!("no versions for row {}", id), + }, + None => return Ok(false), + } + let tx = txs + .get_mut(&tx) + .ok_or(DatabaseError::NoSuchTransactionID(tx))?; + tx.insert_to_write_set(id); + Ok(true) + } + + /// Retrieves a row from the table with the given `id`. + /// + /// This operation is performed within the scope of the transaction identified + /// by `tx_id`. + /// + /// # Arguments + /// + /// * `tx_id` - The ID of the transaction to perform the read operation in. + /// * `id` - The ID of the row to retrieve. + /// + /// # Returns + /// + /// Returns `Some(row)` with the row data if the row with the given `id` exists, + /// and `None` otherwise. + pub fn read(&self, tx_id: TxID, id: u64) -> Result> { + let inner = self.inner.lock().unwrap(); + let txs = inner.txs.borrow_mut(); + let tx = txs.get(&tx_id).unwrap(); + let rows = inner.rows.borrow(); + if let Some(row_versions) = rows.get(&id) { + for rv in row_versions.iter().rev() { + if is_version_visible(&txs, tx, rv) { + tx.insert_to_read_set(id); + return Ok(Some(rv.row.clone())); + } + } + } + Ok(None) + } + + /// Begins a new transaction in the database. + /// + /// This function starts a new transaction in the database and returns a `TxID` value + /// that you can use to perform operations within the transaction. All changes made within the + /// transaction are isolated from other transactions until you commit the transaction. + pub fn begin_tx(&self) -> TxID { + let mut inner = self.inner.lock().unwrap(); + let tx_id = get_tx_id(&mut inner); + let begin_ts = get_timestamp(&mut inner); + let tx = Transaction::new(tx_id, begin_ts); + let mut txs = inner.txs.borrow_mut(); + txs.insert(tx_id, tx); + tx_id + } + + /// Commits a transaction with the specified transaction ID. + /// + /// This function commits the changes made within the specified transaction and finalizes the + /// transaction. Once a transaction has been committed, all changes made within the transaction + /// are visible to other transactions that access the same data. + /// + /// # Arguments + /// + /// * `tx_id` - The ID of the transaction to commit. + pub fn commit_tx(&self, tx_id: TxID) { + let mut inner = self.inner.lock().unwrap(); + let end_ts = get_timestamp(&mut inner); + let mut txs = inner.txs.borrow_mut(); + let mut tx = txs.get_mut(&tx_id).unwrap(); + let mut rows = inner.rows.borrow_mut(); + tx.state = TransactionState::Preparing; + for id in &tx.write_set { + if let Some(row_versions) = rows.get_mut(id) { + for row_version in row_versions.iter_mut() { + if let TxTimestampOrID::TxID(id) = row_version.begin { + if id == tx_id { + row_version.begin = TxTimestampOrID::Timestamp(tx.begin_ts); + } + } + if let Some(TxTimestampOrID::TxID(id)) = row_version.end { + if id == tx_id { + row_version.end = Some(TxTimestampOrID::Timestamp(end_ts)); + } + } + } + } + } + tx.state = TransactionState::Committed; + } + + /// Rolls back a transaction with the specified ID. + /// + /// This function rolls back a transaction with the specified `tx_id` by + /// discarding any changes made by the transaction. + /// + /// # Arguments + /// + /// * `tx_id` - The ID of the transaction to abort. + pub fn rollback_tx(&self, tx_id: TxID) { + let inner = self.inner.lock().unwrap(); + let mut txs = inner.txs.borrow_mut(); + let mut tx = txs.get_mut(&tx_id).unwrap(); + tx.state = TransactionState::Aborted; + let mut rows = inner.rows.borrow_mut(); + for id in &tx.write_set { + if let Some(row_versions) = rows.get_mut(id) { + row_versions.retain(|rv| rv.begin != TxTimestampOrID::TxID(tx_id)); + if row_versions.is_empty() { + rows.remove(id); + } + } + } + tx.state = TransactionState::Terminated; + } +} + +fn is_version_visible(txs: &HashMap, tx: &Transaction, rv: &RowVersion) -> bool { + is_begin_visible(txs, tx, rv) && is_end_visible(txs, tx, rv) +} + +fn is_begin_visible(txs: &HashMap, tx: &Transaction, rv: &RowVersion) -> bool { + match rv.begin { + TxTimestampOrID::Timestamp(rv_begin_ts) => tx.begin_ts >= rv_begin_ts, + TxTimestampOrID::TxID(rv_begin) => { + let tb = txs.get(&rv_begin).unwrap(); + match tb.state { + TransactionState::Active => tx.tx_id == tb.tx_id && rv.end.is_none(), + TransactionState::Preparing => todo!(), + TransactionState::Committed => todo!(), + TransactionState::Aborted => todo!(), + TransactionState::Terminated => todo!(), + } + } + } +} + +fn is_end_visible(txs: &HashMap, tx: &Transaction, rv: &RowVersion) -> bool { + match rv.end { + Some(TxTimestampOrID::Timestamp(rv_end_ts)) => tx.begin_ts < rv_end_ts, + Some(TxTimestampOrID::TxID(rv_end)) => { + let te = txs.get(&rv_end).unwrap(); + match te.state { + TransactionState::Active => tx.tx_id == te.tx_id && rv.end.is_none(), + TransactionState::Preparing => todo!(), + TransactionState::Committed => todo!(), + TransactionState::Aborted => todo!(), + TransactionState::Terminated => todo!(), + } + } + None => true, + } +} + +fn get_tx_id(inner: &mut DatabaseInner) -> u64 { + inner.tx_ids.fetch_add(1, Ordering::SeqCst) +} + +fn get_timestamp(inner: &mut DatabaseInner) -> u64 { + inner.clock.get_timestamp() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_insert_read() { + let clock = LocalClock::default(); + let db = Database::new(clock); + + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: 1, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()).unwrap(); + let row = db.read(tx1, 1).unwrap().unwrap(); + assert_eq!(tx1_row, row); + db.commit_tx(tx1); + + let tx2 = db.begin_tx(); + let row = db.read(tx2, 1).unwrap().unwrap(); + assert_eq!(tx1_row, row); + } + + #[test] + fn test_read_nonexistent() { + let clock = LocalClock::default(); + let db = Database::new(clock); + let tx = db.begin_tx(); + let row = db.read(tx, 1); + assert!(row.unwrap().is_none()); + } + + #[test] + fn test_delete() { + let clock = LocalClock::default(); + let db = Database::new(clock); + + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: 1, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()).unwrap(); + let row = db.read(tx1, 1).unwrap().unwrap(); + assert_eq!(tx1_row, row); + db.delete(tx1, 1).unwrap(); + let row = db.read(tx1, 1).unwrap(); + assert!(row.is_none()); + db.commit_tx(tx1); + + let tx2 = db.begin_tx(); + let row = db.read(tx2, 1).unwrap(); + assert!(row.is_none()); + } + + #[test] + fn test_delete_nonexistent() { + let clock = LocalClock::default(); + let db = Database::new(clock); + let tx = db.begin_tx(); + assert_eq!(false, db.delete(tx, 1).unwrap()); + } + + #[test] + fn test_commit() { + let clock = LocalClock::default(); + let db = Database::new(clock); + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: 1, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()).unwrap(); + let row = db.read(tx1, 1).unwrap().unwrap(); + assert_eq!(tx1_row, row); + let tx1_updated_row = Row { + id: 1, + data: "World".to_string(), + }; + db.update(tx1, tx1_updated_row.clone()).unwrap(); + let row = db.read(tx1, 1).unwrap().unwrap(); + assert_eq!(tx1_updated_row, row); + db.commit_tx(tx1); + + let tx2 = db.begin_tx(); + let row = db.read(tx2, 1).unwrap().unwrap(); + db.commit_tx(tx2); + assert_eq!(tx1_updated_row, row); + } + + #[test] + fn test_rollback() { + let clock = LocalClock::default(); + let db = Database::new(clock); + let tx1 = db.begin_tx(); + let row1 = Row { + id: 1, + data: "Hello".to_string(), + }; + db.insert(tx1.clone(), row1.clone()).unwrap(); + let row2 = db.read(tx1.clone(), 1).unwrap().unwrap(); + assert_eq!(row1, row2); + let row3 = Row { + id: 1, + data: "World".to_string(), + }; + db.update(tx1.clone(), row3.clone()).unwrap(); + let row4 = db.read(tx1.clone(), 1).unwrap().unwrap(); + assert_eq!(row3, row4); + db.rollback_tx(tx1); + let tx2 = db.begin_tx(); + let row5 = db.read(tx2.clone(), 1).unwrap(); + assert_eq!(row5, None); + } + + #[test] + fn test_dirty_write() { + let clock = LocalClock::default(); + let db = Database::new(clock); + + // T1 inserts a row with ID 1, but does not commit. + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: 1, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()).unwrap(); + let row = db.read(tx1.clone(), 1).unwrap().unwrap(); + assert_eq!(tx1_row, row); + + // T2 attempts to delete row with ID 1, but fails because T1 has not committed. + let tx2 = db.begin_tx(); + let tx2_row = Row { + id: 1, + data: "World".to_string(), + }; + assert_eq!(false, db.update(tx2, tx2_row.clone()).unwrap()); + + let row = db.read(tx1, 1).unwrap().unwrap(); + assert_eq!(tx1_row, row); + } + + #[test] + fn test_dirty_read() { + let clock = LocalClock::default(); + let db = Database::new(clock); + + // T1 inserts a row with ID 1, but does not commit. + let tx1 = db.begin_tx(); + let row1 = Row { + id: 1, + data: "Hello".to_string(), + }; + db.insert(tx1, row1.clone()).unwrap(); + + // T2 attempts to read row with ID 1, but doesn't see one because T1 has not committed. + let tx2 = db.begin_tx(); + let row2 = db.read(tx2, 1).unwrap(); + assert_eq!(row2, None); + } + + #[ignore] + #[test] + fn test_dirty_read_deleted() { + let clock = LocalClock::default(); + let db = Database::new(clock); + + // T1 inserts a row with ID 1 and commits. + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: 1, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()).unwrap(); + db.commit_tx(tx1); + + // T2 deletes row with ID 1, but does not commit. + let tx2 = db.begin_tx(); + assert_eq!(true, db.delete(tx2, 1).unwrap()); + + // T3 reads row with ID 1, but doesn't see the delete because T2 hasn't committed. + let tx3 = db.begin_tx(); + let row = db.read(tx3, 1).unwrap().unwrap(); + assert_eq!(tx1_row, row); + } + + #[test] + fn test_fuzzy_read() { + let clock = LocalClock::default(); + let db = Database::new(clock); + + // T1 inserts a row with ID 1 and commits. + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: 1, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()).unwrap(); + let row = db.read(tx1.clone(), 1).unwrap().unwrap(); + assert_eq!(tx1_row, row); + db.commit_tx(tx1); + + // T2 reads the row with ID 1 within an active transaction. + let tx2 = db.begin_tx(); + let row = db.read(tx2, 1).unwrap().unwrap(); + assert_eq!(tx1_row, row); + + // T3 updates the row and commits. + let tx3 = db.begin_tx(); + let tx3_row = Row { + id: 1, + data: "World".to_string(), + }; + db.update(tx3, tx3_row.clone()).unwrap(); + db.commit_tx(tx3); + + // T2 still reads the same version of the row as before. + let row = db.read(tx2, 1).unwrap().unwrap(); + assert_eq!(tx1_row, row); + } + + #[ignore] + #[test] + fn test_lost_update() { + let clock = LocalClock::default(); + let db = Database::new(clock); + + // T1 inserts a row with ID 1 and commits. + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: 1, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()).unwrap(); + let row = db.read(tx1.clone(), 1).unwrap().unwrap(); + assert_eq!(tx1_row, row); + db.commit_tx(tx1); + + // T2 attempts to update row ID 1 within an active transaction. + let tx2 = db.begin_tx(); + let tx2_row = Row { + id: 1, + data: "World".to_string(), + }; + db.update(tx2, tx2_row.clone()).unwrap(); + + // T3 also attempts to update row ID 1 within an active transaction. + let tx3 = db.begin_tx(); + let tx3_row = Row { + id: 1, + data: "Hello, world!".to_string(), + }; + db.update(tx3, tx3_row.clone()).unwrap(); + + db.commit_tx(tx2); + db.commit_tx(tx3); // TODO: this should fail + + let tx4 = db.begin_tx(); + let row = db.read(tx4, 1).unwrap().unwrap(); + assert_eq!(tx2_row, row); + } +} \ No newline at end of file diff --git a/core/mvcc/database/src/lib.rs b/core/mvcc/database/src/lib.rs index 550b91d22..534e2ec26 100644 --- a/core/mvcc/database/src/lib.rs +++ b/core/mvcc/database/src/lib.rs @@ -32,646 +32,4 @@ //! * Garbage collection pub mod errors; - -use crate::errors::DatabaseError; -use std::cell::RefCell; -use std::collections::{HashMap, HashSet}; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::{Arc, Mutex}; - -type Result = std::result::Result; - -#[derive(Clone, Debug, PartialEq)] -pub struct Row { - pub id: u64, - pub data: String, -} - -/// A row version. -#[derive(Clone, Debug)] -struct RowVersion { - begin: TxTimestampOrID, - end: Option, - row: Row, -} - -/// A transaction timestamp or ID. -/// -/// Versions either track a timestamp or a transaction ID, depending on the -/// phase of the transaction. During the active phase, new versions track the -/// transaction ID in the `begin` and `end` fields. After a transaction commits, -/// versions switch to tracking timestamps. -#[derive(Clone, Debug, PartialEq)] -enum TxTimestampOrID { - Timestamp(u64), - TxID(u64), -} - -/// Transaction -#[derive(Debug, Clone)] -pub struct Transaction { - /// The state of the transaction. - state: TransactionState, - /// The transaction ID. - tx_id: u64, - /// The transaction begin timestamp. - begin_ts: u64, - /// The transaction write set. - write_set: HashSet, - /// The transaction read set. - read_set: RefCell>, -} - -impl Transaction { - fn new(tx_id: u64, begin_ts: u64) -> Transaction { - Transaction { - state: TransactionState::Active, - tx_id, - begin_ts, - write_set: HashSet::new(), - read_set: RefCell::new(HashSet::new()), - } - } - - fn insert_to_read_set(&self, id: u64) { - let mut read_set = self.read_set.borrow_mut(); - read_set.insert(id); - } - - fn insert_to_write_set(&mut self, id: u64) { - self.write_set.insert(id); - } -} - -/// Transaction state. -#[derive(Debug, Clone)] -enum TransactionState { - Active, - Preparing, - Committed, - Aborted, - Terminated, -} - -/// A database with MVCC. -#[derive(Debug)] -pub struct Database { - inner: Arc>>, -} - -type TxID = u64; - -/// Logical clock. -pub trait LogicalClock { - fn get_timestamp(&self) -> u64; -} - -/// A node-local clock backed by an atomic counter. -#[derive(Debug, Default)] -pub struct LocalClock { - ts_sequence: AtomicU64, -} - -impl LocalClock { - pub fn new() -> Self { - Self { - ts_sequence: AtomicU64::new(0), - } - } -} - -impl LogicalClock for LocalClock { - fn get_timestamp(&self) -> u64 { - self.ts_sequence.fetch_add(1, Ordering::SeqCst) - } -} - -#[derive(Debug)] -pub struct DatabaseInner { - rows: RefCell>>, - txs: RefCell>, - tx_ids: AtomicU64, - clock: Clock, -} - -impl Database { - /// Creates a new database. - pub fn new(clock: Clock) -> Self { - let inner = DatabaseInner { - rows: RefCell::new(HashMap::new()), - txs: RefCell::new(HashMap::new()), - tx_ids: AtomicU64::new(0), - clock, - }; - Self { - inner: Arc::new(Mutex::new(inner)), - } - } - - /// Inserts a new row into the database. - /// - /// This function inserts a new `row` into the database within the context - /// of the transaction `tx_id`. - /// - /// # Arguments - /// - /// * `tx_id` - the ID of the transaction in which to insert the new row. - /// * `row` - the row object containing the values to be inserted. - /// - pub fn insert(&self, tx_id: TxID, row: Row) -> Result<()> { - let inner = self.inner.lock().unwrap(); - let mut txs = inner.txs.borrow_mut(); - let tx = txs - .get_mut(&tx_id) - .ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; - let id = row.id; - let row_version = RowVersion { - begin: TxTimestampOrID::TxID(tx.tx_id), - end: None, - row, - }; - let mut rows = inner.rows.borrow_mut(); - rows.entry(id).or_insert_with(Vec::new).push(row_version); - tx.insert_to_write_set(id); - Ok(()) - } - - /// Updates a row in the database with new values. - /// - /// This function updates an existing row in the database within the - /// context of the transaction `tx_id`. The `row` argument identifies the - /// row to be updated as `id` and contains the new values to be inserted. - /// - /// If the row identified by the `id` does not exist, this function does - /// nothing and returns `false`. Otherwise, the function updates the row - /// with the new values and returns `true`. - /// - /// # Arguments - /// - /// * `tx_id` - the ID of the transaction in which to update the new row. - /// * `row` - the row object containing the values to be updated. - /// - /// # Returns - /// - /// Returns `true` if the row was successfully updated, and `false` otherwise. - pub fn update(&self, tx_id: TxID, row: Row) -> Result { - if !self.delete(tx_id, row.id)? { - return Ok(false); - } - self.insert(tx_id, row)?; - Ok(true) - } - - /// Deletes a row from the table with the given `id`. - /// - /// This function deletes an existing row `id` in the database within the - /// context of the transaction `tx_id`. - /// - /// # Arguments - /// - /// * `tx_id` - the ID of the transaction in which to delete the new row. - /// * `id` - the ID of the row to delete. - /// - /// # Returns - /// - /// Returns `true` if the row was successfully deleted, and `false` otherwise. - /// - pub fn delete(&self, tx: TxID, id: u64) -> Result { - let inner = self.inner.lock().unwrap(); - let mut rows = inner.rows.borrow_mut(); - let mut txs = inner.txs.borrow_mut(); - match rows.get_mut(&id) { - Some(row_versions) => match row_versions.last_mut() { - Some(v) => { - let tx = txs.get(&tx).ok_or(DatabaseError::NoSuchTransactionID(tx))?; - if is_version_visible(&txs, tx, v) { - v.end = Some(TxTimestampOrID::TxID(tx.tx_id)); - } else { - return Ok(false); - } - } - None => unreachable!("no versions for row {}", id), - }, - None => return Ok(false), - } - let tx = txs - .get_mut(&tx) - .ok_or(DatabaseError::NoSuchTransactionID(tx))?; - tx.insert_to_write_set(id); - Ok(true) - } - - /// Retrieves a row from the table with the given `id`. - /// - /// This operation is performed within the scope of the transaction identified - /// by `tx_id`. - /// - /// # Arguments - /// - /// * `tx_id` - The ID of the transaction to perform the read operation in. - /// * `id` - The ID of the row to retrieve. - /// - /// # Returns - /// - /// Returns `Some(row)` with the row data if the row with the given `id` exists, - /// and `None` otherwise. - pub fn read(&self, tx_id: TxID, id: u64) -> Result> { - let inner = self.inner.lock().unwrap(); - let txs = inner.txs.borrow_mut(); - let tx = txs.get(&tx_id).unwrap(); - let rows = inner.rows.borrow(); - if let Some(row_versions) = rows.get(&id) { - for rv in row_versions.iter().rev() { - if is_version_visible(&txs, tx, rv) { - tx.insert_to_read_set(id); - return Ok(Some(rv.row.clone())); - } - } - } - Ok(None) - } - - /// Begins a new transaction in the database. - /// - /// This function starts a new transaction in the database and returns a `TxID` value - /// that you can use to perform operations within the transaction. All changes made within the - /// transaction are isolated from other transactions until you commit the transaction. - pub fn begin_tx(&self) -> TxID { - let mut inner = self.inner.lock().unwrap(); - let tx_id = get_tx_id(&mut inner); - let begin_ts = get_timestamp(&mut inner); - let tx = Transaction::new(tx_id, begin_ts); - let mut txs = inner.txs.borrow_mut(); - txs.insert(tx_id, tx); - tx_id - } - - /// Commits a transaction with the specified transaction ID. - /// - /// This function commits the changes made within the specified transaction and finalizes the - /// transaction. Once a transaction has been committed, all changes made within the transaction - /// are visible to other transactions that access the same data. - /// - /// # Arguments - /// - /// * `tx_id` - The ID of the transaction to commit. - pub fn commit_tx(&self, tx_id: TxID) { - let mut inner = self.inner.lock().unwrap(); - let end_ts = get_timestamp(&mut inner); - let mut txs = inner.txs.borrow_mut(); - let mut tx = txs.get_mut(&tx_id).unwrap(); - let mut rows = inner.rows.borrow_mut(); - tx.state = TransactionState::Preparing; - for id in &tx.write_set { - if let Some(row_versions) = rows.get_mut(id) { - for row_version in row_versions.iter_mut() { - if let TxTimestampOrID::TxID(id) = row_version.begin { - if id == tx_id { - row_version.begin = TxTimestampOrID::Timestamp(tx.begin_ts); - } - } - if let Some(TxTimestampOrID::TxID(id)) = row_version.end { - if id == tx_id { - row_version.end = Some(TxTimestampOrID::Timestamp(end_ts)); - } - } - } - } - } - tx.state = TransactionState::Committed; - } - - /// Rolls back a transaction with the specified ID. - /// - /// This function rolls back a transaction with the specified `tx_id` by - /// discarding any changes made by the transaction. - /// - /// # Arguments - /// - /// * `tx_id` - The ID of the transaction to abort. - pub fn rollback_tx(&self, tx_id: TxID) { - let inner = self.inner.lock().unwrap(); - let mut txs = inner.txs.borrow_mut(); - let mut tx = txs.get_mut(&tx_id).unwrap(); - tx.state = TransactionState::Aborted; - let mut rows = inner.rows.borrow_mut(); - for id in &tx.write_set { - if let Some(row_versions) = rows.get_mut(id) { - row_versions.retain(|rv| rv.begin != TxTimestampOrID::TxID(tx_id)); - if row_versions.is_empty() { - rows.remove(id); - } - } - } - tx.state = TransactionState::Terminated; - } -} - -fn is_version_visible(txs: &HashMap, tx: &Transaction, rv: &RowVersion) -> bool { - is_begin_visible(txs, tx, rv) && is_end_visible(txs, tx, rv) -} - -fn is_begin_visible(txs: &HashMap, tx: &Transaction, rv: &RowVersion) -> bool { - match rv.begin { - TxTimestampOrID::Timestamp(rv_begin_ts) => tx.begin_ts >= rv_begin_ts, - TxTimestampOrID::TxID(rv_begin) => { - let tb = txs.get(&rv_begin).unwrap(); - match tb.state { - TransactionState::Active => tx.tx_id == tb.tx_id && rv.end.is_none(), - TransactionState::Preparing => todo!(), - TransactionState::Committed => todo!(), - TransactionState::Aborted => todo!(), - TransactionState::Terminated => todo!(), - } - } - } -} - -fn is_end_visible(txs: &HashMap, tx: &Transaction, rv: &RowVersion) -> bool { - match rv.end { - Some(TxTimestampOrID::Timestamp(rv_end_ts)) => tx.begin_ts < rv_end_ts, - Some(TxTimestampOrID::TxID(rv_end)) => { - let te = txs.get(&rv_end).unwrap(); - match te.state { - TransactionState::Active => tx.tx_id == te.tx_id && rv.end.is_none(), - TransactionState::Preparing => todo!(), - TransactionState::Committed => todo!(), - TransactionState::Aborted => todo!(), - TransactionState::Terminated => todo!(), - } - } - None => true, - } -} - -fn get_tx_id(inner: &mut DatabaseInner) -> u64 { - inner.tx_ids.fetch_add(1, Ordering::SeqCst) -} - -fn get_timestamp(inner: &mut DatabaseInner) -> u64 { - inner.clock.get_timestamp() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_insert_read() { - let clock = LocalClock::default(); - let db = Database::new(clock); - - let tx1 = db.begin_tx(); - let tx1_row = Row { - id: 1, - data: "Hello".to_string(), - }; - db.insert(tx1, tx1_row.clone()).unwrap(); - let row = db.read(tx1, 1).unwrap().unwrap(); - assert_eq!(tx1_row, row); - db.commit_tx(tx1); - - let tx2 = db.begin_tx(); - let row = db.read(tx2, 1).unwrap().unwrap(); - assert_eq!(tx1_row, row); - } - - #[test] - fn test_read_nonexistent() { - let clock = LocalClock::default(); - let db = Database::new(clock); - let tx = db.begin_tx(); - let row = db.read(tx, 1); - assert!(row.unwrap().is_none()); - } - - #[test] - fn test_delete() { - let clock = LocalClock::default(); - let db = Database::new(clock); - - let tx1 = db.begin_tx(); - let tx1_row = Row { - id: 1, - data: "Hello".to_string(), - }; - db.insert(tx1, tx1_row.clone()).unwrap(); - let row = db.read(tx1, 1).unwrap().unwrap(); - assert_eq!(tx1_row, row); - db.delete(tx1, 1).unwrap(); - let row = db.read(tx1, 1).unwrap(); - assert!(row.is_none()); - db.commit_tx(tx1); - - let tx2 = db.begin_tx(); - let row = db.read(tx2, 1).unwrap(); - assert!(row.is_none()); - } - - #[test] - fn test_delete_nonexistent() { - let clock = LocalClock::default(); - let db = Database::new(clock); - let tx = db.begin_tx(); - assert_eq!(false, db.delete(tx, 1).unwrap()); - } - - #[test] - fn test_commit() { - let clock = LocalClock::default(); - let db = Database::new(clock); - let tx1 = db.begin_tx(); - let tx1_row = Row { - id: 1, - data: "Hello".to_string(), - }; - db.insert(tx1, tx1_row.clone()).unwrap(); - let row = db.read(tx1, 1).unwrap().unwrap(); - assert_eq!(tx1_row, row); - let tx1_updated_row = Row { - id: 1, - data: "World".to_string(), - }; - db.update(tx1, tx1_updated_row.clone()).unwrap(); - let row = db.read(tx1, 1).unwrap().unwrap(); - assert_eq!(tx1_updated_row, row); - db.commit_tx(tx1); - - let tx2 = db.begin_tx(); - let row = db.read(tx2, 1).unwrap().unwrap(); - db.commit_tx(tx2); - assert_eq!(tx1_updated_row, row); - } - - #[test] - fn test_rollback() { - let clock = LocalClock::default(); - let db = Database::new(clock); - let tx1 = db.begin_tx(); - let row1 = Row { - id: 1, - data: "Hello".to_string(), - }; - db.insert(tx1.clone(), row1.clone()).unwrap(); - let row2 = db.read(tx1.clone(), 1).unwrap().unwrap(); - assert_eq!(row1, row2); - let row3 = Row { - id: 1, - data: "World".to_string(), - }; - db.update(tx1.clone(), row3.clone()).unwrap(); - let row4 = db.read(tx1.clone(), 1).unwrap().unwrap(); - assert_eq!(row3, row4); - db.rollback_tx(tx1); - let tx2 = db.begin_tx(); - let row5 = db.read(tx2.clone(), 1).unwrap(); - assert_eq!(row5, None); - } - - #[test] - fn test_dirty_write() { - let clock = LocalClock::default(); - let db = Database::new(clock); - - // T1 inserts a row with ID 1, but does not commit. - let tx1 = db.begin_tx(); - let tx1_row = Row { - id: 1, - data: "Hello".to_string(), - }; - db.insert(tx1, tx1_row.clone()).unwrap(); - let row = db.read(tx1.clone(), 1).unwrap().unwrap(); - assert_eq!(tx1_row, row); - - // T2 attempts to delete row with ID 1, but fails because T1 has not committed. - let tx2 = db.begin_tx(); - let tx2_row = Row { - id: 1, - data: "World".to_string(), - }; - assert_eq!(false, db.update(tx2, tx2_row.clone()).unwrap()); - - let row = db.read(tx1, 1).unwrap().unwrap(); - assert_eq!(tx1_row, row); - } - - #[test] - fn test_dirty_read() { - let clock = LocalClock::default(); - let db = Database::new(clock); - - // T1 inserts a row with ID 1, but does not commit. - let tx1 = db.begin_tx(); - let row1 = Row { - id: 1, - data: "Hello".to_string(), - }; - db.insert(tx1, row1.clone()).unwrap(); - - // T2 attempts to read row with ID 1, but doesn't see one because T1 has not committed. - let tx2 = db.begin_tx(); - let row2 = db.read(tx2, 1).unwrap(); - assert_eq!(row2, None); - } - - #[ignore] - #[test] - fn test_dirty_read_deleted() { - let clock = LocalClock::default(); - let db = Database::new(clock); - - // T1 inserts a row with ID 1 and commits. - let tx1 = db.begin_tx(); - let tx1_row = Row { - id: 1, - data: "Hello".to_string(), - }; - db.insert(tx1, tx1_row.clone()).unwrap(); - db.commit_tx(tx1); - - // T2 deletes row with ID 1, but does not commit. - let tx2 = db.begin_tx(); - assert_eq!(true, db.delete(tx2, 1).unwrap()); - - // T3 reads row with ID 1, but doesn't see the delete because T2 hasn't committed. - let tx3 = db.begin_tx(); - let row = db.read(tx3, 1).unwrap().unwrap(); - assert_eq!(tx1_row, row); - } - - #[test] - fn test_fuzzy_read() { - let clock = LocalClock::default(); - let db = Database::new(clock); - - // T1 inserts a row with ID 1 and commits. - let tx1 = db.begin_tx(); - let tx1_row = Row { - id: 1, - data: "Hello".to_string(), - }; - db.insert(tx1, tx1_row.clone()).unwrap(); - let row = db.read(tx1.clone(), 1).unwrap().unwrap(); - assert_eq!(tx1_row, row); - db.commit_tx(tx1); - - // T2 reads the row with ID 1 within an active transaction. - let tx2 = db.begin_tx(); - let row = db.read(tx2, 1).unwrap().unwrap(); - assert_eq!(tx1_row, row); - - // T3 updates the row and commits. - let tx3 = db.begin_tx(); - let tx3_row = Row { - id: 1, - data: "World".to_string(), - }; - db.update(tx3, tx3_row.clone()).unwrap(); - db.commit_tx(tx3); - - // T2 still reads the same version of the row as before. - let row = db.read(tx2, 1).unwrap().unwrap(); - assert_eq!(tx1_row, row); - } - - #[ignore] - #[test] - fn test_lost_update() { - let clock = LocalClock::default(); - let db = Database::new(clock); - - // T1 inserts a row with ID 1 and commits. - let tx1 = db.begin_tx(); - let tx1_row = Row { - id: 1, - data: "Hello".to_string(), - }; - db.insert(tx1, tx1_row.clone()).unwrap(); - let row = db.read(tx1.clone(), 1).unwrap().unwrap(); - assert_eq!(tx1_row, row); - db.commit_tx(tx1); - - // T2 attempts to update row ID 1 within an active transaction. - let tx2 = db.begin_tx(); - let tx2_row = Row { - id: 1, - data: "World".to_string(), - }; - db.update(tx2, tx2_row.clone()).unwrap(); - - // T3 also attempts to update row ID 1 within an active transaction. - let tx3 = db.begin_tx(); - let tx3_row = Row { - id: 1, - data: "Hello, world!".to_string(), - }; - db.update(tx3, tx3_row.clone()).unwrap(); - - db.commit_tx(tx2); - db.commit_tx(tx3); // TODO: this should fail - - let tx4 = db.begin_tx(); - let row = db.read(tx4, 1).unwrap().unwrap(); - assert_eq!(tx2_row, row); - } -} +pub mod database; \ No newline at end of file From fc93642643c02ac1f7b4a518a5b110cf17ddfcef Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Mon, 10 Apr 2023 18:22:10 +0300 Subject: [PATCH 010/128] Simple microbenchmarks --- core/mvcc/Cargo.lock | 510 ++++++++++++++++++++- core/mvcc/database/Cargo.toml | 7 + core/mvcc/database/benches/my_benchmark.rs | 35 ++ 3 files changed, 550 insertions(+), 2 deletions(-) create mode 100644 core/mvcc/database/benches/my_benchmark.rs diff --git a/core/mvcc/Cargo.lock b/core/mvcc/Cargo.lock index 0f4712b90..bbf607dba 100644 --- a/core/mvcc/Cargo.lock +++ b/core/mvcc/Cargo.lock @@ -2,18 +2,53 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anyhow" version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bumpalo" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.0.79" @@ -26,6 +61,54 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "ciborium" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c137568cc60b904a7724001b35ce2630fd00d5d84805fbb608ab89509d788f" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346de753af073cc87b52b2083a506b38ac176a44cfb05497b622e27be899b369" + +[[package]] +name = "ciborium-ll" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213030a2b5a4e0c0892b6652260cf6ccac84827b83a85a534e178e3906c4cf1b" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "3.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" +dependencies = [ + "bitflags", + "clap_lex", + "indexmap", + "textwrap", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "clipboard-win" version = "4.5.0" @@ -37,6 +120,85 @@ dependencies = [ "winapi", ] +[[package]] +name = "criterion" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" +dependencies = [ + "anes", + "atty", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools", + "lazy_static", + "num-traits", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +dependencies = [ + "cfg-if", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -58,6 +220,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + [[package]] name = "endian-type" version = "0.1.2" @@ -117,23 +285,93 @@ dependencies = [ "wasi", ] +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", +] + [[package]] name = "io-lifetimes" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.1", "libc", "windows-sys 0.48.0", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "js-sys" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.141" @@ -161,11 +399,21 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memoffset" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +dependencies = [ + "autocfg", +] + [[package]] name = "mvcc-rs" version = "0.0.0" dependencies = [ "anyhow", + "criterion", "rustyline", "thiserror", ] @@ -191,6 +439,71 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi 0.2.6", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + +[[package]] +name = "os_str_bytes" +version = "6.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" + +[[package]] +name = "plotters" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b639e642295546c50fcd545198c9d64ee2a38620a628724a3b266d5fbf97" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142" + +[[package]] +name = "plotters-svg" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a81d2759aae1dae668f783c308bc5c8ebd191ff4184aaa1b37f65a6ae5a56f" +dependencies = [ + "plotters-backend", +] + [[package]] name = "proc-macro2" version = "1.0.56" @@ -219,6 +532,28 @@ dependencies = [ "nibble_vec", ] +[[package]] +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -239,6 +574,21 @@ dependencies = [ "thiserror", ] +[[package]] +name = "regex" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "rustix" version = "0.37.7" @@ -276,12 +626,58 @@ dependencies = [ "winapi", ] +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "serde" +version = "1.0.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.13", +] + +[[package]] +name = "serde_json" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "smallvec" version = "1.10.0" @@ -300,6 +696,17 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.13" @@ -311,6 +718,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + [[package]] name = "thiserror" version = "1.0.40" @@ -328,7 +741,17 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.13", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", ] [[package]] @@ -355,12 +778,86 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "walkdir" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 1.0.109", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" + +[[package]] +name = "web-sys" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -377,6 +874,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/core/mvcc/database/Cargo.toml b/core/mvcc/database/Cargo.toml index 7029f5419..9e304558c 100644 --- a/core/mvcc/database/Cargo.toml +++ b/core/mvcc/database/Cargo.toml @@ -7,3 +7,10 @@ edition = "2021" anyhow = "1.0.70" rustyline = "11.0.0" thiserror = "1.0.40" + +[dev-dependencies] +criterion = { version = "0.4", features = ["html_reports"] } + +[[bench]] +name = "my_benchmark" +harness = false \ No newline at end of file diff --git a/core/mvcc/database/benches/my_benchmark.rs b/core/mvcc/database/benches/my_benchmark.rs new file mode 100644 index 000000000..681409602 --- /dev/null +++ b/core/mvcc/database/benches/my_benchmark.rs @@ -0,0 +1,35 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use mvcc_rs::database::{LocalClock, Database, Row}; + +fn criterion_benchmark(c: &mut Criterion) { + let clock = LocalClock::default(); + let db = Database::new(clock); + c.bench_function("begin_tx", |b| b.iter(|| { + db.begin_tx(); + })); + + let clock = LocalClock::default(); + let db = Database::new(clock); + c.bench_function("begin_tx + rollback_tx", |b| b.iter(|| { + let tx_id = db.begin_tx(); + db.rollback_tx(tx_id) + })); + + let clock = LocalClock::default(); + let db = Database::new(clock); + c.bench_function("begin_tx + commit_tx", |b| b.iter(|| { + let tx_id = db.begin_tx(); + db.commit_tx(tx_id) + })); + + let clock = LocalClock::default(); + let db = Database::new(clock); + let tx = db.begin_tx(); + db.insert(tx, Row {id: 1, data: "Hello".to_string()}).unwrap(); + c.bench_function("read", |b| b.iter(|| { + db.read(tx, 1).unwrap(); + })); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); \ No newline at end of file From 22042612d538dfea16a316f56a756752d3f10530 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 12 Apr 2023 11:37:58 +0300 Subject: [PATCH 011/128] Concurrency test The test is disabled because it triggers an assertion in the MVCC implementation. --- core/mvcc/Cargo.lock | 175 +++++++++++++++++++ core/mvcc/database/Cargo.toml | 1 + core/mvcc/database/benches/my_benchmark.rs | 49 ++++-- core/mvcc/database/src/database.rs | 2 +- core/mvcc/database/src/lib.rs | 2 +- core/mvcc/database/tests/concurrency_test.rs | 56 ++++++ 6 files changed, 266 insertions(+), 19 deletions(-) create mode 100644 core/mvcc/database/tests/concurrency_test.rs diff --git a/core/mvcc/Cargo.lock b/core/mvcc/Cargo.lock index bbf607dba..351153968 100644 --- a/core/mvcc/Cargo.lock +++ b/core/mvcc/Cargo.lock @@ -37,6 +37,18 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "bumpalo" version = "3.12.0" @@ -274,6 +286,25 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "generator" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33a20a288a94683f5f4da0adecdbe095c94a77c295e514cc6484e9394dd8376e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows", +] + [[package]] name = "getrandom" version = "0.2.8" @@ -321,6 +352,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "indexmap" version = "1.9.3" @@ -415,6 +452,7 @@ dependencies = [ "anyhow", "criterion", "rustyline", + "shuttle", "thiserror", ] @@ -476,6 +514,18 @@ version = "6.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + [[package]] name = "plotters" version = "0.3.4" @@ -504,6 +554,12 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "proc-macro2" version = "1.0.56" @@ -522,6 +578,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "radix_trie" version = "0.2.1" @@ -532,6 +594,45 @@ dependencies = [ "nibble_vec", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_pcg" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e" +dependencies = [ + "rand_core", +] + [[package]] name = "rayon" version = "1.7.0" @@ -603,6 +704,12 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "rustversion" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" + [[package]] name = "rustyline" version = "11.0.0" @@ -641,6 +748,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.1.0" @@ -678,6 +791,24 @@ dependencies = [ "serde", ] +[[package]] +name = "shuttle" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b45350ea43c87491a8cc7c212cfd163aff964eafcf898cbf72637547e0bccb" +dependencies = [ + "bitvec", + "generator", + "hex", + "owo-colors", + "rand", + "rand_core", + "rand_pcg", + "scoped-tls", + "smallvec", + "tracing", +] + [[package]] name = "smallvec" version = "1.10.0" @@ -718,6 +849,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "textwrap" version = "0.16.0" @@ -754,6 +891,26 @@ dependencies = [ "serde_json", ] +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", +] + [[package]] name = "unicode-ident" version = "1.0.8" @@ -889,6 +1046,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -1020,3 +1186,12 @@ name = "windows_x86_64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] diff --git a/core/mvcc/database/Cargo.toml b/core/mvcc/database/Cargo.toml index 9e304558c..c8ffad031 100644 --- a/core/mvcc/database/Cargo.toml +++ b/core/mvcc/database/Cargo.toml @@ -10,6 +10,7 @@ thiserror = "1.0.40" [dev-dependencies] criterion = { version = "0.4", features = ["html_reports"] } +shuttle = "0.6.0" [[bench]] name = "my_benchmark" diff --git a/core/mvcc/database/benches/my_benchmark.rs b/core/mvcc/database/benches/my_benchmark.rs index 681409602..fc7ff4b52 100644 --- a/core/mvcc/database/benches/my_benchmark.rs +++ b/core/mvcc/database/benches/my_benchmark.rs @@ -1,35 +1,50 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use mvcc_rs::database::{LocalClock, Database, Row}; +use mvcc_rs::database::{Database, LocalClock, Row}; fn criterion_benchmark(c: &mut Criterion) { let clock = LocalClock::default(); let db = Database::new(clock); - c.bench_function("begin_tx", |b| b.iter(|| { - db.begin_tx(); - })); + c.bench_function("begin_tx", |b| { + b.iter(|| { + db.begin_tx(); + }) + }); let clock = LocalClock::default(); let db = Database::new(clock); - c.bench_function("begin_tx + rollback_tx", |b| b.iter(|| { - let tx_id = db.begin_tx(); - db.rollback_tx(tx_id) - })); + c.bench_function("begin_tx + rollback_tx", |b| { + b.iter(|| { + let tx_id = db.begin_tx(); + db.rollback_tx(tx_id) + }) + }); let clock = LocalClock::default(); let db = Database::new(clock); - c.bench_function("begin_tx + commit_tx", |b| b.iter(|| { - let tx_id = db.begin_tx(); - db.commit_tx(tx_id) - })); + c.bench_function("begin_tx + commit_tx", |b| { + b.iter(|| { + let tx_id = db.begin_tx(); + db.commit_tx(tx_id) + }) + }); let clock = LocalClock::default(); let db = Database::new(clock); let tx = db.begin_tx(); - db.insert(tx, Row {id: 1, data: "Hello".to_string()}).unwrap(); - c.bench_function("read", |b| b.iter(|| { - db.read(tx, 1).unwrap(); - })); + db.insert( + tx, + Row { + id: 1, + data: "Hello".to_string(), + }, + ) + .unwrap(); + c.bench_function("read", |b| { + b.iter(|| { + db.read(tx, 1).unwrap(); + }) + }); } criterion_group!(benches, criterion_benchmark); -criterion_main!(benches); \ No newline at end of file +criterion_main!(benches); diff --git a/core/mvcc/database/src/database.rs b/core/mvcc/database/src/database.rs index cdebed823..d299b69f6 100644 --- a/core/mvcc/database/src/database.rs +++ b/core/mvcc/database/src/database.rs @@ -639,4 +639,4 @@ mod tests { let row = db.read(tx4, 1).unwrap().unwrap(); assert_eq!(tx2_row, row); } -} \ No newline at end of file +} diff --git a/core/mvcc/database/src/lib.rs b/core/mvcc/database/src/lib.rs index 534e2ec26..e5ad14df6 100644 --- a/core/mvcc/database/src/lib.rs +++ b/core/mvcc/database/src/lib.rs @@ -31,5 +31,5 @@ //! * Optimistic reads and writes //! * Garbage collection +pub mod database; pub mod errors; -pub mod database; \ No newline at end of file diff --git a/core/mvcc/database/tests/concurrency_test.rs b/core/mvcc/database/tests/concurrency_test.rs new file mode 100644 index 000000000..c45baddca --- /dev/null +++ b/core/mvcc/database/tests/concurrency_test.rs @@ -0,0 +1,56 @@ +use mvcc_rs::database::{Database, LocalClock, Row}; +use shuttle::sync::atomic::AtomicU64; +use shuttle::sync::Arc; +use shuttle::thread; +use std::sync::atomic::Ordering; + +#[ignore] +#[test] +fn test_non_overlapping_concurrent_inserts() { + // Two threads insert to the database concurrently using non-overlapping + // row IDs. + let clock = LocalClock::default(); + let db = Arc::new(Database::new(clock)); + let ids = Arc::new(AtomicU64::new(0)); + shuttle::check_random( + move || { + { + let db = db.clone(); + let ids = ids.clone(); + thread::spawn(move || { + let tx = db.begin_tx(); + let id = ids.fetch_add(1, Ordering::SeqCst); + let row = Row { + id, + data: "Hello".to_string(), + }; + db.insert(id, row.clone()).unwrap(); + db.commit_tx(tx); + let tx = db.begin_tx(); + let committed_row = db.read(tx, id).unwrap(); + db.commit_tx(tx); + assert_eq!(committed_row, Some(row)); + }); + } + { + let db = db.clone(); + let ids = ids.clone(); + thread::spawn(move || { + let tx = db.begin_tx(); + let id = ids.fetch_add(1, Ordering::SeqCst); + let row = Row { + id, + data: "World".to_string(), + }; + db.insert(id, row.clone()).unwrap(); + db.commit_tx(tx); + let tx = db.begin_tx(); + let committed_row = db.read(tx, id).unwrap(); + db.commit_tx(tx); + assert_eq!(committed_row, Some(row)); + }); + } + }, + 100, + ); +} From 3cecf777cf15e18f58fbd22a93b93db0859409bd Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 12 Apr 2023 11:55:34 +0300 Subject: [PATCH 012/128] Assert that we're manipulating an active transaction --- core/mvcc/database/src/database.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/mvcc/database/src/database.rs b/core/mvcc/database/src/database.rs index d299b69f6..8399252f2 100644 --- a/core/mvcc/database/src/database.rs +++ b/core/mvcc/database/src/database.rs @@ -69,7 +69,7 @@ impl Transaction { } /// Transaction state. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] enum TransactionState { Active, Preparing, @@ -149,6 +149,7 @@ impl Database { let tx = txs .get_mut(&tx_id) .ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; + assert!(tx.state == TransactionState::Active); let id = row.id; let row_version = RowVersion { begin: TxTimestampOrID::TxID(tx.tx_id), @@ -209,6 +210,7 @@ impl Database { Some(row_versions) => match row_versions.last_mut() { Some(v) => { let tx = txs.get(&tx).ok_or(DatabaseError::NoSuchTransactionID(tx))?; + assert!(tx.state == TransactionState::Active); if is_version_visible(&txs, tx, v) { v.end = Some(TxTimestampOrID::TxID(tx.tx_id)); } else { @@ -244,6 +246,7 @@ impl Database { let inner = self.inner.lock().unwrap(); let txs = inner.txs.borrow_mut(); let tx = txs.get(&tx_id).unwrap(); + assert!(tx.state == TransactionState::Active); let rows = inner.rows.borrow(); if let Some(row_versions) = rows.get(&id) { for rv in row_versions.iter().rev() { @@ -285,6 +288,7 @@ impl Database { let end_ts = get_timestamp(&mut inner); let mut txs = inner.txs.borrow_mut(); let mut tx = txs.get_mut(&tx_id).unwrap(); + assert!(tx.state == TransactionState::Active); let mut rows = inner.rows.borrow_mut(); tx.state = TransactionState::Preparing; for id in &tx.write_set { @@ -318,6 +322,7 @@ impl Database { let inner = self.inner.lock().unwrap(); let mut txs = inner.txs.borrow_mut(); let mut tx = txs.get_mut(&tx_id).unwrap(); + assert!(tx.state == TransactionState::Active); tx.state = TransactionState::Aborted; let mut rows = inner.rows.borrow_mut(); for id in &tx.write_set { From 77d639fc208e9eea204b429efe906f1e9504d949 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 12 Apr 2023 11:56:22 +0300 Subject: [PATCH 013/128] Fix concurrency test It was accidentally using row ID as transaction ID... --- core/mvcc/database/tests/concurrency_test.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/mvcc/database/tests/concurrency_test.rs b/core/mvcc/database/tests/concurrency_test.rs index c45baddca..a39a87dec 100644 --- a/core/mvcc/database/tests/concurrency_test.rs +++ b/core/mvcc/database/tests/concurrency_test.rs @@ -4,7 +4,6 @@ use shuttle::sync::Arc; use shuttle::thread; use std::sync::atomic::Ordering; -#[ignore] #[test] fn test_non_overlapping_concurrent_inserts() { // Two threads insert to the database concurrently using non-overlapping @@ -24,7 +23,7 @@ fn test_non_overlapping_concurrent_inserts() { id, data: "Hello".to_string(), }; - db.insert(id, row.clone()).unwrap(); + db.insert(tx, row.clone()).unwrap(); db.commit_tx(tx); let tx = db.begin_tx(); let committed_row = db.read(tx, id).unwrap(); @@ -42,7 +41,7 @@ fn test_non_overlapping_concurrent_inserts() { id, data: "World".to_string(), }; - db.insert(id, row.clone()).unwrap(); + db.insert(tx, row.clone()).unwrap(); db.commit_tx(tx); let tx = db.begin_tx(); let committed_row = db.read(tx, id).unwrap(); From a52bf9158ba058a3b1f07b7368ad1782c7d09e40 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 12 Apr 2023 12:12:37 +0300 Subject: [PATCH 014/128] Fix delete() TX ID parameter name Align it with the rest of the code. --- core/mvcc/database/src/database.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/mvcc/database/src/database.rs b/core/mvcc/database/src/database.rs index 8399252f2..3998db2b6 100644 --- a/core/mvcc/database/src/database.rs +++ b/core/mvcc/database/src/database.rs @@ -202,14 +202,14 @@ impl Database { /// /// Returns `true` if the row was successfully deleted, and `false` otherwise. /// - pub fn delete(&self, tx: TxID, id: u64) -> Result { + pub fn delete(&self, tx_id: TxID, id: u64) -> Result { let inner = self.inner.lock().unwrap(); let mut rows = inner.rows.borrow_mut(); let mut txs = inner.txs.borrow_mut(); match rows.get_mut(&id) { Some(row_versions) => match row_versions.last_mut() { Some(v) => { - let tx = txs.get(&tx).ok_or(DatabaseError::NoSuchTransactionID(tx))?; + let tx = txs.get(&tx_id).ok_or(DatabaseError::NoSuchTransactionID(tx))?; assert!(tx.state == TransactionState::Active); if is_version_visible(&txs, tx, v) { v.end = Some(TxTimestampOrID::TxID(tx.tx_id)); From 477da5b60a3adbe6b1c8f65ae183d145d3b8436f Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 12 Apr 2023 12:17:34 +0300 Subject: [PATCH 015/128] Fix compile error --- core/mvcc/database/src/database.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/mvcc/database/src/database.rs b/core/mvcc/database/src/database.rs index 3998db2b6..a1dd27265 100644 --- a/core/mvcc/database/src/database.rs +++ b/core/mvcc/database/src/database.rs @@ -209,7 +209,7 @@ impl Database { match rows.get_mut(&id) { Some(row_versions) => match row_versions.last_mut() { Some(v) => { - let tx = txs.get(&tx_id).ok_or(DatabaseError::NoSuchTransactionID(tx))?; + let tx = txs.get(&tx_id).ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; assert!(tx.state == TransactionState::Active); if is_version_visible(&txs, tx, v) { v.end = Some(TxTimestampOrID::TxID(tx.tx_id)); @@ -222,8 +222,8 @@ impl Database { None => return Ok(false), } let tx = txs - .get_mut(&tx) - .ok_or(DatabaseError::NoSuchTransactionID(tx))?; + .get_mut(&tx_id) + .ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; tx.insert_to_write_set(id); Ok(true) } From eb250e1e83e7292ab8ebf9f0a320ab9d1380e4fa Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 12 Apr 2023 12:39:14 +0300 Subject: [PATCH 016/128] Wire up flamegraphs to `cargo bench` --- core/mvcc/Cargo.lock | 326 ++++++++++++++++++++- core/mvcc/README.md | 21 ++ core/mvcc/database/Cargo.toml | 3 +- core/mvcc/database/benches/my_benchmark.rs | 9 +- 4 files changed, 355 insertions(+), 4 deletions(-) diff --git a/core/mvcc/Cargo.lock b/core/mvcc/Cargo.lock index 351153968..d1d24e4b9 100644 --- a/core/mvcc/Cargo.lock +++ b/core/mvcc/Cargo.lock @@ -2,6 +2,33 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "anes" version = "0.1.6" @@ -14,6 +41,12 @@ version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + [[package]] name = "atty" version = "0.2.14" @@ -31,6 +64,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -55,6 +103,12 @@ version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +[[package]] +name = "bytemuck" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" + [[package]] name = "cast" version = "0.3.0" @@ -132,6 +186,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "cpp_demangle" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b446fd40bcc17eddd6a4a78f24315eb90afdb3334999ddfd4909985c47722442" +dependencies = [ + "cfg-if", +] + [[package]] name = "criterion" version = "0.4.0" @@ -211,6 +274,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -275,6 +347,15 @@ dependencies = [ "str-buf", ] +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + [[package]] name = "fd-lock" version = "3.0.11" @@ -286,6 +367,18 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "findshlibs" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64" +dependencies = [ + "cc", + "lazy_static", + "libc", + "winapi", +] + [[package]] name = "funty" version = "2.0.0" @@ -316,6 +409,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "gimli" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" + [[package]] name = "half" version = "1.8.2" @@ -368,6 +467,33 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inferno" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fb7c1b80a1dfa604bb4a649a5c5aeef3d913f7c520cb42b40e534e8a61bcdfc" +dependencies = [ + "ahash", + "indexmap", + "is-terminal", + "itoa", + "log", + "num-format", + "once_cell", + "quick-xml", + "rgb", + "str_stack", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + [[package]] name = "io-lifetimes" version = "1.0.10" @@ -379,6 +505,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "is-terminal" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "itertools" version = "0.10.5" @@ -421,6 +559,16 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.17" @@ -436,6 +584,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.8.0" @@ -445,12 +602,22 @@ dependencies = [ "autocfg", ] +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + [[package]] name = "mvcc-rs" version = "0.0.0" dependencies = [ "anyhow", "criterion", + "pprof", "rustyline", "shuttle", "thiserror", @@ -477,6 +644,16 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "num-format" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" +dependencies = [ + "arrayvec", + "itoa", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -496,6 +673,15 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.30.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.17.1" @@ -520,6 +706,29 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "windows-sys 0.45.0", +] + [[package]] name = "pin-project-lite" version = "0.2.9" @@ -554,6 +763,28 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "pprof" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "196ded5d4be535690899a4631cc9f18cdc41b7ebf24a79400f46f48e49a11059" +dependencies = [ + "backtrace", + "cfg-if", + "criterion", + "findshlibs", + "inferno", + "libc", + "log", + "nix", + "once_cell", + "parking_lot", + "smallvec", + "symbolic-demangle", + "tempfile", + "thiserror", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -569,6 +800,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.26" @@ -664,6 +904,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_users" version = "0.4.3" @@ -671,7 +920,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ "getrandom", - "redox_syscall", + "redox_syscall 0.2.16", "thiserror", ] @@ -690,6 +939,21 @@ version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +[[package]] +name = "rgb" +version = "0.8.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ec2d3e3fc7a92ced357df9cebd5a10b6fb2aa1ee797bf7e9ce2f17dffc8f59" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a36c42d1873f9a77c53bde094f9664d9891bc604a45b4798fd2c389ed12e5b" + [[package]] name = "rustix" version = "0.37.7" @@ -815,6 +1079,12 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -827,6 +1097,35 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" +[[package]] +name = "str_stack" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9091b6114800a5f2141aee1d1b9d6ca3592ac062dc5decb3764ec5895a47b4eb" + +[[package]] +name = "symbolic-common" +version = "10.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b55cdc318ede251d0957f07afe5fed912119b8c1bc5a7804151826db999e737" +dependencies = [ + "debugid", + "memmap2", + "stable_deref_trait", + "uuid", +] + +[[package]] +name = "symbolic-demangle" +version = "10.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79be897be8a483a81fff6a3a4e195b4ac838ef73ca42d348b3f722da9902e489" +dependencies = [ + "cpp_demangle", + "rustc-demangle", + "symbolic-common", +] + [[package]] name = "syn" version = "1.0.109" @@ -855,6 +1154,19 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tempfile" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall 0.3.5", + "rustix", + "windows-sys 0.45.0", +] + [[package]] name = "textwrap" version = "0.16.0" @@ -935,6 +1247,18 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "uuid" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b55a3fef2a1e3b3a00ce878640918820d3c51081576ac657d23af9fc7928fdb" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "walkdir" version = "2.3.3" diff --git a/core/mvcc/README.md b/core/mvcc/README.md index de059824b..a15efee67 100644 --- a/core/mvcc/README.md +++ b/core/mvcc/README.md @@ -2,6 +2,27 @@ This is a _work-in-progress_ Rust implementation of the Hekaton optimistic multiversion concurrency control algorithm. +## Development + +Run tests: + +```console +cargo test +``` + +Run benchmarks: + +```console +cargo bench +``` + +Run benchmarks and generate flamegraphs: + +```console +echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid +cargo bench --bench my_benchmark -- --profile-time=5 +``` + ## References Larson et al. [High-Performance Concurrency Control Mechanisms for Main-Memory Databases](https://vldb.org/pvldb/vol5/p298_per-akelarson_vldb2012.pdf). VLDB '11 diff --git a/core/mvcc/database/Cargo.toml b/core/mvcc/database/Cargo.toml index c8ffad031..3044bc809 100644 --- a/core/mvcc/database/Cargo.toml +++ b/core/mvcc/database/Cargo.toml @@ -10,8 +10,9 @@ thiserror = "1.0.40" [dev-dependencies] criterion = { version = "0.4", features = ["html_reports"] } +pprof = { version = "0.11.1", features = ["criterion", "flamegraph"] } shuttle = "0.6.0" [[bench]] name = "my_benchmark" -harness = false \ No newline at end of file +harness = false diff --git a/core/mvcc/database/benches/my_benchmark.rs b/core/mvcc/database/benches/my_benchmark.rs index fc7ff4b52..7f6f8ea50 100644 --- a/core/mvcc/database/benches/my_benchmark.rs +++ b/core/mvcc/database/benches/my_benchmark.rs @@ -1,7 +1,8 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; use mvcc_rs::database::{Database, LocalClock, Row}; +use pprof::criterion::{Output, PProfProfiler}; -fn criterion_benchmark(c: &mut Criterion) { +fn bench(c: &mut Criterion) { let clock = LocalClock::default(); let db = Database::new(clock); c.bench_function("begin_tx", |b| { @@ -46,5 +47,9 @@ fn criterion_benchmark(c: &mut Criterion) { }); } -criterion_group!(benches, criterion_benchmark); +criterion_group! { + name = benches; + config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); + targets = bench +} criterion_main!(benches); From d7ecfc054c8d7fed64216093cdc067783b46b822 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Thu, 13 Apr 2023 09:36:10 +0300 Subject: [PATCH 017/128] Fix lost update anomaly Fixes #5 --- core/mvcc/database/src/database.rs | 128 ++++++++++++------- core/mvcc/database/src/errors.rs | 6 +- core/mvcc/database/tests/concurrency_test.rs | 8 +- 3 files changed, 92 insertions(+), 50 deletions(-) diff --git a/core/mvcc/database/src/database.rs b/core/mvcc/database/src/database.rs index a1dd27265..51176c617 100644 --- a/core/mvcc/database/src/database.rs +++ b/core/mvcc/database/src/database.rs @@ -119,6 +119,25 @@ pub struct DatabaseInner { clock: Clock, } +impl DatabaseInner { + fn rollback_tx(&self, tx_id: TxID) { + let mut txs = self.txs.borrow_mut(); + let mut tx = txs.get_mut(&tx_id).unwrap(); + assert!(tx.state == TransactionState::Active); + tx.state = TransactionState::Aborted; + let mut rows = self.rows.borrow_mut(); + for id in &tx.write_set { + if let Some(row_versions) = rows.get_mut(id) { + row_versions.retain(|rv| rv.begin != TxTimestampOrID::TxID(tx_id)); + if row_versions.is_empty() { + rows.remove(id); + } + } + } + tx.state = TransactionState::Terminated; + } +} + impl Database { /// Creates a new database. pub fn new(clock: Clock) -> Self { @@ -206,26 +225,29 @@ impl Database { let inner = self.inner.lock().unwrap(); let mut rows = inner.rows.borrow_mut(); let mut txs = inner.txs.borrow_mut(); - match rows.get_mut(&id) { - Some(row_versions) => match row_versions.last_mut() { - Some(v) => { - let tx = txs.get(&tx_id).ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; - assert!(tx.state == TransactionState::Active); - if is_version_visible(&txs, tx, v) { - v.end = Some(TxTimestampOrID::TxID(tx.tx_id)); - } else { - return Ok(false); - } + if let Some(row_versions) = rows.get_mut(&id) { + for rv in row_versions.iter_mut().rev() { + let tx = txs + .get(&tx_id) + .ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; + assert!(tx.state == TransactionState::Active); + if is_write_write_conflict(&txs, tx, rv) { + drop(txs); + drop(rows); + inner.rollback_tx(tx_id); + return Err(DatabaseError::WriteWriteConflict); } - None => unreachable!("no versions for row {}", id), - }, - None => return Ok(false), + if is_version_visible(&txs, tx, rv) { + rv.end = Some(TxTimestampOrID::TxID(tx.tx_id)); + let tx = txs + .get_mut(&tx_id) + .ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; + tx.insert_to_write_set(id); + return Ok(true); + } + } } - let tx = txs - .get_mut(&tx_id) - .ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; - tx.insert_to_write_set(id); - Ok(true) + Ok(false) } /// Retrieves a row from the table with the given `id`. @@ -283,12 +305,17 @@ impl Database { /// # Arguments /// /// * `tx_id` - The ID of the transaction to commit. - pub fn commit_tx(&self, tx_id: TxID) { + pub fn commit_tx(&self, tx_id: TxID) -> Result<()> { let mut inner = self.inner.lock().unwrap(); let end_ts = get_timestamp(&mut inner); let mut txs = inner.txs.borrow_mut(); let mut tx = txs.get_mut(&tx_id).unwrap(); - assert!(tx.state == TransactionState::Active); + match tx.state { + TransactionState::Terminated => return Err(DatabaseError::TxTerminated), + _ => { + assert!(tx.state == TransactionState::Active); + } + } let mut rows = inner.rows.borrow_mut(); tx.state = TransactionState::Preparing; for id in &tx.write_set { @@ -308,6 +335,7 @@ impl Database { } } tx.state = TransactionState::Committed; + Ok(()) } /// Rolls back a transaction with the specified ID. @@ -320,20 +348,28 @@ impl Database { /// * `tx_id` - The ID of the transaction to abort. pub fn rollback_tx(&self, tx_id: TxID) { let inner = self.inner.lock().unwrap(); - let mut txs = inner.txs.borrow_mut(); - let mut tx = txs.get_mut(&tx_id).unwrap(); - assert!(tx.state == TransactionState::Active); - tx.state = TransactionState::Aborted; - let mut rows = inner.rows.borrow_mut(); - for id in &tx.write_set { - if let Some(row_versions) = rows.get_mut(id) { - row_versions.retain(|rv| rv.begin != TxTimestampOrID::TxID(tx_id)); - if row_versions.is_empty() { - rows.remove(id); - } + inner.rollback_tx(tx_id); + } +} + +fn is_write_write_conflict( + txs: &HashMap, + tx: &Transaction, + rv: &RowVersion, +) -> bool { + match rv.end { + Some(TxTimestampOrID::TxID(rv_end)) => { + let te = txs.get(&rv_end).unwrap(); + match te.state { + TransactionState::Active => tx.tx_id != te.tx_id, + TransactionState::Preparing => todo!(), + TransactionState::Committed => todo!(), + TransactionState::Aborted => todo!(), + TransactionState::Terminated => todo!(), } } - tx.state = TransactionState::Terminated; + Some(TxTimestampOrID::Timestamp(_)) => false, + None => false, } } @@ -399,7 +435,7 @@ mod tests { db.insert(tx1, tx1_row.clone()).unwrap(); let row = db.read(tx1, 1).unwrap().unwrap(); assert_eq!(tx1_row, row); - db.commit_tx(tx1); + db.commit_tx(tx1).unwrap(); let tx2 = db.begin_tx(); let row = db.read(tx2, 1).unwrap().unwrap(); @@ -431,7 +467,7 @@ mod tests { db.delete(tx1, 1).unwrap(); let row = db.read(tx1, 1).unwrap(); assert!(row.is_none()); - db.commit_tx(tx1); + db.commit_tx(tx1).unwrap(); let tx2 = db.begin_tx(); let row = db.read(tx2, 1).unwrap(); @@ -465,11 +501,11 @@ mod tests { db.update(tx1, tx1_updated_row.clone()).unwrap(); let row = db.read(tx1, 1).unwrap().unwrap(); assert_eq!(tx1_updated_row, row); - db.commit_tx(tx1); + db.commit_tx(tx1).unwrap(); let tx2 = db.begin_tx(); let row = db.read(tx2, 1).unwrap().unwrap(); - db.commit_tx(tx2); + db.commit_tx(tx2).unwrap(); assert_eq!(tx1_updated_row, row); } @@ -557,7 +593,7 @@ mod tests { data: "Hello".to_string(), }; db.insert(tx1, tx1_row.clone()).unwrap(); - db.commit_tx(tx1); + db.commit_tx(tx1).unwrap(); // T2 deletes row with ID 1, but does not commit. let tx2 = db.begin_tx(); @@ -583,7 +619,7 @@ mod tests { db.insert(tx1, tx1_row.clone()).unwrap(); let row = db.read(tx1.clone(), 1).unwrap().unwrap(); assert_eq!(tx1_row, row); - db.commit_tx(tx1); + db.commit_tx(tx1).unwrap(); // T2 reads the row with ID 1 within an active transaction. let tx2 = db.begin_tx(); @@ -597,14 +633,13 @@ mod tests { data: "World".to_string(), }; db.update(tx3, tx3_row.clone()).unwrap(); - db.commit_tx(tx3); + db.commit_tx(tx3).unwrap(); // T2 still reads the same version of the row as before. let row = db.read(tx2, 1).unwrap().unwrap(); assert_eq!(tx1_row, row); } - #[ignore] #[test] fn test_lost_update() { let clock = LocalClock::default(); @@ -619,7 +654,7 @@ mod tests { db.insert(tx1, tx1_row.clone()).unwrap(); let row = db.read(tx1.clone(), 1).unwrap().unwrap(); assert_eq!(tx1_row, row); - db.commit_tx(tx1); + db.commit_tx(tx1).unwrap(); // T2 attempts to update row ID 1 within an active transaction. let tx2 = db.begin_tx(); @@ -627,7 +662,7 @@ mod tests { id: 1, data: "World".to_string(), }; - db.update(tx2, tx2_row.clone()).unwrap(); + assert!(db.update(tx2, tx2_row.clone()).unwrap()); // T3 also attempts to update row ID 1 within an active transaction. let tx3 = db.begin_tx(); @@ -635,10 +670,13 @@ mod tests { id: 1, data: "Hello, world!".to_string(), }; - db.update(tx3, tx3_row.clone()).unwrap(); + assert_eq!( + Err(DatabaseError::WriteWriteConflict), + db.update(tx3, tx3_row.clone()) + ); - db.commit_tx(tx2); - db.commit_tx(tx3); // TODO: this should fail + db.commit_tx(tx2).unwrap(); + assert_eq!(Err(DatabaseError::TxTerminated), db.commit_tx(tx3)); let tx4 = db.begin_tx(); let row = db.read(tx4, 1).unwrap().unwrap(); diff --git a/core/mvcc/database/src/errors.rs b/core/mvcc/database/src/errors.rs index 95901137b..7bd5bab57 100644 --- a/core/mvcc/database/src/errors.rs +++ b/core/mvcc/database/src/errors.rs @@ -1,7 +1,11 @@ use thiserror::Error; -#[derive(Error, Debug)] +#[derive(Error, Debug, PartialEq)] pub enum DatabaseError { #[error("no such transaction ID: `{0}`")] NoSuchTransactionID(u64), + #[error("transaction aborted because of a write-write conflict")] + WriteWriteConflict, + #[error("transaction is terminated")] + TxTerminated, } diff --git a/core/mvcc/database/tests/concurrency_test.rs b/core/mvcc/database/tests/concurrency_test.rs index a39a87dec..b18e9a9bd 100644 --- a/core/mvcc/database/tests/concurrency_test.rs +++ b/core/mvcc/database/tests/concurrency_test.rs @@ -24,10 +24,10 @@ fn test_non_overlapping_concurrent_inserts() { data: "Hello".to_string(), }; db.insert(tx, row.clone()).unwrap(); - db.commit_tx(tx); + db.commit_tx(tx).unwrap(); let tx = db.begin_tx(); let committed_row = db.read(tx, id).unwrap(); - db.commit_tx(tx); + db.commit_tx(tx).unwrap(); assert_eq!(committed_row, Some(row)); }); } @@ -42,10 +42,10 @@ fn test_non_overlapping_concurrent_inserts() { data: "World".to_string(), }; db.insert(tx, row.clone()).unwrap(); - db.commit_tx(tx); + db.commit_tx(tx).unwrap(); let tx = db.begin_tx(); let committed_row = db.read(tx, id).unwrap(); - db.commit_tx(tx); + db.commit_tx(tx).unwrap(); assert_eq!(committed_row, Some(row)); }); } From 824669d471d90cc5e1568cebeff99de840697616 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Thu, 13 Apr 2023 10:05:09 +0300 Subject: [PATCH 018/128] Move code into DatabaseInner Let's move rest of the code into DatabaseInner like we did for `rollback_tx` as a code cleanup. --- core/mvcc/database/src/database.rs | 212 ++++++++++++++++------------- 1 file changed, 116 insertions(+), 96 deletions(-) diff --git a/core/mvcc/database/src/database.rs b/core/mvcc/database/src/database.rs index 51176c617..837df7ef1 100644 --- a/core/mvcc/database/src/database.rs +++ b/core/mvcc/database/src/database.rs @@ -120,6 +120,109 @@ pub struct DatabaseInner { } impl DatabaseInner { + fn insert(&self, tx_id: TxID, row: Row) -> Result<()> { + let mut txs = self.txs.borrow_mut(); + let tx = txs + .get_mut(&tx_id) + .ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; + assert!(tx.state == TransactionState::Active); + let id = row.id; + let row_version = RowVersion { + begin: TxTimestampOrID::TxID(tx.tx_id), + end: None, + row, + }; + let mut rows = self.rows.borrow_mut(); + rows.entry(id).or_insert_with(Vec::new).push(row_version); + tx.insert_to_write_set(id); + Ok(()) + } + + fn delete(&self, tx_id: TxID, id: u64) -> Result { + let mut rows = self.rows.borrow_mut(); + let mut txs = self.txs.borrow_mut(); + if let Some(row_versions) = rows.get_mut(&id) { + for rv in row_versions.iter_mut().rev() { + let tx = txs + .get(&tx_id) + .ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; + assert!(tx.state == TransactionState::Active); + if is_write_write_conflict(&txs, tx, rv) { + drop(txs); + drop(rows); + self.rollback_tx(tx_id); + return Err(DatabaseError::WriteWriteConflict); + } + if is_version_visible(&txs, tx, rv) { + rv.end = Some(TxTimestampOrID::TxID(tx.tx_id)); + let tx = txs + .get_mut(&tx_id) + .ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; + tx.insert_to_write_set(id); + return Ok(true); + } + } + } + Ok(false) + } + + fn read(&self, tx_id: TxID, id: u64) -> Result> { + let txs = self.txs.borrow_mut(); + let tx = txs.get(&tx_id).unwrap(); + assert!(tx.state == TransactionState::Active); + let rows = self.rows.borrow(); + if let Some(row_versions) = rows.get(&id) { + for rv in row_versions.iter().rev() { + if is_version_visible(&txs, tx, rv) { + tx.insert_to_read_set(id); + return Ok(Some(rv.row.clone())); + } + } + } + Ok(None) + } + + fn begin_tx(&mut self) -> TxID { + let tx_id = self.get_tx_id(); + let begin_ts = self.get_timestamp(); + let tx = Transaction::new(tx_id, begin_ts); + let mut txs = self.txs.borrow_mut(); + txs.insert(tx_id, tx); + tx_id + } + + fn commit_tx(&mut self, tx_id: TxID) -> Result<()> { + let end_ts = self.get_timestamp(); + let mut txs = self.txs.borrow_mut(); + let mut tx = txs.get_mut(&tx_id).unwrap(); + match tx.state { + TransactionState::Terminated => return Err(DatabaseError::TxTerminated), + _ => { + assert!(tx.state == TransactionState::Active); + } + } + let mut rows = self.rows.borrow_mut(); + tx.state = TransactionState::Preparing; + for id in &tx.write_set { + if let Some(row_versions) = rows.get_mut(id) { + for row_version in row_versions.iter_mut() { + if let TxTimestampOrID::TxID(id) = row_version.begin { + if id == tx_id { + row_version.begin = TxTimestampOrID::Timestamp(tx.begin_ts); + } + } + if let Some(TxTimestampOrID::TxID(id)) = row_version.end { + if id == tx_id { + row_version.end = Some(TxTimestampOrID::Timestamp(end_ts)); + } + } + } + } + } + tx.state = TransactionState::Committed; + Ok(()) + } + fn rollback_tx(&self, tx_id: TxID) { let mut txs = self.txs.borrow_mut(); let mut tx = txs.get_mut(&tx_id).unwrap(); @@ -136,6 +239,14 @@ impl DatabaseInner { } tx.state = TransactionState::Terminated; } + + fn get_tx_id(&mut self) -> u64 { + self.tx_ids.fetch_add(1, Ordering::SeqCst) + } + + fn get_timestamp(&mut self) -> u64 { + self.clock.get_timestamp() + } } impl Database { @@ -164,21 +275,7 @@ impl Database { /// pub fn insert(&self, tx_id: TxID, row: Row) -> Result<()> { let inner = self.inner.lock().unwrap(); - let mut txs = inner.txs.borrow_mut(); - let tx = txs - .get_mut(&tx_id) - .ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; - assert!(tx.state == TransactionState::Active); - let id = row.id; - let row_version = RowVersion { - begin: TxTimestampOrID::TxID(tx.tx_id), - end: None, - row, - }; - let mut rows = inner.rows.borrow_mut(); - rows.entry(id).or_insert_with(Vec::new).push(row_version); - tx.insert_to_write_set(id); - Ok(()) + inner.insert(tx_id, row) } /// Updates a row in the database with new values. @@ -223,31 +320,7 @@ impl Database { /// pub fn delete(&self, tx_id: TxID, id: u64) -> Result { let inner = self.inner.lock().unwrap(); - let mut rows = inner.rows.borrow_mut(); - let mut txs = inner.txs.borrow_mut(); - if let Some(row_versions) = rows.get_mut(&id) { - for rv in row_versions.iter_mut().rev() { - let tx = txs - .get(&tx_id) - .ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; - assert!(tx.state == TransactionState::Active); - if is_write_write_conflict(&txs, tx, rv) { - drop(txs); - drop(rows); - inner.rollback_tx(tx_id); - return Err(DatabaseError::WriteWriteConflict); - } - if is_version_visible(&txs, tx, rv) { - rv.end = Some(TxTimestampOrID::TxID(tx.tx_id)); - let tx = txs - .get_mut(&tx_id) - .ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; - tx.insert_to_write_set(id); - return Ok(true); - } - } - } - Ok(false) + inner.delete(tx_id, id) } /// Retrieves a row from the table with the given `id`. @@ -266,19 +339,7 @@ impl Database { /// and `None` otherwise. pub fn read(&self, tx_id: TxID, id: u64) -> Result> { let inner = self.inner.lock().unwrap(); - let txs = inner.txs.borrow_mut(); - let tx = txs.get(&tx_id).unwrap(); - assert!(tx.state == TransactionState::Active); - let rows = inner.rows.borrow(); - if let Some(row_versions) = rows.get(&id) { - for rv in row_versions.iter().rev() { - if is_version_visible(&txs, tx, rv) { - tx.insert_to_read_set(id); - return Ok(Some(rv.row.clone())); - } - } - } - Ok(None) + inner.read(tx_id, id) } /// Begins a new transaction in the database. @@ -288,12 +349,7 @@ impl Database { /// transaction are isolated from other transactions until you commit the transaction. pub fn begin_tx(&self) -> TxID { let mut inner = self.inner.lock().unwrap(); - let tx_id = get_tx_id(&mut inner); - let begin_ts = get_timestamp(&mut inner); - let tx = Transaction::new(tx_id, begin_ts); - let mut txs = inner.txs.borrow_mut(); - txs.insert(tx_id, tx); - tx_id + inner.begin_tx() } /// Commits a transaction with the specified transaction ID. @@ -307,35 +363,7 @@ impl Database { /// * `tx_id` - The ID of the transaction to commit. pub fn commit_tx(&self, tx_id: TxID) -> Result<()> { let mut inner = self.inner.lock().unwrap(); - let end_ts = get_timestamp(&mut inner); - let mut txs = inner.txs.borrow_mut(); - let mut tx = txs.get_mut(&tx_id).unwrap(); - match tx.state { - TransactionState::Terminated => return Err(DatabaseError::TxTerminated), - _ => { - assert!(tx.state == TransactionState::Active); - } - } - let mut rows = inner.rows.borrow_mut(); - tx.state = TransactionState::Preparing; - for id in &tx.write_set { - if let Some(row_versions) = rows.get_mut(id) { - for row_version in row_versions.iter_mut() { - if let TxTimestampOrID::TxID(id) = row_version.begin { - if id == tx_id { - row_version.begin = TxTimestampOrID::Timestamp(tx.begin_ts); - } - } - if let Some(TxTimestampOrID::TxID(id)) = row_version.end { - if id == tx_id { - row_version.end = Some(TxTimestampOrID::Timestamp(end_ts)); - } - } - } - } - } - tx.state = TransactionState::Committed; - Ok(()) + inner.commit_tx(tx_id) } /// Rolls back a transaction with the specified ID. @@ -410,14 +438,6 @@ fn is_end_visible(txs: &HashMap, tx: &Transaction, rv: &RowVe } } -fn get_tx_id(inner: &mut DatabaseInner) -> u64 { - inner.tx_ids.fetch_add(1, Ordering::SeqCst) -} - -fn get_timestamp(inner: &mut DatabaseInner) -> u64 { - inner.clock.get_timestamp() -} - #[cfg(test)] mod tests { use super::*; From 44ba56c5a8b815ef6f685139e24fe5c403d67142 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Thu, 13 Apr 2023 10:09:13 +0300 Subject: [PATCH 019/128] Update README --- core/mvcc/README.md | 6 ++++++ core/mvcc/database/src/clock.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 core/mvcc/database/src/clock.rs diff --git a/core/mvcc/README.md b/core/mvcc/README.md index a15efee67..eff22ea86 100644 --- a/core/mvcc/README.md +++ b/core/mvcc/README.md @@ -10,6 +10,12 @@ Run tests: cargo test ``` +Test coverage report: + +```console +cargo tarpaulin -o html +``` + Run benchmarks: ```console diff --git a/core/mvcc/database/src/clock.rs b/core/mvcc/database/src/clock.rs new file mode 100644 index 000000000..e6ef0dfc4 --- /dev/null +++ b/core/mvcc/database/src/clock.rs @@ -0,0 +1,26 @@ +use std::sync::atomic::{AtomicU64, Ordering}; + +/// Logical clock. +pub trait LogicalClock { + fn get_timestamp(&self) -> u64; +} + +/// A node-local clock backed by an atomic counter. +#[derive(Debug, Default)] +pub struct LocalClock { + ts_sequence: AtomicU64, +} + +impl LocalClock { + pub fn new() -> Self { + Self { + ts_sequence: AtomicU64::new(0), + } + } +} + +impl LogicalClock for LocalClock { + fn get_timestamp(&self) -> u64 { + self.ts_sequence.fetch_add(1, Ordering::SeqCst) + } +} From 204d65ad050ba14bb44fce93fba537c0cb87eed1 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Thu, 13 Apr 2023 10:09:23 +0300 Subject: [PATCH 020/128] Move clock code to `clock.rs` --- core/mvcc/database/src/database.rs | 27 ++------------------ core/mvcc/database/src/lib.rs | 1 + core/mvcc/database/tests/concurrency_test.rs | 3 ++- 3 files changed, 5 insertions(+), 26 deletions(-) diff --git a/core/mvcc/database/src/database.rs b/core/mvcc/database/src/database.rs index 837df7ef1..14f11e252 100644 --- a/core/mvcc/database/src/database.rs +++ b/core/mvcc/database/src/database.rs @@ -1,3 +1,4 @@ +use crate::clock::LogicalClock; use crate::errors::DatabaseError; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; @@ -86,31 +87,6 @@ pub struct Database { type TxID = u64; -/// Logical clock. -pub trait LogicalClock { - fn get_timestamp(&self) -> u64; -} - -/// A node-local clock backed by an atomic counter. -#[derive(Debug, Default)] -pub struct LocalClock { - ts_sequence: AtomicU64, -} - -impl LocalClock { - pub fn new() -> Self { - Self { - ts_sequence: AtomicU64::new(0), - } - } -} - -impl LogicalClock for LocalClock { - fn get_timestamp(&self) -> u64 { - self.ts_sequence.fetch_add(1, Ordering::SeqCst) - } -} - #[derive(Debug)] pub struct DatabaseInner { rows: RefCell>>, @@ -441,6 +417,7 @@ fn is_end_visible(txs: &HashMap, tx: &Transaction, rv: &RowVe #[cfg(test)] mod tests { use super::*; + use crate::clock::LocalClock; #[test] fn test_insert_read() { diff --git a/core/mvcc/database/src/lib.rs b/core/mvcc/database/src/lib.rs index e5ad14df6..a3b2e105f 100644 --- a/core/mvcc/database/src/lib.rs +++ b/core/mvcc/database/src/lib.rs @@ -31,5 +31,6 @@ //! * Optimistic reads and writes //! * Garbage collection +pub mod clock; pub mod database; pub mod errors; diff --git a/core/mvcc/database/tests/concurrency_test.rs b/core/mvcc/database/tests/concurrency_test.rs index b18e9a9bd..bc1e4f90a 100644 --- a/core/mvcc/database/tests/concurrency_test.rs +++ b/core/mvcc/database/tests/concurrency_test.rs @@ -1,4 +1,5 @@ -use mvcc_rs::database::{Database, LocalClock, Row}; +use mvcc_rs::clock::LocalClock; +use mvcc_rs::database::{Database, Row}; use shuttle::sync::atomic::AtomicU64; use shuttle::sync::Arc; use shuttle::thread; From b73c11015a6599fe5f2c40cf02d0489e6556fb5e Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Thu, 13 Apr 2023 10:13:19 +0300 Subject: [PATCH 021/128] Reorder code --- core/mvcc/database/src/database.rs | 276 ++++++++++++++--------------- 1 file changed, 138 insertions(+), 138 deletions(-) diff --git a/core/mvcc/database/src/database.rs b/core/mvcc/database/src/database.rs index 14f11e252..8fc964edb 100644 --- a/core/mvcc/database/src/database.rs +++ b/core/mvcc/database/src/database.rs @@ -87,144 +87,6 @@ pub struct Database { type TxID = u64; -#[derive(Debug)] -pub struct DatabaseInner { - rows: RefCell>>, - txs: RefCell>, - tx_ids: AtomicU64, - clock: Clock, -} - -impl DatabaseInner { - fn insert(&self, tx_id: TxID, row: Row) -> Result<()> { - let mut txs = self.txs.borrow_mut(); - let tx = txs - .get_mut(&tx_id) - .ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; - assert!(tx.state == TransactionState::Active); - let id = row.id; - let row_version = RowVersion { - begin: TxTimestampOrID::TxID(tx.tx_id), - end: None, - row, - }; - let mut rows = self.rows.borrow_mut(); - rows.entry(id).or_insert_with(Vec::new).push(row_version); - tx.insert_to_write_set(id); - Ok(()) - } - - fn delete(&self, tx_id: TxID, id: u64) -> Result { - let mut rows = self.rows.borrow_mut(); - let mut txs = self.txs.borrow_mut(); - if let Some(row_versions) = rows.get_mut(&id) { - for rv in row_versions.iter_mut().rev() { - let tx = txs - .get(&tx_id) - .ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; - assert!(tx.state == TransactionState::Active); - if is_write_write_conflict(&txs, tx, rv) { - drop(txs); - drop(rows); - self.rollback_tx(tx_id); - return Err(DatabaseError::WriteWriteConflict); - } - if is_version_visible(&txs, tx, rv) { - rv.end = Some(TxTimestampOrID::TxID(tx.tx_id)); - let tx = txs - .get_mut(&tx_id) - .ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; - tx.insert_to_write_set(id); - return Ok(true); - } - } - } - Ok(false) - } - - fn read(&self, tx_id: TxID, id: u64) -> Result> { - let txs = self.txs.borrow_mut(); - let tx = txs.get(&tx_id).unwrap(); - assert!(tx.state == TransactionState::Active); - let rows = self.rows.borrow(); - if let Some(row_versions) = rows.get(&id) { - for rv in row_versions.iter().rev() { - if is_version_visible(&txs, tx, rv) { - tx.insert_to_read_set(id); - return Ok(Some(rv.row.clone())); - } - } - } - Ok(None) - } - - fn begin_tx(&mut self) -> TxID { - let tx_id = self.get_tx_id(); - let begin_ts = self.get_timestamp(); - let tx = Transaction::new(tx_id, begin_ts); - let mut txs = self.txs.borrow_mut(); - txs.insert(tx_id, tx); - tx_id - } - - fn commit_tx(&mut self, tx_id: TxID) -> Result<()> { - let end_ts = self.get_timestamp(); - let mut txs = self.txs.borrow_mut(); - let mut tx = txs.get_mut(&tx_id).unwrap(); - match tx.state { - TransactionState::Terminated => return Err(DatabaseError::TxTerminated), - _ => { - assert!(tx.state == TransactionState::Active); - } - } - let mut rows = self.rows.borrow_mut(); - tx.state = TransactionState::Preparing; - for id in &tx.write_set { - if let Some(row_versions) = rows.get_mut(id) { - for row_version in row_versions.iter_mut() { - if let TxTimestampOrID::TxID(id) = row_version.begin { - if id == tx_id { - row_version.begin = TxTimestampOrID::Timestamp(tx.begin_ts); - } - } - if let Some(TxTimestampOrID::TxID(id)) = row_version.end { - if id == tx_id { - row_version.end = Some(TxTimestampOrID::Timestamp(end_ts)); - } - } - } - } - } - tx.state = TransactionState::Committed; - Ok(()) - } - - fn rollback_tx(&self, tx_id: TxID) { - let mut txs = self.txs.borrow_mut(); - let mut tx = txs.get_mut(&tx_id).unwrap(); - assert!(tx.state == TransactionState::Active); - tx.state = TransactionState::Aborted; - let mut rows = self.rows.borrow_mut(); - for id in &tx.write_set { - if let Some(row_versions) = rows.get_mut(id) { - row_versions.retain(|rv| rv.begin != TxTimestampOrID::TxID(tx_id)); - if row_versions.is_empty() { - rows.remove(id); - } - } - } - tx.state = TransactionState::Terminated; - } - - fn get_tx_id(&mut self) -> u64 { - self.tx_ids.fetch_add(1, Ordering::SeqCst) - } - - fn get_timestamp(&mut self) -> u64 { - self.clock.get_timestamp() - } -} - impl Database { /// Creates a new database. pub fn new(clock: Clock) -> Self { @@ -377,6 +239,144 @@ fn is_write_write_conflict( } } +#[derive(Debug)] +pub struct DatabaseInner { + rows: RefCell>>, + txs: RefCell>, + tx_ids: AtomicU64, + clock: Clock, +} + +impl DatabaseInner { + fn insert(&self, tx_id: TxID, row: Row) -> Result<()> { + let mut txs = self.txs.borrow_mut(); + let tx = txs + .get_mut(&tx_id) + .ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; + assert!(tx.state == TransactionState::Active); + let id = row.id; + let row_version = RowVersion { + begin: TxTimestampOrID::TxID(tx.tx_id), + end: None, + row, + }; + let mut rows = self.rows.borrow_mut(); + rows.entry(id).or_insert_with(Vec::new).push(row_version); + tx.insert_to_write_set(id); + Ok(()) + } + + fn delete(&self, tx_id: TxID, id: u64) -> Result { + let mut rows = self.rows.borrow_mut(); + let mut txs = self.txs.borrow_mut(); + if let Some(row_versions) = rows.get_mut(&id) { + for rv in row_versions.iter_mut().rev() { + let tx = txs + .get(&tx_id) + .ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; + assert!(tx.state == TransactionState::Active); + if is_write_write_conflict(&txs, tx, rv) { + drop(txs); + drop(rows); + self.rollback_tx(tx_id); + return Err(DatabaseError::WriteWriteConflict); + } + if is_version_visible(&txs, tx, rv) { + rv.end = Some(TxTimestampOrID::TxID(tx.tx_id)); + let tx = txs + .get_mut(&tx_id) + .ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; + tx.insert_to_write_set(id); + return Ok(true); + } + } + } + Ok(false) + } + + fn read(&self, tx_id: TxID, id: u64) -> Result> { + let txs = self.txs.borrow_mut(); + let tx = txs.get(&tx_id).unwrap(); + assert!(tx.state == TransactionState::Active); + let rows = self.rows.borrow(); + if let Some(row_versions) = rows.get(&id) { + for rv in row_versions.iter().rev() { + if is_version_visible(&txs, tx, rv) { + tx.insert_to_read_set(id); + return Ok(Some(rv.row.clone())); + } + } + } + Ok(None) + } + + fn begin_tx(&mut self) -> TxID { + let tx_id = self.get_tx_id(); + let begin_ts = self.get_timestamp(); + let tx = Transaction::new(tx_id, begin_ts); + let mut txs = self.txs.borrow_mut(); + txs.insert(tx_id, tx); + tx_id + } + + fn commit_tx(&mut self, tx_id: TxID) -> Result<()> { + let end_ts = self.get_timestamp(); + let mut txs = self.txs.borrow_mut(); + let mut tx = txs.get_mut(&tx_id).unwrap(); + match tx.state { + TransactionState::Terminated => return Err(DatabaseError::TxTerminated), + _ => { + assert!(tx.state == TransactionState::Active); + } + } + let mut rows = self.rows.borrow_mut(); + tx.state = TransactionState::Preparing; + for id in &tx.write_set { + if let Some(row_versions) = rows.get_mut(id) { + for row_version in row_versions.iter_mut() { + if let TxTimestampOrID::TxID(id) = row_version.begin { + if id == tx_id { + row_version.begin = TxTimestampOrID::Timestamp(tx.begin_ts); + } + } + if let Some(TxTimestampOrID::TxID(id)) = row_version.end { + if id == tx_id { + row_version.end = Some(TxTimestampOrID::Timestamp(end_ts)); + } + } + } + } + } + tx.state = TransactionState::Committed; + Ok(()) + } + + fn rollback_tx(&self, tx_id: TxID) { + let mut txs = self.txs.borrow_mut(); + let mut tx = txs.get_mut(&tx_id).unwrap(); + assert!(tx.state == TransactionState::Active); + tx.state = TransactionState::Aborted; + let mut rows = self.rows.borrow_mut(); + for id in &tx.write_set { + if let Some(row_versions) = rows.get_mut(id) { + row_versions.retain(|rv| rv.begin != TxTimestampOrID::TxID(tx_id)); + if row_versions.is_empty() { + rows.remove(id); + } + } + } + tx.state = TransactionState::Terminated; + } + + fn get_tx_id(&mut self) -> u64 { + self.tx_ids.fetch_add(1, Ordering::SeqCst) + } + + fn get_timestamp(&mut self) -> u64 { + self.clock.get_timestamp() + } +} + fn is_version_visible(txs: &HashMap, tx: &Transaction, rv: &RowVersion) -> bool { is_begin_visible(txs, tx, rv) && is_end_visible(txs, tx, rv) } From f51c4ee5a8e914d94029c600941555a272b3d49f Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Thu, 13 Apr 2023 10:14:44 +0300 Subject: [PATCH 022/128] Move TxID type definition --- core/mvcc/database/src/database.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/mvcc/database/src/database.rs b/core/mvcc/database/src/database.rs index 8fc964edb..e9ec67610 100644 --- a/core/mvcc/database/src/database.rs +++ b/core/mvcc/database/src/database.rs @@ -21,6 +21,8 @@ struct RowVersion { row: Row, } +type TxID = u64; + /// A transaction timestamp or ID. /// /// Versions either track a timestamp or a transaction ID, depending on the @@ -30,7 +32,7 @@ struct RowVersion { #[derive(Clone, Debug, PartialEq)] enum TxTimestampOrID { Timestamp(u64), - TxID(u64), + TxID(TxID), } /// Transaction @@ -85,8 +87,6 @@ pub struct Database { inner: Arc>>, } -type TxID = u64; - impl Database { /// Creates a new database. pub fn new(clock: Clock) -> Self { From 87ef3e1cd800b93e4a52377d3ff3ab0ec0db4d77 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Thu, 13 Apr 2023 10:18:19 +0300 Subject: [PATCH 023/128] Add a comment for is_write_write_conflict() --- core/mvcc/database/src/database.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/mvcc/database/src/database.rs b/core/mvcc/database/src/database.rs index e9ec67610..b5b727e1b 100644 --- a/core/mvcc/database/src/database.rs +++ b/core/mvcc/database/src/database.rs @@ -218,6 +218,8 @@ impl Database { } } +/// A write-write conflict happens when transaction T_m attempts to update a +/// row version that is currently being updated by an active transaction T_n. fn is_write_write_conflict( txs: &HashMap, tx: &Transaction, From e2fc8414797c7c73c7ffad5546c5efe02392c2d7 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Thu, 13 Apr 2023 10:19:21 +0300 Subject: [PATCH 024/128] Move is_write_write_conflict() definition --- core/mvcc/database/src/database.rs | 46 +++++++++++++++--------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/core/mvcc/database/src/database.rs b/core/mvcc/database/src/database.rs index b5b727e1b..5439a2a76 100644 --- a/core/mvcc/database/src/database.rs +++ b/core/mvcc/database/src/database.rs @@ -218,29 +218,6 @@ impl Database { } } -/// A write-write conflict happens when transaction T_m attempts to update a -/// row version that is currently being updated by an active transaction T_n. -fn is_write_write_conflict( - txs: &HashMap, - tx: &Transaction, - rv: &RowVersion, -) -> bool { - match rv.end { - Some(TxTimestampOrID::TxID(rv_end)) => { - let te = txs.get(&rv_end).unwrap(); - match te.state { - TransactionState::Active => tx.tx_id != te.tx_id, - TransactionState::Preparing => todo!(), - TransactionState::Committed => todo!(), - TransactionState::Aborted => todo!(), - TransactionState::Terminated => todo!(), - } - } - Some(TxTimestampOrID::Timestamp(_)) => false, - None => false, - } -} - #[derive(Debug)] pub struct DatabaseInner { rows: RefCell>>, @@ -379,6 +356,29 @@ impl DatabaseInner { } } +/// A write-write conflict happens when transaction T_m attempts to update a +/// row version that is currently being updated by an active transaction T_n. +fn is_write_write_conflict( + txs: &HashMap, + tx: &Transaction, + rv: &RowVersion, +) -> bool { + match rv.end { + Some(TxTimestampOrID::TxID(rv_end)) => { + let te = txs.get(&rv_end).unwrap(); + match te.state { + TransactionState::Active => tx.tx_id != te.tx_id, + TransactionState::Preparing => todo!(), + TransactionState::Committed => todo!(), + TransactionState::Aborted => todo!(), + TransactionState::Terminated => todo!(), + } + } + Some(TxTimestampOrID::Timestamp(_)) => false, + None => false, + } +} + fn is_version_visible(txs: &HashMap, tx: &Transaction, rv: &RowVersion) -> bool { is_begin_visible(txs, tx, rv) && is_end_visible(txs, tx, rv) } From bc7269a776f28e0ec11b97fcdca91b0737f27a9d Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Thu, 13 Apr 2023 10:41:01 +0300 Subject: [PATCH 025/128] Fix typo --- core/mvcc/database/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/mvcc/database/src/lib.rs b/core/mvcc/database/src/lib.rs index a3b2e105f..44708bb6a 100644 --- a/core/mvcc/database/src/lib.rs +++ b/core/mvcc/database/src/lib.rs @@ -15,7 +15,7 @@ //! transaction T_n but not yet committed. The MVCC algorithm prevents dirty //! reads by validating that a row version is visible to transaction T_m. //! -//! * A *fuzzy read* (non-repetable read) occurs when transaction T_m reads a +//! * A *fuzzy read* (non-repeatable read) occurs when transaction T_m reads a //! different value in the course of the transaction because another //! transaction T_n has updated the value. //! From 05ee98971be68fc538be4f7ad67f2bf93a531f74 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Fri, 14 Apr 2023 09:30:28 +0200 Subject: [PATCH 026/128] add tracing --- core/mvcc/database/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/core/mvcc/database/Cargo.toml b/core/mvcc/database/Cargo.toml index 3044bc809..920ead0ad 100644 --- a/core/mvcc/database/Cargo.toml +++ b/core/mvcc/database/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" anyhow = "1.0.70" rustyline = "11.0.0" thiserror = "1.0.40" +tracing = "0.1.37" [dev-dependencies] criterion = { version = "0.4", features = ["html_reports"] } From 9d99090f67db3c84c2f046bd1c706a0d2144f016 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Fri, 14 Apr 2023 09:31:24 +0200 Subject: [PATCH 027/128] .gitignore: add Cargo.lock mvcc-rs is considered a library, so let's ignore its Cargo.lock as per https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html --- core/mvcc/.gitignore | 1 + core/mvcc/Cargo.lock | 1521 ------------------------------------------ 2 files changed, 1 insertion(+), 1521 deletions(-) delete mode 100644 core/mvcc/Cargo.lock diff --git a/core/mvcc/.gitignore b/core/mvcc/.gitignore index 2f7896d1d..2c96eb1b6 100644 --- a/core/mvcc/.gitignore +++ b/core/mvcc/.gitignore @@ -1 +1,2 @@ target/ +Cargo.lock diff --git a/core/mvcc/Cargo.lock b/core/mvcc/Cargo.lock deleted file mode 100644 index d1d24e4b9..000000000 --- a/core/mvcc/Cargo.lock +++ /dev/null @@ -1,1521 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "addr2line" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "ahash" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" -dependencies = [ - "cfg-if", - "getrandom", - "once_cell", - "version_check", -] - -[[package]] -name = "anes" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" - -[[package]] -name = "anyhow" -version = "1.0.70" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" - -[[package]] -name = "arrayvec" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "backtrace" -version = "0.3.67" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - -[[package]] -name = "bumpalo" -version = "3.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" - -[[package]] -name = "bytemuck" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" - -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - -[[package]] -name = "cc" -version = "1.0.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "ciborium" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c137568cc60b904a7724001b35ce2630fd00d5d84805fbb608ab89509d788f" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346de753af073cc87b52b2083a506b38ac176a44cfb05497b622e27be899b369" - -[[package]] -name = "ciborium-ll" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213030a2b5a4e0c0892b6652260cf6ccac84827b83a85a534e178e3906c4cf1b" -dependencies = [ - "ciborium-io", - "half", -] - -[[package]] -name = "clap" -version = "3.2.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" -dependencies = [ - "bitflags", - "clap_lex", - "indexmap", - "textwrap", -] - -[[package]] -name = "clap_lex" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" -dependencies = [ - "os_str_bytes", -] - -[[package]] -name = "clipboard-win" -version = "4.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" -dependencies = [ - "error-code", - "str-buf", - "winapi", -] - -[[package]] -name = "cpp_demangle" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b446fd40bcc17eddd6a4a78f24315eb90afdb3334999ddfd4909985c47722442" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "criterion" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" -dependencies = [ - "anes", - "atty", - "cast", - "ciborium", - "clap", - "criterion-plot", - "itertools", - "lazy_static", - "num-traits", - "oorandom", - "plotters", - "rayon", - "regex", - "serde", - "serde_derive", - "serde_json", - "tinytemplate", - "walkdir", -] - -[[package]] -name = "criterion-plot" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" -dependencies = [ - "cast", - "itertools", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" -dependencies = [ - "cfg-if", - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" -dependencies = [ - "autocfg", - "cfg-if", - "crossbeam-utils", - "memoffset", - "scopeguard", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "debugid" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" -dependencies = [ - "uuid", -] - -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - -[[package]] -name = "either" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" - -[[package]] -name = "endian-type" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" - -[[package]] -name = "errno" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0" -dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys 0.45.0", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "error-code" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" -dependencies = [ - "libc", - "str-buf", -] - -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - -[[package]] -name = "fd-lock" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9799aefb4a2e4a01cc47610b1dd47c18ab13d991f27bbcaed9296f5a53d5cbad" -dependencies = [ - "cfg-if", - "rustix", - "windows-sys 0.45.0", -] - -[[package]] -name = "findshlibs" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64" -dependencies = [ - "cc", - "lazy_static", - "libc", - "winapi", -] - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "generator" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33a20a288a94683f5f4da0adecdbe095c94a77c295e514cc6484e9394dd8376e" -dependencies = [ - "cc", - "libc", - "log", - "rustversion", - "windows", -] - -[[package]] -name = "getrandom" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "gimli" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" - -[[package]] -name = "half" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown", -] - -[[package]] -name = "inferno" -version = "0.11.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fb7c1b80a1dfa604bb4a649a5c5aeef3d913f7c520cb42b40e534e8a61bcdfc" -dependencies = [ - "ahash", - "indexmap", - "is-terminal", - "itoa", - "log", - "num-format", - "once_cell", - "quick-xml", - "rgb", - "str_stack", -] - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" -dependencies = [ - "hermit-abi 0.3.1", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "is-terminal" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" -dependencies = [ - "hermit-abi 0.3.1", - "io-lifetimes", - "rustix", - "windows-sys 0.48.0", -] - -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" - -[[package]] -name = "js-sys" -version = "0.3.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "libc" -version = "0.2.141" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" - -[[package]] -name = "linux-raw-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" - -[[package]] -name = "lock_api" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "memmap2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" -dependencies = [ - "libc", -] - -[[package]] -name = "memoffset" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" -dependencies = [ - "autocfg", -] - -[[package]] -name = "miniz_oxide" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" -dependencies = [ - "adler", -] - -[[package]] -name = "mvcc-rs" -version = "0.0.0" -dependencies = [ - "anyhow", - "criterion", - "pprof", - "rustyline", - "shuttle", - "thiserror", -] - -[[package]] -name = "nibble_vec" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" -dependencies = [ - "smallvec", -] - -[[package]] -name = "nix" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" -dependencies = [ - "bitflags", - "cfg-if", - "libc", - "static_assertions", -] - -[[package]] -name = "num-format" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" -dependencies = [ - "arrayvec", - "itoa", -] - -[[package]] -name = "num-traits" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" -dependencies = [ - "hermit-abi 0.2.6", - "libc", -] - -[[package]] -name = "object" -version = "0.30.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" - -[[package]] -name = "oorandom" -version = "11.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" - -[[package]] -name = "os_str_bytes" -version = "6.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" - -[[package]] -name = "owo-colors" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.2.16", - "smallvec", - "windows-sys 0.45.0", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" - -[[package]] -name = "plotters" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b639e642295546c50fcd545198c9d64ee2a38620a628724a3b266d5fbf97" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "plotters-backend" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142" - -[[package]] -name = "plotters-svg" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a81d2759aae1dae668f783c308bc5c8ebd191ff4184aaa1b37f65a6ae5a56f" -dependencies = [ - "plotters-backend", -] - -[[package]] -name = "pprof" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "196ded5d4be535690899a4631cc9f18cdc41b7ebf24a79400f46f48e49a11059" -dependencies = [ - "backtrace", - "cfg-if", - "criterion", - "findshlibs", - "inferno", - "libc", - "log", - "nix", - "once_cell", - "parking_lot", - "smallvec", - "symbolic-demangle", - "tempfile", - "thiserror", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "proc-macro2" -version = "1.0.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quick-xml" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "radix_trie" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" -dependencies = [ - "endian-type", - "nibble_vec", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rand_pcg" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e" -dependencies = [ - "rand_core", -] - -[[package]] -name = "rayon" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-utils", - "num_cpus", -] - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags", -] - -[[package]] -name = "redox_syscall" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" -dependencies = [ - "bitflags", -] - -[[package]] -name = "redox_users" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" -dependencies = [ - "getrandom", - "redox_syscall 0.2.16", - "thiserror", -] - -[[package]] -name = "regex" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" -dependencies = [ - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "rgb" -version = "0.8.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20ec2d3e3fc7a92ced357df9cebd5a10b6fb2aa1ee797bf7e9ce2f17dffc8f59" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a36c42d1873f9a77c53bde094f9664d9891bc604a45b4798fd2c389ed12e5b" - -[[package]] -name = "rustix" -version = "0.37.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aae838e49b3d63e9274e1c01833cc8139d3fec468c3b84688c628f44b1ae11d" -dependencies = [ - "bitflags", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys", - "windows-sys 0.45.0", -] - -[[package]] -name = "rustversion" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" - -[[package]] -name = "rustyline" -version = "11.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfc8644681285d1fb67a467fb3021bfea306b99b4146b166a1fe3ada965eece" -dependencies = [ - "bitflags", - "cfg-if", - "clipboard-win", - "dirs-next", - "fd-lock", - "libc", - "log", - "memchr", - "nix", - "radix_trie", - "scopeguard", - "unicode-segmentation", - "unicode-width", - "utf8parse", - "winapi", -] - -[[package]] -name = "ryu" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "serde" -version = "1.0.159" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.159" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.13", -] - -[[package]] -name = "serde_json" -version = "1.0.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "shuttle" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6b45350ea43c87491a8cc7c212cfd163aff964eafcf898cbf72637547e0bccb" -dependencies = [ - "bitvec", - "generator", - "hex", - "owo-colors", - "rand", - "rand_core", - "rand_pcg", - "scoped-tls", - "smallvec", - "tracing", -] - -[[package]] -name = "smallvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "str-buf" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" - -[[package]] -name = "str_stack" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9091b6114800a5f2141aee1d1b9d6ca3592ac062dc5decb3764ec5895a47b4eb" - -[[package]] -name = "symbolic-common" -version = "10.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b55cdc318ede251d0957f07afe5fed912119b8c1bc5a7804151826db999e737" -dependencies = [ - "debugid", - "memmap2", - "stable_deref_trait", - "uuid", -] - -[[package]] -name = "symbolic-demangle" -version = "10.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79be897be8a483a81fff6a3a4e195b4ac838ef73ca42d348b3f722da9902e489" -dependencies = [ - "cpp_demangle", - "rustc-demangle", - "symbolic-common", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c9da457c5285ac1f936ebd076af6dac17a61cfe7826f2076b4d015cf47bc8ec" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "tempfile" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" -dependencies = [ - "cfg-if", - "fastrand", - "redox_syscall 0.3.5", - "rustix", - "windows-sys 0.45.0", -] - -[[package]] -name = "textwrap" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" - -[[package]] -name = "thiserror" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.13", -] - -[[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "tracing" -version = "0.1.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" -dependencies = [ - "cfg-if", - "pin-project-lite", - "tracing-core", -] - -[[package]] -name = "tracing-core" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "unicode-ident" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" - -[[package]] -name = "unicode-segmentation" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" - -[[package]] -name = "unicode-width" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" - -[[package]] -name = "utf8parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" - -[[package]] -name = "uuid" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b55a3fef2a1e3b3a00ce878640918820d3c51081576ac657d23af9fc7928fdb" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "walkdir" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn 1.0.109", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" - -[[package]] -name = "web-sys" -version = "0.3.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.44.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.0", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" -dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" - -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] From 7622ea5f98fa42b38158d50bea77529c87bea77f Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Fri, 14 Apr 2023 09:51:59 +0200 Subject: [PATCH 028/128] database: add transaction tracing The tracing is also added to tests, so that the behavior can be observed: ``` $ cargo test -- --nocapture --test-threads 1 test database::tests::test_dirty_write ... 2023-04-14T07:51:15.919503Z TRACE test_dirty_write: mvcc_rs::database: BEGIN { id: 0, begin_ts: 0, write_set: {}, read_set: {} } 2023-04-14T07:51:15.919554Z TRACE test_dirty_write: mvcc_rs::database: BEGIN { id: 1, begin_ts: 1, write_set: {}, read_set: {} } ok test database::tests::test_fuzzy_read ... 2023-04-14T07:51:15.919732Z TRACE test_fuzzy_read: mvcc_rs::database: BEGIN { id: 0, begin_ts: 0, write_set: {}, read_set: {} } 2023-04-14T07:51:15.919762Z TRACE test_fuzzy_read: mvcc_rs::database: COMMIT { id: 0, begin_ts: 0, write_set: {1}, read_set: {1} } 2023-04-14T07:51:15.919778Z TRACE test_fuzzy_read: mvcc_rs::database: BEGIN { id: 1, begin_ts: 2, write_set: {}, read_set: {} } 2023-04-14T07:51:15.919793Z TRACE test_fuzzy_read: mvcc_rs::database: BEGIN { id: 2, begin_ts: 3, write_set: {}, read_set: {} } 2023-04-14T07:51:15.919811Z TRACE test_fuzzy_read: mvcc_rs::database: COMMIT { id: 2, begin_ts: 3, write_set: {1}, read_set: {} } ok test database::tests::test_insert_read ... 2023-04-14T07:51:15.919944Z TRACE test_insert_read: mvcc_rs::database: BEGIN { id: 0, begin_ts: 0, write_set: {}, read_set: {} } 2023-04-14T07:51:15.919974Z TRACE test_insert_read: mvcc_rs::database: COMMIT { id: 0, begin_ts: 0, write_set: {1}, read_set: {1} } 2023-04-14T07:51:15.919989Z TRACE test_insert_read: mvcc_rs::database: BEGIN { id: 1, begin_ts: 2, write_set: {}, read_set: {} } ok test database::tests::test_lost_update ... 2023-04-14T07:51:15.920116Z TRACE test_lost_update: mvcc_rs::database: BEGIN { id: 0, begin_ts: 0, write_set: {}, read_set: {} } 2023-04-14T07:51:15.920146Z TRACE test_lost_update: mvcc_rs::database: COMMIT { id: 0, begin_ts: 0, write_set: {1}, read_set: {1} } 2023-04-14T07:51:15.920161Z TRACE test_lost_update: mvcc_rs::database: BEGIN { id: 1, begin_ts: 2, write_set: {}, read_set: {} } 2023-04-14T07:51:15.920178Z TRACE test_lost_update: mvcc_rs::database: BEGIN { id: 2, begin_ts: 3, write_set: {}, read_set: {} } 2023-04-14T07:51:15.920196Z TRACE test_lost_update: mvcc_rs::database: ROLLBACK { id: 2, begin_ts: 3, write_set: {}, read_set: {} } 2023-04-14T07:51:15.920210Z TRACE test_lost_update: mvcc_rs::database: COMMIT { id: 1, begin_ts: 2, write_set: {1}, read_set: {} } 2023-04-14T07:51:15.920223Z TRACE test_lost_update: mvcc_rs::database: BEGIN { id: 3, begin_ts: 6, write_set: {}, read_set: {} } ok test database::tests::test_read_nonexistent ... 2023-04-14T07:51:15.920352Z TRACE test_read_nonexistent: mvcc_rs::database: BEGIN { id: 0, begin_ts: 0, write_set: {}, read_set: {} } ok ``` --- core/mvcc/database/Cargo.toml | 2 ++ core/mvcc/database/src/database.rs | 34 +++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/core/mvcc/database/Cargo.toml b/core/mvcc/database/Cargo.toml index 920ead0ad..16a6d4de8 100644 --- a/core/mvcc/database/Cargo.toml +++ b/core/mvcc/database/Cargo.toml @@ -13,6 +13,8 @@ tracing = "0.1.37" criterion = { version = "0.4", features = ["html_reports"] } pprof = { version = "0.11.1", features = ["criterion", "flamegraph"] } shuttle = "0.6.0" +tracing-subscriber = "0" +tracing-test = "0" [[bench]] name = "my_benchmark" diff --git a/core/mvcc/database/src/database.rs b/core/mvcc/database/src/database.rs index 5439a2a76..e7c8c9ea7 100644 --- a/core/mvcc/database/src/database.rs +++ b/core/mvcc/database/src/database.rs @@ -71,6 +71,23 @@ impl Transaction { } } +impl std::fmt::Display for Transaction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + match self.read_set.try_borrow() { + Ok(read_set) => write!( + f, + "{{ id: {}, begin_ts: {}, write_set: {:?}, read_set: {:?} }}", + self.tx_id, self.begin_ts, self.write_set, read_set + ), + Err(_) => write!( + f, + "{{ id: {}, begin_ts: {}, write_set: {:?}, read_set: }}", + self.tx_id, self.begin_ts, self.write_set + ), + } + } +} + /// Transaction state. #[derive(Debug, Clone, PartialEq)] enum TransactionState { @@ -294,6 +311,7 @@ impl DatabaseInner { let begin_ts = self.get_timestamp(); let tx = Transaction::new(tx_id, begin_ts); let mut txs = self.txs.borrow_mut(); + tracing::trace!("BEGIN {tx}"); txs.insert(tx_id, tx); tx_id } @@ -327,6 +345,7 @@ impl DatabaseInner { } } tx.state = TransactionState::Committed; + tracing::trace!("COMMIT {tx}"); Ok(()) } @@ -344,6 +363,7 @@ impl DatabaseInner { } } } + tracing::trace!("ROLLBACK {tx}"); tx.state = TransactionState::Terminated; } @@ -357,7 +377,7 @@ impl DatabaseInner { } /// A write-write conflict happens when transaction T_m attempts to update a -/// row version that is currently being updated by an active transaction T_n. +/// row version that is currently being updated by an active transaction T_n. fn is_write_write_conflict( txs: &HashMap, tx: &Transaction, @@ -420,7 +440,9 @@ fn is_end_visible(txs: &HashMap, tx: &Transaction, rv: &RowVe mod tests { use super::*; use crate::clock::LocalClock; + use tracing_test::traced_test; + #[traced_test] #[test] fn test_insert_read() { let clock = LocalClock::default(); @@ -441,6 +463,7 @@ mod tests { assert_eq!(tx1_row, row); } + #[traced_test] #[test] fn test_read_nonexistent() { let clock = LocalClock::default(); @@ -450,6 +473,7 @@ mod tests { assert!(row.unwrap().is_none()); } + #[traced_test] #[test] fn test_delete() { let clock = LocalClock::default(); @@ -473,6 +497,7 @@ mod tests { assert!(row.is_none()); } + #[traced_test] #[test] fn test_delete_nonexistent() { let clock = LocalClock::default(); @@ -481,6 +506,7 @@ mod tests { assert_eq!(false, db.delete(tx, 1).unwrap()); } + #[traced_test] #[test] fn test_commit() { let clock = LocalClock::default(); @@ -508,6 +534,7 @@ mod tests { assert_eq!(tx1_updated_row, row); } + #[traced_test] #[test] fn test_rollback() { let clock = LocalClock::default(); @@ -533,6 +560,7 @@ mod tests { assert_eq!(row5, None); } + #[traced_test] #[test] fn test_dirty_write() { let clock = LocalClock::default(); @@ -560,6 +588,7 @@ mod tests { assert_eq!(tx1_row, row); } + #[traced_test] #[test] fn test_dirty_read() { let clock = LocalClock::default(); @@ -580,6 +609,7 @@ mod tests { } #[ignore] + #[traced_test] #[test] fn test_dirty_read_deleted() { let clock = LocalClock::default(); @@ -604,6 +634,7 @@ mod tests { assert_eq!(tx1_row, row); } + #[traced_test] #[test] fn test_fuzzy_read() { let clock = LocalClock::default(); @@ -639,6 +670,7 @@ mod tests { assert_eq!(tx1_row, row); } + #[traced_test] #[test] fn test_lost_update() { let clock = LocalClock::default(); From aebaf623a947817c88ef0c241ec40ce89a4fcf6f Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Fri, 14 Apr 2023 09:54:33 +0200 Subject: [PATCH 029/128] database: apply clippy fixes They were preexisting, but now all the future patches can use `clippy --tests` as well. --- core/mvcc/database/src/database.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/core/mvcc/database/src/database.rs b/core/mvcc/database/src/database.rs index e7c8c9ea7..56eacc0fd 100644 --- a/core/mvcc/database/src/database.rs +++ b/core/mvcc/database/src/database.rs @@ -503,7 +503,7 @@ mod tests { let clock = LocalClock::default(); let db = Database::new(clock); let tx = db.begin_tx(); - assert_eq!(false, db.delete(tx, 1).unwrap()); + assert!(!db.delete(tx, 1).unwrap()); } #[traced_test] @@ -544,19 +544,19 @@ mod tests { id: 1, data: "Hello".to_string(), }; - db.insert(tx1.clone(), row1.clone()).unwrap(); - let row2 = db.read(tx1.clone(), 1).unwrap().unwrap(); + db.insert(tx1, row1.clone()).unwrap(); + let row2 = db.read(tx1, 1).unwrap().unwrap(); assert_eq!(row1, row2); let row3 = Row { id: 1, data: "World".to_string(), }; - db.update(tx1.clone(), row3.clone()).unwrap(); - let row4 = db.read(tx1.clone(), 1).unwrap().unwrap(); + db.update(tx1, row3.clone()).unwrap(); + let row4 = db.read(tx1, 1).unwrap().unwrap(); assert_eq!(row3, row4); db.rollback_tx(tx1); let tx2 = db.begin_tx(); - let row5 = db.read(tx2.clone(), 1).unwrap(); + let row5 = db.read(tx2, 1).unwrap(); assert_eq!(row5, None); } @@ -573,7 +573,7 @@ mod tests { data: "Hello".to_string(), }; db.insert(tx1, tx1_row.clone()).unwrap(); - let row = db.read(tx1.clone(), 1).unwrap().unwrap(); + let row = db.read(tx1, 1).unwrap().unwrap(); assert_eq!(tx1_row, row); // T2 attempts to delete row with ID 1, but fails because T1 has not committed. @@ -582,7 +582,7 @@ mod tests { id: 1, data: "World".to_string(), }; - assert_eq!(false, db.update(tx2, tx2_row.clone()).unwrap()); + assert!(!db.update(tx2, tx2_row).unwrap()); let row = db.read(tx1, 1).unwrap().unwrap(); assert_eq!(tx1_row, row); @@ -600,7 +600,7 @@ mod tests { id: 1, data: "Hello".to_string(), }; - db.insert(tx1, row1.clone()).unwrap(); + db.insert(tx1, row1).unwrap(); // T2 attempts to read row with ID 1, but doesn't see one because T1 has not committed. let tx2 = db.begin_tx(); @@ -626,7 +626,7 @@ mod tests { // T2 deletes row with ID 1, but does not commit. let tx2 = db.begin_tx(); - assert_eq!(true, db.delete(tx2, 1).unwrap()); + assert!(db.delete(tx2, 1).unwrap()); // T3 reads row with ID 1, but doesn't see the delete because T2 hasn't committed. let tx3 = db.begin_tx(); @@ -647,7 +647,7 @@ mod tests { data: "Hello".to_string(), }; db.insert(tx1, tx1_row.clone()).unwrap(); - let row = db.read(tx1.clone(), 1).unwrap().unwrap(); + let row = db.read(tx1, 1).unwrap().unwrap(); assert_eq!(tx1_row, row); db.commit_tx(tx1).unwrap(); @@ -662,7 +662,7 @@ mod tests { id: 1, data: "World".to_string(), }; - db.update(tx3, tx3_row.clone()).unwrap(); + db.update(tx3, tx3_row).unwrap(); db.commit_tx(tx3).unwrap(); // T2 still reads the same version of the row as before. @@ -683,7 +683,7 @@ mod tests { data: "Hello".to_string(), }; db.insert(tx1, tx1_row.clone()).unwrap(); - let row = db.read(tx1.clone(), 1).unwrap().unwrap(); + let row = db.read(tx1, 1).unwrap().unwrap(); assert_eq!(tx1_row, row); db.commit_tx(tx1).unwrap(); @@ -703,7 +703,7 @@ mod tests { }; assert_eq!( Err(DatabaseError::WriteWriteConflict), - db.update(tx3, tx3_row.clone()) + db.update(tx3, tx3_row) ); db.commit_tx(tx2).unwrap(); From f84f18583562ec7a1f93be88c657f08e01ec3520 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Fri, 14 Apr 2023 11:08:35 +0300 Subject: [PATCH 030/128] Improve transaction tracing --- core/mvcc/database/src/database.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/core/mvcc/database/src/database.rs b/core/mvcc/database/src/database.rs index 56eacc0fd..1ac44719a 100644 --- a/core/mvcc/database/src/database.rs +++ b/core/mvcc/database/src/database.rs @@ -310,8 +310,8 @@ impl DatabaseInner { let tx_id = self.get_tx_id(); let begin_ts = self.get_timestamp(); let tx = Transaction::new(tx_id, begin_ts); - let mut txs = self.txs.borrow_mut(); tracing::trace!("BEGIN {tx}"); + let mut txs = self.txs.borrow_mut(); txs.insert(tx_id, tx); tx_id } @@ -328,6 +328,7 @@ impl DatabaseInner { } let mut rows = self.rows.borrow_mut(); tx.state = TransactionState::Preparing; + tracing::trace!("PREPARE {tx}"); for id in &tx.write_set { if let Some(row_versions) = rows.get_mut(id) { for row_version in row_versions.iter_mut() { @@ -345,7 +346,7 @@ impl DatabaseInner { } } tx.state = TransactionState::Committed; - tracing::trace!("COMMIT {tx}"); + tracing::trace!("COMMIT {tx}"); Ok(()) } @@ -354,6 +355,7 @@ impl DatabaseInner { let mut tx = txs.get_mut(&tx_id).unwrap(); assert!(tx.state == TransactionState::Active); tx.state = TransactionState::Aborted; + tracing::trace!("ABORT {tx}"); let mut rows = self.rows.borrow_mut(); for id in &tx.write_set { if let Some(row_versions) = rows.get_mut(id) { @@ -363,8 +365,8 @@ impl DatabaseInner { } } } - tracing::trace!("ROLLBACK {tx}"); tx.state = TransactionState::Terminated; + tracing::trace!("TERMINATE {tx}"); } fn get_tx_id(&mut self) -> u64 { From 1bb752cab9241516791ff43f2cb40af45cd811d7 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Fri, 14 Apr 2023 10:18:37 +0200 Subject: [PATCH 031/128] add basic CI (#8) --- core/mvcc/.github/workflows/smoke_test.yml | 83 ++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 core/mvcc/.github/workflows/smoke_test.yml diff --git a/core/mvcc/.github/workflows/smoke_test.yml b/core/mvcc/.github/workflows/smoke_test.yml new file mode 100644 index 000000000..3b31f99b3 --- /dev/null +++ b/core/mvcc/.github/workflows/smoke_test.yml @@ -0,0 +1,83 @@ +name: Smoke test + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + checks: + runs-on: ubuntu-latest + name: Run Checks + steps: + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - name: Checkout + uses: actions/checkout@v3 + - name: Check format + uses: actions-rs/cargo@v1 + with: + command: check + args: --all-targets --all-features + clippy: + runs-on: ubuntu-latest + name: Run Clippy + steps: + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + components: clippy + override: true + - name: Checkout + uses: actions/checkout@v3 + - name: Clippy + uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-targets --tests --all-features -- -D warnings + rust-fmt: + runs-on: ubuntu-latest + name: Run Rustfmt + steps: + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + components: rustfmt + override: true + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: recursive + - name: Check format + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --check + examples: + runs-on: ubuntu-latest + name: Run tests + steps: + - name: Install minimal stable toolchain with clippy and rustfmt + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - uses: actions/checkout@v3 + - name: Run tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --verbose From fdbe4197899efc497d2d36f05156a331697df762 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Fri, 14 Apr 2023 10:37:30 +0200 Subject: [PATCH 032/128] fix CI (#9) There was a minor import inconsistency in database/benches --- core/mvcc/.github/workflows/smoke_test.yml | 2 +- core/mvcc/database/benches/my_benchmark.rs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/core/mvcc/.github/workflows/smoke_test.yml b/core/mvcc/.github/workflows/smoke_test.yml index 3b31f99b3..33c5f1948 100644 --- a/core/mvcc/.github/workflows/smoke_test.yml +++ b/core/mvcc/.github/workflows/smoke_test.yml @@ -44,7 +44,7 @@ jobs: uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} - args: --all-targets --tests --all-features -- -D warnings + args: --all-targets --all-features -- -D warnings rust-fmt: runs-on: ubuntu-latest name: Run Rustfmt diff --git a/core/mvcc/database/benches/my_benchmark.rs b/core/mvcc/database/benches/my_benchmark.rs index 7f6f8ea50..6aead08e5 100644 --- a/core/mvcc/database/benches/my_benchmark.rs +++ b/core/mvcc/database/benches/my_benchmark.rs @@ -1,5 +1,6 @@ -use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use mvcc_rs::database::{Database, LocalClock, Row}; +use criterion::{criterion_group, criterion_main, Criterion}; +use mvcc_rs::clock::LocalClock; +use mvcc_rs::database::{Database, Row}; use pprof::criterion::{Output, PProfProfiler}; fn bench(c: &mut Criterion) { From 8b798593a1fe4227a2c62dbb5bb0d5074f78a003 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Fri, 14 Apr 2023 10:45:43 +0200 Subject: [PATCH 033/128] simplify CI ... since the old script messed up Clippy sometimes --- core/mvcc/.github/workflows/smoke_test.yml | 77 +++------------------- 1 file changed, 9 insertions(+), 68 deletions(-) diff --git a/core/mvcc/.github/workflows/smoke_test.yml b/core/mvcc/.github/workflows/smoke_test.yml index 33c5f1948..c776a1635 100644 --- a/core/mvcc/.github/workflows/smoke_test.yml +++ b/core/mvcc/.github/workflows/smoke_test.yml @@ -1,4 +1,4 @@ -name: Smoke test +name: Rust on: push: @@ -10,74 +10,15 @@ env: CARGO_TERM_COLOR: always jobs: - checks: + build: + runs-on: ubuntu-latest - name: Run Checks + steps: - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - name: Checkout - uses: actions/checkout@v3 - - name: Check format - uses: actions-rs/cargo@v1 - with: - command: check - args: --all-targets --all-features - clippy: - runs-on: ubuntu-latest - name: Run Clippy - steps: - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - components: clippy - override: true - - name: Checkout - uses: actions/checkout@v3 - - name: Clippy - uses: actions-rs/clippy-check@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - args: --all-targets --all-features -- -D warnings - rust-fmt: - runs-on: ubuntu-latest - name: Run Rustfmt - steps: - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - components: rustfmt - override: true - - name: Checkout - uses: actions/checkout@v3 - with: - submodules: recursive - - name: Check format - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --check - examples: - runs-on: ubuntu-latest - name: Run tests - steps: - - name: Install minimal stable toolchain with clippy and rustfmt - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - uses: actions/checkout@v3 + - name: Check + run: cargo check --all-targets --all-features + - name: Clippy + run: cargo clippy --all-targets --all-features -- -D warnings - name: Run tests - uses: actions-rs/cargo@v1 - with: - command: test - args: --verbose + run: cargo test --verbose From 0f956fa17990d308da8db42b090935f7c4c9b3f0 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Fri, 14 Apr 2023 11:59:36 +0300 Subject: [PATCH 034/128] Use Criterion's throughput estimation Iteration time is of course interesting, but let's also use throughput estimation on MVCC operations such as read(), begin_tx(), etc.. --- core/mvcc/database/benches/my_benchmark.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/core/mvcc/database/benches/my_benchmark.rs b/core/mvcc/database/benches/my_benchmark.rs index 6aead08e5..9425f81e4 100644 --- a/core/mvcc/database/benches/my_benchmark.rs +++ b/core/mvcc/database/benches/my_benchmark.rs @@ -1,12 +1,15 @@ -use criterion::{criterion_group, criterion_main, Criterion}; +use criterion::{criterion_group, criterion_main, Criterion, Throughput}; use mvcc_rs::clock::LocalClock; use mvcc_rs::database::{Database, Row}; use pprof::criterion::{Output, PProfProfiler}; fn bench(c: &mut Criterion) { + let mut group = c.benchmark_group("mvcc-ops-throughput"); + group.throughput(Throughput::Elements(1)); + let clock = LocalClock::default(); let db = Database::new(clock); - c.bench_function("begin_tx", |b| { + group.bench_function("begin_tx", |b| { b.iter(|| { db.begin_tx(); }) @@ -14,7 +17,7 @@ fn bench(c: &mut Criterion) { let clock = LocalClock::default(); let db = Database::new(clock); - c.bench_function("begin_tx + rollback_tx", |b| { + group.bench_function("begin_tx + rollback_tx", |b| { b.iter(|| { let tx_id = db.begin_tx(); db.rollback_tx(tx_id) @@ -23,7 +26,7 @@ fn bench(c: &mut Criterion) { let clock = LocalClock::default(); let db = Database::new(clock); - c.bench_function("begin_tx + commit_tx", |b| { + group.bench_function("begin_tx + commit_tx", |b| { b.iter(|| { let tx_id = db.begin_tx(); db.commit_tx(tx_id) @@ -41,7 +44,7 @@ fn bench(c: &mut Criterion) { }, ) .unwrap(); - c.bench_function("read", |b| { + group.bench_function("read", |b| { b.iter(|| { db.read(tx, 1).unwrap(); }) From ed5e259cfebd01f724d9cd1b8fe9e15c6b5d5d07 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Fri, 14 Apr 2023 12:04:27 +0300 Subject: [PATCH 035/128] Switch to parking_lot mutex It's faster than the standard library mutex. --- core/mvcc/database/Cargo.toml | 1 + core/mvcc/database/src/database.rs | 15 ++++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/core/mvcc/database/Cargo.toml b/core/mvcc/database/Cargo.toml index 16a6d4de8..6f1512e0d 100644 --- a/core/mvcc/database/Cargo.toml +++ b/core/mvcc/database/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] anyhow = "1.0.70" +parking_lot = "0.12.1" rustyline = "11.0.0" thiserror = "1.0.40" tracing = "0.1.37" diff --git a/core/mvcc/database/src/database.rs b/core/mvcc/database/src/database.rs index 1ac44719a..6f196f291 100644 --- a/core/mvcc/database/src/database.rs +++ b/core/mvcc/database/src/database.rs @@ -2,8 +2,9 @@ use crate::clock::LogicalClock; use crate::errors::DatabaseError; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; +use parking_lot::Mutex; use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; type Result = std::result::Result; @@ -129,7 +130,7 @@ impl Database { /// * `row` - the row object containing the values to be inserted. /// pub fn insert(&self, tx_id: TxID, row: Row) -> Result<()> { - let inner = self.inner.lock().unwrap(); + let inner = self.inner.lock(); inner.insert(tx_id, row) } @@ -174,7 +175,7 @@ impl Database { /// Returns `true` if the row was successfully deleted, and `false` otherwise. /// pub fn delete(&self, tx_id: TxID, id: u64) -> Result { - let inner = self.inner.lock().unwrap(); + let inner = self.inner.lock(); inner.delete(tx_id, id) } @@ -193,7 +194,7 @@ impl Database { /// Returns `Some(row)` with the row data if the row with the given `id` exists, /// and `None` otherwise. pub fn read(&self, tx_id: TxID, id: u64) -> Result> { - let inner = self.inner.lock().unwrap(); + let inner = self.inner.lock(); inner.read(tx_id, id) } @@ -203,7 +204,7 @@ impl Database { /// that you can use to perform operations within the transaction. All changes made within the /// transaction are isolated from other transactions until you commit the transaction. pub fn begin_tx(&self) -> TxID { - let mut inner = self.inner.lock().unwrap(); + let mut inner = self.inner.lock(); inner.begin_tx() } @@ -217,7 +218,7 @@ impl Database { /// /// * `tx_id` - The ID of the transaction to commit. pub fn commit_tx(&self, tx_id: TxID) -> Result<()> { - let mut inner = self.inner.lock().unwrap(); + let mut inner = self.inner.lock(); inner.commit_tx(tx_id) } @@ -230,7 +231,7 @@ impl Database { /// /// * `tx_id` - The ID of the transaction to abort. pub fn rollback_tx(&self, tx_id: TxID) { - let inner = self.inner.lock().unwrap(); + let inner = self.inner.lock(); inner.rollback_tx(tx_id); } } From 5f84604d6735c09e46c3de2117d003b1d4f3484d Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Fri, 14 Apr 2023 13:50:24 +0300 Subject: [PATCH 036/128] Add some more micro-benchmarks --- core/mvcc/database/benches/my_benchmark.rs | 51 ++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/core/mvcc/database/benches/my_benchmark.rs b/core/mvcc/database/benches/my_benchmark.rs index 9425f81e4..8f67d6eac 100644 --- a/core/mvcc/database/benches/my_benchmark.rs +++ b/core/mvcc/database/benches/my_benchmark.rs @@ -33,6 +33,33 @@ fn bench(c: &mut Criterion) { }) }); + let clock = LocalClock::default(); + let db = Database::new(clock); + group.bench_function("begin_tx-read-commit_tx", |b| { + b.iter(|| { + let tx_id = db.begin_tx(); + db.read(tx_id, 1).unwrap(); + db.commit_tx(tx_id) + }) + }); + + let clock = LocalClock::default(); + let db = Database::new(clock); + group.bench_function("begin_tx-update-commit_tx", |b| { + b.iter(|| { + let tx_id = db.begin_tx(); + db.update( + tx_id, + Row { + id: 1, + data: "World".to_string(), + }, + ) + .unwrap(); + db.commit_tx(tx_id) + }) + }); + let clock = LocalClock::default(); let db = Database::new(clock); let tx = db.begin_tx(); @@ -49,6 +76,30 @@ fn bench(c: &mut Criterion) { db.read(tx, 1).unwrap(); }) }); + + let clock = LocalClock::default(); + let db = Database::new(clock); + let tx = db.begin_tx(); + db.insert( + tx, + Row { + id: 1, + data: "Hello".to_string(), + }, + ) + .unwrap(); + group.bench_function("update", |b| { + b.iter(|| { + db.update( + tx, + Row { + id: 1, + data: "World".to_string(), + }, + ) + .unwrap(); + }) + }); } criterion_group! { From 4a98d32ce1c52aa83db54a2ee0effaed2ed8054e Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Fri, 14 Apr 2023 13:52:55 +0300 Subject: [PATCH 037/128] Update README.md --- core/mvcc/README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/mvcc/README.md b/core/mvcc/README.md index eff22ea86..2b3656a08 100644 --- a/core/mvcc/README.md +++ b/core/mvcc/README.md @@ -1,6 +1,12 @@ # MVCC for Rust -This is a _work-in-progress_ Rust implementation of the Hekaton optimistic multiversion concurrency control algorithm. +This is a _work-in-progress_ the Hekaton optimistic multiversion concurrency control library in Rust. +The aim of the project is to provide a building block for implementing database management systems. + +## Features + +* Main memory architecture, rows are accessed via an index +* Optimistic multi-version concurrency control ## Development From b8b8d3f7465d30709788f242c4796a9d20d8691f Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Fri, 14 Apr 2023 14:02:02 +0300 Subject: [PATCH 038/128] Remove rustyline dependency It's not needed, likely leftover from some previous prototyping. --- core/mvcc/database/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/core/mvcc/database/Cargo.toml b/core/mvcc/database/Cargo.toml index 6f1512e0d..34b9c5792 100644 --- a/core/mvcc/database/Cargo.toml +++ b/core/mvcc/database/Cargo.toml @@ -6,7 +6,6 @@ edition = "2021" [dependencies] anyhow = "1.0.70" parking_lot = "0.12.1" -rustyline = "11.0.0" thiserror = "1.0.40" tracing = "0.1.37" From 9247e44324c943f9c898b1a48b379434b87056c8 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Fri, 14 Apr 2023 14:06:31 +0300 Subject: [PATCH 039/128] Minimize library binary size Shrinks binary size from 319K to 282K. --- core/mvcc/Cargo.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/mvcc/Cargo.toml b/core/mvcc/Cargo.toml index 04401335b..ba0f2a813 100644 --- a/core/mvcc/Cargo.toml +++ b/core/mvcc/Cargo.toml @@ -3,3 +3,8 @@ resolver = "2" members = [ "database", ] + +[profile.release] +codegen-units = 1 +panic = "abort" +strip = true From 620b8c6362f43176a3f95030f287859c1ad190c1 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Fri, 14 Apr 2023 14:43:28 +0300 Subject: [PATCH 040/128] Update README.md --- core/mvcc/README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/core/mvcc/README.md b/core/mvcc/README.md index 2b3656a08..0c885340a 100644 --- a/core/mvcc/README.md +++ b/core/mvcc/README.md @@ -8,6 +8,19 @@ The aim of the project is to provide a building block for implementing database * Main memory architecture, rows are accessed via an index * Optimistic multi-version concurrency control +## Experimental Evaluation + +**Single-threaded micro-benchmarks** + +Operations | Throughput +-----------------------------------|------------ +`begin_tx`, `read`, and `commit` | 2.2M ops/second +`begin_tx`, `update`, and `commit` | 2.2M ops/second +`read` | 12.9M ops/second +`update` | 6.2M ops/second + +(The `cargo bench` was run on a AMD Ryzen 9 3900XT 2.2 GHz CPU.) + ## Development Run tests: From bfe3bcef71c4d94bb7789c71a21fe00ef05dc3a9 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Fri, 14 Apr 2023 13:38:18 +0200 Subject: [PATCH 041/128] asyncify In order to prepare for #3, the APIs are made asynchronous. It also applies to tests and benches. --- core/mvcc/database/Cargo.toml | 4 +- core/mvcc/database/benches/my_benchmark.rs | 51 ++-- core/mvcc/database/src/database.rs | 238 +++++++++---------- core/mvcc/database/tests/concurrency_test.rs | 52 ++-- 4 files changed, 177 insertions(+), 168 deletions(-) diff --git a/core/mvcc/database/Cargo.toml b/core/mvcc/database/Cargo.toml index 34b9c5792..366c9a36e 100644 --- a/core/mvcc/database/Cargo.toml +++ b/core/mvcc/database/Cargo.toml @@ -5,14 +5,16 @@ edition = "2021" [dependencies] anyhow = "1.0.70" +futures = "0.3.28" parking_lot = "0.12.1" thiserror = "1.0.40" tracing = "0.1.37" [dev-dependencies] -criterion = { version = "0.4", features = ["html_reports"] } +criterion = { version = "0.4", features = ["html_reports", "async", "async_futures"] } pprof = { version = "0.11.1", features = ["criterion", "flamegraph"] } shuttle = "0.6.0" +tokio = { version = "1.27.0", features = ["full"] } tracing-subscriber = "0" tracing-test = "0" diff --git a/core/mvcc/database/benches/my_benchmark.rs b/core/mvcc/database/benches/my_benchmark.rs index 8f67d6eac..56331d1ff 100644 --- a/core/mvcc/database/benches/my_benchmark.rs +++ b/core/mvcc/database/benches/my_benchmark.rs @@ -1,3 +1,4 @@ +use criterion::async_executor::FuturesExecutor; use criterion::{criterion_group, criterion_main, Criterion, Throughput}; use mvcc_rs::clock::LocalClock; use mvcc_rs::database::{Database, Row}; @@ -10,44 +11,44 @@ fn bench(c: &mut Criterion) { let clock = LocalClock::default(); let db = Database::new(clock); group.bench_function("begin_tx", |b| { - b.iter(|| { - db.begin_tx(); + b.to_async(FuturesExecutor).iter(|| async { + db.begin_tx().await; }) }); let clock = LocalClock::default(); let db = Database::new(clock); group.bench_function("begin_tx + rollback_tx", |b| { - b.iter(|| { - let tx_id = db.begin_tx(); - db.rollback_tx(tx_id) + b.to_async(FuturesExecutor).iter(|| async { + let tx_id = db.begin_tx().await; + db.rollback_tx(tx_id).await }) }); let clock = LocalClock::default(); let db = Database::new(clock); group.bench_function("begin_tx + commit_tx", |b| { - b.iter(|| { - let tx_id = db.begin_tx(); - db.commit_tx(tx_id) + b.to_async(FuturesExecutor).iter(|| async { + let tx_id = db.begin_tx().await; + db.commit_tx(tx_id).await }) }); let clock = LocalClock::default(); let db = Database::new(clock); group.bench_function("begin_tx-read-commit_tx", |b| { - b.iter(|| { - let tx_id = db.begin_tx(); - db.read(tx_id, 1).unwrap(); - db.commit_tx(tx_id) + b.to_async(FuturesExecutor).iter(|| async { + let tx_id = db.begin_tx().await; + db.read(tx_id, 1).await.unwrap(); + db.commit_tx(tx_id).await }) }); let clock = LocalClock::default(); let db = Database::new(clock); group.bench_function("begin_tx-update-commit_tx", |b| { - b.iter(|| { - let tx_id = db.begin_tx(); + b.to_async(FuturesExecutor).iter(|| async { + let tx_id = db.begin_tx().await; db.update( tx_id, Row { @@ -55,41 +56,42 @@ fn bench(c: &mut Criterion) { data: "World".to_string(), }, ) + .await .unwrap(); - db.commit_tx(tx_id) + db.commit_tx(tx_id).await }) }); let clock = LocalClock::default(); let db = Database::new(clock); - let tx = db.begin_tx(); - db.insert( + let tx = futures::executor::block_on(db.begin_tx()); + futures::executor::block_on(db.insert( tx, Row { id: 1, data: "Hello".to_string(), }, - ) + )) .unwrap(); group.bench_function("read", |b| { - b.iter(|| { - db.read(tx, 1).unwrap(); + b.to_async(FuturesExecutor).iter(|| async { + db.read(tx, 1).await.unwrap(); }) }); let clock = LocalClock::default(); let db = Database::new(clock); - let tx = db.begin_tx(); - db.insert( + let tx = futures::executor::block_on(db.begin_tx()); + futures::executor::block_on(db.insert( tx, Row { id: 1, data: "Hello".to_string(), }, - ) + )) .unwrap(); group.bench_function("update", |b| { - b.iter(|| { + b.to_async(FuturesExecutor).iter(|| async { db.update( tx, Row { @@ -97,6 +99,7 @@ fn bench(c: &mut Criterion) { data: "World".to_string(), }, ) + .await .unwrap(); }) }); diff --git a/core/mvcc/database/src/database.rs b/core/mvcc/database/src/database.rs index 6f196f291..fa0248915 100644 --- a/core/mvcc/database/src/database.rs +++ b/core/mvcc/database/src/database.rs @@ -1,8 +1,8 @@ use crate::clock::LogicalClock; use crate::errors::DatabaseError; +use parking_lot::Mutex; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; -use parking_lot::Mutex; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; @@ -129,9 +129,9 @@ impl Database { /// * `tx_id` - the ID of the transaction in which to insert the new row. /// * `row` - the row object containing the values to be inserted. /// - pub fn insert(&self, tx_id: TxID, row: Row) -> Result<()> { + pub async fn insert(&self, tx_id: TxID, row: Row) -> Result<()> { let inner = self.inner.lock(); - inner.insert(tx_id, row) + inner.insert(tx_id, row).await } /// Updates a row in the database with new values. @@ -152,11 +152,11 @@ impl Database { /// # Returns /// /// Returns `true` if the row was successfully updated, and `false` otherwise. - pub fn update(&self, tx_id: TxID, row: Row) -> Result { - if !self.delete(tx_id, row.id)? { + pub async fn update(&self, tx_id: TxID, row: Row) -> Result { + if !self.delete(tx_id, row.id).await? { return Ok(false); } - self.insert(tx_id, row)?; + self.insert(tx_id, row).await?; Ok(true) } @@ -174,9 +174,9 @@ impl Database { /// /// Returns `true` if the row was successfully deleted, and `false` otherwise. /// - pub fn delete(&self, tx_id: TxID, id: u64) -> Result { + pub async fn delete(&self, tx_id: TxID, id: u64) -> Result { let inner = self.inner.lock(); - inner.delete(tx_id, id) + inner.delete(tx_id, id).await } /// Retrieves a row from the table with the given `id`. @@ -193,9 +193,9 @@ impl Database { /// /// Returns `Some(row)` with the row data if the row with the given `id` exists, /// and `None` otherwise. - pub fn read(&self, tx_id: TxID, id: u64) -> Result> { + pub async fn read(&self, tx_id: TxID, id: u64) -> Result> { let inner = self.inner.lock(); - inner.read(tx_id, id) + inner.read(tx_id, id).await } /// Begins a new transaction in the database. @@ -203,9 +203,9 @@ impl Database { /// This function starts a new transaction in the database and returns a `TxID` value /// that you can use to perform operations within the transaction. All changes made within the /// transaction are isolated from other transactions until you commit the transaction. - pub fn begin_tx(&self) -> TxID { + pub async fn begin_tx(&self) -> TxID { let mut inner = self.inner.lock(); - inner.begin_tx() + inner.begin_tx().await } /// Commits a transaction with the specified transaction ID. @@ -217,9 +217,9 @@ impl Database { /// # Arguments /// /// * `tx_id` - The ID of the transaction to commit. - pub fn commit_tx(&self, tx_id: TxID) -> Result<()> { + pub async fn commit_tx(&self, tx_id: TxID) -> Result<()> { let mut inner = self.inner.lock(); - inner.commit_tx(tx_id) + inner.commit_tx(tx_id).await } /// Rolls back a transaction with the specified ID. @@ -230,9 +230,9 @@ impl Database { /// # Arguments /// /// * `tx_id` - The ID of the transaction to abort. - pub fn rollback_tx(&self, tx_id: TxID) { + pub async fn rollback_tx(&self, tx_id: TxID) { let inner = self.inner.lock(); - inner.rollback_tx(tx_id); + inner.rollback_tx(tx_id).await; } } @@ -245,7 +245,7 @@ pub struct DatabaseInner { } impl DatabaseInner { - fn insert(&self, tx_id: TxID, row: Row) -> Result<()> { + async fn insert(&self, tx_id: TxID, row: Row) -> Result<()> { let mut txs = self.txs.borrow_mut(); let tx = txs .get_mut(&tx_id) @@ -263,7 +263,7 @@ impl DatabaseInner { Ok(()) } - fn delete(&self, tx_id: TxID, id: u64) -> Result { + async fn delete(&self, tx_id: TxID, id: u64) -> Result { let mut rows = self.rows.borrow_mut(); let mut txs = self.txs.borrow_mut(); if let Some(row_versions) = rows.get_mut(&id) { @@ -275,7 +275,7 @@ impl DatabaseInner { if is_write_write_conflict(&txs, tx, rv) { drop(txs); drop(rows); - self.rollback_tx(tx_id); + self.rollback_tx(tx_id).await; return Err(DatabaseError::WriteWriteConflict); } if is_version_visible(&txs, tx, rv) { @@ -291,7 +291,7 @@ impl DatabaseInner { Ok(false) } - fn read(&self, tx_id: TxID, id: u64) -> Result> { + async fn read(&self, tx_id: TxID, id: u64) -> Result> { let txs = self.txs.borrow_mut(); let tx = txs.get(&tx_id).unwrap(); assert!(tx.state == TransactionState::Active); @@ -307,7 +307,7 @@ impl DatabaseInner { Ok(None) } - fn begin_tx(&mut self) -> TxID { + async fn begin_tx(&mut self) -> TxID { let tx_id = self.get_tx_id(); let begin_ts = self.get_timestamp(); let tx = Transaction::new(tx_id, begin_ts); @@ -317,7 +317,7 @@ impl DatabaseInner { tx_id } - fn commit_tx(&mut self, tx_id: TxID) -> Result<()> { + async fn commit_tx(&mut self, tx_id: TxID) -> Result<()> { let end_ts = self.get_timestamp(); let mut txs = self.txs.borrow_mut(); let mut tx = txs.get_mut(&tx_id).unwrap(); @@ -351,7 +351,7 @@ impl DatabaseInner { Ok(()) } - fn rollback_tx(&self, tx_id: TxID) { + async fn rollback_tx(&self, tx_id: TxID) { let mut txs = self.txs.borrow_mut(); let mut tx = txs.get_mut(&tx_id).unwrap(); assert!(tx.state == TransactionState::Active); @@ -446,274 +446,274 @@ mod tests { use tracing_test::traced_test; #[traced_test] - #[test] - fn test_insert_read() { + #[tokio::test] + async fn test_insert_read() { let clock = LocalClock::default(); let db = Database::new(clock); - let tx1 = db.begin_tx(); + let tx1 = db.begin_tx().await; let tx1_row = Row { id: 1, data: "Hello".to_string(), }; - db.insert(tx1, tx1_row.clone()).unwrap(); - let row = db.read(tx1, 1).unwrap().unwrap(); + db.insert(tx1, tx1_row.clone()).await.unwrap(); + let row = db.read(tx1, 1).await.unwrap().unwrap(); assert_eq!(tx1_row, row); - db.commit_tx(tx1).unwrap(); + db.commit_tx(tx1).await.unwrap(); - let tx2 = db.begin_tx(); - let row = db.read(tx2, 1).unwrap().unwrap(); + let tx2 = db.begin_tx().await; + let row = db.read(tx2, 1).await.unwrap().unwrap(); assert_eq!(tx1_row, row); } #[traced_test] - #[test] - fn test_read_nonexistent() { + #[tokio::test] + async fn test_read_nonexistent() { let clock = LocalClock::default(); let db = Database::new(clock); - let tx = db.begin_tx(); - let row = db.read(tx, 1); + let tx = db.begin_tx().await; + let row = db.read(tx, 1).await; assert!(row.unwrap().is_none()); } #[traced_test] - #[test] - fn test_delete() { + #[tokio::test] + async fn test_delete() { let clock = LocalClock::default(); let db = Database::new(clock); - let tx1 = db.begin_tx(); + let tx1 = db.begin_tx().await; let tx1_row = Row { id: 1, data: "Hello".to_string(), }; - db.insert(tx1, tx1_row.clone()).unwrap(); - let row = db.read(tx1, 1).unwrap().unwrap(); + db.insert(tx1, tx1_row.clone()).await.unwrap(); + let row = db.read(tx1, 1).await.unwrap().unwrap(); assert_eq!(tx1_row, row); - db.delete(tx1, 1).unwrap(); - let row = db.read(tx1, 1).unwrap(); + db.delete(tx1, 1).await.unwrap(); + let row = db.read(tx1, 1).await.unwrap(); assert!(row.is_none()); - db.commit_tx(tx1).unwrap(); + db.commit_tx(tx1).await.unwrap(); - let tx2 = db.begin_tx(); - let row = db.read(tx2, 1).unwrap(); + let tx2 = db.begin_tx().await; + let row = db.read(tx2, 1).await.unwrap(); assert!(row.is_none()); } #[traced_test] - #[test] - fn test_delete_nonexistent() { + #[tokio::test] + async fn test_delete_nonexistent() { let clock = LocalClock::default(); let db = Database::new(clock); - let tx = db.begin_tx(); - assert!(!db.delete(tx, 1).unwrap()); + let tx = db.begin_tx().await; + assert!(!db.delete(tx, 1).await.unwrap()); } #[traced_test] - #[test] - fn test_commit() { + #[tokio::test] + async fn test_commit() { let clock = LocalClock::default(); let db = Database::new(clock); - let tx1 = db.begin_tx(); + let tx1 = db.begin_tx().await; let tx1_row = Row { id: 1, data: "Hello".to_string(), }; - db.insert(tx1, tx1_row.clone()).unwrap(); - let row = db.read(tx1, 1).unwrap().unwrap(); + db.insert(tx1, tx1_row.clone()).await.unwrap(); + let row = db.read(tx1, 1).await.unwrap().unwrap(); assert_eq!(tx1_row, row); let tx1_updated_row = Row { id: 1, data: "World".to_string(), }; - db.update(tx1, tx1_updated_row.clone()).unwrap(); - let row = db.read(tx1, 1).unwrap().unwrap(); + db.update(tx1, tx1_updated_row.clone()).await.unwrap(); + let row = db.read(tx1, 1).await.unwrap().unwrap(); assert_eq!(tx1_updated_row, row); - db.commit_tx(tx1).unwrap(); + db.commit_tx(tx1).await.unwrap(); - let tx2 = db.begin_tx(); - let row = db.read(tx2, 1).unwrap().unwrap(); - db.commit_tx(tx2).unwrap(); + let tx2 = db.begin_tx().await; + let row = db.read(tx2, 1).await.unwrap().unwrap(); + db.commit_tx(tx2).await.unwrap(); assert_eq!(tx1_updated_row, row); } #[traced_test] - #[test] - fn test_rollback() { + #[tokio::test] + async fn test_rollback() { let clock = LocalClock::default(); let db = Database::new(clock); - let tx1 = db.begin_tx(); + let tx1 = db.begin_tx().await; let row1 = Row { id: 1, data: "Hello".to_string(), }; - db.insert(tx1, row1.clone()).unwrap(); - let row2 = db.read(tx1, 1).unwrap().unwrap(); + db.insert(tx1, row1.clone()).await.unwrap(); + let row2 = db.read(tx1, 1).await.unwrap().unwrap(); assert_eq!(row1, row2); let row3 = Row { id: 1, data: "World".to_string(), }; - db.update(tx1, row3.clone()).unwrap(); - let row4 = db.read(tx1, 1).unwrap().unwrap(); + db.update(tx1, row3.clone()).await.unwrap(); + let row4 = db.read(tx1, 1).await.unwrap().unwrap(); assert_eq!(row3, row4); - db.rollback_tx(tx1); - let tx2 = db.begin_tx(); - let row5 = db.read(tx2, 1).unwrap(); + db.rollback_tx(tx1).await; + let tx2 = db.begin_tx().await; + let row5 = db.read(tx2, 1).await.unwrap(); assert_eq!(row5, None); } #[traced_test] - #[test] - fn test_dirty_write() { + #[tokio::test] + async fn test_dirty_write() { let clock = LocalClock::default(); let db = Database::new(clock); // T1 inserts a row with ID 1, but does not commit. - let tx1 = db.begin_tx(); + let tx1 = db.begin_tx().await; let tx1_row = Row { id: 1, data: "Hello".to_string(), }; - db.insert(tx1, tx1_row.clone()).unwrap(); - let row = db.read(tx1, 1).unwrap().unwrap(); + db.insert(tx1, tx1_row.clone()).await.unwrap(); + let row = db.read(tx1, 1).await.unwrap().unwrap(); assert_eq!(tx1_row, row); // T2 attempts to delete row with ID 1, but fails because T1 has not committed. - let tx2 = db.begin_tx(); + let tx2 = db.begin_tx().await; let tx2_row = Row { id: 1, data: "World".to_string(), }; - assert!(!db.update(tx2, tx2_row).unwrap()); + assert!(!db.update(tx2, tx2_row).await.unwrap()); - let row = db.read(tx1, 1).unwrap().unwrap(); + let row = db.read(tx1, 1).await.unwrap().unwrap(); assert_eq!(tx1_row, row); } #[traced_test] - #[test] - fn test_dirty_read() { + #[tokio::test] + async fn test_dirty_read() { let clock = LocalClock::default(); let db = Database::new(clock); // T1 inserts a row with ID 1, but does not commit. - let tx1 = db.begin_tx(); + let tx1 = db.begin_tx().await; let row1 = Row { id: 1, data: "Hello".to_string(), }; - db.insert(tx1, row1).unwrap(); + db.insert(tx1, row1).await.unwrap(); // T2 attempts to read row with ID 1, but doesn't see one because T1 has not committed. - let tx2 = db.begin_tx(); - let row2 = db.read(tx2, 1).unwrap(); + let tx2 = db.begin_tx().await; + let row2 = db.read(tx2, 1).await.unwrap(); assert_eq!(row2, None); } #[ignore] #[traced_test] - #[test] - fn test_dirty_read_deleted() { + #[tokio::test] + async fn test_dirty_read_deleted() { let clock = LocalClock::default(); let db = Database::new(clock); // T1 inserts a row with ID 1 and commits. - let tx1 = db.begin_tx(); + let tx1 = db.begin_tx().await; let tx1_row = Row { id: 1, data: "Hello".to_string(), }; - db.insert(tx1, tx1_row.clone()).unwrap(); - db.commit_tx(tx1).unwrap(); + db.insert(tx1, tx1_row.clone()).await.unwrap(); + db.commit_tx(tx1).await.unwrap(); // T2 deletes row with ID 1, but does not commit. - let tx2 = db.begin_tx(); - assert!(db.delete(tx2, 1).unwrap()); + let tx2 = db.begin_tx().await; + assert!(db.delete(tx2, 1).await.unwrap()); // T3 reads row with ID 1, but doesn't see the delete because T2 hasn't committed. - let tx3 = db.begin_tx(); - let row = db.read(tx3, 1).unwrap().unwrap(); + let tx3 = db.begin_tx().await; + let row = db.read(tx3, 1).await.unwrap().unwrap(); assert_eq!(tx1_row, row); } #[traced_test] - #[test] - fn test_fuzzy_read() { + #[tokio::test] + async fn test_fuzzy_read() { let clock = LocalClock::default(); let db = Database::new(clock); // T1 inserts a row with ID 1 and commits. - let tx1 = db.begin_tx(); + let tx1 = db.begin_tx().await; let tx1_row = Row { id: 1, data: "Hello".to_string(), }; - db.insert(tx1, tx1_row.clone()).unwrap(); - let row = db.read(tx1, 1).unwrap().unwrap(); + db.insert(tx1, tx1_row.clone()).await.unwrap(); + let row = db.read(tx1, 1).await.unwrap().unwrap(); assert_eq!(tx1_row, row); - db.commit_tx(tx1).unwrap(); + db.commit_tx(tx1).await.unwrap(); // T2 reads the row with ID 1 within an active transaction. - let tx2 = db.begin_tx(); - let row = db.read(tx2, 1).unwrap().unwrap(); + let tx2 = db.begin_tx().await; + let row = db.read(tx2, 1).await.unwrap().unwrap(); assert_eq!(tx1_row, row); // T3 updates the row and commits. - let tx3 = db.begin_tx(); + let tx3 = db.begin_tx().await; let tx3_row = Row { id: 1, data: "World".to_string(), }; - db.update(tx3, tx3_row).unwrap(); - db.commit_tx(tx3).unwrap(); + db.update(tx3, tx3_row).await.unwrap(); + db.commit_tx(tx3).await.unwrap(); // T2 still reads the same version of the row as before. - let row = db.read(tx2, 1).unwrap().unwrap(); + let row = db.read(tx2, 1).await.unwrap().unwrap(); assert_eq!(tx1_row, row); } #[traced_test] - #[test] - fn test_lost_update() { + #[tokio::test] + async fn test_lost_update() { let clock = LocalClock::default(); let db = Database::new(clock); // T1 inserts a row with ID 1 and commits. - let tx1 = db.begin_tx(); + let tx1 = db.begin_tx().await; let tx1_row = Row { id: 1, data: "Hello".to_string(), }; - db.insert(tx1, tx1_row.clone()).unwrap(); - let row = db.read(tx1, 1).unwrap().unwrap(); + db.insert(tx1, tx1_row.clone()).await.unwrap(); + let row = db.read(tx1, 1).await.unwrap().unwrap(); assert_eq!(tx1_row, row); - db.commit_tx(tx1).unwrap(); + db.commit_tx(tx1).await.unwrap(); // T2 attempts to update row ID 1 within an active transaction. - let tx2 = db.begin_tx(); + let tx2 = db.begin_tx().await; let tx2_row = Row { id: 1, data: "World".to_string(), }; - assert!(db.update(tx2, tx2_row.clone()).unwrap()); + assert!(db.update(tx2, tx2_row.clone()).await.unwrap()); // T3 also attempts to update row ID 1 within an active transaction. - let tx3 = db.begin_tx(); + let tx3 = db.begin_tx().await; let tx3_row = Row { id: 1, data: "Hello, world!".to_string(), }; assert_eq!( Err(DatabaseError::WriteWriteConflict), - db.update(tx3, tx3_row) + db.update(tx3, tx3_row).await ); - db.commit_tx(tx2).unwrap(); - assert_eq!(Err(DatabaseError::TxTerminated), db.commit_tx(tx3)); + db.commit_tx(tx2).await.unwrap(); + assert_eq!(Err(DatabaseError::TxTerminated), db.commit_tx(tx3).await); - let tx4 = db.begin_tx(); - let row = db.read(tx4, 1).unwrap().unwrap(); + let tx4 = db.begin_tx().await; + let row = db.read(tx4, 1).await.unwrap().unwrap(); assert_eq!(tx2_row, row); } } diff --git a/core/mvcc/database/tests/concurrency_test.rs b/core/mvcc/database/tests/concurrency_test.rs index bc1e4f90a..7673afba7 100644 --- a/core/mvcc/database/tests/concurrency_test.rs +++ b/core/mvcc/database/tests/concurrency_test.rs @@ -18,36 +18,40 @@ fn test_non_overlapping_concurrent_inserts() { let db = db.clone(); let ids = ids.clone(); thread::spawn(move || { - let tx = db.begin_tx(); - let id = ids.fetch_add(1, Ordering::SeqCst); - let row = Row { - id, - data: "Hello".to_string(), - }; - db.insert(tx, row.clone()).unwrap(); - db.commit_tx(tx).unwrap(); - let tx = db.begin_tx(); - let committed_row = db.read(tx, id).unwrap(); - db.commit_tx(tx).unwrap(); - assert_eq!(committed_row, Some(row)); + shuttle::future::block_on(async move { + let tx = db.begin_tx().await; + let id = ids.fetch_add(1, Ordering::SeqCst); + let row = Row { + id, + data: "Hello".to_string(), + }; + db.insert(tx, row.clone()).await.unwrap(); + db.commit_tx(tx).await.unwrap(); + let tx = db.begin_tx().await; + let committed_row = db.read(tx, id).await.unwrap(); + db.commit_tx(tx).await.unwrap(); + assert_eq!(committed_row, Some(row)); + }) }); } { let db = db.clone(); let ids = ids.clone(); thread::spawn(move || { - let tx = db.begin_tx(); - let id = ids.fetch_add(1, Ordering::SeqCst); - let row = Row { - id, - data: "World".to_string(), - }; - db.insert(tx, row.clone()).unwrap(); - db.commit_tx(tx).unwrap(); - let tx = db.begin_tx(); - let committed_row = db.read(tx, id).unwrap(); - db.commit_tx(tx).unwrap(); - assert_eq!(committed_row, Some(row)); + shuttle::future::block_on(async move { + let tx = db.begin_tx().await; + let id = ids.fetch_add(1, Ordering::SeqCst); + let row = Row { + id, + data: "World".to_string(), + }; + db.insert(tx, row.clone()).await.unwrap(); + db.commit_tx(tx).await.unwrap(); + let tx = db.begin_tx().await; + let committed_row = db.read(tx, id).await.unwrap(); + db.commit_tx(tx).await.unwrap(); + assert_eq!(committed_row, Some(row)); + }); }); } }, From 546db5a983eaa03fcc447f87ff49df9376ef9ab1 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Fri, 14 Apr 2023 15:04:51 +0200 Subject: [PATCH 042/128] sync: add AsyncMutex trait With AsyncMutex, we can use different mutex mechanisms in the database - e.g. tokio::sync::Mutex. --- core/mvcc/database/Cargo.toml | 9 +++- core/mvcc/database/benches/my_benchmark.rs | 14 +++--- core/mvcc/database/src/database.rs | 53 +++++++++++--------- core/mvcc/database/src/lib.rs | 1 + core/mvcc/database/src/sync.rs | 43 ++++++++++++++++ core/mvcc/database/tests/concurrency_test.rs | 2 +- 6 files changed, 90 insertions(+), 32 deletions(-) create mode 100644 core/mvcc/database/src/sync.rs diff --git a/core/mvcc/database/Cargo.toml b/core/mvcc/database/Cargo.toml index 366c9a36e..36809c922 100644 --- a/core/mvcc/database/Cargo.toml +++ b/core/mvcc/database/Cargo.toml @@ -5,10 +5,11 @@ edition = "2021" [dependencies] anyhow = "1.0.70" +async-trait = "0.1.68" futures = "0.3.28" -parking_lot = "0.12.1" thiserror = "1.0.40" tracing = "0.1.37" +tokio = { version = "1.27.0", features = ["full"], optional = true } [dev-dependencies] criterion = { version = "0.4", features = ["html_reports", "async", "async_futures"] } @@ -17,7 +18,13 @@ shuttle = "0.6.0" tokio = { version = "1.27.0", features = ["full"] } tracing-subscriber = "0" tracing-test = "0" +mvcc-rs = { path = ".", features = ["tokio"] } [[bench]] name = "my_benchmark" harness = false + +[features] +default = [] +full = ["tokio"] +tokio = ["dep:tokio"] diff --git a/core/mvcc/database/benches/my_benchmark.rs b/core/mvcc/database/benches/my_benchmark.rs index 56331d1ff..1515b7579 100644 --- a/core/mvcc/database/benches/my_benchmark.rs +++ b/core/mvcc/database/benches/my_benchmark.rs @@ -9,7 +9,7 @@ fn bench(c: &mut Criterion) { group.throughput(Throughput::Elements(1)); let clock = LocalClock::default(); - let db = Database::new(clock); + let db = Database::>::new(clock); group.bench_function("begin_tx", |b| { b.to_async(FuturesExecutor).iter(|| async { db.begin_tx().await; @@ -17,7 +17,7 @@ fn bench(c: &mut Criterion) { }); let clock = LocalClock::default(); - let db = Database::new(clock); + let db = Database::>::new(clock); group.bench_function("begin_tx + rollback_tx", |b| { b.to_async(FuturesExecutor).iter(|| async { let tx_id = db.begin_tx().await; @@ -26,7 +26,7 @@ fn bench(c: &mut Criterion) { }); let clock = LocalClock::default(); - let db = Database::new(clock); + let db = Database::>::new(clock); group.bench_function("begin_tx + commit_tx", |b| { b.to_async(FuturesExecutor).iter(|| async { let tx_id = db.begin_tx().await; @@ -35,7 +35,7 @@ fn bench(c: &mut Criterion) { }); let clock = LocalClock::default(); - let db = Database::new(clock); + let db = Database::>::new(clock); group.bench_function("begin_tx-read-commit_tx", |b| { b.to_async(FuturesExecutor).iter(|| async { let tx_id = db.begin_tx().await; @@ -45,7 +45,7 @@ fn bench(c: &mut Criterion) { }); let clock = LocalClock::default(); - let db = Database::new(clock); + let db = Database::>::new(clock); group.bench_function("begin_tx-update-commit_tx", |b| { b.to_async(FuturesExecutor).iter(|| async { let tx_id = db.begin_tx().await; @@ -63,7 +63,7 @@ fn bench(c: &mut Criterion) { }); let clock = LocalClock::default(); - let db = Database::new(clock); + let db = Database::>::new(clock); let tx = futures::executor::block_on(db.begin_tx()); futures::executor::block_on(db.insert( tx, @@ -80,7 +80,7 @@ fn bench(c: &mut Criterion) { }); let clock = LocalClock::default(); - let db = Database::new(clock); + let db = Database::>::new(clock); let tx = futures::executor::block_on(db.begin_tx()); futures::executor::block_on(db.insert( tx, diff --git a/core/mvcc/database/src/database.rs b/core/mvcc/database/src/database.rs index fa0248915..6a1c7ac1f 100644 --- a/core/mvcc/database/src/database.rs +++ b/core/mvcc/database/src/database.rs @@ -1,6 +1,5 @@ use crate::clock::LogicalClock; use crate::errors::DatabaseError; -use parking_lot::Mutex; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::sync::atomic::{AtomicU64, Ordering}; @@ -101,11 +100,16 @@ enum TransactionState { /// A database with MVCC. #[derive(Debug)] -pub struct Database { - inner: Arc>>, +pub struct Database< + Clock: LogicalClock, + AsyncMutex: crate::sync::AsyncMutex>, +> { + inner: Arc, } -impl Database { +impl>> + Database +{ /// Creates a new database. pub fn new(clock: Clock) -> Self { let inner = DatabaseInner { @@ -115,7 +119,7 @@ impl Database { clock, }; Self { - inner: Arc::new(Mutex::new(inner)), + inner: Arc::new(AsyncMutex::new(inner)), } } @@ -130,7 +134,7 @@ impl Database { /// * `row` - the row object containing the values to be inserted. /// pub async fn insert(&self, tx_id: TxID, row: Row) -> Result<()> { - let inner = self.inner.lock(); + let inner = self.inner.lock().await; inner.insert(tx_id, row).await } @@ -175,7 +179,7 @@ impl Database { /// Returns `true` if the row was successfully deleted, and `false` otherwise. /// pub async fn delete(&self, tx_id: TxID, id: u64) -> Result { - let inner = self.inner.lock(); + let inner = self.inner.lock().await; inner.delete(tx_id, id).await } @@ -194,7 +198,7 @@ impl Database { /// Returns `Some(row)` with the row data if the row with the given `id` exists, /// and `None` otherwise. pub async fn read(&self, tx_id: TxID, id: u64) -> Result> { - let inner = self.inner.lock(); + let inner = self.inner.lock().await; inner.read(tx_id, id).await } @@ -204,7 +208,7 @@ impl Database { /// that you can use to perform operations within the transaction. All changes made within the /// transaction are isolated from other transactions until you commit the transaction. pub async fn begin_tx(&self) -> TxID { - let mut inner = self.inner.lock(); + let mut inner = self.inner.lock().await; inner.begin_tx().await } @@ -218,7 +222,7 @@ impl Database { /// /// * `tx_id` - The ID of the transaction to commit. pub async fn commit_tx(&self, tx_id: TxID) -> Result<()> { - let mut inner = self.inner.lock(); + let mut inner = self.inner.lock().await; inner.commit_tx(tx_id).await } @@ -231,7 +235,7 @@ impl Database { /// /// * `tx_id` - The ID of the transaction to abort. pub async fn rollback_tx(&self, tx_id: TxID) { - let inner = self.inner.lock(); + let inner = self.inner.lock().await; inner.rollback_tx(tx_id).await; } } @@ -263,9 +267,12 @@ impl DatabaseInner { Ok(()) } + #[allow(clippy::await_holding_refcell_ref)] async fn delete(&self, tx_id: TxID, id: u64) -> Result { - let mut rows = self.rows.borrow_mut(); + // NOTICE: They *are* dropped before an await point!!! But the await is conditional, + // so I think clippy is just confused. let mut txs = self.txs.borrow_mut(); + let mut rows = self.rows.borrow_mut(); if let Some(row_versions) = rows.get_mut(&id) { for rv in row_versions.iter_mut().rev() { let tx = txs @@ -449,7 +456,7 @@ mod tests { #[tokio::test] async fn test_insert_read() { let clock = LocalClock::default(); - let db = Database::new(clock); + let db = Database::>::new(clock); let tx1 = db.begin_tx().await; let tx1_row = Row { @@ -470,7 +477,7 @@ mod tests { #[tokio::test] async fn test_read_nonexistent() { let clock = LocalClock::default(); - let db = Database::new(clock); + let db = Database::>::new(clock); let tx = db.begin_tx().await; let row = db.read(tx, 1).await; assert!(row.unwrap().is_none()); @@ -480,7 +487,7 @@ mod tests { #[tokio::test] async fn test_delete() { let clock = LocalClock::default(); - let db = Database::new(clock); + let db = Database::>::new(clock); let tx1 = db.begin_tx().await; let tx1_row = Row { @@ -504,7 +511,7 @@ mod tests { #[tokio::test] async fn test_delete_nonexistent() { let clock = LocalClock::default(); - let db = Database::new(clock); + let db = Database::>::new(clock); let tx = db.begin_tx().await; assert!(!db.delete(tx, 1).await.unwrap()); } @@ -513,7 +520,7 @@ mod tests { #[tokio::test] async fn test_commit() { let clock = LocalClock::default(); - let db = Database::new(clock); + let db = Database::>::new(clock); let tx1 = db.begin_tx().await; let tx1_row = Row { id: 1, @@ -541,7 +548,7 @@ mod tests { #[tokio::test] async fn test_rollback() { let clock = LocalClock::default(); - let db = Database::new(clock); + let db = Database::>::new(clock); let tx1 = db.begin_tx().await; let row1 = Row { id: 1, @@ -567,7 +574,7 @@ mod tests { #[tokio::test] async fn test_dirty_write() { let clock = LocalClock::default(); - let db = Database::new(clock); + let db = Database::>::new(clock); // T1 inserts a row with ID 1, but does not commit. let tx1 = db.begin_tx().await; @@ -595,7 +602,7 @@ mod tests { #[tokio::test] async fn test_dirty_read() { let clock = LocalClock::default(); - let db = Database::new(clock); + let db = Database::>::new(clock); // T1 inserts a row with ID 1, but does not commit. let tx1 = db.begin_tx().await; @@ -616,7 +623,7 @@ mod tests { #[tokio::test] async fn test_dirty_read_deleted() { let clock = LocalClock::default(); - let db = Database::new(clock); + let db = Database::>::new(clock); // T1 inserts a row with ID 1 and commits. let tx1 = db.begin_tx().await; @@ -641,7 +648,7 @@ mod tests { #[tokio::test] async fn test_fuzzy_read() { let clock = LocalClock::default(); - let db = Database::new(clock); + let db = Database::>::new(clock); // T1 inserts a row with ID 1 and commits. let tx1 = db.begin_tx().await; @@ -677,7 +684,7 @@ mod tests { #[tokio::test] async fn test_lost_update() { let clock = LocalClock::default(); - let db = Database::new(clock); + let db = Database::>::new(clock); // T1 inserts a row with ID 1 and commits. let tx1 = db.begin_tx().await; diff --git a/core/mvcc/database/src/lib.rs b/core/mvcc/database/src/lib.rs index 44708bb6a..a6b1082ba 100644 --- a/core/mvcc/database/src/lib.rs +++ b/core/mvcc/database/src/lib.rs @@ -34,3 +34,4 @@ pub mod clock; pub mod database; pub mod errors; +pub mod sync; diff --git a/core/mvcc/database/src/sync.rs b/core/mvcc/database/src/sync.rs new file mode 100644 index 000000000..de643abd4 --- /dev/null +++ b/core/mvcc/database/src/sync.rs @@ -0,0 +1,43 @@ +#[async_trait::async_trait] +pub trait AsyncMutex { + type Inner; + type Guard<'a>: std::ops::DerefMut + where + Self: 'a, + Self::Inner: 'a; + + fn new(inner: Self::Inner) -> Self; + + async fn lock<'a>(&'a self) -> Self::Guard<'a>; +} + +#[async_trait::async_trait] +impl AsyncMutex for std::sync::Mutex { + type Inner = T; + type Guard<'a> = std::sync::MutexGuard<'a, T> where T: 'a; + + fn new(inner: Self::Inner) -> Self { + Self::new(inner) + } + + async fn lock<'a>(&'a self) -> Self::Guard<'a> { + self.lock().unwrap() + } +} + +#[cfg(feature = "tokio")] +mod tokio_mutex { + #[async_trait::async_trait] + impl super::AsyncMutex for tokio::sync::Mutex { + type Inner = T; + type Guard<'a> = tokio::sync::MutexGuard<'a, T> where T: 'a; + + fn new(inner: Self::Inner) -> Self { + Self::new(inner) + } + + async fn lock<'a>(&'a self) -> Self::Guard<'a> { + self.lock().await + } + } +} diff --git a/core/mvcc/database/tests/concurrency_test.rs b/core/mvcc/database/tests/concurrency_test.rs index 7673afba7..b7b22b0e4 100644 --- a/core/mvcc/database/tests/concurrency_test.rs +++ b/core/mvcc/database/tests/concurrency_test.rs @@ -10,7 +10,7 @@ fn test_non_overlapping_concurrent_inserts() { // Two threads insert to the database concurrently using non-overlapping // row IDs. let clock = LocalClock::default(); - let db = Arc::new(Database::new(clock)); + let db = Arc::new(Database::>::new(clock)); let ids = Arc::new(AtomicU64::new(0)); shuttle::check_random( move || { From aff901baeaaf8d55115116108ac1eef757cd6955 Mon Sep 17 00:00:00 2001 From: avi Date: Fri, 14 Apr 2023 21:59:35 +0530 Subject: [PATCH 043/128] bugfix: make committed rows visibile (fixes #15) --- core/mvcc/database/src/database.rs | 59 +++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/core/mvcc/database/src/database.rs b/core/mvcc/database/src/database.rs index 6a1c7ac1f..05a662435 100644 --- a/core/mvcc/database/src/database.rs +++ b/core/mvcc/database/src/database.rs @@ -435,7 +435,7 @@ fn is_end_visible(txs: &HashMap, tx: &Transaction, rv: &RowVe Some(TxTimestampOrID::TxID(rv_end)) => { let te = txs.get(&rv_end).unwrap(); match te.state { - TransactionState::Active => tx.tx_id == te.tx_id && rv.end.is_none(), + TransactionState::Active => tx.tx_id != te.tx_id, TransactionState::Preparing => todo!(), TransactionState::Committed => todo!(), TransactionState::Aborted => todo!(), @@ -723,4 +723,61 @@ mod tests { let row = db.read(tx4, 1).await.unwrap().unwrap(); assert_eq!(tx2_row, row); } + + // Test for the visibility to check if a new transaction can see old committed values. + // This test checks for the typo present in the paper, explained in https://github.com/penberg/mvcc-rs/issues/15 + #[traced_test] + #[tokio::test] + async fn test_committed_visibility() { + let clock = LocalClock::default(); + let db = Database::>::new(clock); + + // let's add $10 to my account since I like money + let tx1 = db.begin_tx().await; + let tx1_row = Row { + id: 1, + data: "10".to_string(), + }; + db.insert(tx1, tx1_row.clone()).await.unwrap(); + db.commit_tx(tx1).await.unwrap(); + + // but I like more money, so let me try adding $10 more + let tx2 = db.begin_tx().await; + let tx2_row = Row { + id: 1, + data: "20".to_string(), + }; + assert!(db.update(tx2, tx2_row.clone()).await.unwrap()); + + // can I check how much money I have? + let tx3 = db.begin_tx().await; + let row = db.read(tx3, 1).await.unwrap().unwrap(); + assert_eq!(tx1_row, row); + } + + // Test to check if a older transaction can see (un)committed future rows + #[traced_test] + #[tokio::test] + async fn test_future_row() { + let clock = LocalClock::default(); + let db = Database::>::new(clock); + + let tx1 = db.begin_tx().await; + + let tx2 = db.begin_tx().await; + let tx2_row = Row { + id: 1, + data: "10".to_string(), + }; + db.insert(tx2, tx2_row.clone()).await.unwrap(); + + // transaction in progress, so tx1 shouldn't be able to see the value yet + let row = db.read(tx1, 1).await.unwrap(); + assert_eq!(row, None); + + // lets commit the transaction and check if tx1 can see it now + db.commit_tx(tx2).await.unwrap(); + let row = db.read(tx1, 1).await.unwrap(); + assert_eq!(row, None); + } } From e00617748069a363b0d6656d30daa50c48853fcf Mon Sep 17 00:00:00 2001 From: avi Date: Fri, 14 Apr 2023 22:06:55 +0530 Subject: [PATCH 044/128] fix typos --- core/mvcc/database/src/database.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/mvcc/database/src/database.rs b/core/mvcc/database/src/database.rs index 05a662435..a2b6c235a 100644 --- a/core/mvcc/database/src/database.rs +++ b/core/mvcc/database/src/database.rs @@ -771,11 +771,11 @@ mod tests { }; db.insert(tx2, tx2_row.clone()).await.unwrap(); - // transaction in progress, so tx1 shouldn't be able to see the value yet + // transaction in progress, so tx1 shouldn't be able to see the value let row = db.read(tx1, 1).await.unwrap(); assert_eq!(row, None); - // lets commit the transaction and check if tx1 can see it now + // lets commit the transaction and check if tx1 can see it db.commit_tx(tx2).await.unwrap(); let row = db.read(tx1, 1).await.unwrap(); assert_eq!(row, None); From 87b9b272159a3d8816d9e26a1a071a1fb6adb01e Mon Sep 17 00:00:00 2001 From: avi Date: Fri, 14 Apr 2023 22:56:07 +0530 Subject: [PATCH 045/128] check if tx can see its own updates --- core/mvcc/database/src/database.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/mvcc/database/src/database.rs b/core/mvcc/database/src/database.rs index a2b6c235a..ecfe8e743 100644 --- a/core/mvcc/database/src/database.rs +++ b/core/mvcc/database/src/database.rs @@ -748,6 +748,8 @@ mod tests { data: "20".to_string(), }; assert!(db.update(tx2, tx2_row.clone()).await.unwrap()); + let row = db.read(tx2, 1).await.unwrap().unwrap(); + assert_eq!(row, tx2_row); // can I check how much money I have? let tx3 = db.begin_tx().await; From c0881944e03633bdd6cb3891f1ca439f8e6b280f Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Fri, 14 Apr 2023 21:31:19 +0300 Subject: [PATCH 046/128] Update README.md --- core/mvcc/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/mvcc/README.md b/core/mvcc/README.md index 0c885340a..21a934ccf 100644 --- a/core/mvcc/README.md +++ b/core/mvcc/README.md @@ -51,3 +51,5 @@ cargo bench --bench my_benchmark -- --profile-time=5 ## References Larson et al. [High-Performance Concurrency Control Mechanisms for Main-Memory Databases](https://vldb.org/pvldb/vol5/p298_per-akelarson_vldb2012.pdf). VLDB '11 + +Paper errata: The visibility check in Table 2 is wrong and causes uncommitted delete to become visible to transactions (fixed in commit 6ca377320bb59b52ecc0430b9e5e422e8d61658d). From 43544c9fb61ed366eb66f58914b50233e1578aed Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Fri, 14 Apr 2023 21:32:31 +0300 Subject: [PATCH 047/128] Update README.md --- core/mvcc/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/mvcc/README.md b/core/mvcc/README.md index 21a934ccf..fa279fcba 100644 --- a/core/mvcc/README.md +++ b/core/mvcc/README.md @@ -52,4 +52,4 @@ cargo bench --bench my_benchmark -- --profile-time=5 Larson et al. [High-Performance Concurrency Control Mechanisms for Main-Memory Databases](https://vldb.org/pvldb/vol5/p298_per-akelarson_vldb2012.pdf). VLDB '11 -Paper errata: The visibility check in Table 2 is wrong and causes uncommitted delete to become visible to transactions (fixed in commit 6ca377320bb59b52ecc0430b9e5e422e8d61658d). +Paper errata: The visibility check in Table 2 is wrong and causes uncommitted delete to become visible to transactions (fixed in [commit 6ca3773]( https://github.com/penberg/mvcc-rs/commit/6ca377320bb59b52ecc0430b9e5e422e8d61658d)). From 74a4d2d11af94d421572d69429c6becc42df7e41 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Sat, 15 Apr 2023 09:51:26 +0300 Subject: [PATCH 048/128] Garbage collect transactions Fixes #2 --- core/mvcc/database/src/database.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/mvcc/database/src/database.rs b/core/mvcc/database/src/database.rs index ecfe8e743..c601e7a38 100644 --- a/core/mvcc/database/src/database.rs +++ b/core/mvcc/database/src/database.rs @@ -355,6 +355,13 @@ impl DatabaseInner { } tx.state = TransactionState::Committed; tracing::trace!("COMMIT {tx}"); + // We have now updated all the versions with a reference to the + // transaction ID to a timestamp and can, therefore, remove the + // transaction. Please note that when we move to lockless, the + // invariant doesn't necessarily hold anymore because another thread + // might have speculatively read a version that we want to remove. + // But that's a problem for another day. + txs.remove(&tx_id); Ok(()) } From db71c7e4e38cea013dc5b9d417a8794923697ab2 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Mon, 17 Apr 2023 10:45:25 +0200 Subject: [PATCH 049/128] database: make transactions (de)serializable --- core/mvcc/database/src/database.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/core/mvcc/database/src/database.rs b/core/mvcc/database/src/database.rs index c601e7a38..0e4616f5d 100644 --- a/core/mvcc/database/src/database.rs +++ b/core/mvcc/database/src/database.rs @@ -1,20 +1,21 @@ use crate::clock::LogicalClock; use crate::errors::DatabaseError; +use serde::{Deserialize, Serialize}; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; -type Result = std::result::Result; +pub type Result = std::result::Result; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Row { pub id: u64, pub data: String, } /// A row version. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] struct RowVersion { begin: TxTimestampOrID, end: Option, @@ -29,14 +30,14 @@ type TxID = u64; /// phase of the transaction. During the active phase, new versions track the /// transaction ID in the `begin` and `end` fields. After a transaction commits, /// versions switch to tracking timestamps. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] enum TxTimestampOrID { Timestamp(u64), TxID(TxID), } /// Transaction -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Transaction { /// The state of the transaction. state: TransactionState, @@ -89,7 +90,7 @@ impl std::fmt::Display for Transaction { } /// Transaction state. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] enum TransactionState { Active, Preparing, From 7ca68b3d9691ae163d05bf71aba7a01030a6ad50 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Mon, 17 Apr 2023 12:06:34 +0200 Subject: [PATCH 050/128] errors: Add I/O error class --- core/mvcc/database/src/errors.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/mvcc/database/src/errors.rs b/core/mvcc/database/src/errors.rs index 7bd5bab57..6cdad8ca3 100644 --- a/core/mvcc/database/src/errors.rs +++ b/core/mvcc/database/src/errors.rs @@ -8,4 +8,6 @@ pub enum DatabaseError { WriteWriteConflict, #[error("transaction is terminated")] TxTerminated, + #[error("I/O error: {0}")] + Io(String), } From 04a78f73fb7a40d816a73aeb97a493569b85ea47 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Mon, 17 Apr 2023 12:52:42 +0200 Subject: [PATCH 051/128] treewide: add persistent storage trait This draft adds a persistent storage trait that can be used to store transaction logs and read the log for recovery purposes. Work in heavy progress, because ideally the design should also allow reading versions from the storage, so that data can be spilled from memory to disk if there's not enough RAM available. --- core/mvcc/database/Cargo.toml | 6 +- core/mvcc/database/benches/my_benchmark.rs | 33 ++-- core/mvcc/database/src/database.rs | 178 +++++++++++++++---- core/mvcc/database/src/lib.rs | 1 + core/mvcc/database/src/persistent_storage.rs | 97 ++++++++++ core/mvcc/database/tests/concurrency_test.rs | 3 +- 6 files changed, 268 insertions(+), 50 deletions(-) create mode 100644 core/mvcc/database/src/persistent_storage.rs diff --git a/core/mvcc/database/Cargo.toml b/core/mvcc/database/Cargo.toml index 36809c922..a39fb4069 100644 --- a/core/mvcc/database/Cargo.toml +++ b/core/mvcc/database/Cargo.toml @@ -10,6 +10,10 @@ futures = "0.3.28" thiserror = "1.0.40" tracing = "0.1.37" tokio = { version = "1.27.0", features = ["full"], optional = true } +tokio-stream = { version = "0.1.12", optional = true, features = ["io-util"] } +serde = { version = "1.0.160", features = ["derive"] } +serde_json = "1.0.96" +pin-project = "1.0.12" [dev-dependencies] criterion = { version = "0.4", features = ["html_reports", "async", "async_futures"] } @@ -27,4 +31,4 @@ harness = false [features] default = [] full = ["tokio"] -tokio = ["dep:tokio"] +tokio = ["dep:tokio", "dep:tokio-stream"] diff --git a/core/mvcc/database/benches/my_benchmark.rs b/core/mvcc/database/benches/my_benchmark.rs index 1515b7579..3c360107a 100644 --- a/core/mvcc/database/benches/my_benchmark.rs +++ b/core/mvcc/database/benches/my_benchmark.rs @@ -4,20 +4,30 @@ use mvcc_rs::clock::LocalClock; use mvcc_rs::database::{Database, Row}; use pprof::criterion::{Output, PProfProfiler}; +fn bench_db() -> Database< + LocalClock, + mvcc_rs::persistent_storage::Noop, + tokio::sync::Mutex< + mvcc_rs::database::DatabaseInner, + >, +> { + let clock = LocalClock::default(); + let storage = mvcc_rs::persistent_storage::Noop {}; + Database::<_, _, tokio::sync::Mutex<_>>::new(clock, storage) +} + fn bench(c: &mut Criterion) { let mut group = c.benchmark_group("mvcc-ops-throughput"); group.throughput(Throughput::Elements(1)); - let clock = LocalClock::default(); - let db = Database::>::new(clock); + let db = bench_db(); group.bench_function("begin_tx", |b| { b.to_async(FuturesExecutor).iter(|| async { db.begin_tx().await; }) }); - let clock = LocalClock::default(); - let db = Database::>::new(clock); + let db = bench_db(); group.bench_function("begin_tx + rollback_tx", |b| { b.to_async(FuturesExecutor).iter(|| async { let tx_id = db.begin_tx().await; @@ -25,8 +35,7 @@ fn bench(c: &mut Criterion) { }) }); - let clock = LocalClock::default(); - let db = Database::>::new(clock); + let db = bench_db(); group.bench_function("begin_tx + commit_tx", |b| { b.to_async(FuturesExecutor).iter(|| async { let tx_id = db.begin_tx().await; @@ -34,8 +43,7 @@ fn bench(c: &mut Criterion) { }) }); - let clock = LocalClock::default(); - let db = Database::>::new(clock); + let db = bench_db(); group.bench_function("begin_tx-read-commit_tx", |b| { b.to_async(FuturesExecutor).iter(|| async { let tx_id = db.begin_tx().await; @@ -44,8 +52,7 @@ fn bench(c: &mut Criterion) { }) }); - let clock = LocalClock::default(); - let db = Database::>::new(clock); + let db = bench_db(); group.bench_function("begin_tx-update-commit_tx", |b| { b.to_async(FuturesExecutor).iter(|| async { let tx_id = db.begin_tx().await; @@ -62,8 +69,7 @@ fn bench(c: &mut Criterion) { }) }); - let clock = LocalClock::default(); - let db = Database::>::new(clock); + let db = bench_db(); let tx = futures::executor::block_on(db.begin_tx()); futures::executor::block_on(db.insert( tx, @@ -79,8 +85,7 @@ fn bench(c: &mut Criterion) { }) }); - let clock = LocalClock::default(); - let db = Database::>::new(clock); + let db = bench_db(); let tx = futures::executor::block_on(db.begin_tx()); futures::executor::block_on(db.insert( tx, diff --git a/core/mvcc/database/src/database.rs b/core/mvcc/database/src/database.rs index 0e4616f5d..98cbefcea 100644 --- a/core/mvcc/database/src/database.rs +++ b/core/mvcc/database/src/database.rs @@ -16,13 +16,28 @@ pub struct Row { /// A row version. #[derive(Clone, Debug, Serialize, Deserialize)] -struct RowVersion { +pub struct RowVersion { begin: TxTimestampOrID, end: Option, row: Row, } -type TxID = u64; +pub type TxID = u64; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Mutation { + tx_id: TxID, + row_versions: Vec, +} + +impl Mutation { + fn new(tx_id: TxID) -> Self { + Self { + tx_id, + row_versions: Vec::new(), + } + } +} /// A transaction timestamp or ID. /// @@ -103,21 +118,26 @@ enum TransactionState { #[derive(Debug)] pub struct Database< Clock: LogicalClock, - AsyncMutex: crate::sync::AsyncMutex>, + Storage: crate::persistent_storage::Storage, + AsyncMutex: crate::sync::AsyncMutex>, > { inner: Arc, } -impl>> - Database +impl< + Clock: LogicalClock, + Storage: crate::persistent_storage::Storage, + AsyncMutex: crate::sync::AsyncMutex>, + > Database { /// Creates a new database. - pub fn new(clock: Clock) -> Self { + pub fn new(clock: Clock, storage: Storage) -> Self { let inner = DatabaseInner { rows: RefCell::new(HashMap::new()), txs: RefCell::new(HashMap::new()), tx_ids: AtomicU64::new(0), clock, + storage, }; Self { inner: Arc::new(AsyncMutex::new(inner)), @@ -239,17 +259,32 @@ impl Result> { + use futures::StreamExt; + let inner = self.inner.lock().await; + Ok(inner + .storage + .scan() + .await? + .collect::>() + .await) + } } #[derive(Debug)] -pub struct DatabaseInner { +pub struct DatabaseInner { rows: RefCell>>, txs: RefCell>, tx_ids: AtomicU64, clock: Clock, + storage: Storage, } -impl DatabaseInner { +impl + DatabaseInner +{ async fn insert(&self, tx_id: TxID, row: Row) -> Result<()> { let mut txs = self.txs.borrow_mut(); let tx = txs @@ -325,6 +360,7 @@ impl DatabaseInner { tx_id } + #[allow(clippy::await_holding_refcell_ref)] async fn commit_tx(&mut self, tx_id: TxID) -> Result<()> { let end_ts = self.get_timestamp(); let mut txs = self.txs.borrow_mut(); @@ -338,17 +374,20 @@ impl DatabaseInner { let mut rows = self.rows.borrow_mut(); tx.state = TransactionState::Preparing; tracing::trace!("PREPARE {tx}"); + let mut mutation: Mutation = Mutation::new(tx_id); for id in &tx.write_set { if let Some(row_versions) = rows.get_mut(id) { for row_version in row_versions.iter_mut() { if let TxTimestampOrID::TxID(id) = row_version.begin { if id == tx_id { row_version.begin = TxTimestampOrID::Timestamp(tx.begin_ts); + mutation.row_versions.push(row_version.clone()); // FIXME: optimize cloning out } } if let Some(TxTimestampOrID::TxID(id)) = row_version.end { if id == tx_id { row_version.end = Some(TxTimestampOrID::Timestamp(end_ts)); + mutation.row_versions.push(row_version.clone()); // FIXME: optimize cloning out } } } @@ -363,6 +402,11 @@ impl DatabaseInner { // might have speculatively read a version that we want to remove. // But that's a problem for another day. txs.remove(&tx_id); + drop(rows); + drop(txs); + if !mutation.row_versions.is_empty() { + self.storage.store(mutation).await?; + } Ok(()) } @@ -460,11 +504,20 @@ mod tests { use crate::clock::LocalClock; use tracing_test::traced_test; + fn test_db() -> Database< + LocalClock, + crate::persistent_storage::Noop, + tokio::sync::Mutex>, + > { + let clock = LocalClock::new(); + let storage = crate::persistent_storage::Noop {}; + Database::<_, _, tokio::sync::Mutex<_>>::new(clock, storage) + } + #[traced_test] #[tokio::test] async fn test_insert_read() { - let clock = LocalClock::default(); - let db = Database::>::new(clock); + let db = test_db(); let tx1 = db.begin_tx().await; let tx1_row = Row { @@ -484,8 +537,7 @@ mod tests { #[traced_test] #[tokio::test] async fn test_read_nonexistent() { - let clock = LocalClock::default(); - let db = Database::>::new(clock); + let db = test_db(); let tx = db.begin_tx().await; let row = db.read(tx, 1).await; assert!(row.unwrap().is_none()); @@ -494,8 +546,7 @@ mod tests { #[traced_test] #[tokio::test] async fn test_delete() { - let clock = LocalClock::default(); - let db = Database::>::new(clock); + let db = test_db(); let tx1 = db.begin_tx().await; let tx1_row = Row { @@ -518,8 +569,7 @@ mod tests { #[traced_test] #[tokio::test] async fn test_delete_nonexistent() { - let clock = LocalClock::default(); - let db = Database::>::new(clock); + let db = test_db(); let tx = db.begin_tx().await; assert!(!db.delete(tx, 1).await.unwrap()); } @@ -527,8 +577,7 @@ mod tests { #[traced_test] #[tokio::test] async fn test_commit() { - let clock = LocalClock::default(); - let db = Database::>::new(clock); + let db = test_db(); let tx1 = db.begin_tx().await; let tx1_row = Row { id: 1, @@ -555,8 +604,7 @@ mod tests { #[traced_test] #[tokio::test] async fn test_rollback() { - let clock = LocalClock::default(); - let db = Database::>::new(clock); + let db = test_db(); let tx1 = db.begin_tx().await; let row1 = Row { id: 1, @@ -581,8 +629,7 @@ mod tests { #[traced_test] #[tokio::test] async fn test_dirty_write() { - let clock = LocalClock::default(); - let db = Database::>::new(clock); + let db = test_db(); // T1 inserts a row with ID 1, but does not commit. let tx1 = db.begin_tx().await; @@ -609,8 +656,7 @@ mod tests { #[traced_test] #[tokio::test] async fn test_dirty_read() { - let clock = LocalClock::default(); - let db = Database::>::new(clock); + let db = test_db(); // T1 inserts a row with ID 1, but does not commit. let tx1 = db.begin_tx().await; @@ -630,8 +676,7 @@ mod tests { #[traced_test] #[tokio::test] async fn test_dirty_read_deleted() { - let clock = LocalClock::default(); - let db = Database::>::new(clock); + let db = test_db(); // T1 inserts a row with ID 1 and commits. let tx1 = db.begin_tx().await; @@ -655,8 +700,7 @@ mod tests { #[traced_test] #[tokio::test] async fn test_fuzzy_read() { - let clock = LocalClock::default(); - let db = Database::>::new(clock); + let db = test_db(); // T1 inserts a row with ID 1 and commits. let tx1 = db.begin_tx().await; @@ -691,8 +735,7 @@ mod tests { #[traced_test] #[tokio::test] async fn test_lost_update() { - let clock = LocalClock::default(); - let db = Database::>::new(clock); + let db = test_db(); // T1 inserts a row with ID 1 and commits. let tx1 = db.begin_tx().await; @@ -737,8 +780,7 @@ mod tests { #[traced_test] #[tokio::test] async fn test_committed_visibility() { - let clock = LocalClock::default(); - let db = Database::>::new(clock); + let db = test_db(); // let's add $10 to my account since I like money let tx1 = db.begin_tx().await; @@ -769,8 +811,7 @@ mod tests { #[traced_test] #[tokio::test] async fn test_future_row() { - let clock = LocalClock::default(); - let db = Database::>::new(clock); + let db = test_db(); let tx1 = db.begin_tx().await; @@ -790,4 +831,73 @@ mod tests { let row = db.read(tx1, 1).await.unwrap(); assert_eq!(row, None); } + + #[traced_test] + #[tokio::test] + async fn test_storage1() { + let clock = LocalClock::new(); + let mut path = std::env::temp_dir(); + path.push(format!( + "mvcc-rs-storage-test-{}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos(), + )); + let storage = crate::persistent_storage::JsonOnDisk { path }; + let db: Database<_, _, tokio::sync::Mutex<_>> = Database::new(clock, storage); + + let tx1 = db.begin_tx().await; + let tx2 = db.begin_tx().await; + let tx3 = db.begin_tx().await; + + db.insert( + tx3, + Row { + id: 1, + data: "testme".to_string(), + }, + ) + .await + .unwrap(); + + db.commit_tx(tx1).await.unwrap(); + db.rollback_tx(tx2).await; + db.commit_tx(tx3).await.unwrap(); + + let tx4 = db.begin_tx().await; + db.insert( + tx4, + Row { + id: 2, + data: "testme2".to_string(), + }, + ) + .await + .unwrap(); + db.insert( + tx4, + Row { + id: 3, + data: "testme3".to_string(), + }, + ) + .await + .unwrap(); + + let mutation = db + .scan_storage() + .await + .unwrap(); + println!("{:?}", mutation); + + db.commit_tx(tx4).await.unwrap(); + + let mutation = db + .scan_storage() + .await + .unwrap(); + println!("{:?}", mutation); + + } } diff --git a/core/mvcc/database/src/lib.rs b/core/mvcc/database/src/lib.rs index a6b1082ba..d88011290 100644 --- a/core/mvcc/database/src/lib.rs +++ b/core/mvcc/database/src/lib.rs @@ -34,4 +34,5 @@ pub mod clock; pub mod database; pub mod errors; +pub mod persistent_storage; pub mod sync; diff --git a/core/mvcc/database/src/persistent_storage.rs b/core/mvcc/database/src/persistent_storage.rs new file mode 100644 index 000000000..6ec92c810 --- /dev/null +++ b/core/mvcc/database/src/persistent_storage.rs @@ -0,0 +1,97 @@ +use crate::database::{Result, Mutation}; +use crate::errors::DatabaseError; + +/// Persistent storage API for storing and retrieving transactions. +/// TODO: final design in heavy progress! +#[async_trait::async_trait] +pub trait Storage { + type Stream: futures::stream::Stream; + + async fn store(&mut self, m: Mutation) -> Result<()>; + async fn scan(&self) -> Result; +} + +pub struct Noop {} + +#[async_trait::async_trait] +impl Storage for Noop { + type Stream = futures::stream::Empty; + + async fn store(&mut self, _m: Mutation) -> Result<()> { + Ok(()) + } + + async fn scan(&self) -> Result { + Ok(futures::stream::empty()) + } +} + +pub struct JsonOnDisk { + pub path: std::path::PathBuf, +} + +impl JsonOnDisk { + pub fn new(path: impl Into) -> Self { + let path = path.into(); + Self { path } + } +} + +#[cfg(feature = "tokio")] +#[pin_project::pin_project] +pub struct JsonOnDiskStream { + #[pin] + inner: tokio_stream::wrappers::LinesStream>, +} + +impl futures::stream::Stream for JsonOnDiskStream { + type Item = Mutation; + + fn poll_next( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let this = self.project(); + this.inner + .poll_next(cx) + .map(|x| x.and_then(|x| x.ok().and_then(|x| serde_json::from_str(x.as_str()).ok()))) + } +} + +#[cfg(feature = "tokio")] +#[async_trait::async_trait] +impl Storage for JsonOnDisk { + type Stream = JsonOnDiskStream; + + async fn store(&mut self, m: Mutation) -> Result<()> { + use tokio::io::AsyncWriteExt; + let t = serde_json::to_vec(&m).map_err(|e| DatabaseError::Io(e.to_string()))?; + let mut file = tokio::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&self.path) + .await + .map_err(|e| DatabaseError::Io(e.to_string()))?; + file.write_all(&t) + .await + .map_err(|e| DatabaseError::Io(e.to_string()))?; + file.write_all(b"\n") + .await + .map_err(|e| DatabaseError::Io(e.to_string()))?; + Ok(()) + } + + async fn scan(&self) -> Result { + use tokio::io::AsyncBufReadExt; + let file = tokio::fs::OpenOptions::new() + .read(true) + .open(&self.path) + .await + .unwrap(); + Ok(JsonOnDiskStream { + inner: tokio_stream::wrappers::LinesStream::new( + tokio::io::BufReader::new(file).lines(), + ), + }) + } +} diff --git a/core/mvcc/database/tests/concurrency_test.rs b/core/mvcc/database/tests/concurrency_test.rs index b7b22b0e4..0626baab3 100644 --- a/core/mvcc/database/tests/concurrency_test.rs +++ b/core/mvcc/database/tests/concurrency_test.rs @@ -10,7 +10,8 @@ fn test_non_overlapping_concurrent_inserts() { // Two threads insert to the database concurrently using non-overlapping // row IDs. let clock = LocalClock::default(); - let db = Arc::new(Database::>::new(clock)); + let storage = mvcc_rs::persistent_storage::Noop {}; + let db = Arc::new(Database::<_, _, tokio::sync::Mutex<_>>::new(clock, storage)); let ids = Arc::new(AtomicU64::new(0)); shuttle::check_random( move || { From b6b36a0d94da3126f4550379c03a40f5f60e48c6 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Thu, 20 Apr 2023 14:46:14 +0200 Subject: [PATCH 052/128] fixup: implement Stream for JsonOnDiskStream under a feature --- core/mvcc/database/src/persistent_storage.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/core/mvcc/database/src/persistent_storage.rs b/core/mvcc/database/src/persistent_storage.rs index 6ec92c810..60c3d0a80 100644 --- a/core/mvcc/database/src/persistent_storage.rs +++ b/core/mvcc/database/src/persistent_storage.rs @@ -44,6 +44,7 @@ pub struct JsonOnDiskStream { inner: tokio_stream::wrappers::LinesStream>, } +#[cfg(feature = "tokio")] impl futures::stream::Stream for JsonOnDiskStream { type Item = Mutation; From 2a018ea9a3fc17068021fbc919f7898f8eaa360b Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Thu, 20 Apr 2023 15:46:58 +0200 Subject: [PATCH 053/128] fixup: move DatabaseError under a feature --- core/mvcc/database/src/persistent_storage.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/mvcc/database/src/persistent_storage.rs b/core/mvcc/database/src/persistent_storage.rs index 60c3d0a80..8687c43a0 100644 --- a/core/mvcc/database/src/persistent_storage.rs +++ b/core/mvcc/database/src/persistent_storage.rs @@ -1,5 +1,4 @@ use crate::database::{Result, Mutation}; -use crate::errors::DatabaseError; /// Persistent storage API for storing and retrieving transactions. /// TODO: final design in heavy progress! @@ -65,6 +64,7 @@ impl Storage for JsonOnDisk { type Stream = JsonOnDiskStream; async fn store(&mut self, m: Mutation) -> Result<()> { + use crate::errors::DatabaseError; use tokio::io::AsyncWriteExt; let t = serde_json::to_vec(&m).map_err(|e| DatabaseError::Io(e.to_string()))?; let mut file = tokio::fs::OpenOptions::new() From fb6ce709935ba0d12749291253510cfb4fc8d1cf Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Thu, 20 Apr 2023 15:34:17 +0200 Subject: [PATCH 054/128] database: add dropping unused row versions When a row version is not visible by any transactions, active or future ones, it should be dropped. --- core/mvcc/database/src/database.rs | 89 ++++++++++++++++++++++++------ 1 file changed, 73 insertions(+), 16 deletions(-) diff --git a/core/mvcc/database/src/database.rs b/core/mvcc/database/src/database.rs index 98cbefcea..dccfdcd17 100644 --- a/core/mvcc/database/src/database.rs +++ b/core/mvcc/database/src/database.rs @@ -2,7 +2,7 @@ use crate::clock::LogicalClock; use crate::errors::DatabaseError; use serde::{Deserialize, Serialize}; use std::cell::RefCell; -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; @@ -135,6 +135,7 @@ impl< let inner = DatabaseInner { rows: RefCell::new(HashMap::new()), txs: RefCell::new(HashMap::new()), + tx_timestamps: RefCell::new(BTreeMap::new()), tx_ids: AtomicU64::new(0), clock, storage, @@ -260,16 +261,20 @@ impl< inner.rollback_tx(tx_id).await; } + /// Drops all unused row versions from the database. + /// + /// A version is considered unused if it is not visible to any active transaction + /// and it is not the most recent version of the row. + pub async fn drop_unused_row_versions(&self) { + let inner = self.inner.lock().await; + inner.drop_unused_row_versions(); + } + #[cfg(test)] pub(crate) async fn scan_storage(&self) -> Result> { use futures::StreamExt; let inner = self.inner.lock().await; - Ok(inner - .storage - .scan() - .await? - .collect::>() - .await) + Ok(inner.storage.scan().await?.collect::>().await) } } @@ -277,6 +282,7 @@ impl< pub struct DatabaseInner { rows: RefCell>>, txs: RefCell>, + tx_timestamps: RefCell>, tx_ids: AtomicU64, clock: Clock, storage: Storage, @@ -356,7 +362,9 @@ impl let tx = Transaction::new(tx_id, begin_ts); tracing::trace!("BEGIN {tx}"); let mut txs = self.txs.borrow_mut(); + let mut tx_timestamps = self.tx_timestamps.borrow_mut(); txs.insert(tx_id, tx); + *tx_timestamps.entry(begin_ts).or_insert(0) += 1; tx_id } @@ -401,6 +409,13 @@ impl // invariant doesn't necessarily hold anymore because another thread // might have speculatively read a version that we want to remove. // But that's a problem for another day. + let mut tx_timestamps = self.tx_timestamps.borrow_mut(); + if let Some(timestamp_entry) = tx_timestamps.get_mut(&tx.begin_ts) { + *timestamp_entry -= 1; + if timestamp_entry == &0 { + tx_timestamps.remove(&tx.begin_ts); + } + } txs.remove(&tx_id); drop(rows); drop(txs); @@ -436,6 +451,54 @@ impl fn get_timestamp(&mut self) -> u64 { self.clock.get_timestamp() } + + /// Drops all rows that are not visible to any transaction. + /// The logic is as follows. If a row version has an end marker + /// which denotes a transaction that is not active, then we can + /// drop the row version -- it is not visible to any transaction. + /// If a row version has an end marker that denotes a timestamp T_END, + /// then we can drop the row version only if all active transactions + /// have a begin timestamp that is greater than timestamp T_END. + /// FIXME: this function is a full scan over all rows and row versions. + /// We can do better by keeping an index of row versions ordered + /// by their end timestamps. + fn drop_unused_row_versions(&self) { + let txs = self.txs.borrow(); + let tx_timestamps = self.tx_timestamps.borrow(); + let mut rows = self.rows.borrow_mut(); + let mut to_remove = Vec::new(); + for (id, row_versions) in rows.iter_mut() { + row_versions.retain(|rv| { + let should_stay = match rv.end { + Some(TxTimestampOrID::Timestamp(version_end_ts)) => { + match tx_timestamps.first_key_value() { + // a transaction started before this row version ended, + // ergo row version is needed + Some((begin_ts, _)) => version_end_ts >= *begin_ts, + // no transaction => row version is not needed + None => false, + } + } + // Let's skip potentially complex logic if the transaction is still + // active/tracked. We will drop the row version when the transaction + // gets garbage-collected itself, it will always happen eventually. + Some(TxTimestampOrID::TxID(tx_id)) => !txs.contains_key(&tx_id), + // this row version is current, ergo visible + None => true, + }; + if !should_stay { + tracing::debug!("Dropping row version {} {:?}-{:?}", id, rv.begin, rv.end); + } + should_stay + }); + if row_versions.is_empty() { + to_remove.push(*id); + } + } + for id in to_remove { + rows.remove(&id); + } + } } /// A write-write conflict happens when transaction T_m attempts to update a @@ -599,6 +662,7 @@ mod tests { let row = db.read(tx2, 1).await.unwrap().unwrap(); db.commit_tx(tx2).await.unwrap(); assert_eq!(tx1_updated_row, row); + db.drop_unused_row_versions().await; } #[traced_test] @@ -885,19 +949,12 @@ mod tests { .await .unwrap(); - let mutation = db - .scan_storage() - .await - .unwrap(); + let mutation = db.scan_storage().await.unwrap(); println!("{:?}", mutation); db.commit_tx(tx4).await.unwrap(); - let mutation = db - .scan_storage() - .await - .unwrap(); + let mutation = db.scan_storage().await.unwrap(); println!("{:?}", mutation); - } } From aac835fce9ba775fb5fae3a511d6084d4cdb7b8e Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Sat, 6 May 2023 21:15:08 +0300 Subject: [PATCH 055/128] Document persistent storage design (#24) --- core/mvcc/DESIGN.md | 19 + core/mvcc/figures/mutations.excalidraw | 656 +++++++++++++++++++++++++ core/mvcc/figures/mutations.png | Bin 0 -> 311628 bytes 3 files changed, 675 insertions(+) create mode 100644 core/mvcc/DESIGN.md create mode 100644 core/mvcc/figures/mutations.excalidraw create mode 100644 core/mvcc/figures/mutations.png diff --git a/core/mvcc/DESIGN.md b/core/mvcc/DESIGN.md new file mode 100644 index 000000000..cae0d19d9 --- /dev/null +++ b/core/mvcc/DESIGN.md @@ -0,0 +1,19 @@ +# Design + +## Persistent storage + +Persistent storage must implement the `Storage` trait that the MVCC module uses to essentially store a write-ahead log (WAL) of mutations. + +Figure 1 shows an example of write-ahead log across three transactions. +The first transaction T0 executes a `INSERT (id) VALUES (1)` statement, which results in a mutation with `id` set to `1`, begin timestamp to 0 (which is the transaction ID) and end timestamp as infinity (meaning the row version is still visible). +The second transaction T1 executes another `INSERT` statement, which adds another mutation to the WAL with `id` set to `2`, begin timesstamp to 1 and end timestamp as infinity, similar to what T0 did. +Finally, a third transaction T2 executes two statements: `DELETE WHERE id = 1` and `INSERT (id) VALUES (3)`. The first one results in a mutation with `id` set to `1` and begin timestamp set to 0 (which is the transaction that created the entry). However, the end timestamp is now set to 2 (the current transaction), which means the entry is now deleted. +The second statement results in an entry in the WAL similar to the `INSERT` statements in T0 and T1. + +![Mutations](figures/mutations.png) +

+Figure 1. Write-ahead log of mutations across three transactions. +

+ +When MVCC bootstraps or recovers, it simply reads the write-ahead log into the in-memory index, and it's good to go. +If the WAL grows big, we can compact it by dropping all entries that are no longer visible after the the latest transaction. diff --git a/core/mvcc/figures/mutations.excalidraw b/core/mvcc/figures/mutations.excalidraw new file mode 100644 index 000000000..cee1947f9 --- /dev/null +++ b/core/mvcc/figures/mutations.excalidraw @@ -0,0 +1,656 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "tFvpBUMWe3qPFUTQVV14X", + "type": "text", + "x": 233.14035848761839, + "y": 205.73272444200816, + "width": 278.57781982421875, + "height": 25, + "angle": 0, + "strokeColor": "#087f5b", + "backgroundColor": "#82c91e", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "roundness": null, + "seed": 94988319, + "version": 510, + "versionNonce": 1210831775, + "isDeleted": false, + "boundElements": null, + "updated": 1683370319070, + "link": null, + "locked": false, + "text": "", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18, + "containerId": null, + "originalText": "", + "lineHeight": 1.25 + }, + { + "type": "text", + "version": 515, + "versionNonce": 1881893969, + "isDeleted": false, + "id": "7i88n1PIb89NxUbVQmTTi", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 938.4614491858606, + "y": 311.23272444200813, + "strokeColor": "#0b7285", + "backgroundColor": "#82c91e", + "width": 279.0400085449219, + "height": 25, + "seed": 1123646321, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1683370316909, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "text", + "version": 556, + "versionNonce": 153125934, + "isDeleted": false, + "id": "Yh8XLtKqXUUYmcmG4SEXn", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 581.1603475012903, + "y": 256.23272444200813, + "strokeColor": "#e67700", + "backgroundColor": "#82c91e", + "width": 270.71783447265625, + "height": 25, + "seed": 1685524017, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1683371076075, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "id": "8l0CCJzCAtOLt_2GRcNpa", + "type": "text", + "x": 256.1403584876185, + "y": 409.73272444200813, + "width": 234.41998291015625, + "height": 75, + "angle": 0, + "strokeColor": "#087f5b", + "backgroundColor": "#82c91e", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "roundness": null, + "seed": 583129809, + "version": 570, + "versionNonce": 561756721, + "isDeleted": false, + "boundElements": null, + "updated": 1683370316909, + "link": null, + "locked": false, + "text": "BEGIN\nINSERT (id) VALUEs (1)\nCOMMIT", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 68, + "containerId": null, + "originalText": "BEGIN\nINSERT (id) VALUEs (1)\nCOMMIT", + "lineHeight": 1.25 + }, + { + "type": "text", + "version": 628, + "versionNonce": 282656095, + "isDeleted": false, + "id": "3m7VluAP5tair6-60b_sp", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 962.0903554358606, + "y": 416.23272444200813, + "strokeColor": "#0b7285", + "backgroundColor": "#82c91e", + "width": 243.91998291015625, + "height": 100, + "seed": 479705617, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1683370316909, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "BEGIN\nDELETE WHERE id =1\nINSERT (id) VALUES (3)\nCOMMIT", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "BEGIN\nDELETE WHERE id =1\nINSERT (id) VALUES (3)\nCOMMIT", + "lineHeight": 1.25, + "baseline": 93 + }, + { + "type": "text", + "version": 574, + "versionNonce": 1128746001, + "isDeleted": false, + "id": "Z-Mh1kti2oC6sIMnuGluo", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 613.0903554358607, + "y": 417.23272444200813, + "strokeColor": "#e67700", + "backgroundColor": "#82c91e", + "width": 243.239990234375, + "height": 75, + "seed": 580440625, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1683370316909, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "BEGIN\nINSERT (id) VALUEs (2)\nCOMMIT", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "BEGIN\nINSERT (id) VALUEs (2)\nCOMMIT", + "lineHeight": 1.25, + "baseline": 68 + }, + { + "type": "line", + "version": 1502, + "versionNonce": 1835608607, + "isDeleted": false, + "id": "VuJNZCgz1Y0WEWwug7pGk", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 226.3083636621349, + "y": 173.11701218356845, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 20.336010349032712, + "height": 203.23377930246647, + "seed": 1879839231, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1683370316909, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -20.264781987976257, + -0.0011773927935071482 + ], + [ + -20.336010349032712, + 203.23260190967298 + ], + [ + -0.07239358683375485, + 203.135377672515 + ] + ] + }, + { + "type": "line", + "version": 1755, + "versionNonce": 1487752017, + "isDeleted": false, + "id": "GpZg3Rw4Hszxzxf38Q4Hn", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 3.141592653589793, + "x": 539.3083636621348, + "y": 178.11701218356845, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 20.336010349032712, + "height": 203.23377930246647, + "seed": 470135121, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1683370316909, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -20.264781987976257, + -0.0011773927935071482 + ], + [ + -20.336010349032712, + 203.23260190967298 + ], + [ + -0.07239358683375485, + 203.135377672515 + ] + ] + }, + { + "type": "text", + "version": 528, + "versionNonce": 1276939839, + "isDeleted": false, + "id": "AGEyNvBxBm2cwm1WRW8n8", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 576.6403584876185, + "y": 210.23272444200816, + "strokeColor": "#087f5b", + "backgroundColor": "#82c91e", + "width": 278.57781982421875, + "height": 25, + "seed": 877528401, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1683370316909, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "line", + "version": 1557, + "versionNonce": 773679889, + "isDeleted": false, + "id": "Q8E0gAcLvq6VXqMDZhLdA", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 581.8083636621351, + "y": 177.61701218356845, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 20.336010349032712, + "height": 203.23377930246647, + "seed": 153279217, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1683370316909, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -20.264781987976257, + -0.0011773927935071482 + ], + [ + -20.336010349032712, + 203.23260190967298 + ], + [ + -0.07239358683375485, + 203.135377672515 + ] + ] + }, + { + "type": "line", + "version": 1810, + "versionNonce": 1561283199, + "isDeleted": false, + "id": "uhh3ZkPO6bwwf0-AI8syI", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 3.141592653589793, + "x": 894.8083636621349, + "y": 182.61701218356845, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 20.336010349032712, + "height": 203.23377930246647, + "seed": 315380945, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1683370316909, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -20.264781987976257, + -0.0011773927935071482 + ], + [ + -20.336010349032712, + 203.23260190967298 + ], + [ + -0.07239358683375485, + 203.135377672515 + ] + ] + }, + { + "type": "text", + "version": 575, + "versionNonce": 910156017, + "isDeleted": false, + "id": "jI5YKyaOdGYYKiBWZmCMs", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 929.6403584876182, + "y": 215.23272444200813, + "strokeColor": "#087f5b", + "backgroundColor": "#82c91e", + "width": 278.57781982421875, + "height": 25, + "seed": 121503167, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1683370316909, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "line", + "version": 1604, + "versionNonce": 19920575, + "isDeleted": false, + "id": "QqIk7VTnRWYq499wkttvv", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 934.8083636621348, + "y": 182.61701218356842, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 20.336010349032712, + "height": 203.23377930246647, + "seed": 2012037663, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1683370316909, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -20.264781987976257, + -0.0011773927935071482 + ], + [ + -20.336010349032712, + 203.23260190967298 + ], + [ + -0.07239358683375485, + 203.135377672515 + ] + ] + }, + { + "type": "line", + "version": 1857, + "versionNonce": 1660885169, + "isDeleted": false, + "id": "gk89VsYpnf9Jby9KEUBd3", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 3.141592653589793, + "x": 1247.808363662135, + "y": 187.61701218356842, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 20.336010349032712, + "height": 203.23377930246647, + "seed": 509453887, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1683370316909, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -20.264781987976257, + -0.0011773927935071482 + ], + [ + -20.336010349032712, + 203.23260190967298 + ], + [ + -0.07239358683375485, + 203.135377672515 + ] + ] + }, + { + "type": "text", + "version": 620, + "versionNonce": 1588681010, + "isDeleted": false, + "id": "a1c-iZI0SafCiy0u4xieZ", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 934.3714375891809, + "y": 261.23272444200813, + "strokeColor": "#e67700", + "backgroundColor": "#82c91e", + "width": 270.71783447265625, + "height": 25, + "seed": 1742829553, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1683371080181, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "text", + "version": 564, + "versionNonce": 1968863633, + "isDeleted": false, + "id": "hdhhgp5nA06o5EcSgHQE8", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 937.6203542151575, + "y": 354.23272444200813, + "strokeColor": "#0b7285", + "backgroundColor": "#82c91e", + "width": 287.73785400390625, + "height": 25, + "seed": 309558367, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1683370363648, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "", + "lineHeight": 1.25, + "baseline": 18 + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} \ No newline at end of file diff --git a/core/mvcc/figures/mutations.png b/core/mvcc/figures/mutations.png new file mode 100644 index 0000000000000000000000000000000000000000..3b8fe59bcffbce4e1cee3db09024bf6d4763dc8f GIT binary patch literal 311628 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 z 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 z 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%0DPj<eee8C$B~+=z!TtVH z%VsFWRVw05NhRhvAnj7OrpzK>ud8`o~ZBjQqZAPV*W6hkmDP=Zi2mF8v!TJ36Gt-svs&pQtGs z?3eDg&t&PkS7kgKAD?r9x0uUevc&a%J<(*~dxTQkp&D^2F9{UfAVB9# zo(D_78=RU>4^t)OthdGk=g&(<{HMx-;ZP&#dqxg1+y0+95V z`sB9aIC*C+e-dMD=w~IGq(rbB&sW&Kfh0>4$6vu`qc4gEEC!+;u6T(xmg~*Q&>)j9 z4(lqKoJ^#(_=knB4Gg)*8p9$B2!N>tqs`KfvUpGdJRyhZI|Pwp4#*E74Uz8tx>Ls? z=D~386PDq+HT?2w0n*c=#IZ2cN{#Hr zSHGPv%bwXlEex{C@zEUMX3&^RYG_|AuT7J;9d2LsDZOQS5d(kl+gI)IAF^%#9EAyZ zra)PGC`MNLLeOkA@!QS0(}BM95_v$2jt=C?{NU8vnH_}TFe$fKX1=u|p{&YeqGK#- zi8m;t5EQXL-?^SV$*7sYT$nkaj7oa8fvUqT7vDIiH8kH`U{pe@k~7QVG*-Qe!<0}Q zLC%DOPV@cvSDO(dlVQM=S!s{R$nW#C(62JK;c<^XFgrQ8g;x`!PWl-xWn)O$x?!$* zV|xg7oWA&Q%Y$=Kx6IQ*u+rpb6#1@yf3t@cS%ztASpi27lEzG}sxKisNS%#E*x<#i z^nm+tqd!YXThmoL@m^J7dPEIOYTA$p5S7U|$29(D6{}So6e?Y{8}-we`FH)9xeN-W zufjf#p(I%f{ih)>!mP}31vUY36mhv3avbGPf`LUt-`wd_tNa%GlM?Vy@``BpHo%9L ze!g~io-vJd*@~}u(Y`&GX=lSmZDi4>50v?YpZ3WG|NUfP^!OffNY{UdK=98Z!=96&DN^S{|7@j7e{vm0 zB#5E)qNFadi@Y2yUc)Z}Z9cJB^_7g>60sG1OKgbt&@KooP0DY|GEK2@swVy7W|Hd*&DOO44*#;lwHjK=hq!t*I!?X~;I9=}ZbpeP*>&QL`p5?kMZ0 z2Bd|pB_+n2Oe^6HZ2i#$xwQFi=7AE$EpyMsiS6v3N6u4JGt#c>-#>_XI`~$7BF}vr z^Hj)^sOycgcwET~6y)6|s&u~Tw(tdbAoG%`o8}VGdDX0E4IftwlE?U)9Qxwvr6i0| z!E`L8#Lym|KV|3JD-qXVSEds8MPf?bMr=hhP>|FO*zcCBs<`w&Od3Xwin9MPiA`1} zv;JPTmN{B4T9RTO`tePHv4N`@_akLgy6ffC2-8;G$fR&44<&1HNToI}x1A7kQ{o3J zHbL(u(*voIOet8`p`D?IRKaN9&94LVc_DV<$pIl)7{&YAV%|t;` z#j1ZKU;=!?(ds&>lX~Wnx0QzL)>Kx=C@0_?#1Ui};Kdm}0{(Ssn~=#ppmRqCTCGuo z7R)|Q4qtLdy5hT3e@%OKpy%9R;ZKtNOFzN)2{jGa0p#`armJ+2&dbg=gKAzWT1Z)T zb<`It+95WxOai1|9$Q7GRbGAYqRN)YIwmhs)Cy{9NDWzpP(hst~4oSy0O;RO;X=tWXq4 z%RMi7Bs1}$5oAr@9%E~z6!mt{VBkz{&8jO?zvSms>y(xR&?5aT!!g@H7PnUW(=?PK zXl;|ED}2p&lFuTnb+%sl($$55aK?q^Vbv;e#d#6);O(+r39KEuCNTDFMl$K z_kvoLo!qiFIWH|uyGP7@v%7!;7IZQmdzg@#T-Pnep#cm1Z}UVqFA7=$#s}RnmO!(L)V81S{jEP$r*)F5%)PR7jc?arFClI(yl!v} z&Of=KAZ2@r!>z5UAsfG8N~dUnP73(Z05@52&|d#gZ333D59r#{#7|Zma&|>wZVIVo zc3@oTpt_duvB0774zzEOE0Y<~t$g*g%peP}HkYdgyhC>$Feg73sr*@&Yh-aWunZLb z%}*}-=mtSp%22uW65q!qQL{hBzm0k-^~>!Sq(XaQ7xPz)CsCd$$Jj6er7EEAG8?_+ zHZX4WnSZ@btk8;VGne;_g{4qvxs}DH5alN58IKP0uL_*@){-yy%hyg7P_z@*SGHaB zrt@{fVygwF7BBm^WPksj{`zl0k$`KtM@Q5(J-MHigwh;NLjT6jtGF_Q-m_tb1bnS< z=FIl+{kHlSdZ?F9aT?BLds)IB%AfT^Ak}|<(TKQ4J`UR{0>?|^OGHxkh zLSGm5ygO}=p{?p{$+oMMG{#2)9F9~&?|*^xU>~C5y^TCWZoaCQHY08B z0mI^Sm?|zE=3TH>dfcPy#$M+;XwSDj{#z!N;dil3ej@NA8w@QB8oX9by}PK~@C9nK zmHjSt03za-zLkxTpB++v;bFgACC;*QykU;%m0A!bsb&C<8EvYXi*lpG1KWIp=sZE(it9MV z@|Bc0ibPE4N%~VpB^u-|561#4rJJB>n-}-fwC0K%(PyVHaS6Qp$V?cK-!hcW1Mm`B zf8%jP4g<}xKAT*NGm=q!Y`bpfPlEHSpz?1k)^C_tdH_D{i0p0OhAse$X+*Fm9-q?0 zyPEFrS8y?ahV3N8X8x<4t!dr&a2Ds_HR7Y_A*98pleO*XOOx#`v#>6W9>9zEHa%5j z9FW1SB!}Tuy2O)rgVvw$(GI=8wHUb0I}|SWI{rIB?&9T3G+sq#5s?>PNX_e0&aKq{ zfO)CNnbddo#Sx0!e73-VP3Y@4sy4Wjq22YCoOSsKmIvEaRf~__*b|2Z;PH(SVHxfKoh+Ubhk1 ztHoy!xkNfal98NYGa`okejFic8LU%zWnnbw=!#h<`(#uwgkXdZga#&E1_)-nJiY z$6_?B4Iy@#Y*81XNFr9&e`wK%$ueDd`=UDX;MuNGjit=H+n3$jE|2q1){P(D0(Qgf zXJV83BD|{XN@VEGS05*T1U?|mG5sJ&`>d4i<&C2m6Zf_?9OF;!^cs|V0y;J8fse*e z+uiZM1kV@K<<|W8ikyRFw|bfLuf9n^^S26YXpo^;0sz^ukJe!xoADU*k%GwlsNSIOT1&w; z4UtJzSYGSBDmxysMKO_auEtk3mHta4{8=r)nWmN7E}owsM77C(E`Q}DRtPKJwX@s` zbVDq0E_uar3qrWy4Xt&=ee z*m|5U2Q-9%>su#mt-ill*R7)7ucGD@E&uV8z4Z5YO*6%~wPaTz&t3t*m%J+a7xj3l z4&;|s0lqo$1Rv**^-K7Gm)bikRhPEVhgwN)EN(?K@J-*~F`&HYh4~P3dVC%vINl7d zV=b{Xh4C^#vg>kHAce&%47D0`F>%gYG~Z;a^uK z1ots9IkKb{1M}+jwI~x?6WFUrrSs>9sfp4+0y7J-yKrtUfC4BBq|IS1{o}gG-d9Zc z@vhVPX(8jAML3@uHIy+efWSg=X!4}Y*jD*gjX$MX9X7gDOupoh?fbsAb)LuZ&=9aS z+tXGAx?|akEL9~ninc&R0pf1uG&A1k2G6Y1&NpDG^n4$xA{o2;8mr9cmVfcV4f{_+ z_MVPa3PGC`@Ut&Jpnj9@ZIQ(UsoLm7M-2CgTFw#O&Y{UK{MgU37%2RCkhImI?%DeXSj0v@or-CjEW`%&(;t49VOELTP^5M z1Qv{)0RW8po6`4~Q7W8PW zyf;zBPpv^2faz#vG{0!{eQ$G%cOUI9McOp)MquN^bzMPHtKzRO*Ixmax7^RkHyXXN zKE0_`@aML%lJ-E-nzfiK7U^dfuC>i8P^J<0dyC#t&pUAr%x9N_{2#%$y+0AMzK3d@7&Qr#MHtjNzu zvnDn!0&K1ZI#QHOG&MW&1vxp!Z7Xv{9v11GWx%8ZYjSvKI*A_EXgo`GaOLN;3)R>w zp8^6FuH8T3-bQBT_~%4sNRdpGJ1@R)d#?PPPD__2+Tr zWMMPYv+Hi?)>$I6e+LZzcC^Sfz1S%6^WGQ}T8h|+Fu}FHW)yk^0wZ2Mt}_f!Hfp4< zjYpT)I|;s|SF-be$Sa6=Z@TL5=_#j>WwiR*8IexoqPi?OJv#0NDg{GkM#W078KhGj z49bci8Wd{om@Xf#quBIJoL>BZq0{4aOjX}z>Hy7uBhMTDzgKK11pMUZe-ANH7!D_Z+wKaQReQ0VD_uH2>1k$uedVx*gx(&fmH>}o!?Ag1jXu4i_i$P&o zSG!@=As>Px814i|P4nOEHyyT^qOBbq;w10S?c=`@PkW^j;=!-ZDD?<`dF3Qs0KkVCm&ViJ$vlebiS3FnPg=d%2M^jjGf27W;m!ONNgEYQ56FPIwS z&RmRJ4{8}{##D(ZWlsX``@M|SrJewL_4&EKi^$1)_?4c=VcAet0*dPMD_d#g>Dmtq z&iHoyW_q=Da-x;Ri>N+GPIZV>#qX0-5M9p0IelYmaPdo;&%8AwYCY*2{##$LJ?a8y zyD;H}3PDtqpTPzas-{1Q8hf63huIx`O?BPm6U8-1DKX4J9xIFQF zuKfjWf|51@u^F;~B~&misy=MtXeE{}dNzmrnOr*bQ)E^hcl=gGMxeJBl)VT3vr*?h z7PWzPx}~uE+V{fsAdnPRnshFj*|^JBVgsY--vyB`{qLbGG5-$W-qNgV|RnexjhPU z2i|i@GBQ=9evYV}`SjpIBqBoGa{PHw!kkQH^!Y!VSBk8P=KeUFHIPyGAG5Pd7Ma?~#!7DS{gH>&$nCu#9#7fvW&?R7@eA7tYR88)(WiRRVxcp`8U8ts(q^|=zh zFG3zaAOmypsc&JgS;Zv$cS*vo8Hwf7x7PK!1MD;_SgWk-1;`Wg0`fW&es^xuaVOm) zw_oBNHvPf0O58X(I&}{2YBgZ*i8f?#H$_x`%XRfLp}U)WqHU(4;c=#xz*6F2+Xu@! zk=tXu^7eF`Z||i$dV?_L4_98%A(%?#=jAU&fd`dqtQQFv_pxJ^~LD;yfX zu(jvDJ9XeZK@Bl@xM-F&n4ejPHt~8LD@;ZAQ4idERfI$jUp~pS{J&iotB@IV?JaEz zctlZ_3c7n~-FBXy#Y4eSg{BeP!?lav-``9+{5A7J!=u>0Hp2oqoPJ(eUtgEj{(86` z@3|);d01JMgDY2!mv6$yv|tWdEd=Led%0=_oMYR^)^tlpRPJE`?aud#w-hcd zjTYX_=(YMFw16AVlI*MZOJ63deLdK^lOcC9;L~Mb=G))oXq9j~yA=n5 zP+xkfQPwK#o3i_`99q`e|KU}_?mhL$*EDN%rN%7WYx!B#_j_AikfV@qX5gJtFBHl5 z&dh1i7gbz2W~xYg?&M9O+G?}rWTmC5qmIuMBxNlvozD!GTcG@KZ+OoFsuJ5LpJvi^ zpw13C`$^~wrk<@R?upCF^th#L=zYMUF$Sdm-Sd3 zl99<0`5l)shlmSAcxw@|N#zB%SEi;d@A$?#Ve>Y3FEZ5*XYKvcz-I`LS;flACTcxG zd(m4GOk-xJ%l(%^CK@g6TGd_LGbC`Cj2KhYHuW(grC|^4$=kq^K=k<0vU(&T*{P?y zwQt8b{4h%DL96iA&rgK8JzPml!rLM2=;xngA50EYqq&;LN#%6*4~$|iDZaU&x;2#K z`|JR`Vy(#gf&qMZLcm7){(yos$ea24?svS~>B|Mp}Jl#?T>AD-}D zPPsq8n<_!t20vU)5IGi8zkBupxX|npvlz(E?`tG#_9)Toq$Fde=wTLh*@49#f+XK+ zu4U{sL5gRyYmwXSTsRt(Jr0t1lMs0FiV;;|Eq4b^>Lnu&5*{KE!teeiFF{z6aO-ny z3i$HW;N}~4%D-&I=i|JyuFEc%WXX@lzfvYm>3*Yb0@@v~!+u_ued}e5z&kOT}x)2>+aqK04_9 zGUJkU>D21yep~gv#h&m^0vfmzRSos&>l?9fHwb39%*btPhFi=6&QN1&7T5b|Sku_H zS=oR6GsnkT=sQ1wJa7iWCW+2+S1#Ixukr$S#MfR9NzM7$U>i*(W{ezS4lIzv3EuhiW{Z7z$Inkpo z-d9m-yo{Kp27a{SzU?U<(tsLDBKSlJq%B&nrqQuU&&bTR z+CHTgOxSZ>m_740$?YN4^^R;7El_TYwRpG#zpKbGvDH)QbTMPQSUg~HlXw_Tkb`J3 zOFP@=_L#tl-IBY@v5}=8AEHx5yuC>{9pF|LRHwCL3kRBu)5Oz-jN$DYCsdefg6Q40 z7>eZb+a{Hh<9c2ZEkD$sFT(ei z>TnOE0O2q7gV#7YI`W?j@(RK!1@p#m&u#8bkX9 znIT?V#WSnCIJGq-^c}A*(%VjH&W=O99KHdHTJ<~ZZ(T(lwWfH^JwR4y8v5hrS5%HttWkd;75@2~;8 zdRCQgeWJgznFE7n$SP9d>kXDKp8~Hmm(lIPIa6nLU86-~Raf@ZsN) zw&v4I+^%hjuf3FI&JI);3szf&hlWF$cq;geCI`4g$EY#G(9Tr^wQcoIjQnxM(VPi& z#~o^9Xw(_0egbnwlzH>wlPG!mb5g``WXTMy(@x;ge7>!74;I8P!2op$rdFR7q^}*U zQB6b?&&2k_ou{gHd#PKbn=LicL4`Bot9ANS?>;l1?V0o?mO`emnG>=yqd?_Y3mO^!L}L=@yHPYnIjwb9e}x%(mLoHBP%a z^T&Fk|04qO5HUSNpq>-wy2+q#J$pl=bzmtoSSq4UQ)z3y+%MQ$^g5U7qz|&`g!;*L zs(e@vnuMQ$aahi);A1iAUIO`+2Ao!{^^b>Qn+xC)f{s2PJx-VA7!0_Qj25EVPVaMp z2{>bu)9_|Fl;73G#3a`-g5s?qf+|? zf!c=D4isCJX(-Z)o8ej~UgjG@gBh()~t1TdnOlil3wULlUwC{%KE`h zJcIBfJ^!qiL1V>44~F|4yiKpyf;G`H&3s1BEW?=(Ir$yK^+5dX++1^6minuYU>`=} zGqXksE^0d;u`t@U-2?3?$C1XHY*3?_SXtuHqB5(o6QN`B7^U~7GuF?t;xANtO!?px zrz{cfV?H2Bz#3p_2zJ;E?4D9E%Pn!g%~PLgl~sbY^$$&JeE)QPSLryqRNL&;9dNVz z^!j$LuQFm`F9Kq-*eyYR6ISk{ zlGg7+2Y%a)zb>lUqU$}%@~Ncoek&YLY`Zg0zP4&(hGdtK8sNv*WVc8Io2HtD#%D`= zO>HSpF{w3jmiN4mRM~nhZ+t|0Xa~G}#vsJ#3M$+jjzUUu${F%TFVZNpzTqHfE!RhQ zt6xl6_t%8ws_9|GKt-lH)!6?4kY@V0U!K)T=e+1U(Fg0&B_O^2t;HetLZoy}+pi6S zMVw<{wPN5{`a||_IX|^6$o>n0WaeCP+Yd;HbV-kh{V%S))_4ir>=}lJ?6(|^t#^$k z?;8}ktW9XegP0DQ$9L{IZ=};O)j4EEyx|Heso`Vzg4jaAjcs&vzz8*`4g&Jg-8QYJ zcPVRx0eZnm+RvJW``AI4Rv_z_Lr~etzQ_@@hCM#6i#wQIBK+9hH4? zm1`Mw(H1t%`~B%DtU&o%%g4eWG2|BDm5FBjW|7}dPdpU|jpwCs9?etSs5 zeR5aYoZQS}dH+-J?(Z6nNY_I&?-0>nKf}};9>qN-NIc99e`-6u!QftuKWM!4^UG+% zIc0Gn&^kIF9~X3W1hd`cj_s7jr!)@Z=MeQnb{l68qf!iEJc%(DHi3L{qGEh8)On=o z(Xnmvyqz?@^ReIXytSKuAYva~!}g;Y6p?^u66MQ>to(lBqwt4MQ`BQ8E0_EuNzYxy zQ)l&WJu#XAc#oIe>{t8m zAOnF57WQ;kc7yCMIc3?iM%cnd8&qPGKuE49XzA6_bAaeP?JC&SS!T57!L9BmBiAe7 z+;f|Uz;Df`umWgdZ)}W!L4gH_J4W z?a%HJw*5R$f^?M6y0Zv+unIqI$v-=6XMTF6)3r_2&2!^!H_jeh|CapN%N#a6YMNZ_ z+*1GET|iaIFcG<~IGKC$MgV$%O}(yrJmr8FN#f7p?2O@(I9vaYUoPFSgY%n}aGPTh ze1-hv)_I$p&vv&^*F1UDBy8=cGbo!kSp-np(@9|1i%dQr}f4#fk8zh|ndU z;bFh6vMxG4b}G`Mf%W1@4M?rXE$BES{c`p6`SmfSg%GhhVQU+;{8_eB$#7_CS-d8s zH%zx8?7*o0d4{7&4oK{$wE7`h);HAoK&215uv4cU8R>5T)!;Y&o@$J*@?@rkReu;2 z0O$$KLuY%h&>x&_w0f4NndX`ESK;XfxAO8O({z(skyWqli@vK{Iw_hy8>Tc1P)}J} zPdWLAbH-4CA&WzV5WraO$`?ZD)`Vo_{FfA~Ks28U-WYLz=whqhl;YN3P3LoZXqh`1 zA5e%O)$L;nq9jGW&$6X^$)$r`ZTU=V`@XYc4j0wFh>h z5rX6|^2{Sa7J<4^@YYA^MyI+h(d3GJD4_*6>=5^h0}Rt5C3UyuR8ll7pFP_RG%@-8 z(qe1+EE#Ei48Ah!-g~&UJIRC%H$(aH_`4ywo!62&t^EvB$S2iNpDu#{m?9&BJ(y^Q zIX-@M4I$xx4e^dj;~9o=MhNqNLCh@k?v=lS$478Sz{ExJ%hG1^Jv}PYcBvEFPx_7C z+k2SV_U6$-pi>L5%n^93deaN0W+ZW2OsQ@kuc8RDwuE%P3>zX{4rOiEN~;vj)-7g3 z`~KIY`JdSHKZlgU6Y^sD)1j7-_lQ;bv2Oc?Q~4iG&q?vp2B|`qatm|>&CcM-vN+Kg zuIc=c5qkJy+syF5o`W`Y`!whqlN@lRvlb&U!)HD`&1!AQ}A9Inasj8cw>7r5cgkRw}_pCVy33X$7s(yKz0B6>S& zmTbrwPnX<0{`fxzhiT^=K_^i=yw{Fbw*x@(n%5{^RqQ2JAS@Bwaq9UWS1*A&Eor8r zk|nr?o}UWmt3|S!Fv|Zg_P#Qpsx|xjiUI=CrF2M_NP{#;Np}lKNOy;nbV*A$(jZ-j zM(OT6bR4?#-OT;Zj5yvqbLY$ZW$ry6j=CtXE%ax08TtrV_0j*EqnsSDRv&D#0Q ztcEDL3y5+4saNGy60I#hn{CqNEZy^*WWM+C%r~Ge@vpbe!oSeLPfnkRFLe}pXc$Cz zKGX@$076KDJ}-{E*qcf8y?*Yu#f}zO!B%Z&gIq}vQfSyK@FIn^^s{19huuX%@QJiL z5gT!1S)a*!1k-QHXt&^@EA;wKLr-!@O(xuTJ=jVJ#d6irxtR2H(Gzw~!`lX9JbFzo zi^BPEXj{{sGEviDpak1qE2&;K-di&srJrqaT2!J)*_>e(TICVD!aJz`&Kq9Vxk&GLF?6ttAE1j`n@=~eABtI7&+sZ2yl zyX$FHD786wx(zaIO+71J8H~E;zpqwrUhS~@rL#URF97!R#2k-uKKKuC3yUfm&kY2t zg8LU;Ec>P>M@RaqN}+Bdh(Qm_ata+b6z1Of>bN<#yQS2m|C`N`*dN3zJ<;;eY!1Ba?&}qtKqNH*DHz=Ffl@!XP zT7b@rBwOeYWblfT5UQ0D+`Z(LvPEw9Kb>tBp0=@s@ys55#3?m|zp1^+SQEZ*n}~kt zalXk}mqQEF`R1u+j+SsInh`R9&$Fa$73KIrG$6qAV&~kcVYB{sBA1ba(%07~8zoF4 z=KH{B179yEjnX++clP{xbvvi>akY{I1V>c$ll#rw@RFBzP8;&B6Z?+S`KYEz$<$m@ zS2uVaP7hrF@HF%gX@bkyFrMAMqDNbnt;w|OTUr@6gxZvA0@N^rFg4{+hOfc-5qJja zqeoWEP~XXa6+592-4`J6Hxrni92SM&>RkHiHOqSFt`Cdk#Zp-wY2^22xGThbC!u4Y zb*G{CSQxC{|2|o&e|eH*TrZJxz;3#2ZUtW6w`w6+(!G^!CDH$G)c4ZjJdd*%!lXN1 z<}&hLMmPb($O4esL6T`Cc90Z0QOPhMHv7*#NcYE<3PDg%P$<9VfBN8;5S|Ke0xB$P z5XiEQce)07dOa((x=2&SRQ(Q65yZK5Y6d#M42Ol)X(eJG#uNuTsTw3>ykg{4XEmS2 zp=y0SzcZ_6S)9yhE_2Inbc)Jo*OSX`e6=%}|E$bxge`r{Wt@EMb5C?xJ?2@{)Bk_k(NcDS3{kVXZ+OhKjcQx|?JcM7g$UxR^1YGSDsgsxQn z8K@NQouT*7VqPnhQ00YyFx}LUsc`_+J?D1D_w-ge;NZ z0apiPE)DNVz>wa4i4%fErZM=-4NQ8!K%MH8%eYkFXuaGVK(wLkFsaR8vFhvQz*P zD$#%dRN;O}XF9Bv0^xF`G|!|!RMzBnHv-yMcjQ%+j+ZoBDLmxYEvDDj%<*;!dn%VB1??&R z;M7YTuK3PwSUiEj;5jaM){QMGO>jXcN5#g9I2UT*TxhtYNF{N$k9?7bwk&Tsw-Nof zBtZ>@iHVstgn13X=Nptfuu$5mrLC}(D-s&(n*rmGOh&w*Mi($#GMrR>u}A?AoNe|W@1UkAY&Ju;XmAOO<5W!VrS0e$D%78VLFfy3^u!l)+uHLEC zcr@=dtk4KjTrNIQ#IB8=HyzHfefPde{J$dSr=P6nLcKTl7Wlx|iOTLCR%++PPS24J z;YO)iN-PcKk{dUk<~n8LITmo;U)`|dyt%8AmhdzzY0uedB-wodx07{mjqE1r@5wxV z{@@g9{LawTq*B-U@!nG|b%t5gpuYe2WB46RWaN9%kzs*`*mNPFp5%nB$xEcqXd=>2 z<#iiI@S#wPMk^gKHMq8FypnGA8^YS+_^-j>Ruh;II+Y4?1h9&dA;`)b7SPQgBlHXc z=c*H%G1Q|#(gA9Iy!L0GGyXFS^QAIBslqsY232V% zQ1|XM=A!EaAM>tq8M1~A!c5`aDTzDZY&nbNBK%g@ZSKW>P-{B^SbwPVYDmIwo-tS` zZG%HCQ@^OG{xpJrX6ohPO}xpDLK=Hp2{@+L^E(O8cs&+MCo1(s=FCKnAo$$1aY~=D zMacff>_)FW4-8BtZX+#8>6)*=1bKS|iWky>^{af4?srTV^Rn=7XM7Fxbi z8rd;6%YP?=JFss9?&Wimg7^K~r<%bAVw#WMR*$~{WztoE4 z?JbX?K{FBj-!52x3{`l&Ail3=7QlOtEbMvyWPX2!i2+tSH6r^-++V0=WP+No$;4lErUc+h`*;pOmMd5+4jtW|CsXseK9(Os+w~)*`6pR zrKYCNi%GgpJSl9HK0b|xuvQeExL5Fw^Vqv}ZbfmsZP}z;YP?BgFYQfXd-&6Q&BTbV zoiu?6>Tkow5PSq@nsT==7@xF%J@q|pxNdXw2CCPS$=39M%0IHhbqA!Oj4C-CkB@?8^=|I;m>-Xmwb zEgW8+wWzagM!$~Rgp;>3dn1RF-UT;?{XkV2_4fRRDFfI_6!N}{^>-lhF~I?rO)lDd zk6iesF;yB}om`JH2t(R~u&ZYInFnmpq2M1LZ;sNinoh_#dfV3zZ2D;)p1Oej zP{x!m-BZdh2aB(jnx3BCm0D0xu-U-&Gl)7-05V|g`zrg(!2RDyrjQoEakTV@*8fWW zf~hv0PqY8+gFP#WOPmf@tG9~k)Ed45nWONopb6gBb?*ZECUuJj7W?f9R3KDTln_iA zt~@rAchPKw9FDTsfn3d2@@W_t!us8IY#84UWg0{Ml%05n-r3<&k`3w4RQR*B(XiNe zs+V+Ed*aDv1T{_QJ-_yG=GS~SMu+soGf`wqr_7OGZEDt|F7-Se$iEjX7sK9+ zO+VyH3i2+DfZMq|G6I9(b+J|he6i`82@De6M4yO}<4-r&c@WQz!&(M?&!T<-2FlM( ziprXpY%`XEgQ3dne1N2Ul`2Ht*7}l`%DU-IA}S@m4BEOEgKjP;fwuL(a~#4bi$OxU z#T5N~W%L&Hbg^r1mDzFa<}S_Y7uUjuqLa09&Er~n&Du7ySPePx90Be7^5V;X9HsBE zAvXiEdmpXC`8(hKH47aWgeLH~+K=P3GY(x07eoqB{c3<2omrnNV)Np^u(c0-u$TbO z`K9Z1{u`*a0~gC`Scs7R-NXEa$#CWZr6?q4Fk-?lt@FNGf3`QuBH&`MGuIz^x<4Do z06+92E878di-|vf#xL)|X9Zl0F+M$bU$g(w6g-ir0kiT>c5MAOX7x+#2GRf*SI>pM z{#h2k9OwN3*~$Q35C5nQm-Uyo{`0xWXn6Lub0=opb7=SP{|o#2eNW}bRqo0(>Upyl7i9h!JosCG{_J1jijV6bqyusE$zmx#7}^Y-`@$$!}(keJ9x30 z_Nv3UM&M-q$+w?9m?gJ^XaD-70eM3dQ|lX$daD1ci?f~s=!xq4*!bVb38Dl<1a;gM zF#iqqfVvh@s2nHmg}{FS+;a81aP4FS=jnd~TyY@B6W=ghNBA!;9xw+Cy?k2ULEaf3Sl8Df+L*4ga&~U%90}@rZo? zv*;fT?mrQp%>StSFZ!(i`RM-yS3msC|9tdc<^BH!V}HW&zx>tzqVB)Q`~M5Z{;E;_ ze+|ZxB)%|R2&C3L(o!vbzVrF=%@Jgb^ul2`TAlm}9v0~(l2MVM(O+3V`agc&j^a}l zR$u?s(JD1Gq)ADiKC4wvWVQma+i%ZBcyYcyCB&QMa`^H!=?|CG z{y0+`n7La8R9A2*q~9A!DDFT7ovpn-&H&yI|M7H(P!IM=fYrl~t595WmLh88NI1#e z3xl~IO@;pWhCle1dxU_4vOd$~oDWxS;u+;{uwScKqu=4hVb{T#sXWjAZsd&J@m=M+ zVtElse}3-Ekx5|pfE>m|nVY)nWg~9~Q;>ELY3G~JFhc9}ttt%ZD4xHR7}`DsN}`-S zUv}6Z&c?sNla%Fn+cv@O(Y3gr+<@Gzzh@+ldqOPp#M7g8>vaFC$W8(0$ef@ceFIF5VlkbXis$6mrk7A0n(P1_Bqg86G+=2)h_1Dh!9(1;eAal~!+JBnJ zZg0j<9fY#5m`2NJy!|d(Jlf3`%FEE`ygfOZU;kHDmI0bc6$Nw|_z3c%9W(BFtfks? zY8Tw7tnD8d7>LDtR{cP5Nn7d_Z(sLI+bI28N}#mdl>c>U(AP#!W!K$mB#v9JrAdct z-k&?~(N5x_LgM8mp|V?%Yh=;rWXi4BipbqQj=z#vV#oqD6Vy`tyfY{ZeN~P*jVfy3 zxCdERcbD#d?Wk3|-tbb|xR!B5A0E54#?`_I@~Rh#+VXFZlEL@Iukhzx7Ni+X7VB{` zxLkI>Sgq7C1l-KJjOL!ESwU&n9qf9OKNn<& z@*G2d`dQyjRN~lre8zC|kyZVXZ;F8@fx6!Ji&53o&ODV8XQx*cXQPg;gI6l$_WH-O zTW+^^w!&3>u61>G6xMbd?=06aT{+Ta##Pzb-HqLOD1p6v(RctsEY<1GULP(L3Ep92 zKf!rPo&98*`@(K_QclxIp^mHU2^E!k7iO4Lf8BM8VSnHX?pS+yP$O1SSJ$EWFj}-a z6Ud|HH2bXeE3%=%>{83aun>e3?w1a^z9!0TqzJ1*T}Nf~Opwtm^`p%xx(kEKTgQ=j zpz-v&y!@0#qGtEG_-@hcp<@1DEKa9BzQfUnZImps@Rv3t^uR%sE@;+mj(6`Milxlm zs}Wut1e#~!GdDx%``|KRyUH(Gr470wu8W~2x;I@}``7ZkDEgDUbgHZt^D+9Rak$8! z8|y2`ajfoZtal|kTdOe096Adn)g;}vRy`99I5U+TY@*G$AdgjxeQ!VW0A(fGFI5}N z$sy{jY!a4j7qsi(!$~J6^Y30yc`c1xw-?aC%K?|8>^CheG}hBI9BW_lE{f;uG&8f( ztXmJ6z;i($RMLwlMn=Qw1=>l_yUR3G=zf2(_697W0*jdV4kBj4o^0b|h+vgU9kXTv z#O{|sf8qnrnw4paMz+;=s`NKDW7#?`afB({gI~WNP!}$zD9*w3lze5QNeH>Chqv?7aE1q z&IQ!{rC}btlsIQySN)3$#_MQ9crzSc*Y&Zd!yckB$BSoP)6DkmW3+FV_k9n+F|h<; zxL*s9&%I=Erj=IgsUEMOd)x@WnKjeR?7^c`j4|YYseQXe2nMCNQ&N}x7S$0G#X7Ww13A{0kkIF%^4kQ>a%ZWYiZTLF%Z;oIom-9 z2wyF%uSYhpW@QqkupI=mRoJa2!?I11B)mCD=x z`G<x7NEMl3iHVJ3M~EPR9z*B%dz#6DB3IeaL{5WHF8f?4Fs*e<=c*q0`Ue!b#Dx zPnw{SyNKg-h+X$+`89sk#cPSN!cbc+lUP(Ym^M$+&atAC`&$YAEr)xO+*NMT%3Gs8 z@_n93E-o+u-hZgimGfqXfuMR>-Rh)*H;`gJQCn5>_%qbAM)y-f-3wvu;=KGDa{SA^ z+ba;G$I4LNCPvBNSoaL31UQ3D`&k&n=kd(6%+ya7DQ9!!5Oud4jK}f?WiB9oM*F4# za{L{x(|HZdrU)EIz1~2_HJ14xL*C1bf?i^-(bd4JL&F#jyP+G|nCt&^!`a_m!2lzeAL51!1AZLsAoSa<>HY0b(4)#LbG&C$K@l-}9XpDKvsbj< zB0#$hPTcKx!T^8jIi}nU7Q6JXr_j089s|zQpKiKp^`&!Z{-&YrTo{jPaSJRMaJ7CFZ3Zp7`UPYuL^R)ym+nr^z^-A=5cx@q)mS z6Y3ogJNu;MEK?li-de6kk1p8Fg8X7#{+JpvZ4T3_fb+c3zZB}A5Xn3T77T>F?Bj+| zCTjK6=R&u|$wRwfB&4g2l-7tgwBMuv_KNO6@JMRaFhJ80~$Ya*I`c5v%r-ya@k2zHR- zB-*{mR!FYwe89+wP)><4F(8vbRo9W_mxOkSOb8rkK?(t4I-yvpTbr%zlhg5!Q$|MD z)6;WE@SGHcTyi&+;v`Uz6WZQgeyKAQ>58*W|J*89E69VrI9W9!%5U0u3i z{G{Nfh$Zj61WLKhYp)y)$`wbk}FdV{+3#7m_ zRT!YpLT!6cH&%Pi_%*u)QF|vc2VFO^}<4{2kkW6lmW!a6W`PKlVUDGNys7tTSBq582)7145m`0*VF0J=UrgX)-%KY56OKU=!GQhR=211qtG#(; z)2^3R7}Qh}okMmex`0LbvVM^TnRT8x9`v5GY!3M`shP6tJH2~rUbpR=FaX08qQk` zdYTU>-<$vm)zuaip?@U#PMy3e+2@Qf#EQF+W;^$r$J_}nPi@+)gVS{j%lSiBYFsH* z4IJFI2&nh0=7qQzFR8j6|Gs(aJ}CcKQ3JJi!yca>45BS@>kO?InVAWWXTM{DIlb*U z3(L6+SvT0%@I#Ky`zCf{vC+}(y!xt4jrQ9dWbwj_38ynCypHQ*oYzHKgB9{?X9t73 zp$vvg>vHMwL}O-sgm0P39Y9Yx$Cq|k8wG?jct_x$WD`+L3`u9c$=uOc`cNV8q^X@X95b}^)xaD27pyqT6 zd(@}*UBEr`YX|a~VktKh<;)(;lo|*K{F!36xQga3v&3aMlY8^5(xqX#;-rfufprzq zlI$(AT4=G;6>m{5XAyb$l_<0$#!tJSu+6Hg?HXs=2XY_XKZY)U6MkwN^eVmjGY1ee z!-efG*0qamKOJzO&OhQ>?S|i%Oe(|WM;%c*0rMgA2(&HXfQHxiny7c%qQ}oBp#~p_ z+@Q^Wdns=^*+^%h4jY0OTg9W;=rHfFj0j#IZ!m0jQ!^39nTIv6ezoIx{@uCGa@`z# zDf`re>f_O-`mTLeiS4kpX&qVRKdk-}Xby zTI_!u&o8nonHDHuW3>O0yS-$AyNi61)(*F;Xkt0g_{IPn7x%)5Z=0t?+jiH~Sq7gH z^T&G`2|w@bGD{X&WfsxgNyi<33qnH{2CmQr5uUk~Ou%<3VmP}dkEvHH+f8}UP%XRpM~KXCq7dhoQT5BGTO!bMoV zXa{W3Na9?-B){?+h^?spbiKF_p=Qfw-px(ZJGH^uPqpZXBeO5>=hK+ zY$UMT3V>j>Vze84Y^qvsu4p<}Nmr?_CppPHWCCvtw5)M?vOCL*)c4ZY)pavq5ad$q z<#)An4YWV&=h3iqQ`^rX?f2hJylSMS&7yU`UcR=1Fd?^};B%ENLHs?ro!gh2NtXyi zIhj5l?|Oo2rIfn2*F|-%6YejP1U0d6BGI{yg+48z(&nm~Y&IDw)!a z^~R@bP<4rz-$YlbY2x$csQ+WR6EQa?u)X< zd)TpNCs9?^v(vcmpKVWu@DM9D?9tLI_;t$NLRbGx~RYc}8N7U>n{UR{>L zoJw6!CqmEJ(u9j;mwDPEuF+5gr+g~@X4+ji}t`<_E+}x4S7SoQPdsoG`%wWqq?-ra4|eGtdCuoxKLXXXxvti-zErOz*LuerNzEUY`a6P2`l7 zT*zdsq3#;a#axY76Y(NqLqEL1$Z zoMp%4_)W>Ah%=BigDG;fCzkCUMj{tU^@q=48fgT%mhrH;95S9w#*{bZ<`fU8t!dG_8h~WXj~1ce}MPJa5{!3 zMVg=Kb9Ij<$x{lvc`r&9%>4yX>VY9o%Xo>f`hl_F3;CLL9;U}C|OK} zJ4EvKCSx|#+J_07D0dtn0_}t@r1eEuzueZ4A?RMmNIIQPM|X`Y(RD-nY%|$bq10m|V z*R%8fg+a-IMN6Tf$$oA_$LgO9#dCh}xRflOz-iZ&GDO$&n7XAKWvy<4k_l^WS8zys zL`*lKBXc9r&%tWP)5Q?kK;bjV-oniI(%?9@FsFw|2q8Y;hmFLUPR{H2YER^eL?G6X z0z+u4k=OhGNKjLkf%6g%t1s}D-+5D0c$a_V{KzV_?bjTKMNlvB9VYE@CY-}cOJ zmdBR4Z71tP;+Tyb-}IK*XZGOxh0T#(yyK^@Q(G-6NN9hKpOe)s$6h-iK?Ik{gh?==`2{E9z+}cG zFVz$bk|wXZH1|)KJpB1@*E}hAIdYdT@&tcmZa}ULACUv5Z~9@uz*$FdFUyv7aO_}9oYV;TrvwDZ+lgcdh_9EyIdh z>&7~63ml(as;BbnB!f+fgSLNTG_+W0I*(;hpe1WKA3%rK7EzY>CgIRFX$dP&RF6SH z&LBozJ1?H3+V1}JjfioVCY;zFtN^RhK!n_R48*M~+nwpLr|6kmH_q&sRsu4Dl9 ztu&nM@C1>n;V;q@M=dYGlm8-cvIr?F;7a z&G4Abm7UntaX7H!;#Nwiqp>9 zcZBuvkOA!@nnPv_Co89c171Bq2O#Ti?WHb1gBAQ+y8cFt?s1D?F5ctjb%nWl8`=YcJz#aIYS;gk4dPjS z6R?)yuzC_9cp7wy&rDWj4|FJHJRJ39bopMU5?gb!IYs7dAORwn1xs{-0IW~PimR5{ zyUi6H?Izrf7WakH1BW^(vXeDNP2riV zZ)%i-v4Cp4=??le}&XYNW4!JRgS6yQ2c84}spFjBxek5#IFsfUo zwpl^xy&OS?*7Ja;)n*cAZ0^K&;?-&Lb_piXnxhcGu>G^C>g_;SM*o-j5Q0k(1DDc1 zIdu6%lUmv$-Qi9U9fE-nr^jpuK6(<2q^Yk~d5WNUyn}QmU+9$n68iXKaBc{g=^t(b z_+l?dq`#W0Ic71J2+j5+?6Nx>L&v6C={&Cq*>UKLi*|76D^Q~O z9@BGpAZgI&vU%m;(KsL4$JC##sdAMx9TAU`lgoYj1)g|8R@m4yn7S7aF8sHDerzeB zFsy8I!@PsuTR^cA%dso(&s>mEQqpvou(`s*MKpU55#*a*C$P!uf-QyTEPFQD(1tIc ze8f=oK^tP!vpwaRLY;6=L(x%~+=3cbJ1>fmkHCZ6vLeratU^lgvFk)yI7hT*62I*1 zPH6mKJL+iF=S}yyAh%Xyl81p#KHegKIbmA4(kK_P6TvbsTW({I1Z0v|ZMEm9_?ZKZ z=)wJuClX)NkUo*zj^+~wNk)z6&VEw^Yz7}opT1%2;;3Z8T;oI3z*lawR3g za#Z^*I?1EgMvt~#u+yEt7cai59V-?bNv#o0ynOX$(ACq6Fb)Ob#4RX_j{AgKKUwVl z^MJ>D4#4FK;pO-)dCS_ON)aeO+0MrY)#_58AmjwCDVen#(y$3|YCmn4?;Z=lrc)!j zEY}xBZ42}kd0{b()H7d+9eupnd(pf~OG(vPU^v(Gl}&SW8F){ooBC)Ww=3T5?yPjl zv7^;D!LC@JN>-#;#f1)!YizC{DHA~mC8sZP<*sn~D2WiOq4v5g#f(Y|B1b28aYHeO zpAXk-^^-{RmwDC9^V-$v#;x}in`i+jHgQ+_!(oa%)PdiZUeiI=%aoviSiG6gcn02& zVdj@qncrF{^R_RlkZ>xFdmREKT4p>m7!8|@g1h?=z8|+%dDx6|1%~0U);NsVwsKmu z&llCBJA@^ER1`VUSqgyK##n;zxYf^|fg0_6Es37_tACU_pii4T{0_7SOn9&{8+~3W zs>D13UUE~n6(kesTpZnM49VBJ_AcrpC6IF~F(lo=w=31w)M}pshPTdSj(Yggjredo4r({^ocVPJU#mjNMG;Lr`=ez#eAdV>@N3NN@-tI8r;I za=X!e-scR(xI_RyRjElm8)EdT0=6%5y4=o1l*_*V@4WyvyB8|Qn=qn(vyqclc?r|> znd2#{tQh2_TM!IpUy>q%`-iD$Y05g5ZAaLhsbSHEU7IG`9W6YlSoF~#56_F$rEBh(4$VRc|H~8k*t)XvMw1K zbs@d^^n(rlZjbAa@s!!EqQ-E7*r8z86Cwh6HEpPNaCxD4-8|rUW^G=Sh{>P#rxXg< zi<9gSaMe>`_xTR5V1?pR>@9{iIJz9t=lW36;VZ+-5V+brpzqqr=8!Kh_mG&1eH_ul zz;l6js|rE{_g@nG*@pO|2?UM#BwhyQXnTu~$n2V$_72oGY7GCk>SOd(BpmNLkvFZGi8tX=I1o(el}$E5A%6~=NT$z8 zN=0TW2BG$jxZ~_NdmA@}XIpu`6P7b){9(eT?*6FeB*#)*iRQ6YE??JnJ&xdu`Uri` zzO5Tfcfn9oAZ@((6g}W>G3e^jsg@PK!n&ySvf-5I+b%c)zJVKZHM?a?61q0#0dsCi z@*=ZNycpwFlF!pjh5RQ@0>ByfNJFvH4&l%r@jRx;+cWs4meb)_OtNQ@y$lDDYaW{< zK(m(B9d4hi?%>)Yt@P-F<%*+*LdE=7Xz7eIO$Iz#JinNrC7_7DU^Q|0 zWlhQP=e#`g`9u_p5esKb770Q@z3+{wHs4(i*iEK|@D{B`QmtV+0i^v@#>AI*n$deE^I*|c z2cv@KCh)Nd#=8_&L}D*X3|hhKnDNva#gTkA$43dnWRJ+R7&HPix3tZz;~$ST7H(#u zWE}* z)Vw}{PF2ZPNN@$OkoA>?)2zq7)ptiZk&AtJBzpfNKuW|`m7iYQTqN!nRmTj%=DFR$ zkGpJ9pLnkE6iN{OnS1G5DjxUsVEIgohD3)e?HgagEW=w?254Mt0X$l&R-i+3Du=(1 z4X>Qa!8)dV>&E7|2)sj|;*qF0pOm?hr*V2YG+9k2+x z;bOSu+Szs4xuf2+xFBPv1a2%f^wZ-Iq{X8&?3YhAK8Jv`tYGa^c+vu-vX`GU`6ORI z86P3Q)C7<4*5~^I@3{P;_gPP@`p}?_4BU|5;iGzcPTIA3gdCA+DfrQLc|A5J3t#Sv zI3qm`>$KMc3E{+aq|5V`)R!eKkUdmk6QAK=xDFa=(qN@^C8uLb)qGnW)2)OPGuup zbb_ep(itlnR8(Nqt{0ullT0QNmydNP%y~TSPmgi6u!h(18A4WLick%GuwK)B?(eRV zNJ)T+&TU>&un;wJSFl6qY|i>@5>#<1yb06$E{il616Ek5gV>-A0(M;$M(?#9Rz^q* z%Ln`OFuas~Xh3uL3s36vx`|$cK|$6GS-Xw6Qz9&=y>q@EI`-8ub!7_s;i>DqQw$-k z&nxodos9%6a0oo_13zv3r@j-SxV0Uf#8jMU0fXQb4A}^9PCe|&>sZU9+_s&_oYtom zE1FtrU!jpfAfSLBzV#8gX?|5JdseX9@N}24-bMLV2sZ}&XqVu7KpfG`O9SNZol%Y4 zt=32!#CXav`0NGh@wUT`<}0sXv6|Et(W?l!kL5j`{aW`gAvyb9NM0T0gJ5g{^}ckI zmmZfxz^5#EX8Fl0hu&*P#{<(>_3JFITa4E#0ZzAxw9I;cu@G6W#RfFcE58@*FQ`n6CMH{YwiKLz zD2xY0dwI1~HX_vGcIQO`Z-=yqeR=ZwT@2=RB<00RrJPNnq{cUWg{6zmA5|fzc=b{s zEt(Op%e0d-QH8U6xMJW$CUFx6XKwg?Vv>mH<=IMB~&H_ks5?98L1iOdqyJg_Di3zI@U9@?Yr{;qf=g z{k&Y`0T7YQD7}glJf#-sP_5^x)8h&cv)+xBfNCg!of;RXepDxk39MJa*turk$xi(I zX;d_-g3w-7mdV~Xum0B0G*MFKrJyB>L+qt^Ok9EYMJA-0v;-h~as&8I|H{Dbi&Oqe z6g50X6jo-oFxzoQ%OT1qkTJ}fFW+@R{LHkgOnn&U!noG)Q^c zM<}K}b};7jde&3BSxL=ft(^CA86&C(f$VDH!4NKP&tR2fm}_|!y7?!lj|FR~dM-Oo zt-95v7p`Y72)A!%8*!w2YjFp$)a8{oz-fV6-!&CBY(C?T7RD~RGMHUJood?#kh8up z#Sriv&T>cam#B|mR5?N`C~UD32-JfT3^W!%j7Aq3cdU3Qma7M2QnlWsq<`GgD41|y zPZ0#Vj+Sj_lA7O$DVy+O`2q^RWvPn$hS4Z z@8N5AU#zDn^IcWO&|`c#Wf<+HUE{uPdt5D!oLq?R6@oyFBXN?UJ6C84 z4;x`AQ0x6%<3xpB^ZLRuv2eNv%%;KwZ6n9pL)Fpm;$>H?dXNve2UCh9X7Loz(`URh zG#x#NV~WaaMWY}ELDs-chnlnb$C@tWKY1?Tp8^yMw)wYXS|%0^{(?mU<_EVoBc*2n zmT8ZJTU+c%U(kYgFX#}duxgxTcxRZ6QL>M|dro3pShVj-<0ANV`)YgCzd?5i#!9#l zF)*yKh3h&&3<;?|tw4U^F-@S~K>dt3W{!;;v$f7p*S4Q_hR5J(dZAgJrbKOo>&YzF zUe^>IyPQ&hQ358Z%e!=SxmPcJyaQ=>spy-J85*-N6U7mU*3(za?v^))c3(s z%Hna5*rB81Gjqqo;tFmApC)l6RiESZ!-lf}~nfP@cj^pImyMtO7ugrWjqTwL($;N_} z{>@{3{K$XiB!ACO@KZk6R>=snOW`#x#erWkN4(zPrGPg?xJH1%gVyFFsML~ot4k{_ zZ_;jBRGvN%@S#Im^?4&CMAnRv66z3yXEWu9L}78HCvnlni?Q*~-(=s0#js$+>>oB( zzI_O%d^5S5{Fz4d6iZR%Yv-4to`@eK*uQ-*|L`G0_Lf9wQWG9!irV>k_x#}nyVe&E zIN_4sOHWdb=WSTXZ}5EcVNV5vQ3!l+d28N55q!3dK=Fv1dh^1nB=bpC;{ics3Z=Qd zYQxYcs}z*iGpx>(pVC9x5qgnEykO$ttj;#FI?UsT9y}MgqBx~{)RD`IR`V!yOWpj% znN}C;aZ6T4bsmgnJGT#7h(K=ndG3ucuqnVEOdnfu}iK zN8Y8LK-_>Ds$U}O9F}kQ;YXdh6UAD^0xtU|={BqMT8;77LuW zLX>iGGFv4X9kRs%40+qfC2X8J*rC`2#Gn1qO@hLLu|zP+!1Cd-F#+!HRX?kcS`y18 zTFo|R(jmVO=4 zeA;UCtm;WpCP&oMQ6}jh^4{MuHc$%Z*p}_6Sl$uP%9V^E^L+jSNjxP9yq3FA=s=)Z zjBe$Kb7V1aREfM|8o1^4O-o*XI4lKsy)D3iN^B^UXe!p9)F$8^pBbdPP|6$$4PG2XX@-XhVew(5vM5amsA z-g15&Bx^z2m}b5*F>F}RpI;`1wZ&5#16w5luAwp+!>ar#IoHpAn(sHy*ZbCDip>5Y z2WFU?+WveMEfB$4A!h8>T8H+OwVtMl#GPIi#2Ox{YNv9=`mg%fjmKjGBa}P+4xtY; zhxaE&hZ!5Ri?M3N&q4>a#_6kI4}=r)o+d`8iC4YfQSsGUSmJ6XUIe4%`Rh&Z>yB_7 z_YYhQ`L<$H75|tHaIynFit`M%ZKZ?i!o&Uh(- zq(}+rGOQxVGIcu>MUUAZ7XKKGVo2PRi@L^{BUn$?`UQ?9Gty7+F|!)(`zS{3U}3Hupzmk@jE5-P zjA4UYCHct`uX+s4RvFr-LEeUhGWr)aXHAt&QA$Y5RD|!M#>b5Wo^6bg@WN6Bi-m>? zrbHEm+k8YrI5m@Fjcf^OdI2XrTJun3=?hB`RK8ctE)FA$5jV|`lm6YM08_8JK_0Mf zCvcHWpq-&*J@1q{FC~BX7_X$Xfat~ObC?0;r-qp-&y5H0gpfgy;6v~4rZeoRuZwz@ ztWz(BMVL$dB-xo%T!N8|FtucB6BT;u5D};6&6|HiEy+CfuDBE}4K4MrStk%u$xt5v zoo5e)JU>oT%+7eoz*5k)pQ3!K zh+I1TJRKu}V4YpFQt3X=i?)>Z{XH`D{o!ZmTm{}hJ;1}0mrnL7^tk5PdHOd(&6fnc z$vh8~Y&vbdax16Zlw40IMlZ4uRM*No>hP1tmU?zTjs7{ zZ{#8zl@$N{$4ZT-&6^J-p*Bwvhm$;@CdI&JJ}_3Ik4dITl(2SuZD(YJ-JN5!FWyyL zgv8LmpYjo`QH%RTb~<2PA=G?-7B}Z2GhEJwL{JO|$C@N#x_p)wz3tNP(!a1KG1r!v ze9cHjp^YmNNO%&9rL((%J|S<7w-tsz(ncZajjN$C=I}Io73oDT^z*OCK8`||y#c)S zZkg!cBoZ3;u&((GpWe{s#mWf=~$>Od~2+8F~Rx~TxDL0hj zg4uM9sc>^+5x-qY{Gd8*7wz#JWn*}P8rrk@NxP&zS%E;E#>*1{tK#MqvgOR@I&-t* zI&ZTt=K2S%6OyvawCKR=u{!mB*^~V018*TZ*ESUMJJ4jC4NQ$@_2c>QBP5{`hb~SD zdvEShIezkQ!g%;Mq+qC4Vj{MgSolt#kvR7KN|@U!ZWwec(Pn-VLGuxi!wnPIVso=f zU33j*o?R9F0$Fz69b$*SPDFxFJlD7ZTfI5T+al_-pc#areC5z-4B1;%bM6z9>eMc- zCN-+UW_3r(@q{kG_>kK|KO;VdpaZ=8)xm_GDBs7$KjKA(T6KzRz4Y~f7|RI(v7FPo zH}(q>8f@>{BN>)KYw8FY<2SFsX0ly8Zt`^koF=b8i%j}%Z$HuFc?$hYb<$Hxv`K%V zZSW%rQZx$RrvvsQ^Rywd^qLZTTu-0;6raMiv(?WZH3(6k5n4~L;;@<~_ys!bpqrCl zTpmJ9HeNmkB9&)AT~SD_VX?jHnxi46pZ}HlO4L|ST9>Oj`E2xNFX|izjs(J61lhL5 zSEbMdhDlW>!}0WZl9Z18_k;^*y^sW@TBL<5u0i&y(p2~xK8mz^DHcfW3>uIAScTL% z7y4#A>}f8B)pKEyN?l>dxZ>QrS#f<(nEehi=lAvGS>^{K&KhS7UmtVw<0{3Bj?##1 zC75qFe%VGbus%*(APH^F!Xyr>7hi0Pz(YsO*wy~_@GJ2>%SaTwTNV+VNX)y2+ot3R z6!F1rGlX?8!aZ7P4M=Wr(t8uu7#G8ZGLD4j{WKp@?weI+x=l6H zu2)XyejlN{A3U3lh+nvc=^So>q-FSEm>x+C^D2A~mhLcgxP=<8KQS!e#<5pXM)E8!d_|_Et#E>z3&%=)Dc2#ig;rt= z^r9gB4hLyYge)`*)dr$G$)Iy%A$9ND-dorCAcvb_mWnwEkj<{`EjO)uw3z~BVPR{d zd=|e5mx2;O`J8%GWJj5DHc@HQml|++IPETWhl8PjF%O+tpRyo zdABo%Tbj#sA&zca@UN~mOwLxaXqR0gB!?7in&Ypyxh0UD#^i}rZ;;um`A^l0=n zyBsFEaRTT-!9B*Ds_|gkLys++($)}&h6*+#V)bQI9XN6y52E&Sa9Y6!yyG1`B&I^r|HsMVEtY8p39_W^P z1ie{o=;X-J(Hgyd^*E}xcS=MGPDM~{x_Y)d*^KMJwYjdHJ%7Sas{;pT>D)xahaA`U zT7&tP=FBq10lOHH`@{R0*7S>}xbXIV_Ym2ss80!OI%i-0{xKw8xk0JLNY2cdEvIH! z&_)BP;jcE+Ss^>(+jjmR7UOE0o~{#}tp#z8)MX=mZuXKj-9AK=at7^D7@XQ_-fiVl zn5wGn;CZoicWz&O(O@+TY>ZV>%WZs;fR(n_>x(C3k(fkS4%glMI^i4~hw!tTaARF7 zQ|hu~P8TAZAfMm-!z>*OY{7}zl0wOwQtqDe?lhW)%|F?lD4v5KsPOYoQz(mTiT zgyyMr1A4T+fy)OKpr;-hV>t?!XQOknpSb>shOsq zLJ;z0Vsn1BiI|YU|6K=p%f)(%+ek-BCYkvt>Hc!MzjS|?o_0R(^B%R$P)~@hSsJ)W z$3^z<;7vaK4gr_*tqcCaj|MW-j|oq(s%h{w2noVm@l|(JM*N?rWu-vkA}Z144>xk3 zJ04W;w?4D(Kfc;lDw=s8Wp{VS*l7e~IR$#N;J90(syTW(*n3JCp#CC7+OzTVJKcOfK*NEJ+J$LouQn9SRR_8)+hw3ftrnKD8+QR3p)Vszk+Z<>TP z9Jg`b>}gf&tvnpKp0pLnUT-IHWI1aFA2EL`UILn05p+t;Zi4LP@ZLZ5wsigLyHY=a zx}qCluLnJGgiNLGe|5zg>a1uy*x!>DH@lcwvZ`ry}8pBFeUV;I43gFv6tK#WdusumxrU!&@7+R^)%xt$RpcgE)jRg zPv3*GT;yQDf{*ZSP8ZAq`75DJ+tuj-KV#AujL5vdVj3wJxvf z`h45f79y-zTOY}_)6$mkmSI?_25;dwhKBnC&jfW>%kia zkqw@!HV^$EjvSu3^6u7no_7fXF$(p@-welI&ZaFnY|LG;(kut!yAefOU2~8Y?Jn(Y zU7?&;)SJug#E`z1+$;Q9V>GIx^`&d9l|u_6g(UQ?{N!9W|FCm%3;mVv77bXoP3-SU z=P?_lK3h^8Og87*?A8|bo@Pw)cs=DKuZgjSFvQp1Pm$9%tEF?eyu1IF zBy7j4rf5c08uhT@yvhPzMF`b3>lKQO#Z5H+`WwJpW>3rm&U!38Hv ze9m)1a2*hx*zk8g|G{({AjB7~WSf4TZ9tNRNZp4!(a3R|=Pc}Gh+p2xsq~6P5Z)-8 zK$3HWM)W~Vo=cnx#DM#FMIg}9wFax93n4%blY80!v(?Cbs9}XHmTB7mw{Q9jh|@99 z81Whs-*Wm^Q+3#*yS6Hg|M0jCP0#qcMRB5juNUH<5GKp?o$fHp5sg9D2HVo-Y+Do3 zhuW&@gj-yIaSZXD3xvq;WIwNQ3GzX!Oti3NeEp$q))LdOiyvOu!ETt#Jf1iBcHjwN zmcpt}9?kQHL?j7*{D!|JvC6khhQ_=-BlBI%)JtG(aO$pNw0k-Z zS20-rcG6^H8W>W!;v&i>_0b&6ao=wfLi8{vpDgr4r`fuV#~8BJZiE&2B@*CW9{jIU zY?i6Me?RO0gru~8Jo=;J0fw;rFdePS7hQ>$G~z~&OX&kehr!1sZE(88_`Je;t%($R z-D?WL!3kpU5f5YbF<9Xz`gCs9hP*Qg{LILPjhPLo8&EGPWL?Y2>?MuO8kGzDo=n&!zhq4Z7%rENmxK=FHE|gu-mNi&t_Yxy*uDq{`^g|l4Ma%dzoIMbfN>c0Oxs(7o;fEMG&iD4QrMI zT0h(~t9$<3rem70LcJU+3Gt})L{0%55H;_oUtKW9TXb^K&KFK*sJ#01Db!4ZA@R~9y_Y5{h>*Pt z0=NcC)h&ObqfDy-fhfBr6aPbuUrj7PbEdK|v@U~9u6xF6D7KUrcW{S|5D-a3 zB|m-T{-q9?0qk+h04C!3DLKPmwY8-btgT`oIi#F3$Ex1k_-RU0)_2TQ-1jx}_WUVN zQLw~LN9%8`>OaDxDF#&3mPAJs0}LTjGq4*Blbma{tDj0w7lRAUPZEaIY4&DW%;K$c z&x73aq)zsOk@@kS`j>Rri0u6@ujGT3s%+-qDT50>9(Q#e)D@2&WQp5B-Dc;(Z-rao z#*87qx34o7!qHj7ra(!ZM3P(QJs>)=SRgvZ^xJ4n!*MU)^Rg@Ib^rY4Zw*;NaSB0@ zxcH+3q#7HPIBEWB-TzwjuBJL)PmAL)WmMFD5#oN0nF!@^O)!9E9@e3HL-tUh2k0mC zzQU##HHihS^m&l6O+~cy0p(WNVN!FqbW(gKBp00zT^ZcTHmXRb4sV$KOcc>bPA%;d zTI6eAY@Jnf=m4{rQ?>_&wh{qPp#9E~r<=+lV^3)ogTM*@+iw09p;}O!wHZg46}@CLyV>4v9x)+ika{r>|%+yTwguXK*G7n^Lsn#1$`BWwM`4QAvHJ z1FT(qdvZ=6j;?d>OZ>dZZZFDgQmwC|8O8{+I53L-r6i8eckON}GE>0D_o zYGVuz_@!9SmiAW;rgTlHhe!Neiov412YAFLjh8oU0s(M#Veyqs{<0g+LcuOj?EnR{ z`N66he!cA)ZkEvhO7!Y!#v+^ zvYg5|o4&~iwM1DUhuCG0uLIgiSYESsgjmfS$Akl2C&5xGbKUoSSA&o5r(V|j3+G_X zmbu7#v#W+q1lP9TtsUBu*(f^2ADx5sc(myWf8>u>9i>b*k)rGr8Im^)(}|Izgt&4b z?hKae2i)F_Uh8f>!4it?8K?es7(?Nclk}{_DgYYYQsR}+{mH%TO2cvArcq*zJC;*2 zZ>EF3X*+dQMs(I%WLl~pp1Z%k+9TCV%(mxZcW5?vysTdr^sG`9#KY0ddAT26IOg;i zcTt)L82GRkY`^Z{J8L>|zc*h~g78|&+BlpI$2V_OhQ+rV{Fci9Awc1N<87S$a@KAp zKJ-zdNfkni3-NT>vYXml8mHRBcWQuE{y+$5OnotW%zBWYGy|6!0Y0n^LF9qipCzpA zPEY*?43RtX$=Q3={5%^vp#HK_ulW0`1y1YF3-T@CJ?+=KH5}|0ce_Ht;@Wc&nJYBn zh{{6Pq<1nnZ9elQ^9puV=JuH`6y%#9BW&V`Mv=|scE4j(Y~xok8!Xi`eh|mGnSsHu znog;RQUavInHg~fXjDULB z`25IcWue|sLG49o2aVm?lRp{d@rJjM#sI}UN9z-8l9K81LG-7b}ndbYynPT%LboL zIzf+~pDatGyylrW=2BKes0t8z@EqU&zWUs>zfzWc@peBy;a5N}4;n`&lZg0)qH`_I zh*MY9f2>NC3X2{E>!K>Zd@8GJ#7i?khi8iO!%d-ieGkupO~}9trZyt7SwrE<35sIN zwp3>PnYP{x6Qw%Nyr2r+(J1tG5}y?}wUzUzG&6k(oEW?|?;I9ihv2Axi3wgXq3|M! zWMKuftk%7_UgLKH0n`*#GqywkV^VFao+dJY`m1`V5j#QhDBh*Eb?gR&x70G6{1^7eHU0>~cLJPibGE!#UrE0&gUFZ+~Xk7BH z;t$m|LaA-49-O9HJ3X1fJ&R*1W}E-qmu=+w6_rCsFI4(J8B9>D;46l!TQ_4PD0U68 zhO7Es5zIRe#!j1R^QWU0<0l5}JbZqONGpV(ECM5TwVz8=a7l7B8&UA0z5vlV9M69} z(bOnKX3=g$zlec197oNX*K7Od)BmnIb8z@2R5_Oq+(Sz8a@gFa7r{Bz*T#ieCt!wJ zyIhF@0h>pr;&+Di&n!G8TG01m{=TbfL&lf7SXv?!Kg8K@oPJ+SC>9KhV8^GVLMn<% zY(m7__Ppbh9Z$AK2f^ea&Rg!lYq5jiX6Qq)C6%!%OzRH0`?tz7N2G1*>K2Y~i&|CH z(upqJ(@fh%HQuAKor`sZe|k1a{J!m5+nODGO$=FGQ>!+ELs+|ngb5beir+`hPe=Y> z-NeyS`=;{QZ!D5uP5wcnNW9{O8pUWgW^Z4y^WzXia&@o1LZkP*6}Ue3P6GR37vx*n z; za7#f59Xbq%^K(r?%kJt}yIU)j0o6;j zh1EKEX-o@u`nM{NSO9^HAias!lg!+)>A5xHSW2QD3}1l-NUl%KDg0hsXkB=oCxnq& zlu0C?h3dI337x0cmL5G0b4+YbDTOLEue?6^)r}(m>Ctzsi)=haNI!oY#<5x1^n!1e z!g8q|jab~W$7crB<2l(7=&ngr!94;H4lz=G@Z*Ie_@{c>SL_uR1!5=gv>(Ru7>tvH z-YlkCdHM6BZ{gel(0S&&P23-CwKr8c+<>djf5wgNab}!j z@7JjKa0)vyZLADaOvL@nIgH3ZsIdZ^!aB=>DD@_`z2D4UDJIsjNwT&nW1;S20EeEr z^YR!U+{qQgq#?jpEmj^{FnwhekNTX4A(x<~ZXJ8wS|7q_u0Xu%14+BSq(cV~381#Y zG$Y#yZu)!i2}=v*TGSt+4qxMJZ3ubbM8vA^zDRAhjF9U96X~fiJj9~V0J@(#ro`X! zJ?}w5pp`XvOl=zm94CBl@jN}$gt;(hr(#h%2inxSU0}3P)lw2TFy*(n`zO#0vU$UY z93;!WtJzGmi6a&{rLzW;z8SI6Rqs@6uT!AOZWsITREWl=&rW(bI1GagzQD4=o7^v( z&G^V5iktf4f#lw3E-~Z~Ymk__z)F+;^E#0Y;X9wYsxBea_CW&`pj-lts*>&ad)DOF z)Jrwav_~(Nh^|#u+CFSfX^x8qTh6m}N(Cg$PRP@A2NOG0{2f*P=w0o>OVmIE>NIW! zUe=q;+}0d;LE^^IbAlB=YN%>ro}i&itmS@aKaJ{Uw6N8q3$d~X5hmgX7raOID`p)G zGD`R*f29W7sp*yL$I7xENi$vEmAM+WsXR7{3*VxDm|NU6zhZKifs|63(TA^qu6Wh$h|1iC@ffU+Z!)_FkOknN(WPe z_|zZLUB?yN%wV4Gmt&*qh@d`!gpw(?sHLjWS#E$9Hg)XwE#02MB5*kP@P(dGiuO0T zq67Lb0F6cs?gv_xXx77+PY;Kq)~0Zu{u$^2UAClP!{!LBFQH+@Q!V`6qrBqnJZtuR zy#_q7Qk-(#f$A^vRM|~k1ht7+(Wk`c$Cj@#k-#?sdDSD{@4!lck`9nUi=d1ogie{U%e} zX8pz5v65inNd1E{{Zk!s?D*;xM_+xR{I1nR@;F9WjK?#r60_1^83Ih3xm#uZg^!1VCEa1aJ{hi4jQ71!MZX>W)b4w%ub zBvOcE!i2x#>D^Z3N$rdT&eGTz+iIL6gsIuf5>CqX(9 zDb`bU#nxuqMMeTe*#<_2Qy7T&(#b&rv{AJVKO{fSDS%@Yhd_M$6yyuX)?w!Fcb*S) zwktyL!-e~odCNqpwIlnZNI~5h44|)`suG3vsym{DlU62+VtAv^3|OPX!3BYM?j`Zz z_?!e)G-<Z@ULr&+8g901$MK|vmXQ>34#>H2v8MMeiaB#ooHP-r7C<0B3& z{*YJf^^0{!LclVn`MtC9N+g%UmdVNXF8&)+ORLhk)gDsOe{` zMR3bL_{IfDgcsHWr23fv@)f)w5xROgA{L0K}*(!Sm$GT#J*Th{xe5Z1P5OG}5gJC^hXl284uEvaM*TIj0@Y_mL2?F#_HgT67IjsT|DF zINx|RMHb@}aO%DbOVa%e7p(IuydbBCDuq82FM} zh`&#z2+_&z5P>_uvC3i9nR%D(91vEn!C2;8y;M4d%Hh#eb~~)N&;Ol79OYH>lKRV^ zX9OXzzr&^l40jhp=Ac;E-s``v;$L5TJCW)K%c_T4tixwm5Js}*oePa#Fn}Ld)3QFc zqK=f_`FLSxZrcIQ&Zkt{gzJJyReDjP)1jkB(9F}DX*-IK0iFZ3bYC&*3FM|rA1pf% zuA(P>fo4_q`~!kiYMlg!%=@pmKcdSssJZbF)cM2xb&mFKdqv)lup8;dIjC-n66Qjd zlXz9ue7_Yg-II){Jbe}4&>La(r87a7vBhi4SyxdWbXBZhEeXVP#7XXEzeV6~xHu)8 zarU>_Q}7&epF8>X5BvZsig!T>{zMV#YT(z^7X;U9q?Qm1L6U|l$tQF_N55{SjH$24 zca*+pLRi?!rbj7`wS1gPy56h1nzCM`q6m&CCokKrrjqgB>>b#^NBLoDx^#HjvJj;7uVDc;Tq+5rNn1rU?1{>mON2o**lT?My0Hl=eV7 zCk$DfG{02-lo9`#rC?EP3zsSxz1LNsd(Z{EW>M}N* zJNE`xe-I|jL4R!nW#<=kB_Zy(o#lhS2}??Z&HRp-z{AFXdVd?P-RPe@_@8Nx8y`Z? zz)68=SRf#sbWoOPI?#!2cH+Z#YTAUA2sx5kSuEu4t z_{YMxY3q-M(AMsHR)z%ePaUeZ?>{O zMY+)4#Jydl;ba@CxXBYavc_z7Gk|J;sa;Rh>_=OM;y7QU8#GF6Y{ulFFQ#!%*we&n zK4>+RTRaQRh!wCnJ?9E&$GYc$I2Y@0v92s0_xI;6AKJD%4s>lgabVi^bdgvrg{ZdS ztzN0icgvE&?cLr{tT#3^TQY1~&U!PTVhVBF;&z^U)QYKUK2{;`CJ0K{N_iwyrn@)B zNniHhIBb&*>xdLrG!zr(`I!eem`A1g-M@@}pE=A!CyLRrEk0s`eK=bpAN+G5>wI1Y z1WIJ7Y`&&1Qom%9TI4d$SHwE|4LXO-eg#4^{x|fo|JcZXy11721Y3GCTEWeXR z6UQeF&<&c z{V24$a2DjqLMe%)G?Iu?Qh6$jbNnsy z{&VM5B75(zi?n+qN_QSjum`Hbii3$bR7M!qidvSlz5CnxzK}(B*9N15<*pCsNXRJJ z5OB6`LMW|pnG`V4Ex0v5KnQ1^nx+Ig7_ezOI~HdW$&vtsoed_0a=Qmu5NuN@ZFZqX73WI4p$sxe{A~N zN^z#J)-8Bgx*Sh}iOiRx{{oc9X3Ia53TL`>){}E-MV?DPwbXL{umoyO4vRIA?3>8;-jLTm-t+S%< zj$)G~MRAV%%<@78%&&%Ng;jjoZZm<|P6k6|1(rJI{J2u4ZZ3yh{t_&A0@?R+S$hwt z=X3L6dM>_V)U_rrqQ;-Q*$z?HIrjNtnus9Rf*j4$6eoEQ)iVH-ekA7=Y&%mdk?5%E zCrltCg$0x?*pQiY_J5EJd&U6O)VgrKY4h$okzslrs@Nn)h8Q(U&vW4rRqPQ?ECJGyz{P+z>! zoAsClxY)J)Xx9+wbHsM;k@HX1@sSJx|gr3cz9`Ok=lgezfcq_!{*@irzJ>wXNd6%XaX4+?xW-GHUSrXdB_NwSj4>W+ zUd&;btMhwzML!5dDw56f7A#|DzNG#+i~>+?VaT&po9ioftYJ8SAeJ&H?SugE6bo_y zvF-~XLu0p^zDeDD8D+Dv7D#NHRNQP0j6Go8cTo@2()D>*R?28qu#S%efOYWrprF2Z z?mC#B5<@B3uopMI2uWu4iVji6%xI@?6~{2;u#S0U)>euzXVo5*=2!6+qd0Nw_M`o6 z6$cfd7g2bJLFR|!4KffqU*1!!KRxnhd)v>1vp`Up8uuGC{gKtRBJ9(0=hVJ+YdVUo zeyfMbS+0!ElxyNFbCBf2)qVL%{hSChbDz2PVS3S+5lDqw?vh z!vD?!xOpS&{r0JNr$;8x8efiE!yz`(FwElxJqqBG(6~RCW4!YFq~SWfoP%v?LpCb8 zdcm_vN;f#Yp@K-8&ExmR&=0pwz2leZ!rF4yZvzggOssX*10OZ-+y5{3U#r5=*4 zeEBC={O6UXL71Pte8|>IsvbS1#jmk|g5VdD8T4*1#X6~CvFY6Da-z5+d`d2~nMxG{ z+~IA>qtbzt^cP9&pQi`DMYeph@X=eWd3C4W;K=jKs~Odp3!fK74H(f!MexJ&t};%> ztyohRk+-6%zCG)#DcR*NU&1QAXwp#4*_fXY{pKPv(GriN{U@RU`mtrY+Z3A3g@W1~ zh`Fz+FsFe;9tMQqVfGq(-2|WGraO?>F~M5Xc|oH=IUxo?tK|D>c1WTvYKGH=z?Z>; zx!!mdNzTIaGueMhpVDDr}dbClTd^i6k-Nr6}>X7s!UcOt?+rk%PIV=B};B^BPY_Z@{vgf)aVr zfMu2=@SV>oV@ASBKTC9nrhwbA@%-s2k48gYx@^MaYkuqqT`uGrZB`i2KGSOm z*1WfyXyFyn8nU|jd>Y~iuGe5?v8Xb-Dty1dXWg^ zre9-1MG1kWs>TnQ&(}MdBr+t7u%ng!xRl#~N#>BP5Uk-#CUSakl(pZ9$A}JgIeNP& z|1il>)t+R&Gn1Sg(lH|sd!SZ-`L1O{r8)>53QbkQ8F_1@3g99#YP|rqh!ImO=x7J=NOB&b;$x%vvF8X(X_x!dl_tz(TU$$6+#G>n_xK_L$rF zywTsW=&v9C&)24@UJilBST^~eV<#rpuF~k$e~IXf;;!3Qu>#9ASeXQ4Z4LsI zi2730;o^XWf3ebyWYrcZl^1jhhzyDSfk)c9f3X)4=OHP7aR z>)~?ZB4HqDUzP`f@w{=XGN{n%((zx;Q+ zwL11bfV{xX*ry522C!S}?{T3-HRh3h?70Xm?Gi-f(#G(PxJi;@ z|1~VO<7kQy2vp4}vZ*(tGNw^Zf~>XVr325A>{lpk_uOoEeiW$&381F{75U9O9^XK6 zM$(^A-qg)@_c;-Wia!aCSygT~ppiDm`B(4U-~XK;`?c8K*l3)pI&)tpcXQ(|kgpFY z^$KXTqTFp(sqTkWV4IN6v37HbK*D;fs&2wj8~J#Ao&2@83x$U2BO_(3=QjJ_K8c@ z5fGg61k16-&8-Rrf3Q=IVWOfD##ooxN%8?Luo$qjZIM8A5Cp1ZPv_P~o;~ix&p?jC zP|NhY`I|SDmQMm^60SEie%ra;gL1#v_eTV~hku5nKd+>G@v7g%?0aou2n>n&JJtp)z15 zR26MJ1@{D;~M+(}Xp zY43}E@?|B-StlS&Jfw70AB>+&{}N-6p5^We1&|#6c0vX7NCCl0-Yo>8@gJbqdr%6X zX!`V8G9Vyl9qNzqyaoAOF?MkzLIWnvprnSl)(V`m5l7a~qruDww1 zrX9$j+K(;9fl0D<4SL+|#$<*26qgreEeth7<`oeKzyk+Bz6|kvXSm`(;g>qml@q;gq}Q--kR)YeJ6>pR9ko{ulO-$lt3z7cyaPJEflf`Gi4>Pj2~H*;=#D zwX_FTqS*Pcb>PjTdIIWeFcH4R%$j~LA(VN-+tLr`0q+9gSw|$Ppnd1WID;hK20IKy z%HdojJ^93!PKlt_Dx>l3%86{~ZEVSm&PVUp3!od~2b6!!+0z%ftX;QAne*$4nH!9s zTa^m@K>NMXVCPLD(p$L?X(64TVoUoOIe6-#B!JG?H)JO3Q`wRX!^H#B>5C>kd+K8e z$S#8i*T1?e7}*%$?1b<0Tv7Zv1PLy75Djo3Lg=H;SlOod)oNUomV&lw$YC!6ZtPd@=cH*d-(aW(qYjBF zo9(W9q(a3LuBTfJX0zP}BHivUV;Bsq9oWL0-#zhvJJO@7gPqvok(3KhQhl#hZIeNWGHy8~H@NkGS(7X*meG=V?1U_>rco@*_> z)bMkGHuWG2__$czBN-6kOY;=!+yC^eJ7dF?w-GK3Q0u)BDEc97&s5e4__e9D+0hN; z2E756Z$)iHyb#p{xp)`Df5DK_OwfRQeRy;AgXbAmiu(U44_5AQ4w5U)TvZaL z{l>*`zy3wBZFF~u{*$lz6Wbivq2fdDQK~-d{87LOIoN(*smtaO-i7Yh7bt}xcCE5; zN~E6^OW$2fYXS5ulSQD?ONa!IG)ROh9MwOjO5Lv)O6$mMc0^a%rP~qVkE06^(6{Oc zV6junqm1<(TYRl2DMj>f(%fy#KNOllNtgB=p{^3+ulMRIDGa3ci}#6Vj_iyUsmu=O zn^0GOPmKN~*HFEwLSV0CalaGU)jypCzp+(WbC9L$+uc=EobviuuM^0VoL|MGu!a`E z8zLNE*r~W>^D${Z>xY_%7G0zq-YGYKpFg0r{uML!KiIqUU+?un`=A;m}o`Ey=FWP0Fio)-d8^s7+%L5DNIG^VliKv2l$uL3EmP2qqd+rV zqvo7fhvBtKh!!1h;p%IU`KD|K5;$}2xZmS-zKmEaHq6xM@t8XwJ*i0pV(SmTg?Dnr%<$vi` z1l$UMbG)YU*5X}#F)Xju%03#jMrM`0FWZaU9+!dM*5qVpdpmjC9(xoSrti!O+c^F&{K1O}TCLu0e93Wf-lGSEA1gt7B`rO~Ut8-+ z|JEo^$M`j}48y}z`%ismOD&ngn(46bOKH@Dj;3e~P7SqCi4d%zfhLl*^0UDOp;^=1 zmET0V=cO0j5h&-NRSl$2Ho<1skKN$nvB?%<G0NsrqbbO76;W<;(xrw$nv{FZT6i_sWKcyJ5E{0fM@~PQrWdznqUh>E3_+ zcX>}5$camiOon*=+*hXlEw1&GEA=Feq0zH%bfl@>s2*;bS-pLV%b_XhJvW%#Y)#e2 z5Oeyw;A;3o@6U^J=W{Uv7*}fbSmPRTbXKE}AAbmR4Hxq@Qm6iZeCAG=0-Og_p!{k2 zo<_}%Y__zR&XvXb`qMv*r+*7m|MTl{6@~t-<`SRv|4I}wVR(<1yVDwila@XUQ7=d8 z0MyR4XUZFPLV<0{<;V~L&3D1#36E1p{GnEbkX)#Q$2|^>caW(PKBQlNau^TZ!Pj%{ z=E&USI@jixIOmV6+)yVho&gYQ0Z?Wbp+<#2*+#J+FqjC1+Vwx-^=}zz3#>SBbCC|o zHjw{pp#U*s?4V8O=pIA<$GZ=lGEPePx?k_;;PCZ)gDh~-&#k8GI=^=h98)MIdm(+WX(FEr!L|ak%}~BsikywZBfQ-vQkze<^6<;|3gGkReEhK@} z9znQ2ADcRHF7uuufM%|Hf07ab%*Po_TCK%`(VabqCDuCuqr_vGXC;!7{yKxp0T?D# zhAK`6R4}^OX-6CaE^lj-E2m*-Yxh8al|pIK*?&D2$HULDd)$g}Bcj2VDx_+Mojrxq z7mwiqyGqVgnf`e7#+_>Ym}*R-uAd2>0dHIzmk&xg1AYJ%^GL5fb3(5{8AFFond07m_7;J8y z2H`1gU07I<&d5>Njg42jYMQ$Abo_Loo>oT>>?!MAo6eY^LZ6Yfz%%~#V_vM~;Sx*&yysOxR!VbNG_2^n^1bo6 zE3((=D>0X}RWKLAU9pI;k{Y-6K(Ft311%H=G+D3AWLIj$~8MOOyP^EIz14ho8>sQJ;N-ru;F^qg|GHo?#L41Ceh z_7oM%H0SbzU0yq)?o*44*He0lGgcy05jhJt5kYbUA$(+~;Cmp#S2>$Cpt1vfA(I+2kuJUX}8(D{;!&RaWO1 zVQscYv%swkDc`*!y@@U@l%YL4NmY%0)6+`H??usC<-0-1bz;|rJkic$&-dJPjNMb- z8h0dX`q*=+pxDcFtAs!!!3eo0V(_U{X)ek6kp*NSjvut+)J}QCle5wXc3GxYDQjln2|{ zI6L|@=QYExX*sh@lbA3=g?o85PF2=>^AMp>bV$5G z;_CEU-dch6NY#xl?k={^*%K zd0Th$iv{YwYR(^+B^$6RF9U_3MC~qRA@3sLKDjJxbF(27z#e?$@C|n)5RR-di(2pf zbhCdU64I3rvZi6rFEr~m&u2i0&Xw!9kMv&C5!TunHDq?PZ<9Sn;RDFwM4y6u6emu{%FYwMTF4fjfv&7f_5~?H90Q_s z$m{ZWzC{cz`cyUFZ;Q`$jq{)7Y#OyUdU_&PTA7DVPA`!PjCfzhh@O8VsgHv@ah!iS;Wz=fF`9GF8;j%q(aq6N25IZ#>-xnPo}LJ3DVf15c-h z#w<%3Xo4@|@g$TKu=^5T7=rXO?eyjF#dj3!PY1!RW@tO#g;M}tY>|Ko`DiLjs119e z|LA2i+;(ljPWAau@b%E;>&U<(?63>7dBpg|JJRtJ)KT(xj`D-MN%W~o7Gv|yEgS*t zJ7}D&L};MFMX{sZ#*?iKI`oq|4pZZ?! zQq6fD7MP4q5}2U^G%Y^4tA=1Ey#KFpqW}&{W;LI9D#`>b_==wlE0_MJV3h0bDEp;8wr@|#7Un8-FfRy#i#Fkl5*9mByV8o@(d18#P&eym1pCG7-Hr_Br%!C zN1-8KPJ{%bb0D+TF`H%AWns)djnkaBa>3jufQgX9P}_+-}+T*?qNm-2kbrg92ra zGQRP|7h7-s7K1RQc;T0Mo`4gF zPI&LFD>)*skG%z+TFIMS%DuB@SM~l;s+EjlpA013vt7gPXa>CG;Of~g`TFfG3ND#f z64#r{V(-|g<=tvMQ$#|ejkqPCKtysm9l|f7d$#MoNn0?orU*pMvzuQGm6M92Gqkbn zc?KAb=|yk6BLkCCqgbsQQ?q03?v*5(Q2F$5k?t+z$5G8tm{of4R&AfA+k0 zx{9%}&U<9vIa)~RUmj~Te=~<7Hjztmlnfg)cU2(6a+O}NsIwV9HM2KkZke~o{l1fs z(~)h_q9@tioy~PsJLs@|JJ9d(@Zt=*-uM|mdxO_FoF17jL zx?|ST#oITH*`!hK?IWz91wt>M1gG8_i$73Owu!X5nJHk*T{4Xt;MrctE2$Le>EzBx{U{N&bADDA)h8Nn~6Ztucu+K@!BD3niWLt)I z@f1+d-!n1ez825Ey~3~@P#js#dBj*gNo_}VG>dC6RAC8{lYOrAaT?7Yid~1(XmUCX z*+1}=!vfW}$A-0fgBf>*-dNkvDuaW24C(~G@EQ+d-Q&aK@7T`bL&H-6>}}!FiV9yi zugpjBo4ztafAbqJgIFr#r*Bc7e*DtELANzyFbw^VcwK$s~M1{dq)#$H3JLW9M)d789i~*p9yzUdzGT(tuyAEQbSfc zEU_vDw+lHeU-2Cb_o{RV`kb9;{Vqnjt73j~J563p6!qG@{_oNQV5!sn$y&(6@jakb zrgl6%KZHcK7!=-680962u5u>YJSWzCbbk$ADTLlLc#O@<@Qh)nK)t{(Ad9TXZ<;`b z_qIZAyNE$fwQh1jG^M>;%|eb>?jwh4`*|pApj5A`&@F)0#8-)jq8;R4e38^jOa5A+ zsTx#f{At|4;S2Bcsk8EQ*&nyVZ;F7?){$R=(bOZ8g`)P)6gFCC#S%p`q@&P@yg!#>e56QqPMQ89C$dJEs#Rq8{7bQa{ z=-m+WDwbsxSUi7|9=IQMD4ebe76UQpkVspZC8MjU&>+{gHMix6eLZSt~_&PS2h8t!RuL6!~#xXYut# z>;My`<82;>QoyY5&XqGBReuG!cPwV$e4h18KC8&oRU>H3p`7pm*e28XPKN!zwh0}M ze0u^UUZXwP*1f8$p%(eS-y{^I3XZ82Y#__jiUqXv!0pTOuD1qgR>mq1sLmxtg!ZDpG>wF;<|6)xV8!BuC|_@aW$BS!V`oW(g5NharK{3pbx}{?6^&0qOihZ z;bU1^PVvyy3AvhTi7wmESidWJuc3v8iS=i^y@sp`(gma>wHO z+~G=y`r(T+lX9R%WwS*)6||YAXapC~-)?>pNv3p-n3QL~x%Y(Q*aqS*GC7&q=3~xM zTdw%x@FzLeo9b!hv@Y77xhV}0^^>SdX`=lHnh!V|{e4ePZa59zS9R)G==SGuRp&q7 z5TT#3#3Js-wLGvoM!#Klmh;zml=TY?W_%u%9vNfL|7yiM(g`?xG6hu`o5>V z2d=Hd#zYR|IOCVngP!$@D{q&2ZXWaA&n?G{T1*#|)QtpeWx>G8{!r1z=+KfcbE1o-vR(l)@yd z2W4-+4F=@#5#lv{@PsO!Rvjpb!?`@6Lec)3S@4(EFTc`S0-;79DDz5}*LBlP5B>LF zzVUl`zhN2Fld&!+JF+I9t?dsaNjV40aY*cn^h~UB_NPB;GOZ*koCFToGANy!BEr+IRF*Kpr=lTv>G1tWHWZ8lhO z+nW3hsJB-!G!^MJ8EjT#!q8g9yzKYDg7bC+oVP(#!f&`~jy$pOU-%b%?2i@7W)=!2 z?V?31`TAXKXtOL^y8NS*d1O&K7CMlKQdJuU+GJ?-N{Eq>8(j)J<_JrC9;+eeBaO{! zu0QYAiy5?Rtj37X*1e!Akj&W%L8#nZJpk6Y_H6z0zm&rN|5jR1R@_UHknI5_KlhAe zs;)3wGhM`jk=;^h>J{Y%{Y6{GCsQK$GvsFxo}~R+G;O?Ghcu$ri#Q~f7J=pW?TEWr z?)Wv)aXG%^Di1E6Q-~^loXRzg-QC^Y-Cgg>-rwo!bLv)g_4!Ldu?psR$NT6s z{Yd;ng)nrTCQkAmt2|4PZp}4s^)p8NrV|WzYuqYarOu;NT%kC5PH^!eQ3jS|9&J9- z4E=1niepL?2464xXK&@86Bdb1OxQs3GXYWTE;55#sAdb2wks2LFcN8joFnx z2n)j$ZYieHib@W(G#~WJF^r#GnKS=_)~+|)!Pf?{SiEwjy;&_OQuf|DjU$u1BQO@x}p+xPK&Wy6X`TV;zGS{}b zY|u1o=#+7ZRTKv)N%AzO1-(|VFm>#7ElZV|mcs9Tv-s6}lsNV~T&Fwdqz@O1ndb6E zr=FWup;VHdJX)J_+0#;(EE2i@U<<@NSCitrnuqNbIJ2S2%1?*m@B5CgKuSN&7N zOHasjaTo*?ab2AcnL^J5NRl$(wI~)I?bLM1=nM4x@f?1VJ7U;Jic{pZ+?@DuZQ(k= zygwhY7RxBO2j-J9fDwCJr(Szkv9O|CUarr0(ok81Gh>qI%-tdei3((Ih2|9y((6gI z`({w41QYP8IZPsR_>aVyUHm{3ip^kYsigFC1=>tvm?T`pt%}pyKgHC4iNW1RUgUGh z>-Xb0c_;ZYTt3nDpGtVXxsL(}_p?A`R@HN*`dK4H!V6E$MsB5zOzv?&gu`YUXBR8F zK$QKe@6+yHRX7pFaw3Sr`Bm_wA};ccU;?rV-J(G)V*Tz0#rrCN*V>dEMh4VQt$E3~ z06M@k_pnXMa}`PP?ls5p=ks#!%ka$e*tgz*;|1}u=u7t4So6dNj+a&`_cE5_rEeBo zly2>{27ktXI2PQ`8xz{}{_hVlix~ehS96M7TA|uIauER%C~zv4u`AepBZsoLqg!MX zl6`+M8t0)GCPg46PK-L`(BKdF$|5R>L$0_NH6-J2At8JJV)f=>*(w2!V5f=?T%Wj? zjF5mdj*Q*bqV??B?GFOJc@=jSb0TE%kS>7$5c~5@fAa2G&p|@g3jQ!aP=q*a(AesF zuYAKvo2Gcc(~rG&@ozYYljWIEr4#t*3o=53=fmM0F+&nPpsbP}&QHoNAcMx`&nu?M zC8_J!n54?Q#{nfw1u@ugj^ZbVj{SEA2l$=#2`=(C zUQxf_h|o)#mM_)Am`Vk%SwGHss(%jOYiP7QLY~u}vLoMd?g_=botI$<#H<$=fMB|v#o}I@f{MDD5^bWUV+9TTSK9v` zO%41-Q#g9s87cbUBUB@#B3~4WCys`!ETl$S%&NjivDuX4*#VWL{9d7HXfwcdxGH+j zve(msD)1Sy>-K%1+W_l6Ey_yYw z136A>HV4v#w~YI@3#}LUGG1qC^@KU2F&4!k)5u4W%mLbLTq^z;KtyL!W1g(gL}lis z7PZC7=r~x$Ia|XD61%D2DmWEh!bXLY(2B;mc;wEJ(q~|z3!d{sO}?(vc$k}#X%3Z6meTYcg6rNb4U-=lFav;e9aF-zoRh7n*<~yuVXkmgTSBv!WDgPwW&D8A}Oj{S|jSy<-2i=;CJ>gexxjsM!j&K)!G1Rmw?fYB+U7pJU%Q)u_kWlG~ps ztM9DJ+gZ&(TA5mwQS0;)5_pm*Wd(`+FG^v}D3Q`r;0|qbJsr$pVwkXs-Rhysp$s&g ze1TggW!)CmaD(n@E1P9h-@^9QYp4$AsqU%fJ#yhn7`TA_l9}bh?XV@gchgw?i)Vi5oIf>T6F1X?O!fki@2*aza07C0UrZ|RBXJV9%spD zEEts@=ZqXd0?#}WuUCW>I6D+-^7;tJrX)$~gT>T7pkWJnVRlSo z+dv+1`&|EVXy*J6za;LSshsTbO94S>ds7)&6se9MuB!(B2&5;CJOWW5oPA`);uQvd zOx3i6T?^TvA`8Y2v8oL8!-I;_dhv)Mj?G-dzOeWoe^z%bU;bh-cR2i0<^^6A&$R|8 zuqM<*0bqvYH`ae~VbC0&9MJd8E0fRiTdgI;J5ZnH)WBNB*JLXW$Q>e5(1+YX>V zMR;h2Rm{uTEL!O}IYDGWhn)@?QX({TLmwbDBn<|j8Rx6U-5_9u*Tr=xEJ_y2TXwT5 z?EKo1$&Sj4u9+Q&_^wl!2mTX*eWI=-Dh=w{lS?|6m)RNMxIO+t)R2;;ky^nA@Bo&A?)N z7tDDU(sLwEcw8pM9eE2V2uVC5anPd;U{A(ZMFV*QT)H%$*iLWgfq{|&S|OtX#}o3g zhNRxAD*N)jn=rfb+ST9{NxZ*Q*R_dU?~p4&*Sl0_V0LFalv2_gVqDFPwVJ%E17M%& zZL@(iEFY|`4gGfL__pf|@dr=_%9?#Km6zJef|MGKm{h7ZARi@E0f|ja2v1KIF-oT_ z9|}Pc`y@L+mC88hQiX=#K}B2MRIpUPMF^u}&g;`J5z&zgkipy^`IbkR7Z6(Pd6dK# zYSGIE#dbn2b(r_YSga$budC7gw2Xny8-qkE8B~nle_B^n6&MOoL}KBnmO&&WchgEi z*$%C+0AkSe3jBZA0dLumt{1631gy@j&Z)a_ldWNv&MxD&=tO9|7b}l86Z8biHq`r3 z)&Ayopekh+X*G|2VZNyau$j{S4trb1Ym{H#H@%=%Dwl?9_&@k%Y5uI7I_R*8O8>ah zLYbm|k^d~ecn~1EgZTTvEYtr*$6~E{KZp2MT5?9}aKi zDkyA`#Gp10q~w|pkAhwJK(!(W?jzm)et1K%1$P_gX7N-BZm%qS(j+ljFGHA(AJ49; zQ~zBcQ50Rm_@3IZke1R$ZWId?-F$NHPgf6;m8J${PefllPc!4~ABQl~e;of2deHz5 zQBXB|CZ8&K3%#uuN;lcT#tTSVMyk&G^1L=Ucafp-ZW)^()j#c<%MZuS|6w_@qWK-QR__nR4%?!_<+qvt zFE>IOlyC0!a8LI|2Nu2bq40mcLkE29_HN$Fe-aC%P#E|a{kBi!QD(R; zKv5E!Y#`ji4ircK&Ucr-*eVjJ^PCw)3K20=fb`S`>M(*=#)kYPAC?=lDP*l%WDLi# z;wSo(oFR?~T+hB}#go^FP{ih10~UoFN32PaT-cH9#3mZ@yRiUW9#Jtl3mb0Oh@|lP zQ68|0814^TC1M6mtj*eVvzx={#1#BsrSQtMSyz`tkRaOiiTI9uiqhG*1tgN)Mj0rW zB%bhF^_~Wq*u!!9ORvf6!pN5u9Ne$;cLelOl_dd;w5`w*c+45&;<_$%uktEF#Bu94 zMZG;Wle;gbu0WVFp0ICKo!4LCG`^) ziy97WSmbaP=5`E5?Npi@ zOfW+w@SaB4R@2OY3Ps%iP$;^7g2PPG1eeXr3Atvxwf3cUMg_PLWSOV+9EyZbh+_9M~+tl<;zAB~M~=^nB+cal(o*08;wK0wQ}R1yCChNiOwo*n%QG#XOJ z`tTb0AAM3bh~H6r^`0K2a`z?VisPTJ6%fa$DZ^3AiS@=lkrWZ2eQ|cMgwVlZ%3gLL zX@>ldtVtA;919=3i<%A6p#;fn-IK)tT7tyxt@DviqB2HB5fE2ncV(kQ;GnD$tR30*K{l)If5c|hPVp~+4rD0bUU6q>18M<=U z!KLuLLx`fq6icWdR?guKcOZJ^BZJsU6rNL=*N5ygeK~B0hR0U_I!Q~HfM-@eE6wvO zaQa7P6(HP{YPHVkP;YX2;WFDCSc$Taq{w)|~q_xHz5qnY! z4W*vE@*9Jmx2aYu(Vg`gj%fXfwyU?PT$Q+QpFltk_7j|zxf0g{Y8?ANYCdhpx5f(% zsn_bd4+;`)GjZh?t61|8O_V`3s0CPbMj(QL(kc=uV|N!;;0S%0Lo!MT(Z2Zg>tz-_ zgPzMBOZar1xvk@&21x4j@3DCjlM-vSTQGZCMf2o7U6}sxlME+rYyCM}*`3VC>z8l_ zH-1-ElR0leg5)}-P!60_Yd;x7dq>B60ZnN}p7o}2+JF8VbnB7^ry_J;mx^)T!rgxn z-lz)DC|j!;x06JPRz+cFPERlb*0(M+67F^Hr;zl!CLU`0-(^Ax9Yw$w?JyoWx$^$} zd+~KqA{Av=v?93$KB89c3f8gztF~c-qWRejWR~jBHZP5|$u*q&cWg&y%ee?8FIB_e z4YWhp22riCP9dNB5ksE}pWlZfstkzbab?ls=wK=%UB7 zn=Tk(IrM`i_bIere#yWGK9}lYz$c7GB<7Th7Bp1DtfpFt`Nf#X(eQXdnu3BB(hh^w zIBzZ{1YtEuoHUQ3`gR6eLy4B*r%U{nW{rbFPkg&rcL3}9cJ4a$Ul&L~)Nqd5p7h5< zRQ^?W1v*2uj+{%yrTWooQ({rkvf2LAU|QVt+M`}5B%s<7XtTkb)}L3D8j%bR_4a}( z%zW2Y9Lsu0P-dr?Tz&s-U`)X|y}ANir=T`B8Br}IiiY@AkYdfmiF3G3WGwUwV|S)7 z#lHE-=Hi9-A;&wR>#nvTHEiF?TKIQN+h4(3CM;B1U%Dt4^eKg$Mh(4W)r3>RfaS1K zlpCxM$YY{gRUDqg?TYIdTfcE&t&T9}=q)_Jf&+Ncq?>6{M<$vmB=Upj?BKA=QDJ2- z&@o&aF0u4mNJE9G+y>GTuW9!ac=2e%PfE-_&3q4L4o-9jY^_Z< zi0wC*BP!n32I?@w7t=~N8UtdV6UwqduceeXkT-PXDqrVmK%vSX(@$7@3%cuxhE}0K zW6Q7t9GQJ%s|PRVs8+N<_qFoEnbJ4vJmG^eGc_IoY=9he#u`eZhKPa5~} zQ}U)%b%Ol`#WIrIt>R zB5UBGc#aI5dl%ltf0!YXjsN2>V2JSHtmN&rvY{j1N`US&8TykY?7?u*WpQlS*q4SO zij7&1oB78)rmvcw7spN>s3iokTxMk&UbWfEW4$q(VYKnOFnM^|`0r&&8=|tPD(XT~ zGfHw>~Ww0tYit>P)i*yLh|(uFm>yL}KDtN~0^Us0jQB z3^^j?=StyGX6qV$;8>pFEg44)BnUXG$-i|1NDwsTOl9z=b8940#ZP6bGqJc&_Y zMaCKnjL`PF>qtM9lqFxhb__TJcmRN~F`+;dsA6mp61_N;%%ukMC#|CZI*U1e&~Xhz z85I;%5y$xgt5&pP^*X8cf*NiyPv>dJJa>)qX?J7eNBx=i)ec#qVAT}{)U+Pzelgj_)& zqxhjr#?#hL>%bagjOBk5Ax;mH}IADQV1{Ij2N}Nw^T16 zGVo}94XFzZhU{GzKWb`~;yPL>%6N%t%2NQpxprP!!et}krojSjr~Q=aG7Z-L*DXg4 zs5+^;xg0sH%m=kHB(U57!#`b&-5`SZ2g@QtfzVoj3G5ih$<9nqMHlOn;XnnY)ZmCw zQs0vnatf=C``oUkrnDiX6L9MYsxya4$C+~stG*=!f|@P|@Z>&yPGjxd6hb6-rHZvF zs<8(>bs9c*brwH0`SYF_s}2{%WFC^Xc7}EJpX{QXpKd*HPo3G$y@+BBU26ub0>jy= za|;Vg;#ubx2%cN1*U|j65pI_`uh^zyTZp#(DRj4z^^j^z%8ZlRc7VIbEM~9 zazs+b>?d`vJYQ*|ie>jG=ijmUeXoLx$ZPyxpfrLgruJE8X!S8)x$J01m4dw;yvO)* zJLGx5N9j+d_nF$z`a5Mp{@_N5Z@{GwBZ2?tly|A6Z#V}N=<}eL5}D1@d{7m+ldt=E zLlifJzX5eKnHv6ra#0b55ZkBLNRv-VJP{&I;9av&c8)q8QWk?b1RCBan1>s8j+x)1 zN(8^eh=lkjcBRI&73oAzZE5VBGoIf?hE{*>?gh4QiBMKc<+-4tD?VmVPv}3BxJaaH zbse9~!4yJDE<|GL<-BimETGcqTO4ax-p5(Y21C1;n6kS`5|L-wTn0geRKVx2RVa6Z zN`4Jj2*_c(!2akBu-%nLHX#UgN5PV#F#Qv zCnxY(GOHo->^Y9#n@Xj$ur@mqg&F214tGx?`7KXm{87i~=^*Ky3H*)Nl`wrRmYc7( z2&lJhJQR%}k=YU2Z`AiW2!JP*|6b6CFIJ`UbhuUl*n^@VHzu8w_{+=y1&Tw_8YrEe zs!{spoKRzPxKu{+(v5waQogXHqA7|ImyO9Qx<2{WS@ZO8MJ95ns(_j|QvE5(JeGb3 z9w88HX5hrjvaF6AeiNE7U82fUw2KLjX3O0*3q`z&2_m8p-I4(=j4`bq9WUvliDk7_kl_)dH<$z(J;J#I9T#vUS%D+pe7Ae> zzg7$%B5*267{bphymSDXE8pi7-W7rVlYN{lsYPbOXKqt4Lg0jDpviBcg)og4r6P*p zwDC%z?%}{Kg-X<>#q%#4_?M86oCgpf(^hGdXQBLNnsA1d?gC5s;lYRjybb-uU-6i$ zXHIv1&^MBmzcw%CD#(%eoCl6|5(LcP9tJiUmV18VPDK=$kFwF{C11hx9qYW9C$ceh zo8)_;2A6$wyycFZ)+cK}8oH0AG0m9ilE1H>o*}Rv%}b$ z;bZ~~)e8EQwwxJnuhuD&s|+AWDM1IFcrP`qZ$ab6b_ejoU>ZGvHFVTN*O#LV{!X5a z3El&#Z(!4X78|uHav=({*(^{618Raf4Y2B&A-S? zD9@O|b@~tmdpn--(IA0)oj{|0l-w{Bov>~++3xK80;D(9IRTnv9hccZ$}x2-*xx(n z?wCb*Vib8e>Br>0J%7}M+#OE75b@KPWx3}TNahPv z2GRPY#p42f^rsBfxm8s)@F=6#;iR$1qVv
dR__)>vNqjXeVZAMq`eSdm9lu&bj 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%0D+qN^B^m%|yGXM)~|%&}+c=;#aR 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 literal 0 HcmV?d00001 From 488201cab280e852305b14c1a2202f1efc5ac2fd Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Mon, 8 May 2023 14:16:41 +0200 Subject: [PATCH 056/128] add c_bindings feature for exposing functions to C The library can now be compiled to a static or dynamic native lib, with very basic functionality exposed. --- core/mvcc/database/Cargo.toml | 6 ++ core/mvcc/database/src/lib.rs | 82 ++++++++++++++++++++ core/mvcc/database/src/persistent_storage.rs | 2 +- 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/core/mvcc/database/Cargo.toml b/core/mvcc/database/Cargo.toml index a39fb4069..0c29c664d 100644 --- a/core/mvcc/database/Cargo.toml +++ b/core/mvcc/database/Cargo.toml @@ -14,6 +14,8 @@ tokio-stream = { version = "0.1.12", optional = true, features = ["io-util"] } serde = { version = "1.0.160", features = ["derive"] } serde_json = "1.0.96" pin-project = "1.0.12" +tracing-subscriber = { version = "0", optional = true } +base64 = "0.21.0" [dev-dependencies] criterion = { version = "0.4", features = ["html_reports", "async", "async_futures"] } @@ -24,6 +26,9 @@ tracing-subscriber = "0" tracing-test = "0" mvcc-rs = { path = ".", features = ["tokio"] } +[lib] +crate-type = ["rlib", "cdylib", "staticlib"] + [[bench]] name = "my_benchmark" harness = false @@ -31,4 +36,5 @@ harness = false [features] default = [] full = ["tokio"] +c_bindings = ["tokio", "dep:tracing-subscriber"] tokio = ["dep:tokio", "dep:tokio-stream"] diff --git a/core/mvcc/database/src/lib.rs b/core/mvcc/database/src/lib.rs index d88011290..b671c908f 100644 --- a/core/mvcc/database/src/lib.rs +++ b/core/mvcc/database/src/lib.rs @@ -36,3 +36,85 @@ pub mod database; pub mod errors; pub mod persistent_storage; pub mod sync; + +#[cfg(feature = "c_bindings")] +mod c_bindings { + use super::*; + type Clock = clock::LocalClock; + type Storage = persistent_storage::JsonOnDisk; + type Inner = database::DatabaseInner; + type Db = database::Database>; + + static INIT_RUST_LOG: std::sync::Once = std::sync::Once::new(); + + #[repr(C)] + pub struct DbContext { + db: Db, + runtime: tokio::runtime::Runtime, + } + + #[no_mangle] + pub extern "C" fn mvccrs_new_database(path: *const std::ffi::c_char) -> *mut DbContext { + INIT_RUST_LOG.call_once(|| { + tracing_subscriber::fmt::init(); + }); + + tracing::debug!("mvccrs_new_database"); + + let clock = clock::LocalClock::new(); + let path = unsafe { std::ffi::CStr::from_ptr(path) }; + let path = match path.to_str() { + Ok(path) => path, + Err(_) => { + tracing::error!("Invalid UTF-8 path"); + return std::ptr::null_mut(); + } + }; + tracing::debug!("mvccrs: opening persistent storage at {path}"); + let storage = crate::persistent_storage::JsonOnDisk::new(path); + let db = Db::new(clock, storage); + let runtime = tokio::runtime::Runtime::new().unwrap(); + Box::into_raw(Box::new(DbContext { db, runtime })) + } + + #[no_mangle] + pub unsafe extern "C" fn mvccrs_free_database(db: *mut Db) { + tracing::debug!("mvccrs_free_database"); + let _ = Box::from_raw(db); + } + + #[no_mangle] + pub unsafe extern "C" fn mvccrs_insert( + db: *mut DbContext, + id: u64, + value_ptr: *const u8, + value_len: usize, + ) -> i32 { + let value = std::slice::from_raw_parts(value_ptr, value_len); + let data = match std::str::from_utf8(value) { + Ok(value) => value.to_string(), + Err(_) => { + tracing::info!("Invalid UTF-8, let's base64 this fellow"); + use base64::{engine::general_purpose, Engine as _}; + general_purpose::STANDARD.encode(value) + } + }; + let DbContext { db, runtime } = unsafe { &mut *db }; + let row = database::Row { id, data }; + tracing::debug!("mvccrs_insert: {row:?}"); + match runtime.block_on(async move { + let tx = db.begin_tx().await; + db.insert(tx, row).await?; + db.commit_tx(tx).await + }) { + Ok(_) => { + tracing::debug!("mvccrs_insert: success"); + 0 // SQLITE_OK + } + Err(e) => { + tracing::error!("mvccrs_insert: {e}"); + 778 // SQLITE_IOERR_WRITE + } + } + } +} diff --git a/core/mvcc/database/src/persistent_storage.rs b/core/mvcc/database/src/persistent_storage.rs index 8687c43a0..98b1fbd20 100644 --- a/core/mvcc/database/src/persistent_storage.rs +++ b/core/mvcc/database/src/persistent_storage.rs @@ -1,4 +1,4 @@ -use crate::database::{Result, Mutation}; +use crate::database::{Mutation, Result}; /// Persistent storage API for storing and retrieving transactions. /// TODO: final design in heavy progress! From d5b96d5edfae557248792164cecbc33a7bb61d9c Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Tue, 9 May 2023 09:53:03 +0300 Subject: [PATCH 057/128] Move C bindings to separate crate --- core/mvcc/Cargo.toml | 3 +- core/mvcc/bindings/c/Cargo.toml | 18 +++++++ core/mvcc/bindings/c/src/lib.rs | 79 +++++++++++++++++++++++++++++++ core/mvcc/database/src/lib.rs | 84 +-------------------------------- 4 files changed, 100 insertions(+), 84 deletions(-) create mode 100644 core/mvcc/bindings/c/Cargo.toml create mode 100644 core/mvcc/bindings/c/src/lib.rs diff --git a/core/mvcc/Cargo.toml b/core/mvcc/Cargo.toml index ba0f2a813..c9e8d84ba 100644 --- a/core/mvcc/Cargo.toml +++ b/core/mvcc/Cargo.toml @@ -2,9 +2,10 @@ resolver = "2" members = [ "database", + "bindings/c", ] [profile.release] codegen-units = 1 panic = "abort" -strip = true +strip = true \ No newline at end of file diff --git a/core/mvcc/bindings/c/Cargo.toml b/core/mvcc/bindings/c/Cargo.toml new file mode 100644 index 000000000..a54329eca --- /dev/null +++ b/core/mvcc/bindings/c/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "mvcc-c" +version = "0.0.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] +doc = false + +[build-dependencies] +cbindgen = "0.24.0" + +[dependencies] +base64 = "0.21.0" +mvcc-rs = { path = "../../database", features = ["tokio"] } +tokio = { version = "1.27.0", features = ["full"] } +tracing = "0.1.37" +tracing-subscriber = { version = "0" } diff --git a/core/mvcc/bindings/c/src/lib.rs b/core/mvcc/bindings/c/src/lib.rs new file mode 100644 index 000000000..e0cc44fb4 --- /dev/null +++ b/core/mvcc/bindings/c/src/lib.rs @@ -0,0 +1,79 @@ +use mvcc_rs::*; + +type Clock = clock::LocalClock; +type Storage = persistent_storage::JsonOnDisk; +type Inner = database::DatabaseInner; +type Db = database::Database>; + +static INIT_RUST_LOG: std::sync::Once = std::sync::Once::new(); + +#[repr(C)] +pub struct DbContext { + db: Db, + runtime: tokio::runtime::Runtime, +} + +#[no_mangle] +pub extern "C" fn mvccrs_new_database(path: *const std::ffi::c_char) -> *mut DbContext { + INIT_RUST_LOG.call_once(|| { + tracing_subscriber::fmt::init(); + }); + + tracing::debug!("mvccrs_new_database"); + + let clock = clock::LocalClock::new(); + let path = unsafe { std::ffi::CStr::from_ptr(path) }; + let path = match path.to_str() { + Ok(path) => path, + Err(_) => { + tracing::error!("Invalid UTF-8 path"); + return std::ptr::null_mut(); + } + }; + tracing::debug!("mvccrs: opening persistent storage at {path}"); + let storage = crate::persistent_storage::JsonOnDisk::new(path); + let db = Db::new(clock, storage); + let runtime = tokio::runtime::Runtime::new().unwrap(); + Box::into_raw(Box::new(DbContext { db, runtime })) +} + +#[no_mangle] +pub unsafe extern "C" fn mvccrs_free_database(db: *mut Db) { + tracing::debug!("mvccrs_free_database"); + let _ = Box::from_raw(db); +} + +#[no_mangle] +pub unsafe extern "C" fn mvccrs_insert( + db: *mut DbContext, + id: u64, + value_ptr: *const u8, + value_len: usize, +) -> i32 { + let value = std::slice::from_raw_parts(value_ptr, value_len); + let data = match std::str::from_utf8(value) { + Ok(value) => value.to_string(), + Err(_) => { + tracing::info!("Invalid UTF-8, let's base64 this fellow"); + use base64::{engine::general_purpose, Engine as _}; + general_purpose::STANDARD.encode(value) + } + }; + let DbContext { db, runtime } = unsafe { &mut *db }; + let row = database::Row { id, data }; + tracing::debug!("mvccrs_insert: {row:?}"); + match runtime.block_on(async move { + let tx = db.begin_tx().await; + db.insert(tx, row).await?; + db.commit_tx(tx).await + }) { + Ok(_) => { + tracing::debug!("mvccrs_insert: success"); + 0 // SQLITE_OK + } + Err(e) => { + tracing::error!("mvccrs_insert: {e}"); + 778 // SQLITE_IOERR_WRITE + } + } +} diff --git a/core/mvcc/database/src/lib.rs b/core/mvcc/database/src/lib.rs index b671c908f..b14e27be3 100644 --- a/core/mvcc/database/src/lib.rs +++ b/core/mvcc/database/src/lib.rs @@ -35,86 +35,4 @@ pub mod clock; pub mod database; pub mod errors; pub mod persistent_storage; -pub mod sync; - -#[cfg(feature = "c_bindings")] -mod c_bindings { - use super::*; - type Clock = clock::LocalClock; - type Storage = persistent_storage::JsonOnDisk; - type Inner = database::DatabaseInner; - type Db = database::Database>; - - static INIT_RUST_LOG: std::sync::Once = std::sync::Once::new(); - - #[repr(C)] - pub struct DbContext { - db: Db, - runtime: tokio::runtime::Runtime, - } - - #[no_mangle] - pub extern "C" fn mvccrs_new_database(path: *const std::ffi::c_char) -> *mut DbContext { - INIT_RUST_LOG.call_once(|| { - tracing_subscriber::fmt::init(); - }); - - tracing::debug!("mvccrs_new_database"); - - let clock = clock::LocalClock::new(); - let path = unsafe { std::ffi::CStr::from_ptr(path) }; - let path = match path.to_str() { - Ok(path) => path, - Err(_) => { - tracing::error!("Invalid UTF-8 path"); - return std::ptr::null_mut(); - } - }; - tracing::debug!("mvccrs: opening persistent storage at {path}"); - let storage = crate::persistent_storage::JsonOnDisk::new(path); - let db = Db::new(clock, storage); - let runtime = tokio::runtime::Runtime::new().unwrap(); - Box::into_raw(Box::new(DbContext { db, runtime })) - } - - #[no_mangle] - pub unsafe extern "C" fn mvccrs_free_database(db: *mut Db) { - tracing::debug!("mvccrs_free_database"); - let _ = Box::from_raw(db); - } - - #[no_mangle] - pub unsafe extern "C" fn mvccrs_insert( - db: *mut DbContext, - id: u64, - value_ptr: *const u8, - value_len: usize, - ) -> i32 { - let value = std::slice::from_raw_parts(value_ptr, value_len); - let data = match std::str::from_utf8(value) { - Ok(value) => value.to_string(), - Err(_) => { - tracing::info!("Invalid UTF-8, let's base64 this fellow"); - use base64::{engine::general_purpose, Engine as _}; - general_purpose::STANDARD.encode(value) - } - }; - let DbContext { db, runtime } = unsafe { &mut *db }; - let row = database::Row { id, data }; - tracing::debug!("mvccrs_insert: {row:?}"); - match runtime.block_on(async move { - let tx = db.begin_tx().await; - db.insert(tx, row).await?; - db.commit_tx(tx).await - }) { - Ok(_) => { - tracing::debug!("mvccrs_insert: success"); - 0 // SQLITE_OK - } - Err(e) => { - tracing::error!("mvccrs_insert: {e}"); - 778 // SQLITE_IOERR_WRITE - } - } - } -} +pub mod sync; \ No newline at end of file From 124446f17c460cdd7ec987d89be90147d1a0d14b Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Tue, 9 May 2023 10:04:14 +0300 Subject: [PATCH 058/128] Generate C header file on `cargo build` --- core/mvcc/.gitignore | 3 ++- core/mvcc/bindings/c/build.rs | 6 ++++++ core/mvcc/bindings/c/cbindgen.toml | 6 ++++++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 core/mvcc/bindings/c/build.rs create mode 100644 core/mvcc/bindings/c/cbindgen.toml diff --git a/core/mvcc/.gitignore b/core/mvcc/.gitignore index 2c96eb1b6..48e69afa7 100644 --- a/core/mvcc/.gitignore +++ b/core/mvcc/.gitignore @@ -1,2 +1,3 @@ -target/ Cargo.lock +bindings/c/include +target/ diff --git a/core/mvcc/bindings/c/build.rs b/core/mvcc/bindings/c/build.rs new file mode 100644 index 000000000..af21fb9d8 --- /dev/null +++ b/core/mvcc/bindings/c/build.rs @@ -0,0 +1,6 @@ +use std::path::Path; + +fn main() { + let header_file = Path::new("include").join("mvcc.h"); + cbindgen::generate(".").expect("Failed to generate C bindings").write_to_file(header_file); +} diff --git a/core/mvcc/bindings/c/cbindgen.toml b/core/mvcc/bindings/c/cbindgen.toml new file mode 100644 index 000000000..b530dce1d --- /dev/null +++ b/core/mvcc/bindings/c/cbindgen.toml @@ -0,0 +1,6 @@ +language = "C" +cpp_compat = true +include_guard = "MVCC_H" +line_length = 120 +no_includes = true +style = "type" From 34a4f1a2697fdbd88089a3e51a1ea028e0951c15 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Tue, 9 May 2023 10:21:45 +0300 Subject: [PATCH 059/128] Improve generated C bindings Before: ```c typedef LocalClock Clock; typedef JsonOnDisk Storage; typedef DatabaseInner Inner; typedef Database> Db; typedef struct { Db db; Runtime runtime; } DbContext; extern "C" { DbContext *mvccrs_new_database(const char *path); void mvccrs_free_database(Db *db); int32_t mvccrs_insert(DbContext *db, uint64_t id, const uint8_t *value_ptr, uintptr_t value_len); } // extern "C" ``` After: ```c typedef struct DbContext DbContext; typedef const DbContext *MVCCDatabaseRef; extern "C" { MVCCDatabaseRef MVCCDatabaseOpen(const char *path); void MVCCDatabaseClose(MVCCDatabaseRef db); int32_t MVCCDatabaseInsert(MVCCDatabaseRef db, uint64_t id, const uint8_t *value_ptr, uintptr_t value_len); } // extern "C" ``` --- core/mvcc/bindings/c/src/lib.rs | 50 +++++++++++++++++++------------ core/mvcc/bindings/c/src/types.rs | 47 +++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 19 deletions(-) create mode 100644 core/mvcc/bindings/c/src/types.rs diff --git a/core/mvcc/bindings/c/src/lib.rs b/core/mvcc/bindings/c/src/lib.rs index e0cc44fb4..62b067c81 100644 --- a/core/mvcc/bindings/c/src/lib.rs +++ b/core/mvcc/bindings/c/src/lib.rs @@ -1,25 +1,31 @@ +#![allow(non_camel_case_types)] + +mod types; + +use types::{MVCCDatabaseRef, DbContext}; use mvcc_rs::*; +/// cbindgen:ignore type Clock = clock::LocalClock; + +/// cbindgen:ignore type Storage = persistent_storage::JsonOnDisk; + +/// cbindgen:ignore type Inner = database::DatabaseInner; + +/// cbindgen:ignore type Db = database::Database>; static INIT_RUST_LOG: std::sync::Once = std::sync::Once::new(); -#[repr(C)] -pub struct DbContext { - db: Db, - runtime: tokio::runtime::Runtime, -} - #[no_mangle] -pub extern "C" fn mvccrs_new_database(path: *const std::ffi::c_char) -> *mut DbContext { +pub extern "C" fn MVCCDatabaseOpen(path: *const std::ffi::c_char) -> MVCCDatabaseRef { INIT_RUST_LOG.call_once(|| { tracing_subscriber::fmt::init(); }); - tracing::debug!("mvccrs_new_database"); + tracing::debug!("MVCCDatabaseOpen"); let clock = clock::LocalClock::new(); let path = unsafe { std::ffi::CStr::from_ptr(path) }; @@ -27,29 +33,35 @@ pub extern "C" fn mvccrs_new_database(path: *const std::ffi::c_char) -> *mut DbC Ok(path) => path, Err(_) => { tracing::error!("Invalid UTF-8 path"); - return std::ptr::null_mut(); + return MVCCDatabaseRef::null(); } }; tracing::debug!("mvccrs: opening persistent storage at {path}"); let storage = crate::persistent_storage::JsonOnDisk::new(path); let db = Db::new(clock, storage); let runtime = tokio::runtime::Runtime::new().unwrap(); - Box::into_raw(Box::new(DbContext { db, runtime })) + let ctx = DbContext { db, runtime }; + let ctx = Box::leak(Box::new(ctx)); + MVCCDatabaseRef::from(ctx) } #[no_mangle] -pub unsafe extern "C" fn mvccrs_free_database(db: *mut Db) { - tracing::debug!("mvccrs_free_database"); - let _ = Box::from_raw(db); +pub unsafe extern "C" fn MVCCDatabaseClose(db: MVCCDatabaseRef) { + tracing::debug!("MVCCDatabaseClose"); + if db.is_null() { + return; + } + let _ = unsafe { Box::from_raw(db.get_ref_mut()) }; } #[no_mangle] -pub unsafe extern "C" fn mvccrs_insert( - db: *mut DbContext, +pub unsafe extern "C" fn MVCCDatabaseInsert( + db: MVCCDatabaseRef, id: u64, value_ptr: *const u8, value_len: usize, ) -> i32 { + let db = db.get_ref(); let value = std::slice::from_raw_parts(value_ptr, value_len); let data = match std::str::from_utf8(value) { Ok(value) => value.to_string(), @@ -59,20 +71,20 @@ pub unsafe extern "C" fn mvccrs_insert( general_purpose::STANDARD.encode(value) } }; - let DbContext { db, runtime } = unsafe { &mut *db }; + let (db, runtime) = (&db.db, &db.runtime); let row = database::Row { id, data }; - tracing::debug!("mvccrs_insert: {row:?}"); + tracing::debug!("MVCCDatabaseInsert: {row:?}"); match runtime.block_on(async move { let tx = db.begin_tx().await; db.insert(tx, row).await?; db.commit_tx(tx).await }) { Ok(_) => { - tracing::debug!("mvccrs_insert: success"); + tracing::debug!("MVCCDatabaseInsert: success"); 0 // SQLITE_OK } Err(e) => { - tracing::error!("mvccrs_insert: {e}"); + tracing::error!("MVCCDatabaseInsert: {e}"); 778 // SQLITE_IOERR_WRITE } } diff --git a/core/mvcc/bindings/c/src/types.rs b/core/mvcc/bindings/c/src/types.rs new file mode 100644 index 000000000..acb929dcd --- /dev/null +++ b/core/mvcc/bindings/c/src/types.rs @@ -0,0 +1,47 @@ +use crate::Db; + +#[repr(transparent)] +pub struct MVCCDatabaseRef { + ptr: *const DbContext, +} + +impl MVCCDatabaseRef { + pub fn null() -> MVCCDatabaseRef { + MVCCDatabaseRef { + ptr: std::ptr::null(), + } + } + + pub fn is_null(&self) -> bool { + self.ptr.is_null() + } + + pub fn get_ref(&self) -> &DbContext { + unsafe { &*(self.ptr) } + } + + #[allow(clippy::mut_from_ref)] + pub fn get_ref_mut(&self) -> &mut DbContext { + let ptr_mut = self.ptr as *mut DbContext; + unsafe { &mut (*ptr_mut) } + } +} + +#[allow(clippy::from_over_into)] +impl From<&DbContext> for MVCCDatabaseRef { + fn from(value: &DbContext) -> Self { + Self { ptr: value } + } +} + +#[allow(clippy::from_over_into)] +impl From<&mut DbContext> for MVCCDatabaseRef { + fn from(value: &mut DbContext) -> Self { + Self { ptr: value } + } +} + +pub struct DbContext { + pub(crate) db: Db, + pub(crate) runtime: tokio::runtime::Runtime, +} From 779ad3066a38cdfc88c3ca43c0cd46036b4087c2 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Tue, 9 May 2023 10:41:13 +0300 Subject: [PATCH 060/128] Add error codes to C bindings --- core/mvcc/bindings/c/src/errors.rs | 3 +++ core/mvcc/bindings/c/src/lib.rs | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 core/mvcc/bindings/c/src/errors.rs diff --git a/core/mvcc/bindings/c/src/errors.rs b/core/mvcc/bindings/c/src/errors.rs new file mode 100644 index 000000000..e260394f6 --- /dev/null +++ b/core/mvcc/bindings/c/src/errors.rs @@ -0,0 +1,3 @@ +pub const MVCC_OK: i32 = 0; + +pub const MVCC_IO_ERROR_WRITE: i32 = 778; diff --git a/core/mvcc/bindings/c/src/lib.rs b/core/mvcc/bindings/c/src/lib.rs index 62b067c81..6f4fc755a 100644 --- a/core/mvcc/bindings/c/src/lib.rs +++ b/core/mvcc/bindings/c/src/lib.rs @@ -1,8 +1,10 @@ #![allow(non_camel_case_types)] +mod errors; mod types; use types::{MVCCDatabaseRef, DbContext}; +use errors::*; use mvcc_rs::*; /// cbindgen:ignore @@ -81,11 +83,11 @@ pub unsafe extern "C" fn MVCCDatabaseInsert( }) { Ok(_) => { tracing::debug!("MVCCDatabaseInsert: success"); - 0 // SQLITE_OK + MVCC_OK } Err(e) => { tracing::error!("MVCCDatabaseInsert: {e}"); - 778 // SQLITE_IOERR_WRITE + MVCC_IO_ERROR_WRITE } } } From c94561a646b57a061c058c380c28f9c3a9a8fbb8 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Tue, 9 May 2023 10:41:33 +0300 Subject: [PATCH 061/128] Add `mvcc.h` to the tree --- core/mvcc/.gitignore | 1 - core/mvcc/bindings/c/include/mvcc.h | 26 ++++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 core/mvcc/bindings/c/include/mvcc.h diff --git a/core/mvcc/.gitignore b/core/mvcc/.gitignore index 48e69afa7..1e7caa9ea 100644 --- a/core/mvcc/.gitignore +++ b/core/mvcc/.gitignore @@ -1,3 +1,2 @@ Cargo.lock -bindings/c/include target/ diff --git a/core/mvcc/bindings/c/include/mvcc.h b/core/mvcc/bindings/c/include/mvcc.h new file mode 100644 index 000000000..92595e41b --- /dev/null +++ b/core/mvcc/bindings/c/include/mvcc.h @@ -0,0 +1,26 @@ +#ifndef MVCC_H +#define MVCC_H + +#define MVCC_OK 0 + +#define MVCC_IO_ERROR_WRITE 778 + +typedef struct DbContext DbContext; + +typedef const DbContext *MVCCDatabaseRef; + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +MVCCDatabaseRef MVCCDatabaseOpen(const char *path); + +void MVCCDatabaseClose(MVCCDatabaseRef db); + +int32_t MVCCDatabaseInsert(MVCCDatabaseRef db, uint64_t id, const uint8_t *value_ptr, uintptr_t value_len); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus + +#endif /* MVCC_H */ From 1fc99181f13f3765548c0ed05f68d47366e873f1 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Tue, 9 May 2023 10:42:05 +0300 Subject: [PATCH 062/128] Update README.md --- core/mvcc/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/core/mvcc/README.md b/core/mvcc/README.md index fa279fcba..e48c3ac45 100644 --- a/core/mvcc/README.md +++ b/core/mvcc/README.md @@ -7,6 +7,7 @@ The aim of the project is to provide a building block for implementing database * Main memory architecture, rows are accessed via an index * Optimistic multi-version concurrency control +* Rust and C APIs ## Experimental Evaluation From db1e313aca8cc22f0569307e99cdb02fcd076c2f Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Tue, 9 May 2023 10:43:42 +0300 Subject: [PATCH 063/128] Silence clippy --- core/mvcc/bindings/c/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/core/mvcc/bindings/c/src/lib.rs b/core/mvcc/bindings/c/src/lib.rs index 6f4fc755a..85f5a07df 100644 --- a/core/mvcc/bindings/c/src/lib.rs +++ b/core/mvcc/bindings/c/src/lib.rs @@ -1,4 +1,5 @@ #![allow(non_camel_case_types)] +#![allow(clippy::missing_safety_doc)] mod errors; mod types; From 3ecb0fb2a96844a949c0a245b81be36136a65580 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Tue, 9 May 2023 10:44:06 +0300 Subject: [PATCH 064/128] Mark MVCCDatabaseOpen() as unsafe --- core/mvcc/bindings/c/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/mvcc/bindings/c/src/lib.rs b/core/mvcc/bindings/c/src/lib.rs index 85f5a07df..6efecf8d5 100644 --- a/core/mvcc/bindings/c/src/lib.rs +++ b/core/mvcc/bindings/c/src/lib.rs @@ -23,7 +23,7 @@ type Db = database::Database>; static INIT_RUST_LOG: std::sync::Once = std::sync::Once::new(); #[no_mangle] -pub extern "C" fn MVCCDatabaseOpen(path: *const std::ffi::c_char) -> MVCCDatabaseRef { +pub unsafe extern "C" fn MVCCDatabaseOpen(path: *const std::ffi::c_char) -> MVCCDatabaseRef { INIT_RUST_LOG.call_once(|| { tracing_subscriber::fmt::init(); }); From b2f46e156bdd6afa4472cb68bc68705263d922d5 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Tue, 9 May 2023 10:46:56 +0300 Subject: [PATCH 065/128] Improve C binding error reporting --- core/mvcc/bindings/c/include/mvcc.h | 9 +++++---- core/mvcc/bindings/c/src/errors.rs | 8 +++++--- core/mvcc/bindings/c/src/lib.rs | 10 +++++----- core/mvcc/database/src/lib.rs | 2 +- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/core/mvcc/bindings/c/include/mvcc.h b/core/mvcc/bindings/c/include/mvcc.h index 92595e41b..808c4047e 100644 --- a/core/mvcc/bindings/c/include/mvcc.h +++ b/core/mvcc/bindings/c/include/mvcc.h @@ -1,9 +1,10 @@ #ifndef MVCC_H #define MVCC_H -#define MVCC_OK 0 - -#define MVCC_IO_ERROR_WRITE 778 +typedef enum { + MVCC_OK = 0, + MVCC_IO_ERROR_WRITE = 778, +} MVCCError; typedef struct DbContext DbContext; @@ -17,7 +18,7 @@ MVCCDatabaseRef MVCCDatabaseOpen(const char *path); void MVCCDatabaseClose(MVCCDatabaseRef db); -int32_t MVCCDatabaseInsert(MVCCDatabaseRef db, uint64_t id, const uint8_t *value_ptr, uintptr_t value_len); +MVCCError MVCCDatabaseInsert(MVCCDatabaseRef db, uint64_t id, const uint8_t *value_ptr, uintptr_t value_len); #ifdef __cplusplus } // extern "C" diff --git a/core/mvcc/bindings/c/src/errors.rs b/core/mvcc/bindings/c/src/errors.rs index e260394f6..8b40ad2ba 100644 --- a/core/mvcc/bindings/c/src/errors.rs +++ b/core/mvcc/bindings/c/src/errors.rs @@ -1,3 +1,5 @@ -pub const MVCC_OK: i32 = 0; - -pub const MVCC_IO_ERROR_WRITE: i32 = 778; +#[repr(C)] +pub enum MVCCError { + MVCC_OK = 0, + MVCC_IO_ERROR_WRITE = 778, +} diff --git a/core/mvcc/bindings/c/src/lib.rs b/core/mvcc/bindings/c/src/lib.rs index 6efecf8d5..2a429cbdb 100644 --- a/core/mvcc/bindings/c/src/lib.rs +++ b/core/mvcc/bindings/c/src/lib.rs @@ -4,9 +4,9 @@ mod errors; mod types; -use types::{MVCCDatabaseRef, DbContext}; -use errors::*; +use errors::MVCCError; use mvcc_rs::*; +use types::{DbContext, MVCCDatabaseRef}; /// cbindgen:ignore type Clock = clock::LocalClock; @@ -63,7 +63,7 @@ pub unsafe extern "C" fn MVCCDatabaseInsert( id: u64, value_ptr: *const u8, value_len: usize, -) -> i32 { +) -> MVCCError { let db = db.get_ref(); let value = std::slice::from_raw_parts(value_ptr, value_len); let data = match std::str::from_utf8(value) { @@ -84,11 +84,11 @@ pub unsafe extern "C" fn MVCCDatabaseInsert( }) { Ok(_) => { tracing::debug!("MVCCDatabaseInsert: success"); - MVCC_OK + MVCCError::MVCC_OK } Err(e) => { tracing::error!("MVCCDatabaseInsert: {e}"); - MVCC_IO_ERROR_WRITE + MVCCError::MVCC_IO_ERROR_WRITE } } } diff --git a/core/mvcc/database/src/lib.rs b/core/mvcc/database/src/lib.rs index b14e27be3..d88011290 100644 --- a/core/mvcc/database/src/lib.rs +++ b/core/mvcc/database/src/lib.rs @@ -35,4 +35,4 @@ pub mod clock; pub mod database; pub mod errors; pub mod persistent_storage; -pub mod sync; \ No newline at end of file +pub mod sync; From 5ce2bc41f9209db71d78c4437ea42f18a124149a Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Tue, 9 May 2023 10:47:43 +0300 Subject: [PATCH 066/128] cargo fmt --- core/mvcc/bindings/c/build.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/mvcc/bindings/c/build.rs b/core/mvcc/bindings/c/build.rs index af21fb9d8..f418d0a9a 100644 --- a/core/mvcc/bindings/c/build.rs +++ b/core/mvcc/bindings/c/build.rs @@ -2,5 +2,7 @@ use std::path::Path; fn main() { let header_file = Path::new("include").join("mvcc.h"); - cbindgen::generate(".").expect("Failed to generate C bindings").write_to_file(header_file); + cbindgen::generate(".") + .expect("Failed to generate C bindings") + .write_to_file(header_file); } From 41bed4154415ec47a3bef8c2c6b607144ef3cc2d Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Tue, 9 May 2023 10:51:14 +0300 Subject: [PATCH 067/128] Improve logging --- core/mvcc/bindings/c/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/core/mvcc/bindings/c/src/lib.rs b/core/mvcc/bindings/c/src/lib.rs index 2a429cbdb..6eeffdeca 100644 --- a/core/mvcc/bindings/c/src/lib.rs +++ b/core/mvcc/bindings/c/src/lib.rs @@ -52,6 +52,7 @@ pub unsafe extern "C" fn MVCCDatabaseOpen(path: *const std::ffi::c_char) -> MVCC pub unsafe extern "C" fn MVCCDatabaseClose(db: MVCCDatabaseRef) { tracing::debug!("MVCCDatabaseClose"); if db.is_null() { + tracing::debug!("warning: `db` is null in MVCCDatabaseClose()"); return; } let _ = unsafe { Box::from_raw(db.get_ref_mut()) }; From b9575c437574195717432013e10ef1fa4a190074 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Tue, 9 May 2023 10:53:11 +0300 Subject: [PATCH 068/128] Rename database directory to mvcc-rs --- core/mvcc/Cargo.toml | 4 ++-- core/mvcc/bindings/c/Cargo.toml | 2 +- core/mvcc/{database => mvcc-rs}/Cargo.toml | 0 core/mvcc/{database => mvcc-rs}/benches/my_benchmark.rs | 0 core/mvcc/{database => mvcc-rs}/src/clock.rs | 0 core/mvcc/{database => mvcc-rs}/src/database.rs | 0 core/mvcc/{database => mvcc-rs}/src/errors.rs | 0 core/mvcc/{database => mvcc-rs}/src/lib.rs | 0 core/mvcc/{database => mvcc-rs}/src/persistent_storage.rs | 0 core/mvcc/{database => mvcc-rs}/src/sync.rs | 0 core/mvcc/{database => mvcc-rs}/tests/concurrency_test.rs | 0 11 files changed, 3 insertions(+), 3 deletions(-) rename core/mvcc/{database => mvcc-rs}/Cargo.toml (100%) rename core/mvcc/{database => mvcc-rs}/benches/my_benchmark.rs (100%) rename core/mvcc/{database => mvcc-rs}/src/clock.rs (100%) rename core/mvcc/{database => mvcc-rs}/src/database.rs (100%) rename core/mvcc/{database => mvcc-rs}/src/errors.rs (100%) rename core/mvcc/{database => mvcc-rs}/src/lib.rs (100%) rename core/mvcc/{database => mvcc-rs}/src/persistent_storage.rs (100%) rename core/mvcc/{database => mvcc-rs}/src/sync.rs (100%) rename core/mvcc/{database => mvcc-rs}/tests/concurrency_test.rs (100%) diff --git a/core/mvcc/Cargo.toml b/core/mvcc/Cargo.toml index c9e8d84ba..7ebb0ebff 100644 --- a/core/mvcc/Cargo.toml +++ b/core/mvcc/Cargo.toml @@ -1,11 +1,11 @@ [workspace] resolver = "2" members = [ - "database", + "mvcc-rs", "bindings/c", ] [profile.release] codegen-units = 1 panic = "abort" -strip = true \ No newline at end of file +strip = true diff --git a/core/mvcc/bindings/c/Cargo.toml b/core/mvcc/bindings/c/Cargo.toml index a54329eca..ff70199b8 100644 --- a/core/mvcc/bindings/c/Cargo.toml +++ b/core/mvcc/bindings/c/Cargo.toml @@ -12,7 +12,7 @@ cbindgen = "0.24.0" [dependencies] base64 = "0.21.0" -mvcc-rs = { path = "../../database", features = ["tokio"] } +mvcc-rs = { path = "../../mvcc-rs", features = ["tokio"] } tokio = { version = "1.27.0", features = ["full"] } tracing = "0.1.37" tracing-subscriber = { version = "0" } diff --git a/core/mvcc/database/Cargo.toml b/core/mvcc/mvcc-rs/Cargo.toml similarity index 100% rename from core/mvcc/database/Cargo.toml rename to core/mvcc/mvcc-rs/Cargo.toml diff --git a/core/mvcc/database/benches/my_benchmark.rs b/core/mvcc/mvcc-rs/benches/my_benchmark.rs similarity index 100% rename from core/mvcc/database/benches/my_benchmark.rs rename to core/mvcc/mvcc-rs/benches/my_benchmark.rs diff --git a/core/mvcc/database/src/clock.rs b/core/mvcc/mvcc-rs/src/clock.rs similarity index 100% rename from core/mvcc/database/src/clock.rs rename to core/mvcc/mvcc-rs/src/clock.rs diff --git a/core/mvcc/database/src/database.rs b/core/mvcc/mvcc-rs/src/database.rs similarity index 100% rename from core/mvcc/database/src/database.rs rename to core/mvcc/mvcc-rs/src/database.rs diff --git a/core/mvcc/database/src/errors.rs b/core/mvcc/mvcc-rs/src/errors.rs similarity index 100% rename from core/mvcc/database/src/errors.rs rename to core/mvcc/mvcc-rs/src/errors.rs diff --git a/core/mvcc/database/src/lib.rs b/core/mvcc/mvcc-rs/src/lib.rs similarity index 100% rename from core/mvcc/database/src/lib.rs rename to core/mvcc/mvcc-rs/src/lib.rs diff --git a/core/mvcc/database/src/persistent_storage.rs b/core/mvcc/mvcc-rs/src/persistent_storage.rs similarity index 100% rename from core/mvcc/database/src/persistent_storage.rs rename to core/mvcc/mvcc-rs/src/persistent_storage.rs diff --git a/core/mvcc/database/src/sync.rs b/core/mvcc/mvcc-rs/src/sync.rs similarity index 100% rename from core/mvcc/database/src/sync.rs rename to core/mvcc/mvcc-rs/src/sync.rs diff --git a/core/mvcc/database/tests/concurrency_test.rs b/core/mvcc/mvcc-rs/tests/concurrency_test.rs similarity index 100% rename from core/mvcc/database/tests/concurrency_test.rs rename to core/mvcc/mvcc-rs/tests/concurrency_test.rs From f47eea1a72291467552c77fa939d051cc62375c8 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Tue, 9 May 2023 10:59:43 +0300 Subject: [PATCH 069/128] Move DESIGN.md to docs --- core/mvcc/{ => docs}/DESIGN.md | 0 core/mvcc/{ => docs}/figures/mutations.excalidraw | 0 core/mvcc/{ => docs}/figures/mutations.png | Bin 3 files changed, 0 insertions(+), 0 deletions(-) rename core/mvcc/{ => docs}/DESIGN.md (100%) rename core/mvcc/{ => docs}/figures/mutations.excalidraw (100%) rename core/mvcc/{ => docs}/figures/mutations.png (100%) diff --git a/core/mvcc/DESIGN.md b/core/mvcc/docs/DESIGN.md similarity index 100% rename from core/mvcc/DESIGN.md rename to core/mvcc/docs/DESIGN.md diff --git a/core/mvcc/figures/mutations.excalidraw b/core/mvcc/docs/figures/mutations.excalidraw similarity index 100% rename from core/mvcc/figures/mutations.excalidraw rename to core/mvcc/docs/figures/mutations.excalidraw diff --git a/core/mvcc/figures/mutations.png b/core/mvcc/docs/figures/mutations.png similarity index 100% rename from core/mvcc/figures/mutations.png rename to core/mvcc/docs/figures/mutations.png From d4da54b10bc33f64a77ddfb4a78da414d27f696d Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Tue, 9 May 2023 10:28:26 +0200 Subject: [PATCH 070/128] mvcc: build static library for C bindings That's how it's currently consumed by libSQL. --- core/mvcc/bindings/c/Cargo.toml | 2 +- core/mvcc/mvcc-rs/Cargo.toml | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/core/mvcc/bindings/c/Cargo.toml b/core/mvcc/bindings/c/Cargo.toml index ff70199b8..6fa92a9dc 100644 --- a/core/mvcc/bindings/c/Cargo.toml +++ b/core/mvcc/bindings/c/Cargo.toml @@ -4,7 +4,7 @@ version = "0.0.0" edition = "2021" [lib] -crate-type = ["cdylib"] +crate-type = ["cdylib", "staticlib"] doc = false [build-dependencies] diff --git a/core/mvcc/mvcc-rs/Cargo.toml b/core/mvcc/mvcc-rs/Cargo.toml index 0c29c664d..9b6bd5d9d 100644 --- a/core/mvcc/mvcc-rs/Cargo.toml +++ b/core/mvcc/mvcc-rs/Cargo.toml @@ -26,9 +26,6 @@ tracing-subscriber = "0" tracing-test = "0" mvcc-rs = { path = ".", features = ["tokio"] } -[lib] -crate-type = ["rlib", "cdylib", "staticlib"] - [[bench]] name = "my_benchmark" harness = false From 3d0c8a415e5ffc22a918a97a8e52fb4c1c626e35 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Tue, 9 May 2023 13:49:14 +0300 Subject: [PATCH 071/128] Add stdint.h include to mvcc.h --- core/mvcc/bindings/c/cbindgen.toml | 1 + core/mvcc/bindings/c/include/mvcc.h | 2 ++ 2 files changed, 3 insertions(+) diff --git a/core/mvcc/bindings/c/cbindgen.toml b/core/mvcc/bindings/c/cbindgen.toml index b530dce1d..1b5ac2f31 100644 --- a/core/mvcc/bindings/c/cbindgen.toml +++ b/core/mvcc/bindings/c/cbindgen.toml @@ -4,3 +4,4 @@ include_guard = "MVCC_H" line_length = 120 no_includes = true style = "type" +sys_includes = ["stdint.h"] diff --git a/core/mvcc/bindings/c/include/mvcc.h b/core/mvcc/bindings/c/include/mvcc.h index 808c4047e..eb1ed5352 100644 --- a/core/mvcc/bindings/c/include/mvcc.h +++ b/core/mvcc/bindings/c/include/mvcc.h @@ -1,6 +1,8 @@ #ifndef MVCC_H #define MVCC_H +#include + typedef enum { MVCC_OK = 0, MVCC_IO_ERROR_WRITE = 778, From cf31de5d41ebe5e01b1bade5b7e35df036882afc Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Tue, 9 May 2023 14:42:07 +0300 Subject: [PATCH 072/128] Fix MVCCDatabaseInsert() type signature Values are opaque blobs so use "const void *" for C callers. --- core/mvcc/bindings/c/include/mvcc.h | 2 +- core/mvcc/bindings/c/src/lib.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/mvcc/bindings/c/include/mvcc.h b/core/mvcc/bindings/c/include/mvcc.h index eb1ed5352..aea8beb23 100644 --- a/core/mvcc/bindings/c/include/mvcc.h +++ b/core/mvcc/bindings/c/include/mvcc.h @@ -20,7 +20,7 @@ MVCCDatabaseRef MVCCDatabaseOpen(const char *path); void MVCCDatabaseClose(MVCCDatabaseRef db); -MVCCError MVCCDatabaseInsert(MVCCDatabaseRef db, uint64_t id, const uint8_t *value_ptr, uintptr_t value_len); +MVCCError MVCCDatabaseInsert(MVCCDatabaseRef db, uint64_t id, const void *value_ptr, uintptr_t value_len); #ifdef __cplusplus } // extern "C" diff --git a/core/mvcc/bindings/c/src/lib.rs b/core/mvcc/bindings/c/src/lib.rs index 6eeffdeca..2f2529cb2 100644 --- a/core/mvcc/bindings/c/src/lib.rs +++ b/core/mvcc/bindings/c/src/lib.rs @@ -62,11 +62,11 @@ pub unsafe extern "C" fn MVCCDatabaseClose(db: MVCCDatabaseRef) { pub unsafe extern "C" fn MVCCDatabaseInsert( db: MVCCDatabaseRef, id: u64, - value_ptr: *const u8, + value_ptr: *const std::ffi::c_void, value_len: usize, ) -> MVCCError { let db = db.get_ref(); - let value = std::slice::from_raw_parts(value_ptr, value_len); + let value = std::slice::from_raw_parts(value_ptr as *const u8, value_len); let data = match std::str::from_utf8(value) { Ok(value) => value.to_string(), Err(_) => { From 8c56b381c06d3da2b5d6863d5a306f18b372de3a Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Tue, 9 May 2023 21:28:42 +0300 Subject: [PATCH 073/128] Rename mutations to log records The Hekaton paper talks about "log records" so let's just run with that terminology to avoid confusion. --- core/mvcc/docs/DESIGN.md | 18 ++++++------ core/mvcc/mvcc-rs/src/database.rs | 32 ++++++++++++--------- core/mvcc/mvcc-rs/src/persistent_storage.rs | 15 +++++----- 3 files changed, 36 insertions(+), 29 deletions(-) diff --git a/core/mvcc/docs/DESIGN.md b/core/mvcc/docs/DESIGN.md index cae0d19d9..37943d992 100644 --- a/core/mvcc/docs/DESIGN.md +++ b/core/mvcc/docs/DESIGN.md @@ -2,18 +2,18 @@ ## Persistent storage -Persistent storage must implement the `Storage` trait that the MVCC module uses to essentially store a write-ahead log (WAL) of mutations. +Persistent storage must implement the `Storage` trait that the MVCC module uses for transaction logging. Figure 1 shows an example of write-ahead log across three transactions. -The first transaction T0 executes a `INSERT (id) VALUES (1)` statement, which results in a mutation with `id` set to `1`, begin timestamp to 0 (which is the transaction ID) and end timestamp as infinity (meaning the row version is still visible). -The second transaction T1 executes another `INSERT` statement, which adds another mutation to the WAL with `id` set to `2`, begin timesstamp to 1 and end timestamp as infinity, similar to what T0 did. -Finally, a third transaction T2 executes two statements: `DELETE WHERE id = 1` and `INSERT (id) VALUES (3)`. The first one results in a mutation with `id` set to `1` and begin timestamp set to 0 (which is the transaction that created the entry). However, the end timestamp is now set to 2 (the current transaction), which means the entry is now deleted. -The second statement results in an entry in the WAL similar to the `INSERT` statements in T0 and T1. +The first transaction T0 executes a `INSERT (id) VALUES (1)` statement, which results in a log record with `id` set to `1`, begin timestamp to 0 (which is the transaction ID) and end timestamp as infinity (meaning the row version is still visible). +The second transaction T1 executes another `INSERT` statement, which adds another log record to the transaction log with `id` set to `2`, begin timesstamp to 1 and end timestamp as infinity, similar to what T0 did. +Finally, a third transaction T2 executes two statements: `DELETE WHERE id = 1` and `INSERT (id) VALUES (3)`. The first one results in a log record with `id` set to `1` and begin timestamp set to 0 (which is the transaction that created the entry). However, the end timestamp is now set to 2 (the current transaction), which means the entry is now deleted. +The second statement results in an entry in the transaction log similar to the `INSERT` statements in T0 and T1. -![Mutations](figures/mutations.png) +![Transactions](figures/transactions.png)

-Figure 1. Write-ahead log of mutations across three transactions. +Figure 1. Transaction log of three transactions.

-When MVCC bootstraps or recovers, it simply reads the write-ahead log into the in-memory index, and it's good to go. -If the WAL grows big, we can compact it by dropping all entries that are no longer visible after the the latest transaction. +When MVCC bootstraps or recovers, it simply redos the transaction log. +If the transaction log grows big, we can checkpoint it it by dropping all entries that are no longer visible after the the latest transaction and create a snapshot. diff --git a/core/mvcc/mvcc-rs/src/database.rs b/core/mvcc/mvcc-rs/src/database.rs index dccfdcd17..329910237 100644 --- a/core/mvcc/mvcc-rs/src/database.rs +++ b/core/mvcc/mvcc-rs/src/database.rs @@ -24,13 +24,14 @@ pub struct RowVersion { pub type TxID = u64; +/// A log record contains all the versions inserted and deleted by a transaction. #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Mutation { +pub struct LogRecord { tx_id: TxID, row_versions: Vec, } -impl Mutation { +impl LogRecord { fn new(tx_id: TxID) -> Self { Self { tx_id, @@ -271,10 +272,15 @@ impl< } #[cfg(test)] - pub(crate) async fn scan_storage(&self) -> Result> { + pub(crate) async fn scan_storage(&self) -> Result> { use futures::StreamExt; let inner = self.inner.lock().await; - Ok(inner.storage.scan().await?.collect::>().await) + Ok(inner + .storage + .scan() + .await? + .collect::>() + .await) } } @@ -382,20 +388,20 @@ impl let mut rows = self.rows.borrow_mut(); tx.state = TransactionState::Preparing; tracing::trace!("PREPARE {tx}"); - let mut mutation: Mutation = Mutation::new(tx_id); + let mut log_record: LogRecord = LogRecord::new(tx_id); for id in &tx.write_set { if let Some(row_versions) = rows.get_mut(id) { for row_version in row_versions.iter_mut() { if let TxTimestampOrID::TxID(id) = row_version.begin { if id == tx_id { row_version.begin = TxTimestampOrID::Timestamp(tx.begin_ts); - mutation.row_versions.push(row_version.clone()); // FIXME: optimize cloning out + log_record.row_versions.push(row_version.clone()); // FIXME: optimize cloning out } } if let Some(TxTimestampOrID::TxID(id)) = row_version.end { if id == tx_id { row_version.end = Some(TxTimestampOrID::Timestamp(end_ts)); - mutation.row_versions.push(row_version.clone()); // FIXME: optimize cloning out + log_record.row_versions.push(row_version.clone()); // FIXME: optimize cloning out } } } @@ -419,8 +425,8 @@ impl txs.remove(&tx_id); drop(rows); drop(txs); - if !mutation.row_versions.is_empty() { - self.storage.store(mutation).await?; + if !log_record.row_versions.is_empty() { + self.storage.log_tx(log_record).await?; } Ok(()) } @@ -949,12 +955,12 @@ mod tests { .await .unwrap(); - let mutation = db.scan_storage().await.unwrap(); - println!("{:?}", mutation); + let log_record = db.scan_storage().await.unwrap(); + println!("{:?}", log_record); db.commit_tx(tx4).await.unwrap(); - let mutation = db.scan_storage().await.unwrap(); - println!("{:?}", mutation); + let log_record = db.scan_storage().await.unwrap(); + println!("{:?}", log_record); } } diff --git a/core/mvcc/mvcc-rs/src/persistent_storage.rs b/core/mvcc/mvcc-rs/src/persistent_storage.rs index 98b1fbd20..380dd2413 100644 --- a/core/mvcc/mvcc-rs/src/persistent_storage.rs +++ b/core/mvcc/mvcc-rs/src/persistent_storage.rs @@ -1,12 +1,13 @@ -use crate::database::{Mutation, Result}; +use crate::database::{LogRecord, Result}; /// Persistent storage API for storing and retrieving transactions. /// TODO: final design in heavy progress! #[async_trait::async_trait] pub trait Storage { - type Stream: futures::stream::Stream; + type Stream: futures::stream::Stream; + + async fn log_tx(&mut self, m: LogRecord) -> Result<()>; - async fn store(&mut self, m: Mutation) -> Result<()>; async fn scan(&self) -> Result; } @@ -14,9 +15,9 @@ pub struct Noop {} #[async_trait::async_trait] impl Storage for Noop { - type Stream = futures::stream::Empty; + type Stream = futures::stream::Empty; - async fn store(&mut self, _m: Mutation) -> Result<()> { + async fn log_tx(&mut self, _m: LogRecord) -> Result<()> { Ok(()) } @@ -45,7 +46,7 @@ pub struct JsonOnDiskStream { #[cfg(feature = "tokio")] impl futures::stream::Stream for JsonOnDiskStream { - type Item = Mutation; + type Item = LogRecord; fn poll_next( self: std::pin::Pin<&mut Self>, @@ -63,7 +64,7 @@ impl futures::stream::Stream for JsonOnDiskStream { impl Storage for JsonOnDisk { type Stream = JsonOnDiskStream; - async fn store(&mut self, m: Mutation) -> Result<()> { + async fn log_tx(&mut self, m: LogRecord) -> Result<()> { use crate::errors::DatabaseError; use tokio::io::AsyncWriteExt; let t = serde_json::to_vec(&m).map_err(|e| DatabaseError::Io(e.to_string()))?; From ec336d5578933371bacf6468d4b77857f7a2516d Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 10 May 2023 12:48:05 +0300 Subject: [PATCH 074/128] Initial support for recovery We have the transaction log in persistent storage so let's implement recovery by replaying that. --- core/mvcc/mvcc-rs/src/clock.rs | 5 ++ core/mvcc/mvcc-rs/src/database.rs | 60 ++++++++++++++------- core/mvcc/mvcc-rs/src/persistent_storage.rs | 9 ++-- 3 files changed, 52 insertions(+), 22 deletions(-) diff --git a/core/mvcc/mvcc-rs/src/clock.rs b/core/mvcc/mvcc-rs/src/clock.rs index e6ef0dfc4..7bab1fe5d 100644 --- a/core/mvcc/mvcc-rs/src/clock.rs +++ b/core/mvcc/mvcc-rs/src/clock.rs @@ -3,6 +3,7 @@ use std::sync::atomic::{AtomicU64, Ordering}; /// Logical clock. pub trait LogicalClock { fn get_timestamp(&self) -> u64; + fn reset(&self, ts: u64); } /// A node-local clock backed by an atomic counter. @@ -23,4 +24,8 @@ impl LogicalClock for LocalClock { fn get_timestamp(&self) -> u64 { self.ts_sequence.fetch_add(1, Ordering::SeqCst) } + + fn reset(&self, ts: u64) { + self.ts_sequence.store(ts, Ordering::SeqCst); + } } diff --git a/core/mvcc/mvcc-rs/src/database.rs b/core/mvcc/mvcc-rs/src/database.rs index 329910237..42d0d1588 100644 --- a/core/mvcc/mvcc-rs/src/database.rs +++ b/core/mvcc/mvcc-rs/src/database.rs @@ -27,14 +27,14 @@ pub type TxID = u64; /// A log record contains all the versions inserted and deleted by a transaction. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct LogRecord { - tx_id: TxID, + tx_timestamp: TxID, row_versions: Vec, } impl LogRecord { - fn new(tx_id: TxID) -> Self { + fn new(tx_timestamp: TxID) -> Self { Self { - tx_id, + tx_timestamp, row_versions: Vec::new(), } } @@ -271,16 +271,9 @@ impl< inner.drop_unused_row_versions(); } - #[cfg(test)] - pub(crate) async fn scan_storage(&self) -> Result> { - use futures::StreamExt; + pub async fn recover(&self) -> Result<()> { let inner = self.inner.lock().await; - Ok(inner - .storage - .scan() - .await? - .collect::>() - .await) + inner.recover().await } } @@ -388,7 +381,7 @@ impl let mut rows = self.rows.borrow_mut(); tx.state = TransactionState::Preparing; tracing::trace!("PREPARE {tx}"); - let mut log_record: LogRecord = LogRecord::new(tx_id); + let mut log_record: LogRecord = LogRecord::new(end_ts); for id in &tx.write_set { if let Some(row_versions) = rows.get_mut(id) { for row_version in row_versions.iter_mut() { @@ -505,6 +498,26 @@ impl rows.remove(&id); } } + + pub async fn recover(&self) -> Result<()> { + use futures::StreamExt; + let tx_log = self + .storage + .read_tx_log() + .await? + .collect::>() + .await; + for record in tx_log { + println!("RECOVERING {:?}", record); + for version in record.row_versions { + let mut rows = self.rows.borrow_mut(); + let row_versions = rows.entry(version.row.id).or_insert_with(Vec::new); + row_versions.push(version); + } + self.clock.reset(record.tx_timestamp); + } + Ok(()) + } } /// A write-write conflict happens when transaction T_m attempts to update a @@ -914,7 +927,7 @@ mod tests { .unwrap() .as_nanos(), )); - let storage = crate::persistent_storage::JsonOnDisk { path }; + let storage = crate::persistent_storage::JsonOnDisk { path: path.clone() }; let db: Database<_, _, tokio::sync::Mutex<_>> = Database::new(clock, storage); let tx1 = db.begin_tx().await; @@ -955,12 +968,21 @@ mod tests { .await .unwrap(); - let log_record = db.scan_storage().await.unwrap(); - println!("{:?}", log_record); - + assert_eq!(db.read(tx4, 1).await.unwrap().unwrap().data, "testme"); + assert_eq!(db.read(tx4, 2).await.unwrap().unwrap().data, "testme2"); + assert_eq!(db.read(tx4, 3).await.unwrap().unwrap().data, "testme3"); db.commit_tx(tx4).await.unwrap(); - let log_record = db.scan_storage().await.unwrap(); - println!("{:?}", log_record); + let clock = LocalClock::new(); + let storage = crate::persistent_storage::JsonOnDisk { path }; + let db: Database<_, _, tokio::sync::Mutex<_>> = Database::new(clock, storage); + db.recover().await.unwrap(); + println!("{:#?}", db); + + let tx5 = db.begin_tx().await; + println!("{:#?}", db.read(tx5, 1).await); + assert_eq!(db.read(tx5, 1).await.unwrap().unwrap().data, "testme"); + assert_eq!(db.read(tx5, 2).await.unwrap().unwrap().data, "testme2"); + assert_eq!(db.read(tx5, 3).await.unwrap().unwrap().data, "testme3"); } } diff --git a/core/mvcc/mvcc-rs/src/persistent_storage.rs b/core/mvcc/mvcc-rs/src/persistent_storage.rs index 380dd2413..2277e4d2c 100644 --- a/core/mvcc/mvcc-rs/src/persistent_storage.rs +++ b/core/mvcc/mvcc-rs/src/persistent_storage.rs @@ -6,9 +6,11 @@ use crate::database::{LogRecord, Result}; pub trait Storage { type Stream: futures::stream::Stream; + /// Append a transaction in the transaction log. async fn log_tx(&mut self, m: LogRecord) -> Result<()>; - async fn scan(&self) -> Result; + /// Read the transaction log for replay. + async fn read_tx_log(&self) -> Result; } pub struct Noop {} @@ -21,11 +23,12 @@ impl Storage for Noop { Ok(()) } - async fn scan(&self) -> Result { + async fn read_tx_log(&self) -> Result { Ok(futures::stream::empty()) } } +#[derive(Debug)] pub struct JsonOnDisk { pub path: std::path::PathBuf, } @@ -83,7 +86,7 @@ impl Storage for JsonOnDisk { Ok(()) } - async fn scan(&self) -> Result { + async fn read_tx_log(&self) -> Result { use tokio::io::AsyncBufReadExt; let file = tokio::fs::OpenOptions::new() .read(true) From 08adf2118c828b0b56a9ef8865bbe4a8a3e82035 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 10 May 2023 13:01:33 +0300 Subject: [PATCH 075/128] Rename mutations.png to transactions.png --- ...mutations.excalidraw => transactions.excalidraw} | 0 .../figures/{mutations.png => transactions.png} | Bin 2 files changed, 0 insertions(+), 0 deletions(-) rename core/mvcc/docs/figures/{mutations.excalidraw => transactions.excalidraw} (100%) rename core/mvcc/docs/figures/{mutations.png => transactions.png} (100%) diff --git a/core/mvcc/docs/figures/mutations.excalidraw b/core/mvcc/docs/figures/transactions.excalidraw similarity index 100% rename from core/mvcc/docs/figures/mutations.excalidraw rename to core/mvcc/docs/figures/transactions.excalidraw diff --git a/core/mvcc/docs/figures/mutations.png b/core/mvcc/docs/figures/transactions.png similarity index 100% rename from core/mvcc/docs/figures/mutations.png rename to core/mvcc/docs/figures/transactions.png From d047a24a32ef5653e4ea9ae1c8cacd93b6ad0bb2 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Wed, 10 May 2023 12:09:06 +0200 Subject: [PATCH 076/128] bindings: expose reading from the database The results are returned as a CString for now. --- core/mvcc/bindings/c/include/mvcc.h | 5 +++ core/mvcc/bindings/c/src/errors.rs | 1 + core/mvcc/bindings/c/src/lib.rs | 50 +++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/core/mvcc/bindings/c/include/mvcc.h b/core/mvcc/bindings/c/include/mvcc.h index aea8beb23..b30f1cf4a 100644 --- a/core/mvcc/bindings/c/include/mvcc.h +++ b/core/mvcc/bindings/c/include/mvcc.h @@ -5,6 +5,7 @@ typedef enum { MVCC_OK = 0, + MVCC_IO_ERROR_READ = 266, MVCC_IO_ERROR_WRITE = 778, } MVCCError; @@ -22,6 +23,10 @@ void MVCCDatabaseClose(MVCCDatabaseRef db); MVCCError MVCCDatabaseInsert(MVCCDatabaseRef db, uint64_t id, const void *value_ptr, uintptr_t value_len); +MVCCError MVCCDatabaseRead(MVCCDatabaseRef db, uint64_t id, char **value_ptr, int64_t *value_len); + +void MVCCFreeStr(void *ptr); + #ifdef __cplusplus } // extern "C" #endif // __cplusplus diff --git a/core/mvcc/bindings/c/src/errors.rs b/core/mvcc/bindings/c/src/errors.rs index 8b40ad2ba..65a174b50 100644 --- a/core/mvcc/bindings/c/src/errors.rs +++ b/core/mvcc/bindings/c/src/errors.rs @@ -1,5 +1,6 @@ #[repr(C)] pub enum MVCCError { MVCC_OK = 0, + MVCC_IO_ERROR_READ = 266, MVCC_IO_ERROR_WRITE = 778, } diff --git a/core/mvcc/bindings/c/src/lib.rs b/core/mvcc/bindings/c/src/lib.rs index 2f2529cb2..f995be1ea 100644 --- a/core/mvcc/bindings/c/src/lib.rs +++ b/core/mvcc/bindings/c/src/lib.rs @@ -93,3 +93,53 @@ pub unsafe extern "C" fn MVCCDatabaseInsert( } } } + +#[no_mangle] +pub unsafe extern "C" fn MVCCDatabaseRead( + db: MVCCDatabaseRef, + id: u64, + value_ptr: *mut *mut std::ffi::c_char, + value_len: *mut i64, +) -> MVCCError { + let db = db.get_ref(); + let (db, runtime) = (&db.db, &db.runtime); + + match runtime.block_on(async move { + let tx = db.begin_tx().await; + let maybe_row = db.read(tx, id).await?; + match maybe_row { + Some(row) => { + tracing::debug!("Found row {row:?}"); + let str_len = row.data.len() + 1; + let value = std::ffi::CString::new(row.data.as_bytes()).map_err(|e| { + mvcc_rs::errors::DatabaseError::Io(format!( + "Failed to transform read data into CString: {e}" + )) + })?; + unsafe { + *value_ptr = value.into_raw(); + *value_len = str_len as i64; + } + } + None => unsafe { *value_len = -1 }, + }; + Ok::<(), mvcc_rs::errors::DatabaseError>(()) + }) { + Ok(_) => { + tracing::debug!("MVCCDatabaseRead: success"); + MVCCError::MVCC_OK + } + Err(e) => { + tracing::error!("MVCCDatabaseRead: {e}"); + MVCCError::MVCC_IO_ERROR_READ + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn MVCCFreeStr(ptr: *mut std::ffi::c_void) { + if ptr.is_null() { + return; + } + let _ = std::ffi::CString::from_raw(ptr as *mut std::ffi::c_char); +} From ef097362ff32b4acd0e94870db3447dc3dcdc8e5 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Wed, 10 May 2023 14:42:13 +0200 Subject: [PATCH 077/128] mvcc, bindings: expose a scan cursor --- core/mvcc/bindings/c/include/mvcc.h | 12 ++++ core/mvcc/bindings/c/src/lib.rs | 99 ++++++++++++++++++++++++++++- core/mvcc/bindings/c/src/types.rs | 10 +++ core/mvcc/mvcc-rs/src/cursor.rs | 48 ++++++++++++++ core/mvcc/mvcc-rs/src/database.rs | 10 +++ core/mvcc/mvcc-rs/src/lib.rs | 1 + 6 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 core/mvcc/mvcc-rs/src/cursor.rs diff --git a/core/mvcc/bindings/c/include/mvcc.h b/core/mvcc/bindings/c/include/mvcc.h index b30f1cf4a..c12aa5a43 100644 --- a/core/mvcc/bindings/c/include/mvcc.h +++ b/core/mvcc/bindings/c/include/mvcc.h @@ -11,8 +11,12 @@ typedef enum { typedef struct DbContext DbContext; +typedef struct ScanCursorContext ScanCursorContext; + typedef const DbContext *MVCCDatabaseRef; +typedef ScanCursorContext *MVCCScanCursorRef; + #ifdef __cplusplus extern "C" { #endif // __cplusplus @@ -27,6 +31,14 @@ MVCCError MVCCDatabaseRead(MVCCDatabaseRef db, uint64_t id, char **value_ptr, in void MVCCFreeStr(void *ptr); +MVCCScanCursorRef MVCCScanCursorOpen(MVCCDatabaseRef db); + +void MVCCScanCursorClose(MVCCScanCursorRef cursor); + +MVCCError MVCCScanCursorRead(MVCCScanCursorRef cursor, char **value_ptr, int64_t *value_len); + +int MVCCScanCursorNext(MVCCScanCursorRef cursor); + #ifdef __cplusplus } // extern "C" #endif // __cplusplus diff --git a/core/mvcc/bindings/c/src/lib.rs b/core/mvcc/bindings/c/src/lib.rs index f995be1ea..7f3f1ea62 100644 --- a/core/mvcc/bindings/c/src/lib.rs +++ b/core/mvcc/bindings/c/src/lib.rs @@ -6,7 +6,7 @@ mod types; use errors::MVCCError; use mvcc_rs::*; -use types::{DbContext, MVCCDatabaseRef}; +use types::{DbContext, MVCCDatabaseRef, MVCCScanCursorRef, ScanCursorContext}; /// cbindgen:ignore type Clock = clock::LocalClock; @@ -20,6 +20,9 @@ type Inner = database::DatabaseInner; /// cbindgen:ignore type Db = database::Database>; +/// cbindgen:ignore +type ScanCursor = cursor::ScanCursor<'static, Clock, Storage, tokio::sync::Mutex>; + static INIT_RUST_LOG: std::sync::Once = std::sync::Once::new(); #[no_mangle] @@ -143,3 +146,97 @@ pub unsafe extern "C" fn MVCCFreeStr(ptr: *mut std::ffi::c_void) { } let _ = std::ffi::CString::from_raw(ptr as *mut std::ffi::c_char); } + +#[no_mangle] +pub unsafe extern "C" fn MVCCScanCursorOpen(db: MVCCDatabaseRef) -> MVCCScanCursorRef { + tracing::debug!("MVCCScanCursorOpen()"); + // Reference is transmuted to &'static in order to be able to pass the cursor back to C. + // The contract with C is to never use a cursor after MVCCDatabaseClose() has been called. + let database = unsafe { std::mem::transmute::<&DbContext, &'static DbContext>(db.get_ref()) }; + let (database, runtime) = (&database.db, &database.runtime); + match runtime.block_on(async move { mvcc_rs::cursor::ScanCursor::new(database).await }) { + Ok(cursor) => { + tracing::debug!("Cursor open: {cursor:?}"); + MVCCScanCursorRef { + ptr: Box::into_raw(Box::new(ScanCursorContext { cursor, db })), + } + } + Err(e) => { + tracing::error!("MVCCScanCursorOpen: {e}"); + MVCCScanCursorRef { + ptr: std::ptr::null_mut(), + } + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn MVCCScanCursorClose(cursor: MVCCScanCursorRef) { + tracing::debug!("MVCCScanCursorClose()"); + if cursor.ptr.is_null() { + tracing::debug!("warning: `cursor` is null in MVCCScanCursorClose()"); + return; + } + let _ = unsafe { Box::from_raw(cursor.ptr) }; +} + +#[no_mangle] +pub unsafe extern "C" fn MVCCScanCursorRead( + cursor: MVCCScanCursorRef, + value_ptr: *mut *mut std::ffi::c_char, + value_len: *mut i64, +) -> MVCCError { + tracing::debug!("MVCCScanCursorRead()"); + if cursor.ptr.is_null() { + tracing::debug!("warning: `cursor` is null in MVCCScanCursorRead()"); + return MVCCError::MVCC_IO_ERROR_READ; + } + let cursor_ctx = unsafe { &*cursor.ptr }; + let runtime = &cursor_ctx.db.get_ref().runtime; + let cursor = &cursor_ctx.cursor; + + // TODO: deduplicate with MVCCDatabaseRead() + match runtime.block_on(async move { + let maybe_row = cursor.current().await?; + match maybe_row { + Some(row) => { + tracing::debug!("Found row {row:?}"); + let str_len = row.data.len() + 1; + let value = std::ffi::CString::new(row.data.as_bytes()).map_err(|e| { + mvcc_rs::errors::DatabaseError::Io(format!( + "Failed to transform read data into CString: {e}" + )) + })?; + unsafe { + *value_ptr = value.into_raw(); + *value_len = str_len as i64; + } + } + None => unsafe { *value_len = -1 }, + }; + Ok::<(), mvcc_rs::errors::DatabaseError>(()) + }) { + Ok(_) => { + tracing::debug!("MVCCDatabaseRead: success"); + MVCCError::MVCC_OK + } + Err(e) => { + tracing::error!("MVCCDatabaseRead: {e}"); + MVCCError::MVCC_IO_ERROR_READ + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn MVCCScanCursorNext(cursor: MVCCScanCursorRef) -> std::ffi::c_int { + let cursor_ctx = unsafe { &mut *cursor.ptr }; + let cursor = &mut cursor_ctx.cursor; + tracing::debug!("MVCCScanCursorNext(): {}", cursor.index); + if cursor.forward() { + tracing::debug!("Forwarded to {}", cursor.index); + 1 + } else { + tracing::debug!("Forwarded to end"); + 0 + } +} diff --git a/core/mvcc/bindings/c/src/types.rs b/core/mvcc/bindings/c/src/types.rs index acb929dcd..34d035cb8 100644 --- a/core/mvcc/bindings/c/src/types.rs +++ b/core/mvcc/bindings/c/src/types.rs @@ -45,3 +45,13 @@ pub struct DbContext { pub(crate) db: Db, pub(crate) runtime: tokio::runtime::Runtime, } + +pub struct ScanCursorContext { + pub cursor: crate::ScanCursor, + pub db: MVCCDatabaseRef, +} + +#[repr(transparent)] +pub struct MVCCScanCursorRef { + pub ptr: *mut ScanCursorContext, +} diff --git a/core/mvcc/mvcc-rs/src/cursor.rs b/core/mvcc/mvcc-rs/src/cursor.rs new file mode 100644 index 000000000..230ad6ff6 --- /dev/null +++ b/core/mvcc/mvcc-rs/src/cursor.rs @@ -0,0 +1,48 @@ +use crate::clock::LogicalClock; +use crate::database::{Database, DatabaseInner, Result, Row}; +use crate::persistent_storage::Storage; +use crate::sync::AsyncMutex; + +#[derive(Debug)] +pub struct ScanCursor< + 'a, + Clock: LogicalClock, + StorageImpl: Storage, + Mutex: AsyncMutex>, +> { + pub db: &'a Database, + pub row_ids: Vec, + pub index: usize, + tx_id: u64, +} + +impl< + 'a, + Clock: LogicalClock, + StorageImpl: Storage, + Mutex: AsyncMutex>, + > ScanCursor<'a, Clock, StorageImpl, Mutex> +{ + pub async fn new( + db: &'a Database, + ) -> Result> { + let tx_id = db.begin_tx().await; + let row_ids = db.scan_row_ids().await?; + Ok(Self { + db, + tx_id, + row_ids, + index: 0, + }) + } + + pub async fn current(&self) -> Result> { + let id = self.row_ids[self.index]; + self.db.read(self.tx_id, id).await + } + + pub fn forward(&mut self) -> bool { + self.index += 1; + self.index < self.row_ids.len() + } +} diff --git a/core/mvcc/mvcc-rs/src/database.rs b/core/mvcc/mvcc-rs/src/database.rs index 42d0d1588..89e56aa30 100644 --- a/core/mvcc/mvcc-rs/src/database.rs +++ b/core/mvcc/mvcc-rs/src/database.rs @@ -225,6 +225,11 @@ impl< inner.read(tx_id, id).await } + pub async fn scan_row_ids(&self) -> Result> { + let inner = self.inner.lock().await; + inner.scan_row_ids() + } + /// Begins a new transaction in the database. /// /// This function starts a new transaction in the database and returns a `TxID` value @@ -355,6 +360,11 @@ impl Ok(None) } + fn scan_row_ids(&self) -> Result> { + let rows = self.rows.borrow(); + Ok(rows.keys().cloned().collect()) + } + async fn begin_tx(&mut self) -> TxID { let tx_id = self.get_tx_id(); let begin_ts = self.get_timestamp(); diff --git a/core/mvcc/mvcc-rs/src/lib.rs b/core/mvcc/mvcc-rs/src/lib.rs index d88011290..f8d418335 100644 --- a/core/mvcc/mvcc-rs/src/lib.rs +++ b/core/mvcc/mvcc-rs/src/lib.rs @@ -32,6 +32,7 @@ //! * Garbage collection pub mod clock; +pub mod cursor; pub mod database; pub mod errors; pub mod persistent_storage; From 5bdcfc99249065cb8ffa0f9054445f970e0de2c5 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Thu, 11 May 2023 11:08:43 +0200 Subject: [PATCH 078/128] cursor: handle current() graciously when there's no data --- core/mvcc/mvcc-rs/src/cursor.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/mvcc/mvcc-rs/src/cursor.rs b/core/mvcc/mvcc-rs/src/cursor.rs index 230ad6ff6..003242293 100644 --- a/core/mvcc/mvcc-rs/src/cursor.rs +++ b/core/mvcc/mvcc-rs/src/cursor.rs @@ -37,6 +37,9 @@ impl< } pub async fn current(&self) -> Result> { + if self.index >= self.row_ids.len() { + return Ok(None); + } let id = self.row_ids[self.index]; self.db.read(self.tx_id, id).await } From 54ee330912415a776db90cc8bc740dd3a391adec Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Thu, 11 May 2023 12:13:27 +0200 Subject: [PATCH 079/128] cursor: add closing cursor ... which also terminates the read transaction open for scanning. --- core/mvcc/bindings/c/src/lib.rs | 5 ++++- core/mvcc/bindings/c/src/types.rs | 2 ++ core/mvcc/mvcc-rs/src/cursor.rs | 4 ++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/core/mvcc/bindings/c/src/lib.rs b/core/mvcc/bindings/c/src/lib.rs index 7f3f1ea62..75b474e3e 100644 --- a/core/mvcc/bindings/c/src/lib.rs +++ b/core/mvcc/bindings/c/src/lib.rs @@ -177,7 +177,10 @@ pub unsafe extern "C" fn MVCCScanCursorClose(cursor: MVCCScanCursorRef) { tracing::debug!("warning: `cursor` is null in MVCCScanCursorClose()"); return; } - let _ = unsafe { Box::from_raw(cursor.ptr) }; + let cursor_ctx = unsafe { Box::from_raw(cursor.ptr) }; + let db_context = cursor_ctx.db.clone(); + let runtime = &db_context.get_ref().runtime; + runtime.block_on(async move { cursor_ctx.cursor.close().await.ok() }); } #[no_mangle] diff --git a/core/mvcc/bindings/c/src/types.rs b/core/mvcc/bindings/c/src/types.rs index 34d035cb8..6f7874604 100644 --- a/core/mvcc/bindings/c/src/types.rs +++ b/core/mvcc/bindings/c/src/types.rs @@ -1,5 +1,6 @@ use crate::Db; +#[derive(Clone, Debug)] #[repr(transparent)] pub struct MVCCDatabaseRef { ptr: *const DbContext, @@ -51,6 +52,7 @@ pub struct ScanCursorContext { pub db: MVCCDatabaseRef, } +#[derive(Clone, Debug)] #[repr(transparent)] pub struct MVCCScanCursorRef { pub ptr: *mut ScanCursorContext, diff --git a/core/mvcc/mvcc-rs/src/cursor.rs b/core/mvcc/mvcc-rs/src/cursor.rs index 003242293..cced5df5a 100644 --- a/core/mvcc/mvcc-rs/src/cursor.rs +++ b/core/mvcc/mvcc-rs/src/cursor.rs @@ -44,6 +44,10 @@ impl< self.db.read(self.tx_id, id).await } + pub async fn close(self) -> Result<()> { + self.db.commit_tx(self.tx_id).await + } + pub fn forward(&mut self) -> bool { self.index += 1; self.index < self.row_ids.len() From e8bdfc8e7a7ed8c90e875769bd6a1be2681f9649 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Thu, 11 May 2023 13:35:19 +0200 Subject: [PATCH 080/128] cursor, read: update pointers to *mut u8 --- core/mvcc/bindings/c/include/mvcc.h | 4 ++-- core/mvcc/bindings/c/src/lib.rs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/mvcc/bindings/c/include/mvcc.h b/core/mvcc/bindings/c/include/mvcc.h index c12aa5a43..5c3a94cfc 100644 --- a/core/mvcc/bindings/c/include/mvcc.h +++ b/core/mvcc/bindings/c/include/mvcc.h @@ -27,7 +27,7 @@ void MVCCDatabaseClose(MVCCDatabaseRef db); MVCCError MVCCDatabaseInsert(MVCCDatabaseRef db, uint64_t id, const void *value_ptr, uintptr_t value_len); -MVCCError MVCCDatabaseRead(MVCCDatabaseRef db, uint64_t id, char **value_ptr, int64_t *value_len); +MVCCError MVCCDatabaseRead(MVCCDatabaseRef db, uint64_t id, uint8_t **value_ptr, int64_t *value_len); void MVCCFreeStr(void *ptr); @@ -35,7 +35,7 @@ MVCCScanCursorRef MVCCScanCursorOpen(MVCCDatabaseRef db); void MVCCScanCursorClose(MVCCScanCursorRef cursor); -MVCCError MVCCScanCursorRead(MVCCScanCursorRef cursor, char **value_ptr, int64_t *value_len); +MVCCError MVCCScanCursorRead(MVCCScanCursorRef cursor, uint8_t **value_ptr, int64_t *value_len); int MVCCScanCursorNext(MVCCScanCursorRef cursor); diff --git a/core/mvcc/bindings/c/src/lib.rs b/core/mvcc/bindings/c/src/lib.rs index 75b474e3e..21dbe4315 100644 --- a/core/mvcc/bindings/c/src/lib.rs +++ b/core/mvcc/bindings/c/src/lib.rs @@ -101,7 +101,7 @@ pub unsafe extern "C" fn MVCCDatabaseInsert( pub unsafe extern "C" fn MVCCDatabaseRead( db: MVCCDatabaseRef, id: u64, - value_ptr: *mut *mut std::ffi::c_char, + value_ptr: *mut *mut u8, value_len: *mut i64, ) -> MVCCError { let db = db.get_ref(); @@ -120,7 +120,7 @@ pub unsafe extern "C" fn MVCCDatabaseRead( )) })?; unsafe { - *value_ptr = value.into_raw(); + *value_ptr = value.into_raw() as *mut u8; *value_len = str_len as i64; } } @@ -186,7 +186,7 @@ pub unsafe extern "C" fn MVCCScanCursorClose(cursor: MVCCScanCursorRef) { #[no_mangle] pub unsafe extern "C" fn MVCCScanCursorRead( cursor: MVCCScanCursorRef, - value_ptr: *mut *mut std::ffi::c_char, + value_ptr: *mut *mut u8, value_len: *mut i64, ) -> MVCCError { tracing::debug!("MVCCScanCursorRead()"); @@ -211,7 +211,7 @@ pub unsafe extern "C" fn MVCCScanCursorRead( )) })?; unsafe { - *value_ptr = value.into_raw(); + *value_ptr = value.into_raw() as *mut u8; *value_len = str_len as i64; } } From 582bf149341bd5d5945cdd7480a1d1386e0f0c6c Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Thu, 11 May 2023 13:38:38 +0200 Subject: [PATCH 081/128] database: keep rows in BTreeMap We need ordering libSQL-side. --- core/mvcc/mvcc-rs/src/database.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/mvcc/mvcc-rs/src/database.rs b/core/mvcc/mvcc-rs/src/database.rs index 89e56aa30..efd45d966 100644 --- a/core/mvcc/mvcc-rs/src/database.rs +++ b/core/mvcc/mvcc-rs/src/database.rs @@ -134,7 +134,7 @@ impl< /// Creates a new database. pub fn new(clock: Clock, storage: Storage) -> Self { let inner = DatabaseInner { - rows: RefCell::new(HashMap::new()), + rows: RefCell::new(BTreeMap::new()), txs: RefCell::new(HashMap::new()), tx_timestamps: RefCell::new(BTreeMap::new()), tx_ids: AtomicU64::new(0), @@ -284,7 +284,7 @@ impl< #[derive(Debug)] pub struct DatabaseInner { - rows: RefCell>>, + rows: RefCell>>, txs: RefCell>, tx_timestamps: RefCell>, tx_ids: AtomicU64, From d5eec5d528363c2ab6587f08ffa5ea72e7bb8a75 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Thu, 11 May 2023 14:11:10 +0200 Subject: [PATCH 082/128] cursor: add MVCCScanCursorPosition --- core/mvcc/bindings/c/include/mvcc.h | 2 ++ core/mvcc/bindings/c/src/lib.rs | 9 ++++++++- core/mvcc/mvcc-rs/src/cursor.rs | 9 ++++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/core/mvcc/bindings/c/include/mvcc.h b/core/mvcc/bindings/c/include/mvcc.h index 5c3a94cfc..bd6aaa4d8 100644 --- a/core/mvcc/bindings/c/include/mvcc.h +++ b/core/mvcc/bindings/c/include/mvcc.h @@ -39,6 +39,8 @@ MVCCError MVCCScanCursorRead(MVCCScanCursorRef cursor, uint8_t **value_ptr, int6 int MVCCScanCursorNext(MVCCScanCursorRef cursor); +uint64_t MVCCScanCursorPosition(MVCCScanCursorRef cursor); + #ifdef __cplusplus } // extern "C" #endif // __cplusplus diff --git a/core/mvcc/bindings/c/src/lib.rs b/core/mvcc/bindings/c/src/lib.rs index 21dbe4315..16254194a 100644 --- a/core/mvcc/bindings/c/src/lib.rs +++ b/core/mvcc/bindings/c/src/lib.rs @@ -200,7 +200,7 @@ pub unsafe extern "C" fn MVCCScanCursorRead( // TODO: deduplicate with MVCCDatabaseRead() match runtime.block_on(async move { - let maybe_row = cursor.current().await?; + let maybe_row = cursor.current_row().await?; match maybe_row { Some(row) => { tracing::debug!("Found row {row:?}"); @@ -243,3 +243,10 @@ pub unsafe extern "C" fn MVCCScanCursorNext(cursor: MVCCScanCursorRef) -> std::f 0 } } + +#[no_mangle] +pub unsafe extern "C" fn MVCCScanCursorPosition(cursor: MVCCScanCursorRef) -> u64 { + let cursor_ctx = unsafe { &mut *cursor.ptr }; + let cursor = &mut cursor_ctx.cursor; + cursor.current_row_id().unwrap_or(0) +} \ No newline at end of file diff --git a/core/mvcc/mvcc-rs/src/cursor.rs b/core/mvcc/mvcc-rs/src/cursor.rs index cced5df5a..856ce1dbc 100644 --- a/core/mvcc/mvcc-rs/src/cursor.rs +++ b/core/mvcc/mvcc-rs/src/cursor.rs @@ -36,7 +36,14 @@ impl< }) } - pub async fn current(&self) -> Result> { + pub fn current_row_id(&self) -> Option { + if self.index >= self.row_ids.len() { + return None; + } + Some(self.row_ids[self.index]) + } + + pub async fn current_row(&self) -> Result> { if self.index >= self.row_ids.len() { return Ok(None); } From a782ae5a0a234a49730607f7e0e7e899e4450e2c Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Fri, 12 May 2023 10:09:18 +0200 Subject: [PATCH 083/128] cursor: add is_empty --- core/mvcc/bindings/c/src/lib.rs | 8 +++++++- core/mvcc/mvcc-rs/src/cursor.rs | 4 ++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/core/mvcc/bindings/c/src/lib.rs b/core/mvcc/bindings/c/src/lib.rs index 16254194a..6da877c12 100644 --- a/core/mvcc/bindings/c/src/lib.rs +++ b/core/mvcc/bindings/c/src/lib.rs @@ -156,6 +156,12 @@ pub unsafe extern "C" fn MVCCScanCursorOpen(db: MVCCDatabaseRef) -> MVCCScanCurs let (database, runtime) = (&database.db, &database.runtime); match runtime.block_on(async move { mvcc_rs::cursor::ScanCursor::new(database).await }) { Ok(cursor) => { + if cursor.is_empty() { + tracing::debug!("Cursor is empty"); + return MVCCScanCursorRef { + ptr: std::ptr::null_mut(), + }; + } tracing::debug!("Cursor open: {cursor:?}"); MVCCScanCursorRef { ptr: Box::into_raw(Box::new(ScanCursorContext { cursor, db })), @@ -249,4 +255,4 @@ pub unsafe extern "C" fn MVCCScanCursorPosition(cursor: MVCCScanCursorRef) -> u6 let cursor_ctx = unsafe { &mut *cursor.ptr }; let cursor = &mut cursor_ctx.cursor; cursor.current_row_id().unwrap_or(0) -} \ No newline at end of file +} diff --git a/core/mvcc/mvcc-rs/src/cursor.rs b/core/mvcc/mvcc-rs/src/cursor.rs index 856ce1dbc..7d9455426 100644 --- a/core/mvcc/mvcc-rs/src/cursor.rs +++ b/core/mvcc/mvcc-rs/src/cursor.rs @@ -59,4 +59,8 @@ impl< self.index += 1; self.index < self.row_ids.len() } + + pub fn is_empty(&self) -> bool { + self.index >= self.row_ids.len() + } } From 3236421f771a7fe12c0aa3ce264c6b4b77b3d152 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Fri, 12 May 2023 12:00:44 +0200 Subject: [PATCH 084/128] mvcc: switch to (table_id, row_id) for row ids With that, we're able to easily distinguish rows from different tables. --- core/mvcc/bindings/c/include/mvcc.h | 14 +- core/mvcc/bindings/c/src/lib.rs | 22 +- core/mvcc/mvcc-rs/benches/my_benchmark.rs | 42 +- core/mvcc/mvcc-rs/src/cursor.rs | 9 +- core/mvcc/mvcc-rs/src/database.rs | 586 +++++++++++++++++--- core/mvcc/mvcc-rs/tests/concurrency_test.rs | 10 +- 6 files changed, 594 insertions(+), 89 deletions(-) diff --git a/core/mvcc/bindings/c/include/mvcc.h b/core/mvcc/bindings/c/include/mvcc.h index bd6aaa4d8..e0c432cd9 100644 --- a/core/mvcc/bindings/c/include/mvcc.h +++ b/core/mvcc/bindings/c/include/mvcc.h @@ -25,13 +25,21 @@ MVCCDatabaseRef MVCCDatabaseOpen(const char *path); void MVCCDatabaseClose(MVCCDatabaseRef db); -MVCCError MVCCDatabaseInsert(MVCCDatabaseRef db, uint64_t id, const void *value_ptr, uintptr_t value_len); +MVCCError MVCCDatabaseInsert(MVCCDatabaseRef db, + uint64_t table_id, + uint64_t row_id, + const void *value_ptr, + uintptr_t value_len); -MVCCError MVCCDatabaseRead(MVCCDatabaseRef db, uint64_t id, uint8_t **value_ptr, int64_t *value_len); +MVCCError MVCCDatabaseRead(MVCCDatabaseRef db, + uint64_t table_id, + uint64_t row_id, + uint8_t **value_ptr, + int64_t *value_len); void MVCCFreeStr(void *ptr); -MVCCScanCursorRef MVCCScanCursorOpen(MVCCDatabaseRef db); +MVCCScanCursorRef MVCCScanCursorOpen(MVCCDatabaseRef db, uint64_t table_id); void MVCCScanCursorClose(MVCCScanCursorRef cursor); diff --git a/core/mvcc/bindings/c/src/lib.rs b/core/mvcc/bindings/c/src/lib.rs index 6da877c12..a68235835 100644 --- a/core/mvcc/bindings/c/src/lib.rs +++ b/core/mvcc/bindings/c/src/lib.rs @@ -64,7 +64,8 @@ pub unsafe extern "C" fn MVCCDatabaseClose(db: MVCCDatabaseRef) { #[no_mangle] pub unsafe extern "C" fn MVCCDatabaseInsert( db: MVCCDatabaseRef, - id: u64, + table_id: u64, + row_id: u64, value_ptr: *const std::ffi::c_void, value_len: usize, ) -> MVCCError { @@ -79,6 +80,7 @@ pub unsafe extern "C" fn MVCCDatabaseInsert( } }; let (db, runtime) = (&db.db, &db.runtime); + let id = database::RowID { table_id, row_id }; let row = database::Row { id, data }; tracing::debug!("MVCCDatabaseInsert: {row:?}"); match runtime.block_on(async move { @@ -100,7 +102,8 @@ pub unsafe extern "C" fn MVCCDatabaseInsert( #[no_mangle] pub unsafe extern "C" fn MVCCDatabaseRead( db: MVCCDatabaseRef, - id: u64, + table_id: u64, + row_id: u64, value_ptr: *mut *mut u8, value_len: *mut i64, ) -> MVCCError { @@ -109,6 +112,7 @@ pub unsafe extern "C" fn MVCCDatabaseRead( match runtime.block_on(async move { let tx = db.begin_tx().await; + let id = database::RowID { table_id, row_id }; let maybe_row = db.read(tx, id).await?; match maybe_row { Some(row) => { @@ -148,13 +152,18 @@ pub unsafe extern "C" fn MVCCFreeStr(ptr: *mut std::ffi::c_void) { } #[no_mangle] -pub unsafe extern "C" fn MVCCScanCursorOpen(db: MVCCDatabaseRef) -> MVCCScanCursorRef { +pub unsafe extern "C" fn MVCCScanCursorOpen( + db: MVCCDatabaseRef, + table_id: u64, +) -> MVCCScanCursorRef { tracing::debug!("MVCCScanCursorOpen()"); // Reference is transmuted to &'static in order to be able to pass the cursor back to C. // The contract with C is to never use a cursor after MVCCDatabaseClose() has been called. let database = unsafe { std::mem::transmute::<&DbContext, &'static DbContext>(db.get_ref()) }; let (database, runtime) = (&database.db, &database.runtime); - match runtime.block_on(async move { mvcc_rs::cursor::ScanCursor::new(database).await }) { + match runtime + .block_on(async move { mvcc_rs::cursor::ScanCursor::new(database, table_id).await }) + { Ok(cursor) => { if cursor.is_empty() { tracing::debug!("Cursor is empty"); @@ -254,5 +263,8 @@ pub unsafe extern "C" fn MVCCScanCursorNext(cursor: MVCCScanCursorRef) -> std::f pub unsafe extern "C" fn MVCCScanCursorPosition(cursor: MVCCScanCursorRef) -> u64 { let cursor_ctx = unsafe { &mut *cursor.ptr }; let cursor = &mut cursor_ctx.cursor; - cursor.current_row_id().unwrap_or(0) + cursor + .current_row_id() + .map(|row_id| row_id.row_id) + .unwrap_or(0) } diff --git a/core/mvcc/mvcc-rs/benches/my_benchmark.rs b/core/mvcc/mvcc-rs/benches/my_benchmark.rs index 3c360107a..36ccf45eb 100644 --- a/core/mvcc/mvcc-rs/benches/my_benchmark.rs +++ b/core/mvcc/mvcc-rs/benches/my_benchmark.rs @@ -1,7 +1,7 @@ use criterion::async_executor::FuturesExecutor; use criterion::{criterion_group, criterion_main, Criterion, Throughput}; use mvcc_rs::clock::LocalClock; -use mvcc_rs::database::{Database, Row}; +use mvcc_rs::database::{Database, Row, RowID}; use pprof::criterion::{Output, PProfProfiler}; fn bench_db() -> Database< @@ -47,7 +47,15 @@ fn bench(c: &mut Criterion) { group.bench_function("begin_tx-read-commit_tx", |b| { b.to_async(FuturesExecutor).iter(|| async { let tx_id = db.begin_tx().await; - db.read(tx_id, 1).await.unwrap(); + db.read( + tx_id, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await + .unwrap(); db.commit_tx(tx_id).await }) }); @@ -59,7 +67,10 @@ fn bench(c: &mut Criterion) { db.update( tx_id, Row { - id: 1, + id: RowID { + table_id: 1, + row_id: 1, + }, data: "World".to_string(), }, ) @@ -74,14 +85,25 @@ fn bench(c: &mut Criterion) { futures::executor::block_on(db.insert( tx, Row { - id: 1, + id: RowID { + table_id: 1, + row_id: 1, + }, data: "Hello".to_string(), }, )) .unwrap(); group.bench_function("read", |b| { b.to_async(FuturesExecutor).iter(|| async { - db.read(tx, 1).await.unwrap(); + db.read( + tx, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await + .unwrap(); }) }); @@ -90,7 +112,10 @@ fn bench(c: &mut Criterion) { futures::executor::block_on(db.insert( tx, Row { - id: 1, + id: RowID { + table_id: 1, + row_id: 1, + }, data: "Hello".to_string(), }, )) @@ -100,7 +125,10 @@ fn bench(c: &mut Criterion) { db.update( tx, Row { - id: 1, + id: RowID { + table_id: 1, + row_id: 1, + }, data: "World".to_string(), }, ) diff --git a/core/mvcc/mvcc-rs/src/cursor.rs b/core/mvcc/mvcc-rs/src/cursor.rs index 7d9455426..397265d7c 100644 --- a/core/mvcc/mvcc-rs/src/cursor.rs +++ b/core/mvcc/mvcc-rs/src/cursor.rs @@ -1,5 +1,5 @@ use crate::clock::LogicalClock; -use crate::database::{Database, DatabaseInner, Result, Row}; +use crate::database::{Database, DatabaseInner, Result, Row, RowID}; use crate::persistent_storage::Storage; use crate::sync::AsyncMutex; @@ -11,7 +11,7 @@ pub struct ScanCursor< Mutex: AsyncMutex>, > { pub db: &'a Database, - pub row_ids: Vec, + pub row_ids: Vec, pub index: usize, tx_id: u64, } @@ -25,9 +25,10 @@ impl< { pub async fn new( db: &'a Database, + table_id: u64, ) -> Result> { let tx_id = db.begin_tx().await; - let row_ids = db.scan_row_ids().await?; + let row_ids = db.scan_row_ids_for_table(table_id).await?; Ok(Self { db, tx_id, @@ -36,7 +37,7 @@ impl< }) } - pub fn current_row_id(&self) -> Option { + pub fn current_row_id(&self) -> Option { if self.index >= self.row_ids.len() { return None; } diff --git a/core/mvcc/mvcc-rs/src/database.rs b/core/mvcc/mvcc-rs/src/database.rs index efd45d966..8611f392f 100644 --- a/core/mvcc/mvcc-rs/src/database.rs +++ b/core/mvcc/mvcc-rs/src/database.rs @@ -8,9 +8,16 @@ use std::sync::Arc; pub type Result = std::result::Result; +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash)] +pub struct RowID { + pub table_id: u64, + pub row_id: u64, +} + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] + pub struct Row { - pub id: u64, + pub id: RowID, pub data: String, } @@ -62,9 +69,9 @@ pub struct Transaction { /// The transaction begin timestamp. begin_ts: u64, /// The transaction write set. - write_set: HashSet, + write_set: HashSet, /// The transaction read set. - read_set: RefCell>, + read_set: RefCell>, } impl Transaction { @@ -78,12 +85,12 @@ impl Transaction { } } - fn insert_to_read_set(&self, id: u64) { + fn insert_to_read_set(&self, id: RowID) { let mut read_set = self.read_set.borrow_mut(); read_set.insert(id); } - fn insert_to_write_set(&mut self, id: u64) { + fn insert_to_write_set(&mut self, id: RowID) { self.write_set.insert(id); } } @@ -201,7 +208,7 @@ impl< /// /// Returns `true` if the row was successfully deleted, and `false` otherwise. /// - pub async fn delete(&self, tx_id: TxID, id: u64) -> Result { + pub async fn delete(&self, tx_id: TxID, id: RowID) -> Result { let inner = self.inner.lock().await; inner.delete(tx_id, id).await } @@ -220,16 +227,21 @@ impl< /// /// Returns `Some(row)` with the row data if the row with the given `id` exists, /// and `None` otherwise. - pub async fn read(&self, tx_id: TxID, id: u64) -> Result> { + pub async fn read(&self, tx_id: TxID, id: RowID) -> Result> { let inner = self.inner.lock().await; inner.read(tx_id, id).await } - pub async fn scan_row_ids(&self) -> Result> { + pub async fn scan_row_ids(&self) -> Result> { let inner = self.inner.lock().await; inner.scan_row_ids() } + pub async fn scan_row_ids_for_table(&self, table_id: u64) -> Result> { + let inner = self.inner.lock().await; + inner.scan_row_ids_for_table(table_id) + } + /// Begins a new transaction in the database. /// /// This function starts a new transaction in the database and returns a `TxID` value @@ -284,7 +296,7 @@ impl< #[derive(Debug)] pub struct DatabaseInner { - rows: RefCell>>, + rows: RefCell>>, txs: RefCell>, tx_timestamps: RefCell>, tx_ids: AtomicU64, @@ -314,7 +326,7 @@ impl } #[allow(clippy::await_holding_refcell_ref)] - async fn delete(&self, tx_id: TxID, id: u64) -> Result { + async fn delete(&self, tx_id: TxID, id: RowID) -> Result { // NOTICE: They *are* dropped before an await point!!! But the await is conditional, // so I think clippy is just confused. let mut txs = self.txs.borrow_mut(); @@ -344,7 +356,7 @@ impl Ok(false) } - async fn read(&self, tx_id: TxID, id: u64) -> Result> { + async fn read(&self, tx_id: TxID, id: RowID) -> Result> { let txs = self.txs.borrow_mut(); let tx = txs.get(&tx_id).unwrap(); assert!(tx.state == TransactionState::Active); @@ -360,11 +372,28 @@ impl Ok(None) } - fn scan_row_ids(&self) -> Result> { + fn scan_row_ids(&self) -> Result> { let rows = self.rows.borrow(); Ok(rows.keys().cloned().collect()) } + fn scan_row_ids_for_table(&self, table_id: u64) -> Result> { + let rows = self.rows.borrow(); + Ok(rows + .range( + RowID { + table_id, + row_id: 0, + }..RowID { + table_id, + row_id: u64::MAX, + }, + ) + .map(|(k, _)| k) + .cloned() + .collect()) + } + async fn begin_tx(&mut self) -> TxID { let tx_id = self.get_tx_id(); let begin_ts = self.get_timestamp(); @@ -496,7 +525,7 @@ impl None => true, }; if !should_stay { - tracing::debug!("Dropping row version {} {:?}-{:?}", id, rv.begin, rv.end); + tracing::debug!("Dropping row version {:?} {:?}-{:?}", id, rv.begin, rv.end); } should_stay }); @@ -613,16 +642,39 @@ mod tests { let tx1 = db.begin_tx().await; let tx1_row = Row { - id: 1, + id: RowID { + table_id: 1, + row_id: 1, + }, data: "Hello".to_string(), }; db.insert(tx1, tx1_row.clone()).await.unwrap(); - let row = db.read(tx1, 1).await.unwrap().unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await + .unwrap() + .unwrap(); assert_eq!(tx1_row, row); db.commit_tx(tx1).await.unwrap(); let tx2 = db.begin_tx().await; - let row = db.read(tx2, 1).await.unwrap().unwrap(); + let row = db + .read( + tx2, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await + .unwrap() + .unwrap(); assert_eq!(tx1_row, row); } @@ -631,7 +683,15 @@ mod tests { async fn test_read_nonexistent() { let db = test_db(); let tx = db.begin_tx().await; - let row = db.read(tx, 1).await; + let row = db + .read( + tx, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await; assert!(row.unwrap().is_none()); } @@ -642,19 +702,58 @@ mod tests { let tx1 = db.begin_tx().await; let tx1_row = Row { - id: 1, + id: RowID { + table_id: 1, + row_id: 1, + }, data: "Hello".to_string(), }; db.insert(tx1, tx1_row.clone()).await.unwrap(); - let row = db.read(tx1, 1).await.unwrap().unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await + .unwrap() + .unwrap(); assert_eq!(tx1_row, row); - db.delete(tx1, 1).await.unwrap(); - let row = db.read(tx1, 1).await.unwrap(); + db.delete( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await + .unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await + .unwrap(); assert!(row.is_none()); db.commit_tx(tx1).await.unwrap(); let tx2 = db.begin_tx().await; - let row = db.read(tx2, 1).await.unwrap(); + let row = db + .read( + tx2, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await + .unwrap(); assert!(row.is_none()); } @@ -663,7 +762,16 @@ mod tests { async fn test_delete_nonexistent() { let db = test_db(); let tx = db.begin_tx().await; - assert!(!db.delete(tx, 1).await.unwrap()); + assert!(!db + .delete( + tx, + RowID { + table_id: 1, + row_id: 1 + } + ) + .await + .unwrap()); } #[traced_test] @@ -672,23 +780,59 @@ mod tests { let db = test_db(); let tx1 = db.begin_tx().await; let tx1_row = Row { - id: 1, + id: RowID { + table_id: 1, + row_id: 1, + }, data: "Hello".to_string(), }; db.insert(tx1, tx1_row.clone()).await.unwrap(); - let row = db.read(tx1, 1).await.unwrap().unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await + .unwrap() + .unwrap(); assert_eq!(tx1_row, row); let tx1_updated_row = Row { - id: 1, + id: RowID { + table_id: 1, + row_id: 1, + }, data: "World".to_string(), }; db.update(tx1, tx1_updated_row.clone()).await.unwrap(); - let row = db.read(tx1, 1).await.unwrap().unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await + .unwrap() + .unwrap(); assert_eq!(tx1_updated_row, row); db.commit_tx(tx1).await.unwrap(); let tx2 = db.begin_tx().await; - let row = db.read(tx2, 1).await.unwrap().unwrap(); + let row = db + .read( + tx2, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await + .unwrap() + .unwrap(); db.commit_tx(tx2).await.unwrap(); assert_eq!(tx1_updated_row, row); db.drop_unused_row_versions().await; @@ -700,22 +844,57 @@ mod tests { let db = test_db(); let tx1 = db.begin_tx().await; let row1 = Row { - id: 1, + id: RowID { + table_id: 1, + row_id: 1, + }, data: "Hello".to_string(), }; db.insert(tx1, row1.clone()).await.unwrap(); - let row2 = db.read(tx1, 1).await.unwrap().unwrap(); + let row2 = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await + .unwrap() + .unwrap(); assert_eq!(row1, row2); let row3 = Row { - id: 1, + id: RowID { + table_id: 1, + row_id: 1, + }, data: "World".to_string(), }; db.update(tx1, row3.clone()).await.unwrap(); - let row4 = db.read(tx1, 1).await.unwrap().unwrap(); + let row4 = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await + .unwrap() + .unwrap(); assert_eq!(row3, row4); db.rollback_tx(tx1).await; let tx2 = db.begin_tx().await; - let row5 = db.read(tx2, 1).await.unwrap(); + let row5 = db + .read( + tx2, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await + .unwrap(); assert_eq!(row5, None); } @@ -727,22 +906,48 @@ mod tests { // T1 inserts a row with ID 1, but does not commit. let tx1 = db.begin_tx().await; let tx1_row = Row { - id: 1, + id: RowID { + table_id: 1, + row_id: 1, + }, data: "Hello".to_string(), }; db.insert(tx1, tx1_row.clone()).await.unwrap(); - let row = db.read(tx1, 1).await.unwrap().unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await + .unwrap() + .unwrap(); assert_eq!(tx1_row, row); // T2 attempts to delete row with ID 1, but fails because T1 has not committed. let tx2 = db.begin_tx().await; let tx2_row = Row { - id: 1, + id: RowID { + table_id: 1, + row_id: 1, + }, data: "World".to_string(), }; assert!(!db.update(tx2, tx2_row).await.unwrap()); - let row = db.read(tx1, 1).await.unwrap().unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await + .unwrap() + .unwrap(); assert_eq!(tx1_row, row); } @@ -754,14 +959,26 @@ mod tests { // T1 inserts a row with ID 1, but does not commit. let tx1 = db.begin_tx().await; let row1 = Row { - id: 1, + id: RowID { + table_id: 1, + row_id: 1, + }, data: "Hello".to_string(), }; db.insert(tx1, row1).await.unwrap(); // T2 attempts to read row with ID 1, but doesn't see one because T1 has not committed. let tx2 = db.begin_tx().await; - let row2 = db.read(tx2, 1).await.unwrap(); + let row2 = db + .read( + tx2, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await + .unwrap(); assert_eq!(row2, None); } @@ -774,7 +991,10 @@ mod tests { // T1 inserts a row with ID 1 and commits. let tx1 = db.begin_tx().await; let tx1_row = Row { - id: 1, + id: RowID { + table_id: 1, + row_id: 1, + }, data: "Hello".to_string(), }; db.insert(tx1, tx1_row.clone()).await.unwrap(); @@ -782,11 +1002,30 @@ mod tests { // T2 deletes row with ID 1, but does not commit. let tx2 = db.begin_tx().await; - assert!(db.delete(tx2, 1).await.unwrap()); + assert!(db + .delete( + tx2, + RowID { + table_id: 1, + row_id: 1 + } + ) + .await + .unwrap()); // T3 reads row with ID 1, but doesn't see the delete because T2 hasn't committed. let tx3 = db.begin_tx().await; - let row = db.read(tx3, 1).await.unwrap().unwrap(); + let row = db + .read( + tx3, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await + .unwrap() + .unwrap(); assert_eq!(tx1_row, row); } @@ -798,30 +1037,66 @@ mod tests { // T1 inserts a row with ID 1 and commits. let tx1 = db.begin_tx().await; let tx1_row = Row { - id: 1, + id: RowID { + table_id: 1, + row_id: 1, + }, data: "Hello".to_string(), }; db.insert(tx1, tx1_row.clone()).await.unwrap(); - let row = db.read(tx1, 1).await.unwrap().unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await + .unwrap() + .unwrap(); assert_eq!(tx1_row, row); db.commit_tx(tx1).await.unwrap(); // T2 reads the row with ID 1 within an active transaction. let tx2 = db.begin_tx().await; - let row = db.read(tx2, 1).await.unwrap().unwrap(); + let row = db + .read( + tx2, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await + .unwrap() + .unwrap(); assert_eq!(tx1_row, row); // T3 updates the row and commits. let tx3 = db.begin_tx().await; let tx3_row = Row { - id: 1, + id: RowID { + table_id: 1, + row_id: 1, + }, data: "World".to_string(), }; db.update(tx3, tx3_row).await.unwrap(); db.commit_tx(tx3).await.unwrap(); // T2 still reads the same version of the row as before. - let row = db.read(tx2, 1).await.unwrap().unwrap(); + let row = db + .read( + tx2, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await + .unwrap() + .unwrap(); assert_eq!(tx1_row, row); } @@ -833,18 +1108,34 @@ mod tests { // T1 inserts a row with ID 1 and commits. let tx1 = db.begin_tx().await; let tx1_row = Row { - id: 1, + id: RowID { + table_id: 1, + row_id: 1, + }, data: "Hello".to_string(), }; db.insert(tx1, tx1_row.clone()).await.unwrap(); - let row = db.read(tx1, 1).await.unwrap().unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await + .unwrap() + .unwrap(); assert_eq!(tx1_row, row); db.commit_tx(tx1).await.unwrap(); // T2 attempts to update row ID 1 within an active transaction. let tx2 = db.begin_tx().await; let tx2_row = Row { - id: 1, + id: RowID { + table_id: 1, + row_id: 1, + }, data: "World".to_string(), }; assert!(db.update(tx2, tx2_row.clone()).await.unwrap()); @@ -852,7 +1143,10 @@ mod tests { // T3 also attempts to update row ID 1 within an active transaction. let tx3 = db.begin_tx().await; let tx3_row = Row { - id: 1, + id: RowID { + table_id: 1, + row_id: 1, + }, data: "Hello, world!".to_string(), }; assert_eq!( @@ -864,7 +1158,17 @@ mod tests { assert_eq!(Err(DatabaseError::TxTerminated), db.commit_tx(tx3).await); let tx4 = db.begin_tx().await; - let row = db.read(tx4, 1).await.unwrap().unwrap(); + let row = db + .read( + tx4, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await + .unwrap() + .unwrap(); assert_eq!(tx2_row, row); } @@ -878,7 +1182,10 @@ mod tests { // let's add $10 to my account since I like money let tx1 = db.begin_tx().await; let tx1_row = Row { - id: 1, + id: RowID { + table_id: 1, + row_id: 1, + }, data: "10".to_string(), }; db.insert(tx1, tx1_row.clone()).await.unwrap(); @@ -887,16 +1194,39 @@ mod tests { // but I like more money, so let me try adding $10 more let tx2 = db.begin_tx().await; let tx2_row = Row { - id: 1, + id: RowID { + table_id: 1, + row_id: 1, + }, data: "20".to_string(), }; assert!(db.update(tx2, tx2_row.clone()).await.unwrap()); - let row = db.read(tx2, 1).await.unwrap().unwrap(); + let row = db + .read( + tx2, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await + .unwrap() + .unwrap(); assert_eq!(row, tx2_row); // can I check how much money I have? let tx3 = db.begin_tx().await; - let row = db.read(tx3, 1).await.unwrap().unwrap(); + let row = db + .read( + tx3, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await + .unwrap() + .unwrap(); assert_eq!(tx1_row, row); } @@ -910,18 +1240,39 @@ mod tests { let tx2 = db.begin_tx().await; let tx2_row = Row { - id: 1, + id: RowID { + table_id: 1, + row_id: 1, + }, data: "10".to_string(), }; db.insert(tx2, tx2_row.clone()).await.unwrap(); // transaction in progress, so tx1 shouldn't be able to see the value - let row = db.read(tx1, 1).await.unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await + .unwrap(); assert_eq!(row, None); // lets commit the transaction and check if tx1 can see it db.commit_tx(tx2).await.unwrap(); - let row = db.read(tx1, 1).await.unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .await + .unwrap(); assert_eq!(row, None); } @@ -947,7 +1298,10 @@ mod tests { db.insert( tx3, Row { - id: 1, + id: RowID { + table_id: 1, + row_id: 1, + }, data: "testme".to_string(), }, ) @@ -962,7 +1316,10 @@ mod tests { db.insert( tx4, Row { - id: 2, + id: RowID { + table_id: 1, + row_id: 2, + }, data: "testme2".to_string(), }, ) @@ -971,16 +1328,58 @@ mod tests { db.insert( tx4, Row { - id: 3, + id: RowID { + table_id: 1, + row_id: 3, + }, data: "testme3".to_string(), }, ) .await .unwrap(); - assert_eq!(db.read(tx4, 1).await.unwrap().unwrap().data, "testme"); - assert_eq!(db.read(tx4, 2).await.unwrap().unwrap().data, "testme2"); - assert_eq!(db.read(tx4, 3).await.unwrap().unwrap().data, "testme3"); + assert_eq!( + db.read( + tx4, + RowID { + table_id: 1, + row_id: 1 + } + ) + .await + .unwrap() + .unwrap() + .data, + "testme" + ); + assert_eq!( + db.read( + tx4, + RowID { + table_id: 1, + row_id: 2 + } + ) + .await + .unwrap() + .unwrap() + .data, + "testme2" + ); + assert_eq!( + db.read( + tx4, + RowID { + table_id: 1, + row_id: 3 + } + ) + .await + .unwrap() + .unwrap() + .data, + "testme3" + ); db.commit_tx(tx4).await.unwrap(); let clock = LocalClock::new(); @@ -990,9 +1389,58 @@ mod tests { println!("{:#?}", db); let tx5 = db.begin_tx().await; - println!("{:#?}", db.read(tx5, 1).await); - assert_eq!(db.read(tx5, 1).await.unwrap().unwrap().data, "testme"); - assert_eq!(db.read(tx5, 2).await.unwrap().unwrap().data, "testme2"); - assert_eq!(db.read(tx5, 3).await.unwrap().unwrap().data, "testme3"); + println!( + "{:#?}", + db.read( + tx5, + RowID { + table_id: 1, + row_id: 1 + } + ) + .await + ); + assert_eq!( + db.read( + tx5, + RowID { + table_id: 1, + row_id: 1 + } + ) + .await + .unwrap() + .unwrap() + .data, + "testme" + ); + assert_eq!( + db.read( + tx5, + RowID { + table_id: 1, + row_id: 2 + } + ) + .await + .unwrap() + .unwrap() + .data, + "testme2" + ); + assert_eq!( + db.read( + tx5, + RowID { + table_id: 1, + row_id: 3 + } + ) + .await + .unwrap() + .unwrap() + .data, + "testme3" + ); } } diff --git a/core/mvcc/mvcc-rs/tests/concurrency_test.rs b/core/mvcc/mvcc-rs/tests/concurrency_test.rs index 0626baab3..f1806d4ae 100644 --- a/core/mvcc/mvcc-rs/tests/concurrency_test.rs +++ b/core/mvcc/mvcc-rs/tests/concurrency_test.rs @@ -1,5 +1,5 @@ use mvcc_rs::clock::LocalClock; -use mvcc_rs::database::{Database, Row}; +use mvcc_rs::database::{Database, Row, RowID}; use shuttle::sync::atomic::AtomicU64; use shuttle::sync::Arc; use shuttle::thread; @@ -22,6 +22,10 @@ fn test_non_overlapping_concurrent_inserts() { shuttle::future::block_on(async move { let tx = db.begin_tx().await; let id = ids.fetch_add(1, Ordering::SeqCst); + let id = RowID { + table_id: 1, + row_id: id, + }; let row = Row { id, data: "Hello".to_string(), @@ -42,6 +46,10 @@ fn test_non_overlapping_concurrent_inserts() { shuttle::future::block_on(async move { let tx = db.begin_tx().await; let id = ids.fetch_add(1, Ordering::SeqCst); + let id = RowID { + table_id: 1, + row_id: id, + }; let row = Row { id, data: "World".to_string(), From 7772c65f8d3c6a44b8adc9ada5913146d0fba42d Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Fri, 12 May 2023 20:25:08 +0300 Subject: [PATCH 085/128] Switch to Tokio's mutex (#40) The async trait wrapper is very expensive because it's doing pin boxing in the hot path. Switch to Tokio's mutex to get back performance. Fixes #39 --- core/mvcc/bindings/c/Cargo.toml | 2 +- core/mvcc/bindings/c/src/lib.rs | 7 +--- core/mvcc/mvcc-rs/Cargo.toml | 4 +- core/mvcc/mvcc-rs/benches/my_benchmark.rs | 5 +-- core/mvcc/mvcc-rs/src/cursor.rs | 13 +++---- core/mvcc/mvcc-rs/src/database.rs | 16 ++++---- core/mvcc/mvcc-rs/src/lib.rs | 1 - core/mvcc/mvcc-rs/src/sync.rs | 43 --------------------- core/mvcc/mvcc-rs/tests/concurrency_test.rs | 2 +- 9 files changed, 19 insertions(+), 74 deletions(-) delete mode 100644 core/mvcc/mvcc-rs/src/sync.rs diff --git a/core/mvcc/bindings/c/Cargo.toml b/core/mvcc/bindings/c/Cargo.toml index 6fa92a9dc..3e348ff73 100644 --- a/core/mvcc/bindings/c/Cargo.toml +++ b/core/mvcc/bindings/c/Cargo.toml @@ -13,6 +13,6 @@ cbindgen = "0.24.0" [dependencies] base64 = "0.21.0" mvcc-rs = { path = "../../mvcc-rs", features = ["tokio"] } -tokio = { version = "1.27.0", features = ["full"] } +tokio = { version = "1.27.0", features = ["full", "parking_lot"] } tracing = "0.1.37" tracing-subscriber = { version = "0" } diff --git a/core/mvcc/bindings/c/src/lib.rs b/core/mvcc/bindings/c/src/lib.rs index a68235835..b602245c8 100644 --- a/core/mvcc/bindings/c/src/lib.rs +++ b/core/mvcc/bindings/c/src/lib.rs @@ -15,13 +15,10 @@ type Clock = clock::LocalClock; type Storage = persistent_storage::JsonOnDisk; /// cbindgen:ignore -type Inner = database::DatabaseInner; +type Db = database::Database; /// cbindgen:ignore -type Db = database::Database>; - -/// cbindgen:ignore -type ScanCursor = cursor::ScanCursor<'static, Clock, Storage, tokio::sync::Mutex>; +type ScanCursor = cursor::ScanCursor<'static, Clock, Storage>; static INIT_RUST_LOG: std::sync::Once = std::sync::Once::new(); diff --git a/core/mvcc/mvcc-rs/Cargo.toml b/core/mvcc/mvcc-rs/Cargo.toml index 9b6bd5d9d..11cb010d6 100644 --- a/core/mvcc/mvcc-rs/Cargo.toml +++ b/core/mvcc/mvcc-rs/Cargo.toml @@ -9,7 +9,7 @@ async-trait = "0.1.68" futures = "0.3.28" thiserror = "1.0.40" tracing = "0.1.37" -tokio = { version = "1.27.0", features = ["full"], optional = true } +tokio = { version = "1.27.0", features = ["full", "parking_lot"], optional = true } tokio-stream = { version = "0.1.12", optional = true, features = ["io-util"] } serde = { version = "1.0.160", features = ["derive"] } serde_json = "1.0.96" @@ -21,7 +21,7 @@ base64 = "0.21.0" criterion = { version = "0.4", features = ["html_reports", "async", "async_futures"] } pprof = { version = "0.11.1", features = ["criterion", "flamegraph"] } shuttle = "0.6.0" -tokio = { version = "1.27.0", features = ["full"] } +tokio = { version = "1.27.0", features = ["full", "parking_lot"] } tracing-subscriber = "0" tracing-test = "0" mvcc-rs = { path = ".", features = ["tokio"] } diff --git a/core/mvcc/mvcc-rs/benches/my_benchmark.rs b/core/mvcc/mvcc-rs/benches/my_benchmark.rs index 36ccf45eb..494df0cf6 100644 --- a/core/mvcc/mvcc-rs/benches/my_benchmark.rs +++ b/core/mvcc/mvcc-rs/benches/my_benchmark.rs @@ -7,13 +7,10 @@ use pprof::criterion::{Output, PProfProfiler}; fn bench_db() -> Database< LocalClock, mvcc_rs::persistent_storage::Noop, - tokio::sync::Mutex< - mvcc_rs::database::DatabaseInner, - >, > { let clock = LocalClock::default(); let storage = mvcc_rs::persistent_storage::Noop {}; - Database::<_, _, tokio::sync::Mutex<_>>::new(clock, storage) + Database::new(clock, storage) } fn bench(c: &mut Criterion) { diff --git a/core/mvcc/mvcc-rs/src/cursor.rs b/core/mvcc/mvcc-rs/src/cursor.rs index 397265d7c..ea988474a 100644 --- a/core/mvcc/mvcc-rs/src/cursor.rs +++ b/core/mvcc/mvcc-rs/src/cursor.rs @@ -1,16 +1,14 @@ use crate::clock::LogicalClock; -use crate::database::{Database, DatabaseInner, Result, Row, RowID}; +use crate::database::{Database, Result, Row, RowID}; use crate::persistent_storage::Storage; -use crate::sync::AsyncMutex; #[derive(Debug)] pub struct ScanCursor< 'a, Clock: LogicalClock, StorageImpl: Storage, - Mutex: AsyncMutex>, > { - pub db: &'a Database, + pub db: &'a Database, pub row_ids: Vec, pub index: usize, tx_id: u64, @@ -20,13 +18,12 @@ impl< 'a, Clock: LogicalClock, StorageImpl: Storage, - Mutex: AsyncMutex>, - > ScanCursor<'a, Clock, StorageImpl, Mutex> + > ScanCursor<'a, Clock, StorageImpl> { pub async fn new( - db: &'a Database, + db: &'a Database, table_id: u64, - ) -> Result> { + ) -> Result> { let tx_id = db.begin_tx().await; let row_ids = db.scan_row_ids_for_table(table_id).await?; Ok(Self { diff --git a/core/mvcc/mvcc-rs/src/database.rs b/core/mvcc/mvcc-rs/src/database.rs index 8611f392f..a81b1e16e 100644 --- a/core/mvcc/mvcc-rs/src/database.rs +++ b/core/mvcc/mvcc-rs/src/database.rs @@ -5,6 +5,7 @@ use std::cell::RefCell; use std::collections::{BTreeMap, HashMap, HashSet}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; +use tokio::sync::Mutex; pub type Result = std::result::Result; @@ -127,16 +128,14 @@ enum TransactionState { pub struct Database< Clock: LogicalClock, Storage: crate::persistent_storage::Storage, - AsyncMutex: crate::sync::AsyncMutex>, > { - inner: Arc, + inner: Arc>>, } impl< Clock: LogicalClock, Storage: crate::persistent_storage::Storage, - AsyncMutex: crate::sync::AsyncMutex>, - > Database + > Database { /// Creates a new database. pub fn new(clock: Clock, storage: Storage) -> Self { @@ -149,7 +148,7 @@ impl< storage, }; Self { - inner: Arc::new(AsyncMutex::new(inner)), + inner: Arc::new(Mutex::new(inner)), } } @@ -628,11 +627,10 @@ mod tests { fn test_db() -> Database< LocalClock, crate::persistent_storage::Noop, - tokio::sync::Mutex>, > { let clock = LocalClock::new(); let storage = crate::persistent_storage::Noop {}; - Database::<_, _, tokio::sync::Mutex<_>>::new(clock, storage) + Database::new(clock, storage) } #[traced_test] @@ -1289,7 +1287,7 @@ mod tests { .as_nanos(), )); let storage = crate::persistent_storage::JsonOnDisk { path: path.clone() }; - let db: Database<_, _, tokio::sync::Mutex<_>> = Database::new(clock, storage); + let db = Database::new(clock, storage); let tx1 = db.begin_tx().await; let tx2 = db.begin_tx().await; @@ -1384,7 +1382,7 @@ mod tests { let clock = LocalClock::new(); let storage = crate::persistent_storage::JsonOnDisk { path }; - let db: Database<_, _, tokio::sync::Mutex<_>> = Database::new(clock, storage); + let db = Database::new(clock, storage); db.recover().await.unwrap(); println!("{:#?}", db); diff --git a/core/mvcc/mvcc-rs/src/lib.rs b/core/mvcc/mvcc-rs/src/lib.rs index f8d418335..00eaee336 100644 --- a/core/mvcc/mvcc-rs/src/lib.rs +++ b/core/mvcc/mvcc-rs/src/lib.rs @@ -36,4 +36,3 @@ pub mod cursor; pub mod database; pub mod errors; pub mod persistent_storage; -pub mod sync; diff --git a/core/mvcc/mvcc-rs/src/sync.rs b/core/mvcc/mvcc-rs/src/sync.rs deleted file mode 100644 index de643abd4..000000000 --- a/core/mvcc/mvcc-rs/src/sync.rs +++ /dev/null @@ -1,43 +0,0 @@ -#[async_trait::async_trait] -pub trait AsyncMutex { - type Inner; - type Guard<'a>: std::ops::DerefMut - where - Self: 'a, - Self::Inner: 'a; - - fn new(inner: Self::Inner) -> Self; - - async fn lock<'a>(&'a self) -> Self::Guard<'a>; -} - -#[async_trait::async_trait] -impl AsyncMutex for std::sync::Mutex { - type Inner = T; - type Guard<'a> = std::sync::MutexGuard<'a, T> where T: 'a; - - fn new(inner: Self::Inner) -> Self { - Self::new(inner) - } - - async fn lock<'a>(&'a self) -> Self::Guard<'a> { - self.lock().unwrap() - } -} - -#[cfg(feature = "tokio")] -mod tokio_mutex { - #[async_trait::async_trait] - impl super::AsyncMutex for tokio::sync::Mutex { - type Inner = T; - type Guard<'a> = tokio::sync::MutexGuard<'a, T> where T: 'a; - - fn new(inner: Self::Inner) -> Self { - Self::new(inner) - } - - async fn lock<'a>(&'a self) -> Self::Guard<'a> { - self.lock().await - } - } -} diff --git a/core/mvcc/mvcc-rs/tests/concurrency_test.rs b/core/mvcc/mvcc-rs/tests/concurrency_test.rs index f1806d4ae..1ea28453c 100644 --- a/core/mvcc/mvcc-rs/tests/concurrency_test.rs +++ b/core/mvcc/mvcc-rs/tests/concurrency_test.rs @@ -11,7 +11,7 @@ fn test_non_overlapping_concurrent_inserts() { // row IDs. let clock = LocalClock::default(); let storage = mvcc_rs::persistent_storage::Noop {}; - let db = Arc::new(Database::<_, _, tokio::sync::Mutex<_>>::new(clock, storage)); + let db = Arc::new(Database::new(clock, storage)); let ids = Arc::new(AtomicU64::new(0)); shuttle::check_random( move || { From 8b1ef20c0871e4aacf178413d299a459287d960c Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Mon, 15 May 2023 10:50:47 +0200 Subject: [PATCH 086/128] treewide: drop storage trait We're good with an enum, and async_trait has a runtime cost we don't like. --- core/mvcc/bindings/c/Cargo.toml | 2 +- core/mvcc/bindings/c/src/lib.rs | 9 +- core/mvcc/mvcc-rs/Cargo.toml | 12 +- core/mvcc/mvcc-rs/benches/my_benchmark.rs | 7 +- core/mvcc/mvcc-rs/src/cursor.rs | 21 +--- core/mvcc/mvcc-rs/src/database.rs | 31 ++---- core/mvcc/mvcc-rs/src/persistent_storage.rs | 115 ++++++++------------ core/mvcc/mvcc-rs/tests/concurrency_test.rs | 2 +- 8 files changed, 72 insertions(+), 127 deletions(-) diff --git a/core/mvcc/bindings/c/Cargo.toml b/core/mvcc/bindings/c/Cargo.toml index 3e348ff73..4d8bb1427 100644 --- a/core/mvcc/bindings/c/Cargo.toml +++ b/core/mvcc/bindings/c/Cargo.toml @@ -12,7 +12,7 @@ cbindgen = "0.24.0" [dependencies] base64 = "0.21.0" -mvcc-rs = { path = "../../mvcc-rs", features = ["tokio"] } +mvcc-rs = { path = "../../mvcc-rs" } tokio = { version = "1.27.0", features = ["full", "parking_lot"] } tracing = "0.1.37" tracing-subscriber = { version = "0" } diff --git a/core/mvcc/bindings/c/src/lib.rs b/core/mvcc/bindings/c/src/lib.rs index b602245c8..18e08db0b 100644 --- a/core/mvcc/bindings/c/src/lib.rs +++ b/core/mvcc/bindings/c/src/lib.rs @@ -12,13 +12,10 @@ use types::{DbContext, MVCCDatabaseRef, MVCCScanCursorRef, ScanCursorContext}; type Clock = clock::LocalClock; /// cbindgen:ignore -type Storage = persistent_storage::JsonOnDisk; +type Db = database::Database; /// cbindgen:ignore -type Db = database::Database; - -/// cbindgen:ignore -type ScanCursor = cursor::ScanCursor<'static, Clock, Storage>; +type ScanCursor = cursor::ScanCursor<'static, Clock>; static INIT_RUST_LOG: std::sync::Once = std::sync::Once::new(); @@ -40,7 +37,7 @@ pub unsafe extern "C" fn MVCCDatabaseOpen(path: *const std::ffi::c_char) -> MVCC } }; tracing::debug!("mvccrs: opening persistent storage at {path}"); - let storage = crate::persistent_storage::JsonOnDisk::new(path); + let storage = crate::persistent_storage::Storage::new_json_on_disk(path); let db = Db::new(clock, storage); let runtime = tokio::runtime::Runtime::new().unwrap(); let ctx = DbContext { db, runtime }; diff --git a/core/mvcc/mvcc-rs/Cargo.toml b/core/mvcc/mvcc-rs/Cargo.toml index 11cb010d6..e5c6ea399 100644 --- a/core/mvcc/mvcc-rs/Cargo.toml +++ b/core/mvcc/mvcc-rs/Cargo.toml @@ -5,12 +5,11 @@ edition = "2021" [dependencies] anyhow = "1.0.70" -async-trait = "0.1.68" futures = "0.3.28" thiserror = "1.0.40" tracing = "0.1.37" -tokio = { version = "1.27.0", features = ["full", "parking_lot"], optional = true } -tokio-stream = { version = "0.1.12", optional = true, features = ["io-util"] } +tokio = { version = "1.27.0", features = ["full", "parking_lot"] } +tokio-stream = { version = "0.1.12", features = ["io-util"] } serde = { version = "1.0.160", features = ["derive"] } serde_json = "1.0.96" pin-project = "1.0.12" @@ -21,10 +20,9 @@ base64 = "0.21.0" criterion = { version = "0.4", features = ["html_reports", "async", "async_futures"] } pprof = { version = "0.11.1", features = ["criterion", "flamegraph"] } shuttle = "0.6.0" -tokio = { version = "1.27.0", features = ["full", "parking_lot"] } tracing-subscriber = "0" tracing-test = "0" -mvcc-rs = { path = ".", features = ["tokio"] } +mvcc-rs = { path = "." } [[bench]] name = "my_benchmark" @@ -32,6 +30,4 @@ harness = false [features] default = [] -full = ["tokio"] -c_bindings = ["tokio", "dep:tracing-subscriber"] -tokio = ["dep:tokio", "dep:tokio-stream"] +c_bindings = ["dep:tracing-subscriber"] diff --git a/core/mvcc/mvcc-rs/benches/my_benchmark.rs b/core/mvcc/mvcc-rs/benches/my_benchmark.rs index 494df0cf6..8d0c28dce 100644 --- a/core/mvcc/mvcc-rs/benches/my_benchmark.rs +++ b/core/mvcc/mvcc-rs/benches/my_benchmark.rs @@ -4,12 +4,9 @@ use mvcc_rs::clock::LocalClock; use mvcc_rs::database::{Database, Row, RowID}; use pprof::criterion::{Output, PProfProfiler}; -fn bench_db() -> Database< - LocalClock, - mvcc_rs::persistent_storage::Noop, -> { +fn bench_db() -> Database { let clock = LocalClock::default(); - let storage = mvcc_rs::persistent_storage::Noop {}; + let storage = mvcc_rs::persistent_storage::Storage::new_noop(); Database::new(clock, storage) } diff --git a/core/mvcc/mvcc-rs/src/cursor.rs b/core/mvcc/mvcc-rs/src/cursor.rs index ea988474a..e289093ff 100644 --- a/core/mvcc/mvcc-rs/src/cursor.rs +++ b/core/mvcc/mvcc-rs/src/cursor.rs @@ -1,29 +1,16 @@ use crate::clock::LogicalClock; use crate::database::{Database, Result, Row, RowID}; -use crate::persistent_storage::Storage; #[derive(Debug)] -pub struct ScanCursor< - 'a, - Clock: LogicalClock, - StorageImpl: Storage, -> { - pub db: &'a Database, +pub struct ScanCursor<'a, Clock: LogicalClock> { + pub db: &'a Database, pub row_ids: Vec, pub index: usize, tx_id: u64, } -impl< - 'a, - Clock: LogicalClock, - StorageImpl: Storage, - > ScanCursor<'a, Clock, StorageImpl> -{ - pub async fn new( - db: &'a Database, - table_id: u64, - ) -> Result> { +impl<'a, Clock: LogicalClock> ScanCursor<'a, Clock> { + pub async fn new(db: &'a Database, table_id: u64) -> Result> { let tx_id = db.begin_tx().await; let row_ids = db.scan_row_ids_for_table(table_id).await?; Ok(Self { diff --git a/core/mvcc/mvcc-rs/src/database.rs b/core/mvcc/mvcc-rs/src/database.rs index a81b1e16e..8d6521756 100644 --- a/core/mvcc/mvcc-rs/src/database.rs +++ b/core/mvcc/mvcc-rs/src/database.rs @@ -1,5 +1,6 @@ use crate::clock::LogicalClock; use crate::errors::DatabaseError; +use crate::persistent_storage::Storage; use serde::{Deserialize, Serialize}; use std::cell::RefCell; use std::collections::{BTreeMap, HashMap, HashSet}; @@ -125,18 +126,11 @@ enum TransactionState { /// A database with MVCC. #[derive(Debug)] -pub struct Database< - Clock: LogicalClock, - Storage: crate::persistent_storage::Storage, -> { - inner: Arc>>, +pub struct Database { + inner: Arc>>, } -impl< - Clock: LogicalClock, - Storage: crate::persistent_storage::Storage, - > Database -{ +impl Database { /// Creates a new database. pub fn new(clock: Clock, storage: Storage) -> Self { let inner = DatabaseInner { @@ -294,7 +288,7 @@ impl< } #[derive(Debug)] -pub struct DatabaseInner { +pub struct DatabaseInner { rows: RefCell>>, txs: RefCell>, tx_timestamps: RefCell>, @@ -303,9 +297,7 @@ pub struct DatabaseInner - DatabaseInner -{ +impl DatabaseInner { async fn insert(&self, tx_id: TxID, row: Row) -> Result<()> { let mut txs = self.txs.borrow_mut(); let tx = txs @@ -624,12 +616,9 @@ mod tests { use crate::clock::LocalClock; use tracing_test::traced_test; - fn test_db() -> Database< - LocalClock, - crate::persistent_storage::Noop, - > { + fn test_db() -> Database { let clock = LocalClock::new(); - let storage = crate::persistent_storage::Noop {}; + let storage = crate::persistent_storage::Storage::new_noop(); Database::new(clock, storage) } @@ -1286,7 +1275,7 @@ mod tests { .unwrap() .as_nanos(), )); - let storage = crate::persistent_storage::JsonOnDisk { path: path.clone() }; + let storage = crate::persistent_storage::Storage::new_json_on_disk(path.clone()); let db = Database::new(clock, storage); let tx1 = db.begin_tx().await; @@ -1381,7 +1370,7 @@ mod tests { db.commit_tx(tx4).await.unwrap(); let clock = LocalClock::new(); - let storage = crate::persistent_storage::JsonOnDisk { path }; + let storage = crate::persistent_storage::Storage::new_json_on_disk(path); let db = Database::new(clock, storage); db.recover().await.unwrap(); println!("{:#?}", db); diff --git a/core/mvcc/mvcc-rs/src/persistent_storage.rs b/core/mvcc/mvcc-rs/src/persistent_storage.rs index 2277e4d2c..f07379981 100644 --- a/core/mvcc/mvcc-rs/src/persistent_storage.rs +++ b/core/mvcc/mvcc-rs/src/persistent_storage.rs @@ -1,53 +1,29 @@ use crate::database::{LogRecord, Result}; - -/// Persistent storage API for storing and retrieving transactions. -/// TODO: final design in heavy progress! -#[async_trait::async_trait] -pub trait Storage { - type Stream: futures::stream::Stream; - - /// Append a transaction in the transaction log. - async fn log_tx(&mut self, m: LogRecord) -> Result<()>; - - /// Read the transaction log for replay. - async fn read_tx_log(&self) -> Result; -} - -pub struct Noop {} - -#[async_trait::async_trait] -impl Storage for Noop { - type Stream = futures::stream::Empty; - - async fn log_tx(&mut self, _m: LogRecord) -> Result<()> { - Ok(()) - } - - async fn read_tx_log(&self) -> Result { - Ok(futures::stream::empty()) - } -} +use crate::errors::DatabaseError; #[derive(Debug)] -pub struct JsonOnDisk { - pub path: std::path::PathBuf, +pub enum Storage { + Noop, + JsonOnDisk(std::path::PathBuf), } -impl JsonOnDisk { - pub fn new(path: impl Into) -> Self { +impl Storage { + pub fn new_noop() -> Self { + Self::Noop + } + + pub fn new_json_on_disk(path: impl Into) -> Self { let path = path.into(); - Self { path } + Self::JsonOnDisk(path) } } -#[cfg(feature = "tokio")] #[pin_project::pin_project] pub struct JsonOnDiskStream { #[pin] inner: tokio_stream::wrappers::LinesStream>, } -#[cfg(feature = "tokio")] impl futures::stream::Stream for JsonOnDiskStream { type Item = LogRecord; @@ -62,41 +38,44 @@ impl futures::stream::Stream for JsonOnDiskStream { } } -#[cfg(feature = "tokio")] -#[async_trait::async_trait] -impl Storage for JsonOnDisk { - type Stream = JsonOnDiskStream; - - async fn log_tx(&mut self, m: LogRecord) -> Result<()> { - use crate::errors::DatabaseError; - use tokio::io::AsyncWriteExt; - let t = serde_json::to_vec(&m).map_err(|e| DatabaseError::Io(e.to_string()))?; - let mut file = tokio::fs::OpenOptions::new() - .create(true) - .append(true) - .open(&self.path) - .await - .map_err(|e| DatabaseError::Io(e.to_string()))?; - file.write_all(&t) - .await - .map_err(|e| DatabaseError::Io(e.to_string()))?; - file.write_all(b"\n") - .await - .map_err(|e| DatabaseError::Io(e.to_string()))?; +impl Storage { + pub async fn log_tx(&mut self, m: LogRecord) -> Result<()> { + if let Self::JsonOnDisk(path) = self { + use tokio::io::AsyncWriteExt; + let t = serde_json::to_vec(&m).map_err(|e| DatabaseError::Io(e.to_string()))?; + let mut file = tokio::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&path) + .await + .map_err(|e| DatabaseError::Io(e.to_string()))?; + file.write_all(&t) + .await + .map_err(|e| DatabaseError::Io(e.to_string()))?; + file.write_all(b"\n") + .await + .map_err(|e| DatabaseError::Io(e.to_string()))?; + } Ok(()) } - async fn read_tx_log(&self) -> Result { - use tokio::io::AsyncBufReadExt; - let file = tokio::fs::OpenOptions::new() - .read(true) - .open(&self.path) - .await - .unwrap(); - Ok(JsonOnDiskStream { - inner: tokio_stream::wrappers::LinesStream::new( - tokio::io::BufReader::new(file).lines(), - ), - }) + pub async fn read_tx_log(&self) -> Result { + if let Self::JsonOnDisk(path) = self { + use tokio::io::AsyncBufReadExt; + let file = tokio::fs::OpenOptions::new() + .read(true) + .open(&path) + .await + .map_err(|e| DatabaseError::Io(e.to_string()))?; + Ok(JsonOnDiskStream { + inner: tokio_stream::wrappers::LinesStream::new( + tokio::io::BufReader::new(file).lines(), + ), + }) + } else { + Err(crate::errors::DatabaseError::Io( + "cannot read from Noop storage".to_string(), + )) + } } } diff --git a/core/mvcc/mvcc-rs/tests/concurrency_test.rs b/core/mvcc/mvcc-rs/tests/concurrency_test.rs index 1ea28453c..4ad81c645 100644 --- a/core/mvcc/mvcc-rs/tests/concurrency_test.rs +++ b/core/mvcc/mvcc-rs/tests/concurrency_test.rs @@ -10,7 +10,7 @@ fn test_non_overlapping_concurrent_inserts() { // Two threads insert to the database concurrently using non-overlapping // row IDs. let clock = LocalClock::default(); - let storage = mvcc_rs::persistent_storage::Noop {}; + let storage = mvcc_rs::persistent_storage::Storage::new_noop(); let db = Arc::new(Database::new(clock, storage)); let ids = Arc::new(AtomicU64::new(0)); shuttle::check_random( From 5da87739fa06a2771631e6e61615b5a70cd98857 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Mon, 15 May 2023 14:32:22 +0200 Subject: [PATCH 087/128] bindings: split transcation begin from insert/read libSQL expects to be able to begin/commit a transaction independently of reading or inserting data. --- core/mvcc/bindings/c/include/mvcc.h | 10 ++++++- core/mvcc/bindings/c/src/lib.rs | 46 ++++++++++++++++++++++++----- core/mvcc/mvcc-rs/src/cursor.rs | 9 ++++-- core/mvcc/mvcc-rs/src/database.rs | 2 +- 4 files changed, 54 insertions(+), 13 deletions(-) diff --git a/core/mvcc/bindings/c/include/mvcc.h b/core/mvcc/bindings/c/include/mvcc.h index e0c432cd9..eead91b01 100644 --- a/core/mvcc/bindings/c/include/mvcc.h +++ b/core/mvcc/bindings/c/include/mvcc.h @@ -25,13 +25,21 @@ MVCCDatabaseRef MVCCDatabaseOpen(const char *path); void MVCCDatabaseClose(MVCCDatabaseRef db); +uint64_t MVCCTransactionBegin(MVCCDatabaseRef db); + +MVCCError MVCCTransactionCommit(MVCCDatabaseRef db, uint64_t tx_id); + +MVCCError MVCCTransactionRollback(MVCCDatabaseRef db, uint64_t tx_id); + MVCCError MVCCDatabaseInsert(MVCCDatabaseRef db, + uint64_t tx_id, uint64_t table_id, uint64_t row_id, const void *value_ptr, uintptr_t value_len); MVCCError MVCCDatabaseRead(MVCCDatabaseRef db, + uint64_t tx_id, uint64_t table_id, uint64_t row_id, uint8_t **value_ptr, @@ -39,7 +47,7 @@ MVCCError MVCCDatabaseRead(MVCCDatabaseRef db, void MVCCFreeStr(void *ptr); -MVCCScanCursorRef MVCCScanCursorOpen(MVCCDatabaseRef db, uint64_t table_id); +MVCCScanCursorRef MVCCScanCursorOpen(MVCCDatabaseRef db, uint64_t tx_id, uint64_t table_id); void MVCCScanCursorClose(MVCCScanCursorRef cursor); diff --git a/core/mvcc/bindings/c/src/lib.rs b/core/mvcc/bindings/c/src/lib.rs index 18e08db0b..9a64ddee5 100644 --- a/core/mvcc/bindings/c/src/lib.rs +++ b/core/mvcc/bindings/c/src/lib.rs @@ -55,9 +55,42 @@ pub unsafe extern "C" fn MVCCDatabaseClose(db: MVCCDatabaseRef) { let _ = unsafe { Box::from_raw(db.get_ref_mut()) }; } +#[no_mangle] +pub unsafe extern "C" fn MVCCTransactionBegin(db: MVCCDatabaseRef) -> u64 { + let db = db.get_ref(); + let (db, runtime) = (&db.db, &db.runtime); + let tx_id = runtime.block_on(async move { db.begin_tx().await }); + tracing::debug!("MVCCTransactionBegin: {tx_id}"); + tx_id +} + +#[no_mangle] +pub unsafe extern "C" fn MVCCTransactionCommit(db: MVCCDatabaseRef, tx_id: u64) -> MVCCError { + let db = db.get_ref(); + let (db, runtime) = (&db.db, &db.runtime); + tracing::debug!("MVCCTransactionCommit: {tx_id}"); + match runtime.block_on(async move { db.commit_tx(tx_id).await }) { + Ok(()) => MVCCError::MVCC_OK, + Err(e) => { + tracing::error!("MVCCTransactionCommit: {e}"); + MVCCError::MVCC_IO_ERROR_WRITE + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn MVCCTransactionRollback(db: MVCCDatabaseRef, tx_id: u64) -> MVCCError { + let db = db.get_ref(); + let (db, runtime) = (&db.db, &db.runtime); + tracing::debug!("MVCCTransactionRollback: {tx_id}"); + runtime.block_on(async move { db.rollback_tx(tx_id).await }); + MVCCError::MVCC_OK +} + #[no_mangle] pub unsafe extern "C" fn MVCCDatabaseInsert( db: MVCCDatabaseRef, + tx_id: u64, table_id: u64, row_id: u64, value_ptr: *const std::ffi::c_void, @@ -77,11 +110,7 @@ pub unsafe extern "C" fn MVCCDatabaseInsert( let id = database::RowID { table_id, row_id }; let row = database::Row { id, data }; tracing::debug!("MVCCDatabaseInsert: {row:?}"); - match runtime.block_on(async move { - let tx = db.begin_tx().await; - db.insert(tx, row).await?; - db.commit_tx(tx).await - }) { + match runtime.block_on(async move { db.insert(tx_id, row).await }) { Ok(_) => { tracing::debug!("MVCCDatabaseInsert: success"); MVCCError::MVCC_OK @@ -96,6 +125,7 @@ pub unsafe extern "C" fn MVCCDatabaseInsert( #[no_mangle] pub unsafe extern "C" fn MVCCDatabaseRead( db: MVCCDatabaseRef, + tx_id: u64, table_id: u64, row_id: u64, value_ptr: *mut *mut u8, @@ -105,9 +135,8 @@ pub unsafe extern "C" fn MVCCDatabaseRead( let (db, runtime) = (&db.db, &db.runtime); match runtime.block_on(async move { - let tx = db.begin_tx().await; let id = database::RowID { table_id, row_id }; - let maybe_row = db.read(tx, id).await?; + let maybe_row = db.read(tx_id, id).await?; match maybe_row { Some(row) => { tracing::debug!("Found row {row:?}"); @@ -148,6 +177,7 @@ pub unsafe extern "C" fn MVCCFreeStr(ptr: *mut std::ffi::c_void) { #[no_mangle] pub unsafe extern "C" fn MVCCScanCursorOpen( db: MVCCDatabaseRef, + tx_id: u64, table_id: u64, ) -> MVCCScanCursorRef { tracing::debug!("MVCCScanCursorOpen()"); @@ -156,7 +186,7 @@ pub unsafe extern "C" fn MVCCScanCursorOpen( let database = unsafe { std::mem::transmute::<&DbContext, &'static DbContext>(db.get_ref()) }; let (database, runtime) = (&database.db, &database.runtime); match runtime - .block_on(async move { mvcc_rs::cursor::ScanCursor::new(database, table_id).await }) + .block_on(async move { mvcc_rs::cursor::ScanCursor::new(database, tx_id, table_id).await }) { Ok(cursor) => { if cursor.is_empty() { diff --git a/core/mvcc/mvcc-rs/src/cursor.rs b/core/mvcc/mvcc-rs/src/cursor.rs index e289093ff..1c761f663 100644 --- a/core/mvcc/mvcc-rs/src/cursor.rs +++ b/core/mvcc/mvcc-rs/src/cursor.rs @@ -10,8 +10,11 @@ pub struct ScanCursor<'a, Clock: LogicalClock> { } impl<'a, Clock: LogicalClock> ScanCursor<'a, Clock> { - pub async fn new(db: &'a Database, table_id: u64) -> Result> { - let tx_id = db.begin_tx().await; + pub async fn new( + db: &'a Database, + tx_id: u64, + table_id: u64, + ) -> Result> { let row_ids = db.scan_row_ids_for_table(table_id).await?; Ok(Self { db, @@ -37,7 +40,7 @@ impl<'a, Clock: LogicalClock> ScanCursor<'a, Clock> { } pub async fn close(self) -> Result<()> { - self.db.commit_tx(self.tx_id).await + Ok(()) } pub fn forward(&mut self) -> bool { diff --git a/core/mvcc/mvcc-rs/src/database.rs b/core/mvcc/mvcc-rs/src/database.rs index 8d6521756..1c885be6e 100644 --- a/core/mvcc/mvcc-rs/src/database.rs +++ b/core/mvcc/mvcc-rs/src/database.rs @@ -137,7 +137,7 @@ impl Database { rows: RefCell::new(BTreeMap::new()), txs: RefCell::new(HashMap::new()), tx_timestamps: RefCell::new(BTreeMap::new()), - tx_ids: AtomicU64::new(0), + tx_ids: AtomicU64::new(1), // let's reserve transaction 0 for special purposes clock, storage, }; From 34d21b0eb7601b278361fcef8aaf6cfb488bd668 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Tue, 16 May 2023 11:59:28 +0200 Subject: [PATCH 088/128] storage: add S3 storage It's now possible to run with S3 persistent storage if bindings are built with `s3_storage` feature. Regular S3 credentials need to be set up locally, and all customary env variables like AWS_SECRET_ACCESS_KEY and AWS_ACCESS_KEY_ID work just fine. For local development, one can set MVCCRS_ENDPOINT env variable. For testing with MinIO, the following setup customarily works: MVCCRS_ENDPOINT=http://localhost:9000 \ AWS_SECRET_ACCESS_KEY=minioadmin \ AWS_ACCESS_KEY_ID=minioadmin \ ./libsql /tmp/testme.db --- core/mvcc/bindings/c/Cargo.toml | 5 + core/mvcc/bindings/c/src/lib.rs | 34 ++++- core/mvcc/mvcc-rs/Cargo.toml | 3 + core/mvcc/mvcc-rs/src/database.rs | 10 +- core/mvcc/mvcc-rs/src/persistent_storage.rs | 81 ----------- .../mvcc-rs/src/persistent_storage/mod.rs | 81 +++++++++++ .../mvcc/mvcc-rs/src/persistent_storage/s3.rs | 136 ++++++++++++++++++ 7 files changed, 256 insertions(+), 94 deletions(-) delete mode 100644 core/mvcc/mvcc-rs/src/persistent_storage.rs create mode 100644 core/mvcc/mvcc-rs/src/persistent_storage/mod.rs create mode 100644 core/mvcc/mvcc-rs/src/persistent_storage/s3.rs diff --git a/core/mvcc/bindings/c/Cargo.toml b/core/mvcc/bindings/c/Cargo.toml index 4d8bb1427..04aa540b8 100644 --- a/core/mvcc/bindings/c/Cargo.toml +++ b/core/mvcc/bindings/c/Cargo.toml @@ -16,3 +16,8 @@ mvcc-rs = { path = "../../mvcc-rs" } tokio = { version = "1.27.0", features = ["full", "parking_lot"] } tracing = "0.1.37" tracing-subscriber = { version = "0" } + +[features] +default = [] +json_on_disk_storage = [] +s3_storage = [] diff --git a/core/mvcc/bindings/c/src/lib.rs b/core/mvcc/bindings/c/src/lib.rs index 9a64ddee5..a165de501 100644 --- a/core/mvcc/bindings/c/src/lib.rs +++ b/core/mvcc/bindings/c/src/lib.rs @@ -5,6 +5,7 @@ mod errors; mod types; use errors::MVCCError; +use mvcc_rs::persistent_storage::{s3, Storage}; use mvcc_rs::*; use types::{DbContext, MVCCDatabaseRef, MVCCScanCursorRef, ScanCursorContext}; @@ -19,6 +20,21 @@ type ScanCursor = cursor::ScanCursor<'static, Clock>; static INIT_RUST_LOG: std::sync::Once = std::sync::Once::new(); +async fn storage_for(main_db_path: &str) -> database::Result { + // TODO: let's accept an URL instead of main_db_path here, so we can + // pass custom S3 endpoints, options, etc. + if cfg!(feature = "json_on_disk_storage") { + tracing::info!("JSONonDisk storage stored in {main_db_path}-mvcc"); + return Ok(Storage::new_json_on_disk(format!("{main_db_path}-mvcc"))); + } + if cfg!(feature = "s3_storage") { + tracing::info!("S3 storage for {main_db_path}"); + return Storage::new_s3(s3::Options::with_create_bucket_if_not_exists(true)).await; + } + tracing::info!("No persistent storage for {main_db_path}"); + Ok(Storage::new_noop()) +} + #[no_mangle] pub unsafe extern "C" fn MVCCDatabaseOpen(path: *const std::ffi::c_char) -> MVCCDatabaseRef { INIT_RUST_LOG.call_once(|| { @@ -28,18 +44,26 @@ pub unsafe extern "C" fn MVCCDatabaseOpen(path: *const std::ffi::c_char) -> MVCC tracing::debug!("MVCCDatabaseOpen"); let clock = clock::LocalClock::new(); - let path = unsafe { std::ffi::CStr::from_ptr(path) }; - let path = match path.to_str() { + let main_db_path = unsafe { std::ffi::CStr::from_ptr(path) }; + let main_db_path = match main_db_path.to_str() { Ok(path) => path, Err(_) => { tracing::error!("Invalid UTF-8 path"); return MVCCDatabaseRef::null(); } }; - tracing::debug!("mvccrs: opening persistent storage at {path}"); - let storage = crate::persistent_storage::Storage::new_json_on_disk(path); - let db = Db::new(clock, storage); let runtime = tokio::runtime::Runtime::new().unwrap(); + + tracing::debug!("mvccrs: opening persistent storage for {main_db_path}"); + let storage = match runtime.block_on(storage_for(main_db_path)) { + Ok(storage) => storage, + Err(e) => { + tracing::error!("Failed to open persistent storage: {e}"); + return MVCCDatabaseRef::null(); + } + }; + let db = Db::new(clock, storage); + let ctx = DbContext { db, runtime }; let ctx = Box::leak(Box::new(ctx)); MVCCDatabaseRef::from(ctx) diff --git a/core/mvcc/mvcc-rs/Cargo.toml b/core/mvcc/mvcc-rs/Cargo.toml index e5c6ea399..40cbd2a7c 100644 --- a/core/mvcc/mvcc-rs/Cargo.toml +++ b/core/mvcc/mvcc-rs/Cargo.toml @@ -15,6 +15,9 @@ serde_json = "1.0.96" pin-project = "1.0.12" tracing-subscriber = { version = "0", optional = true } base64 = "0.21.0" +aws-sdk-s3 = "0.27.0" +aws-config = "0.55.2" +tokio-util = "0.7.8" [dev-dependencies] criterion = { version = "0.4", features = ["html_reports", "async", "async_futures"] } diff --git a/core/mvcc/mvcc-rs/src/database.rs b/core/mvcc/mvcc-rs/src/database.rs index 1c885be6e..5da0f4dd7 100644 --- a/core/mvcc/mvcc-rs/src/database.rs +++ b/core/mvcc/mvcc-rs/src/database.rs @@ -36,7 +36,7 @@ pub type TxID = u64; /// A log record contains all the versions inserted and deleted by a transaction. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct LogRecord { - tx_timestamp: TxID, + pub(crate) tx_timestamp: TxID, row_versions: Vec, } @@ -530,13 +530,7 @@ impl DatabaseInner { } pub async fn recover(&self) -> Result<()> { - use futures::StreamExt; - let tx_log = self - .storage - .read_tx_log() - .await? - .collect::>() - .await; + let tx_log = self.storage.read_tx_log().await?; for record in tx_log { println!("RECOVERING {:?}", record); for version in record.row_versions { diff --git a/core/mvcc/mvcc-rs/src/persistent_storage.rs b/core/mvcc/mvcc-rs/src/persistent_storage.rs deleted file mode 100644 index f07379981..000000000 --- a/core/mvcc/mvcc-rs/src/persistent_storage.rs +++ /dev/null @@ -1,81 +0,0 @@ -use crate::database::{LogRecord, Result}; -use crate::errors::DatabaseError; - -#[derive(Debug)] -pub enum Storage { - Noop, - JsonOnDisk(std::path::PathBuf), -} - -impl Storage { - pub fn new_noop() -> Self { - Self::Noop - } - - pub fn new_json_on_disk(path: impl Into) -> Self { - let path = path.into(); - Self::JsonOnDisk(path) - } -} - -#[pin_project::pin_project] -pub struct JsonOnDiskStream { - #[pin] - inner: tokio_stream::wrappers::LinesStream>, -} - -impl futures::stream::Stream for JsonOnDiskStream { - type Item = LogRecord; - - fn poll_next( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - let this = self.project(); - this.inner - .poll_next(cx) - .map(|x| x.and_then(|x| x.ok().and_then(|x| serde_json::from_str(x.as_str()).ok()))) - } -} - -impl Storage { - pub async fn log_tx(&mut self, m: LogRecord) -> Result<()> { - if let Self::JsonOnDisk(path) = self { - use tokio::io::AsyncWriteExt; - let t = serde_json::to_vec(&m).map_err(|e| DatabaseError::Io(e.to_string()))?; - let mut file = tokio::fs::OpenOptions::new() - .create(true) - .append(true) - .open(&path) - .await - .map_err(|e| DatabaseError::Io(e.to_string()))?; - file.write_all(&t) - .await - .map_err(|e| DatabaseError::Io(e.to_string()))?; - file.write_all(b"\n") - .await - .map_err(|e| DatabaseError::Io(e.to_string()))?; - } - Ok(()) - } - - pub async fn read_tx_log(&self) -> Result { - if let Self::JsonOnDisk(path) = self { - use tokio::io::AsyncBufReadExt; - let file = tokio::fs::OpenOptions::new() - .read(true) - .open(&path) - .await - .map_err(|e| DatabaseError::Io(e.to_string()))?; - Ok(JsonOnDiskStream { - inner: tokio_stream::wrappers::LinesStream::new( - tokio::io::BufReader::new(file).lines(), - ), - }) - } else { - Err(crate::errors::DatabaseError::Io( - "cannot read from Noop storage".to_string(), - )) - } - } -} diff --git a/core/mvcc/mvcc-rs/src/persistent_storage/mod.rs b/core/mvcc/mvcc-rs/src/persistent_storage/mod.rs new file mode 100644 index 000000000..1dd72c02a --- /dev/null +++ b/core/mvcc/mvcc-rs/src/persistent_storage/mod.rs @@ -0,0 +1,81 @@ +use crate::database::{LogRecord, Result}; +use crate::errors::DatabaseError; + +pub mod s3; + +#[derive(Debug)] +pub enum Storage { + Noop, + JsonOnDisk(std::path::PathBuf), + S3(s3::Replicator), +} + +impl Storage { + pub fn new_noop() -> Self { + Self::Noop + } + + pub fn new_json_on_disk(path: impl Into) -> Self { + let path = path.into(); + Self::JsonOnDisk(path) + } + + pub async fn new_s3(options: s3::Options) -> Result { + Ok(Self::S3(s3::Replicator::new(options).await?)) + } +} + +impl Storage { + pub async fn log_tx(&mut self, m: LogRecord) -> Result<()> { + match self { + Self::JsonOnDisk(path) => { + use tokio::io::AsyncWriteExt; + let t = serde_json::to_vec(&m).map_err(|e| DatabaseError::Io(e.to_string()))?; + let mut file = tokio::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&path) + .await + .map_err(|e| DatabaseError::Io(e.to_string()))?; + file.write_all(&t) + .await + .map_err(|e| DatabaseError::Io(e.to_string()))?; + file.write_all(b"\n") + .await + .map_err(|e| DatabaseError::Io(e.to_string()))?; + } + Self::S3(replicator) => { + replicator.replicate_tx(m).await?; + } + Self::Noop => (), + } + Ok(()) + } + + pub async fn read_tx_log(&self) -> Result> { + match self { + Self::JsonOnDisk(path) => { + use tokio::io::AsyncBufReadExt; + let file = tokio::fs::OpenOptions::new() + .read(true) + .open(&path) + .await + .map_err(|e| DatabaseError::Io(e.to_string()))?; + + let mut records: Vec = Vec::new(); + let mut lines = tokio::io::BufReader::new(file).lines(); + while let Ok(Some(line)) = lines.next_line().await { + records.push( + serde_json::from_str(&line) + .map_err(|e| DatabaseError::Io(e.to_string()))?, + ) + } + Ok(records) + } + Self::S3(replicator) => replicator.read_tx_log().await, + Self::Noop => Err(crate::errors::DatabaseError::Io( + "cannot read from Noop storage".to_string(), + )), + } + } +} diff --git a/core/mvcc/mvcc-rs/src/persistent_storage/s3.rs b/core/mvcc/mvcc-rs/src/persistent_storage/s3.rs new file mode 100644 index 000000000..836c35363 --- /dev/null +++ b/core/mvcc/mvcc-rs/src/persistent_storage/s3.rs @@ -0,0 +1,136 @@ +use crate::database::{LogRecord, Result}; +use crate::errors::DatabaseError; +use aws_sdk_s3::Client; + +#[derive(Clone, Copy, Debug)] +#[non_exhaustive] +pub struct Options { + pub create_bucket_if_not_exists: bool, +} + +impl Options { + pub fn with_create_bucket_if_not_exists(create_bucket_if_not_exists: bool) -> Self { + Self { + create_bucket_if_not_exists, + } + } +} + +#[derive(Debug)] +pub struct Replicator { + pub client: Client, + pub bucket: String, + pub prefix: String, +} + +impl Replicator { + pub async fn new(options: Options) -> Result { + let mut loader = aws_config::from_env(); + if let Ok(endpoint) = std::env::var("MVCCRS_ENDPOINT") { + loader = loader.endpoint_url(endpoint); + } + let sdk_config = loader.load().await; + let config = aws_sdk_s3::config::Builder::from(&sdk_config) + .force_path_style(true) + .build(); + let bucket = std::env::var("MVCCRS_BUCKET").unwrap_or_else(|_| "mvccrs".to_string()); + let prefix = std::env::var("MVCCRS_PREFIX").unwrap_or_else(|_| "tx".to_string()); + let client = Client::from_conf(config); + + match client.head_bucket().bucket(&bucket).send().await { + Ok(_) => tracing::info!("Bucket {bucket} exists and is accessible"), + Err(aws_sdk_s3::error::SdkError::ServiceError(err)) if err.err().is_not_found() => { + if options.create_bucket_if_not_exists { + tracing::info!("Bucket {bucket} not found, recreating"); + client + .create_bucket() + .bucket(&bucket) + .send() + .await + .map_err(|e| DatabaseError::Io(e.to_string()))?; + } else { + tracing::error!("Bucket {bucket} does not exist"); + return Err(DatabaseError::Io(err.err().to_string())); + } + } + Err(e) => { + tracing::error!("Bucket checking error: {e}"); + return Err(DatabaseError::Io(e.to_string())); + } + } + + Ok(Self { + client, + bucket, + prefix, + }) + } + + pub async fn replicate_tx(&self, record: LogRecord) -> Result<()> { + let key = format!("{}-{:020}", self.prefix, record.tx_timestamp); + tracing::trace!("Replicating {key}"); + let body = serde_json::to_vec(&record).map_err(|e| DatabaseError::Io(e.to_string()))?; + let resp = self + .client + .put_object() + .bucket(&self.bucket) + .key(&key) + .body(body.into()) + .send() + .await + .map_err(|e| DatabaseError::Io(e.to_string()))?; + tracing::trace!("Replicator response: {:?}", resp); + Ok(()) + } + + pub async fn read_tx_log(&self) -> Result> { + let mut records: Vec = Vec::new(); + // Read all objects from the bucket, one log record is stored in one object + let mut next_token = None; + loop { + let mut req = self + .client + .list_objects_v2() + .bucket(&self.bucket) + .prefix(&self.prefix); + if let Some(next_token) = next_token { + req = req.continuation_token(next_token); + } + let resp = req + .send() + .await + .map_err(|e| DatabaseError::Io(e.to_string()))?; + tracing::trace!("List objects response: {:?}", resp); + if let Some(contents) = resp.contents { + // read the record from s3 based on the object metadata (`contents`) + // and store it in the `records` vector + for object in contents { + let key = object.key.unwrap(); + let resp = self + .client + .get_object() + .bucket(&self.bucket) + .key(&key) + .send() + .await + .map_err(|e| DatabaseError::Io(e.to_string()))?; + tracing::trace!("Get object response: {:?}", resp); + let body = resp + .body + .collect() + .await + .map_err(|e| DatabaseError::Io(e.to_string()))?; + let record: LogRecord = serde_json::from_slice(&body.into_bytes()) + .map_err(|e| DatabaseError::Io(e.to_string()))?; + records.push(record); + } + } + if resp.next_continuation_token.is_none() { + break; + } + next_token = resp.next_continuation_token; + } + tracing::trace!("Records: {records:?}"); + Ok(records) + } +} From a28c41f919af5602603fb80876132011670733a1 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Tue, 16 May 2023 14:54:35 +0200 Subject: [PATCH 089/128] bindings: run recovery on MVCCDatabaseOpen --- core/mvcc/bindings/c/src/lib.rs | 2 ++ core/mvcc/mvcc-rs/src/database.rs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/mvcc/bindings/c/src/lib.rs b/core/mvcc/bindings/c/src/lib.rs index a165de501..8cb28a477 100644 --- a/core/mvcc/bindings/c/src/lib.rs +++ b/core/mvcc/bindings/c/src/lib.rs @@ -64,6 +64,8 @@ pub unsafe extern "C" fn MVCCDatabaseOpen(path: *const std::ffi::c_char) -> MVCC }; let db = Db::new(clock, storage); + runtime.block_on(db.recover()).ok(); + let ctx = DbContext { db, runtime }; let ctx = Box::leak(Box::new(ctx)); MVCCDatabaseRef::from(ctx) diff --git a/core/mvcc/mvcc-rs/src/database.rs b/core/mvcc/mvcc-rs/src/database.rs index 5da0f4dd7..84425f1af 100644 --- a/core/mvcc/mvcc-rs/src/database.rs +++ b/core/mvcc/mvcc-rs/src/database.rs @@ -532,7 +532,7 @@ impl DatabaseInner { pub async fn recover(&self) -> Result<()> { let tx_log = self.storage.read_tx_log().await?; for record in tx_log { - println!("RECOVERING {:?}", record); + tracing::debug!("RECOVERING {:?}", record); for version in record.row_versions { let mut rows = self.rows.borrow_mut(); let row_versions = rows.entry(version.row.id).or_insert_with(Vec::new); From ae4cc872b672855b30878c8a41aecb39fa22536a Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Wed, 17 May 2023 12:39:08 +0200 Subject: [PATCH 090/128] treewide: overhaul the API to be sync again We dropped all occurrences of Tokio to avoid the cost of allocations induced by async runtimes. The only async part of the code is now S3 storage, which is just wrapped in a futures::executor::block_on() --- core/mvcc/bindings/c/Cargo.toml | 1 - core/mvcc/bindings/c/src/lib.rs | 86 ++--- core/mvcc/bindings/c/src/types.rs | 34 +- core/mvcc/mvcc-rs/Cargo.toml | 7 +- core/mvcc/mvcc-rs/benches/my_benchmark.rs | 34 +- core/mvcc/mvcc-rs/src/cursor.rs | 10 +- core/mvcc/mvcc-rs/src/database.rs | 334 ++++++++---------- .../mvcc-rs/src/persistent_storage/mod.rs | 33 +- core/mvcc/mvcc-rs/tests/concurrency_test.rs | 68 ++-- 9 files changed, 274 insertions(+), 333 deletions(-) diff --git a/core/mvcc/bindings/c/Cargo.toml b/core/mvcc/bindings/c/Cargo.toml index 04aa540b8..be23a9a1b 100644 --- a/core/mvcc/bindings/c/Cargo.toml +++ b/core/mvcc/bindings/c/Cargo.toml @@ -13,7 +13,6 @@ cbindgen = "0.24.0" [dependencies] base64 = "0.21.0" mvcc-rs = { path = "../../mvcc-rs" } -tokio = { version = "1.27.0", features = ["full", "parking_lot"] } tracing = "0.1.37" tracing-subscriber = { version = "0" } diff --git a/core/mvcc/bindings/c/src/lib.rs b/core/mvcc/bindings/c/src/lib.rs index 8cb28a477..509fa94cf 100644 --- a/core/mvcc/bindings/c/src/lib.rs +++ b/core/mvcc/bindings/c/src/lib.rs @@ -20,7 +20,7 @@ type ScanCursor = cursor::ScanCursor<'static, Clock>; static INIT_RUST_LOG: std::sync::Once = std::sync::Once::new(); -async fn storage_for(main_db_path: &str) -> database::Result { +fn storage_for(main_db_path: &str) -> database::Result { // TODO: let's accept an URL instead of main_db_path here, so we can // pass custom S3 endpoints, options, etc. if cfg!(feature = "json_on_disk_storage") { @@ -29,7 +29,8 @@ async fn storage_for(main_db_path: &str) -> database::Result { } if cfg!(feature = "s3_storage") { tracing::info!("S3 storage for {main_db_path}"); - return Storage::new_s3(s3::Options::with_create_bucket_if_not_exists(true)).await; + let options = s3::Options::with_create_bucket_if_not_exists(true); + return Storage::new_s3(options); } tracing::info!("No persistent storage for {main_db_path}"); Ok(Storage::new_noop()) @@ -52,10 +53,9 @@ pub unsafe extern "C" fn MVCCDatabaseOpen(path: *const std::ffi::c_char) -> MVCC return MVCCDatabaseRef::null(); } }; - let runtime = tokio::runtime::Runtime::new().unwrap(); tracing::debug!("mvccrs: opening persistent storage for {main_db_path}"); - let storage = match runtime.block_on(storage_for(main_db_path)) { + let storage = match storage_for(main_db_path) { Ok(storage) => storage, Err(e) => { tracing::error!("Failed to open persistent storage: {e}"); @@ -64,11 +64,10 @@ pub unsafe extern "C" fn MVCCDatabaseOpen(path: *const std::ffi::c_char) -> MVCC }; let db = Db::new(clock, storage); - runtime.block_on(db.recover()).ok(); + db.recover().ok(); - let ctx = DbContext { db, runtime }; - let ctx = Box::leak(Box::new(ctx)); - MVCCDatabaseRef::from(ctx) + let db = Box::leak(Box::new(DbContext { db })); + MVCCDatabaseRef::from(db) } #[no_mangle] @@ -84,8 +83,7 @@ pub unsafe extern "C" fn MVCCDatabaseClose(db: MVCCDatabaseRef) { #[no_mangle] pub unsafe extern "C" fn MVCCTransactionBegin(db: MVCCDatabaseRef) -> u64 { let db = db.get_ref(); - let (db, runtime) = (&db.db, &db.runtime); - let tx_id = runtime.block_on(async move { db.begin_tx().await }); + let tx_id = db.begin_tx(); tracing::debug!("MVCCTransactionBegin: {tx_id}"); tx_id } @@ -93,9 +91,8 @@ pub unsafe extern "C" fn MVCCTransactionBegin(db: MVCCDatabaseRef) -> u64 { #[no_mangle] pub unsafe extern "C" fn MVCCTransactionCommit(db: MVCCDatabaseRef, tx_id: u64) -> MVCCError { let db = db.get_ref(); - let (db, runtime) = (&db.db, &db.runtime); tracing::debug!("MVCCTransactionCommit: {tx_id}"); - match runtime.block_on(async move { db.commit_tx(tx_id).await }) { + match db.commit_tx(tx_id) { Ok(()) => MVCCError::MVCC_OK, Err(e) => { tracing::error!("MVCCTransactionCommit: {e}"); @@ -107,9 +104,8 @@ pub unsafe extern "C" fn MVCCTransactionCommit(db: MVCCDatabaseRef, tx_id: u64) #[no_mangle] pub unsafe extern "C" fn MVCCTransactionRollback(db: MVCCDatabaseRef, tx_id: u64) -> MVCCError { let db = db.get_ref(); - let (db, runtime) = (&db.db, &db.runtime); tracing::debug!("MVCCTransactionRollback: {tx_id}"); - runtime.block_on(async move { db.rollback_tx(tx_id).await }); + db.rollback_tx(tx_id); MVCCError::MVCC_OK } @@ -132,11 +128,10 @@ pub unsafe extern "C" fn MVCCDatabaseInsert( general_purpose::STANDARD.encode(value) } }; - let (db, runtime) = (&db.db, &db.runtime); let id = database::RowID { table_id, row_id }; let row = database::Row { id, data }; tracing::debug!("MVCCDatabaseInsert: {row:?}"); - match runtime.block_on(async move { db.insert(tx_id, row).await }) { + match db.insert(tx_id, row) { Ok(_) => { tracing::debug!("MVCCDatabaseInsert: success"); MVCCError::MVCC_OK @@ -158,29 +153,24 @@ pub unsafe extern "C" fn MVCCDatabaseRead( value_len: *mut i64, ) -> MVCCError { let db = db.get_ref(); - let (db, runtime) = (&db.db, &db.runtime); - match runtime.block_on(async move { + match { let id = database::RowID { table_id, row_id }; - let maybe_row = db.read(tx_id, id).await?; + let maybe_row = db.read(tx_id, id); match maybe_row { - Some(row) => { + Ok(Some(row)) => { tracing::debug!("Found row {row:?}"); let str_len = row.data.len() + 1; - let value = std::ffi::CString::new(row.data.as_bytes()).map_err(|e| { - mvcc_rs::errors::DatabaseError::Io(format!( - "Failed to transform read data into CString: {e}" - )) - })?; + let value = std::ffi::CString::new(row.data.as_bytes()).unwrap_or_default(); unsafe { *value_ptr = value.into_raw() as *mut u8; *value_len = str_len as i64; } } - None => unsafe { *value_len = -1 }, + _ => unsafe { *value_len = -1 }, }; Ok::<(), mvcc_rs::errors::DatabaseError>(()) - }) { + } { Ok(_) => { tracing::debug!("MVCCDatabaseRead: success"); MVCCError::MVCC_OK @@ -209,11 +199,8 @@ pub unsafe extern "C" fn MVCCScanCursorOpen( tracing::debug!("MVCCScanCursorOpen()"); // Reference is transmuted to &'static in order to be able to pass the cursor back to C. // The contract with C is to never use a cursor after MVCCDatabaseClose() has been called. - let database = unsafe { std::mem::transmute::<&DbContext, &'static DbContext>(db.get_ref()) }; - let (database, runtime) = (&database.db, &database.runtime); - match runtime - .block_on(async move { mvcc_rs::cursor::ScanCursor::new(database, tx_id, table_id).await }) - { + let db = unsafe { std::mem::transmute::<&Db, &'static Db>(db.get_ref()) }; + match mvcc_rs::cursor::ScanCursor::new(db, tx_id, table_id) { Ok(cursor) => { if cursor.is_empty() { tracing::debug!("Cursor is empty"); @@ -223,7 +210,7 @@ pub unsafe extern "C" fn MVCCScanCursorOpen( } tracing::debug!("Cursor open: {cursor:?}"); MVCCScanCursorRef { - ptr: Box::into_raw(Box::new(ScanCursorContext { cursor, db })), + ptr: Box::into_raw(Box::new(ScanCursorContext { cursor })), } } Err(e) => { @@ -242,10 +229,8 @@ pub unsafe extern "C" fn MVCCScanCursorClose(cursor: MVCCScanCursorRef) { tracing::debug!("warning: `cursor` is null in MVCCScanCursorClose()"); return; } - let cursor_ctx = unsafe { Box::from_raw(cursor.ptr) }; - let db_context = cursor_ctx.db.clone(); - let runtime = &db_context.get_ref().runtime; - runtime.block_on(async move { cursor_ctx.cursor.close().await.ok() }); + let cursor = unsafe { Box::from_raw(cursor.ptr) }.cursor; + cursor.close().ok(); } #[no_mangle] @@ -259,31 +244,24 @@ pub unsafe extern "C" fn MVCCScanCursorRead( tracing::debug!("warning: `cursor` is null in MVCCScanCursorRead()"); return MVCCError::MVCC_IO_ERROR_READ; } - let cursor_ctx = unsafe { &*cursor.ptr }; - let runtime = &cursor_ctx.db.get_ref().runtime; - let cursor = &cursor_ctx.cursor; + let cursor = cursor.get_ref(); - // TODO: deduplicate with MVCCDatabaseRead() - match runtime.block_on(async move { - let maybe_row = cursor.current_row().await?; + match { + let maybe_row = cursor.current_row(); match maybe_row { - Some(row) => { + Ok(Some(row)) => { tracing::debug!("Found row {row:?}"); let str_len = row.data.len() + 1; - let value = std::ffi::CString::new(row.data.as_bytes()).map_err(|e| { - mvcc_rs::errors::DatabaseError::Io(format!( - "Failed to transform read data into CString: {e}" - )) - })?; + let value = std::ffi::CString::new(row.data.as_bytes()).unwrap_or_default(); unsafe { *value_ptr = value.into_raw() as *mut u8; *value_len = str_len as i64; } } - None => unsafe { *value_len = -1 }, + _ => unsafe { *value_len = -1 }, }; Ok::<(), mvcc_rs::errors::DatabaseError>(()) - }) { + } { Ok(_) => { tracing::debug!("MVCCDatabaseRead: success"); MVCCError::MVCC_OK @@ -297,8 +275,7 @@ pub unsafe extern "C" fn MVCCScanCursorRead( #[no_mangle] pub unsafe extern "C" fn MVCCScanCursorNext(cursor: MVCCScanCursorRef) -> std::ffi::c_int { - let cursor_ctx = unsafe { &mut *cursor.ptr }; - let cursor = &mut cursor_ctx.cursor; + let cursor = cursor.get_ref_mut(); tracing::debug!("MVCCScanCursorNext(): {}", cursor.index); if cursor.forward() { tracing::debug!("Forwarded to {}", cursor.index); @@ -311,8 +288,7 @@ pub unsafe extern "C" fn MVCCScanCursorNext(cursor: MVCCScanCursorRef) -> std::f #[no_mangle] pub unsafe extern "C" fn MVCCScanCursorPosition(cursor: MVCCScanCursorRef) -> u64 { - let cursor_ctx = unsafe { &mut *cursor.ptr }; - let cursor = &mut cursor_ctx.cursor; + let cursor = cursor.get_ref(); cursor .current_row_id() .map(|row_id| row_id.row_id) diff --git a/core/mvcc/bindings/c/src/types.rs b/core/mvcc/bindings/c/src/types.rs index 6f7874604..52c21951d 100644 --- a/core/mvcc/bindings/c/src/types.rs +++ b/core/mvcc/bindings/c/src/types.rs @@ -17,14 +17,14 @@ impl MVCCDatabaseRef { self.ptr.is_null() } - pub fn get_ref(&self) -> &DbContext { - unsafe { &*(self.ptr) } + pub fn get_ref(&self) -> &Db { + &unsafe { &*(self.ptr) }.db } #[allow(clippy::mut_from_ref)] - pub fn get_ref_mut(&self) -> &mut DbContext { + pub fn get_ref_mut(&self) -> &mut Db { let ptr_mut = self.ptr as *mut DbContext; - unsafe { &mut (*ptr_mut) } + &mut unsafe { &mut (*ptr_mut) }.db } } @@ -44,12 +44,10 @@ impl From<&mut DbContext> for MVCCDatabaseRef { pub struct DbContext { pub(crate) db: Db, - pub(crate) runtime: tokio::runtime::Runtime, } pub struct ScanCursorContext { - pub cursor: crate::ScanCursor, - pub db: MVCCDatabaseRef, + pub(crate) cursor: crate::ScanCursor, } #[derive(Clone, Debug)] @@ -57,3 +55,25 @@ pub struct ScanCursorContext { pub struct MVCCScanCursorRef { pub ptr: *mut ScanCursorContext, } + +impl MVCCScanCursorRef { + pub fn null() -> MVCCScanCursorRef { + MVCCScanCursorRef { + ptr: std::ptr::null_mut(), + } + } + + pub fn is_null(&self) -> bool { + self.ptr.is_null() + } + + pub fn get_ref(&self) -> &crate::ScanCursor { + &unsafe { &*(self.ptr) }.cursor + } + + #[allow(clippy::mut_from_ref)] + pub fn get_ref_mut(&self) -> &mut crate::ScanCursor { + let ptr_mut = self.ptr as *mut ScanCursorContext; + &mut unsafe { &mut (*ptr_mut) }.cursor + } +} diff --git a/core/mvcc/mvcc-rs/Cargo.toml b/core/mvcc/mvcc-rs/Cargo.toml index 40cbd2a7c..f2087c651 100644 --- a/core/mvcc/mvcc-rs/Cargo.toml +++ b/core/mvcc/mvcc-rs/Cargo.toml @@ -5,19 +5,16 @@ edition = "2021" [dependencies] anyhow = "1.0.70" -futures = "0.3.28" thiserror = "1.0.40" tracing = "0.1.37" -tokio = { version = "1.27.0", features = ["full", "parking_lot"] } -tokio-stream = { version = "0.1.12", features = ["io-util"] } serde = { version = "1.0.160", features = ["derive"] } serde_json = "1.0.96" -pin-project = "1.0.12" tracing-subscriber = { version = "0", optional = true } base64 = "0.21.0" aws-sdk-s3 = "0.27.0" aws-config = "0.55.2" -tokio-util = "0.7.8" +parking_lot = "0.12.1" +futures = "0.3.28" [dev-dependencies] criterion = { version = "0.4", features = ["html_reports", "async", "async_futures"] } diff --git a/core/mvcc/mvcc-rs/benches/my_benchmark.rs b/core/mvcc/mvcc-rs/benches/my_benchmark.rs index 8d0c28dce..4a9e3d122 100644 --- a/core/mvcc/mvcc-rs/benches/my_benchmark.rs +++ b/core/mvcc/mvcc-rs/benches/my_benchmark.rs @@ -17,30 +17,30 @@ fn bench(c: &mut Criterion) { let db = bench_db(); group.bench_function("begin_tx", |b| { b.to_async(FuturesExecutor).iter(|| async { - db.begin_tx().await; + db.begin_tx(); }) }); let db = bench_db(); group.bench_function("begin_tx + rollback_tx", |b| { b.to_async(FuturesExecutor).iter(|| async { - let tx_id = db.begin_tx().await; - db.rollback_tx(tx_id).await + let tx_id = db.begin_tx(); + db.rollback_tx(tx_id) }) }); let db = bench_db(); group.bench_function("begin_tx + commit_tx", |b| { b.to_async(FuturesExecutor).iter(|| async { - let tx_id = db.begin_tx().await; - db.commit_tx(tx_id).await + let tx_id = db.begin_tx(); + db.commit_tx(tx_id) }) }); let db = bench_db(); group.bench_function("begin_tx-read-commit_tx", |b| { b.to_async(FuturesExecutor).iter(|| async { - let tx_id = db.begin_tx().await; + let tx_id = db.begin_tx(); db.read( tx_id, RowID { @@ -48,16 +48,15 @@ fn bench(c: &mut Criterion) { row_id: 1, }, ) - .await .unwrap(); - db.commit_tx(tx_id).await + db.commit_tx(tx_id) }) }); let db = bench_db(); group.bench_function("begin_tx-update-commit_tx", |b| { b.to_async(FuturesExecutor).iter(|| async { - let tx_id = db.begin_tx().await; + let tx_id = db.begin_tx(); db.update( tx_id, Row { @@ -68,15 +67,14 @@ fn bench(c: &mut Criterion) { data: "World".to_string(), }, ) - .await .unwrap(); - db.commit_tx(tx_id).await + db.commit_tx(tx_id) }) }); let db = bench_db(); - let tx = futures::executor::block_on(db.begin_tx()); - futures::executor::block_on(db.insert( + let tx = db.begin_tx(); + db.insert( tx, Row { id: RowID { @@ -85,7 +83,7 @@ fn bench(c: &mut Criterion) { }, data: "Hello".to_string(), }, - )) + ) .unwrap(); group.bench_function("read", |b| { b.to_async(FuturesExecutor).iter(|| async { @@ -96,14 +94,13 @@ fn bench(c: &mut Criterion) { row_id: 1, }, ) - .await .unwrap(); }) }); let db = bench_db(); - let tx = futures::executor::block_on(db.begin_tx()); - futures::executor::block_on(db.insert( + let tx = db.begin_tx(); + db.insert( tx, Row { id: RowID { @@ -112,7 +109,7 @@ fn bench(c: &mut Criterion) { }, data: "Hello".to_string(), }, - )) + ) .unwrap(); group.bench_function("update", |b| { b.to_async(FuturesExecutor).iter(|| async { @@ -126,7 +123,6 @@ fn bench(c: &mut Criterion) { data: "World".to_string(), }, ) - .await .unwrap(); }) }); diff --git a/core/mvcc/mvcc-rs/src/cursor.rs b/core/mvcc/mvcc-rs/src/cursor.rs index 1c761f663..7042c090f 100644 --- a/core/mvcc/mvcc-rs/src/cursor.rs +++ b/core/mvcc/mvcc-rs/src/cursor.rs @@ -10,12 +10,12 @@ pub struct ScanCursor<'a, Clock: LogicalClock> { } impl<'a, Clock: LogicalClock> ScanCursor<'a, Clock> { - pub async fn new( + pub fn new( db: &'a Database, tx_id: u64, table_id: u64, ) -> Result> { - let row_ids = db.scan_row_ids_for_table(table_id).await?; + let row_ids = db.scan_row_ids_for_table(table_id)?; Ok(Self { db, tx_id, @@ -31,15 +31,15 @@ impl<'a, Clock: LogicalClock> ScanCursor<'a, Clock> { Some(self.row_ids[self.index]) } - pub async fn current_row(&self) -> Result> { + pub fn current_row(&self) -> Result> { if self.index >= self.row_ids.len() { return Ok(None); } let id = self.row_ids[self.index]; - self.db.read(self.tx_id, id).await + self.db.read(self.tx_id, id) } - pub async fn close(self) -> Result<()> { + pub fn close(self) -> Result<()> { Ok(()) } diff --git a/core/mvcc/mvcc-rs/src/database.rs b/core/mvcc/mvcc-rs/src/database.rs index 84425f1af..c69da6c2e 100644 --- a/core/mvcc/mvcc-rs/src/database.rs +++ b/core/mvcc/mvcc-rs/src/database.rs @@ -1,12 +1,12 @@ use crate::clock::LogicalClock; use crate::errors::DatabaseError; use crate::persistent_storage::Storage; +use parking_lot::Mutex; use serde::{Deserialize, Serialize}; use std::cell::RefCell; use std::collections::{BTreeMap, HashMap, HashSet}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; -use tokio::sync::Mutex; pub type Result = std::result::Result; @@ -156,9 +156,9 @@ impl Database { /// * `tx_id` - the ID of the transaction in which to insert the new row. /// * `row` - the row object containing the values to be inserted. /// - pub async fn insert(&self, tx_id: TxID, row: Row) -> Result<()> { - let inner = self.inner.lock().await; - inner.insert(tx_id, row).await + pub fn insert(&self, tx_id: TxID, row: Row) -> Result<()> { + let inner = self.inner.lock(); + inner.insert(tx_id, row) } /// Updates a row in the database with new values. @@ -179,11 +179,11 @@ impl Database { /// # Returns /// /// Returns `true` if the row was successfully updated, and `false` otherwise. - pub async fn update(&self, tx_id: TxID, row: Row) -> Result { - if !self.delete(tx_id, row.id).await? { + pub fn update(&self, tx_id: TxID, row: Row) -> Result { + if !self.delete(tx_id, row.id)? { return Ok(false); } - self.insert(tx_id, row).await?; + self.insert(tx_id, row)?; Ok(true) } @@ -201,9 +201,9 @@ impl Database { /// /// Returns `true` if the row was successfully deleted, and `false` otherwise. /// - pub async fn delete(&self, tx_id: TxID, id: RowID) -> Result { - let inner = self.inner.lock().await; - inner.delete(tx_id, id).await + pub fn delete(&self, tx_id: TxID, id: RowID) -> Result { + let inner = self.inner.lock(); + inner.delete(tx_id, id) } /// Retrieves a row from the table with the given `id`. @@ -220,18 +220,18 @@ impl Database { /// /// Returns `Some(row)` with the row data if the row with the given `id` exists, /// and `None` otherwise. - pub async fn read(&self, tx_id: TxID, id: RowID) -> Result> { - let inner = self.inner.lock().await; - inner.read(tx_id, id).await + pub fn read(&self, tx_id: TxID, id: RowID) -> Result> { + let inner = self.inner.lock(); + inner.read(tx_id, id) } - pub async fn scan_row_ids(&self) -> Result> { - let inner = self.inner.lock().await; + pub fn scan_row_ids(&self) -> Result> { + let inner = self.inner.lock(); inner.scan_row_ids() } - pub async fn scan_row_ids_for_table(&self, table_id: u64) -> Result> { - let inner = self.inner.lock().await; + pub fn scan_row_ids_for_table(&self, table_id: u64) -> Result> { + let inner = self.inner.lock(); inner.scan_row_ids_for_table(table_id) } @@ -240,9 +240,9 @@ impl Database { /// This function starts a new transaction in the database and returns a `TxID` value /// that you can use to perform operations within the transaction. All changes made within the /// transaction are isolated from other transactions until you commit the transaction. - pub async fn begin_tx(&self) -> TxID { - let mut inner = self.inner.lock().await; - inner.begin_tx().await + pub fn begin_tx(&self) -> TxID { + let mut inner = self.inner.lock(); + inner.begin_tx() } /// Commits a transaction with the specified transaction ID. @@ -254,9 +254,9 @@ impl Database { /// # Arguments /// /// * `tx_id` - The ID of the transaction to commit. - pub async fn commit_tx(&self, tx_id: TxID) -> Result<()> { - let mut inner = self.inner.lock().await; - inner.commit_tx(tx_id).await + pub fn commit_tx(&self, tx_id: TxID) -> Result<()> { + let mut inner = self.inner.lock(); + inner.commit_tx(tx_id) } /// Rolls back a transaction with the specified ID. @@ -267,23 +267,23 @@ impl Database { /// # Arguments /// /// * `tx_id` - The ID of the transaction to abort. - pub async fn rollback_tx(&self, tx_id: TxID) { - let inner = self.inner.lock().await; - inner.rollback_tx(tx_id).await; + pub fn rollback_tx(&self, tx_id: TxID) { + let inner = self.inner.lock(); + inner.rollback_tx(tx_id); } /// Drops all unused row versions from the database. /// /// A version is considered unused if it is not visible to any active transaction /// and it is not the most recent version of the row. - pub async fn drop_unused_row_versions(&self) { - let inner = self.inner.lock().await; + pub fn drop_unused_row_versions(&self) { + let inner = self.inner.lock(); inner.drop_unused_row_versions(); } - pub async fn recover(&self) -> Result<()> { - let inner = self.inner.lock().await; - inner.recover().await + pub fn recover(&self) -> Result<()> { + let inner = self.inner.lock(); + inner.recover() } } @@ -298,7 +298,7 @@ pub struct DatabaseInner { } impl DatabaseInner { - async fn insert(&self, tx_id: TxID, row: Row) -> Result<()> { + fn insert(&self, tx_id: TxID, row: Row) -> Result<()> { let mut txs = self.txs.borrow_mut(); let tx = txs .get_mut(&tx_id) @@ -317,7 +317,7 @@ impl DatabaseInner { } #[allow(clippy::await_holding_refcell_ref)] - async fn delete(&self, tx_id: TxID, id: RowID) -> Result { + fn delete(&self, tx_id: TxID, id: RowID) -> Result { // NOTICE: They *are* dropped before an await point!!! But the await is conditional, // so I think clippy is just confused. let mut txs = self.txs.borrow_mut(); @@ -331,7 +331,7 @@ impl DatabaseInner { if is_write_write_conflict(&txs, tx, rv) { drop(txs); drop(rows); - self.rollback_tx(tx_id).await; + self.rollback_tx(tx_id); return Err(DatabaseError::WriteWriteConflict); } if is_version_visible(&txs, tx, rv) { @@ -347,7 +347,7 @@ impl DatabaseInner { Ok(false) } - async fn read(&self, tx_id: TxID, id: RowID) -> Result> { + fn read(&self, tx_id: TxID, id: RowID) -> Result> { let txs = self.txs.borrow_mut(); let tx = txs.get(&tx_id).unwrap(); assert!(tx.state == TransactionState::Active); @@ -385,7 +385,7 @@ impl DatabaseInner { .collect()) } - async fn begin_tx(&mut self) -> TxID { + fn begin_tx(&mut self) -> TxID { let tx_id = self.get_tx_id(); let begin_ts = self.get_timestamp(); let tx = Transaction::new(tx_id, begin_ts); @@ -397,8 +397,7 @@ impl DatabaseInner { tx_id } - #[allow(clippy::await_holding_refcell_ref)] - async fn commit_tx(&mut self, tx_id: TxID) -> Result<()> { + fn commit_tx(&mut self, tx_id: TxID) -> Result<()> { let end_ts = self.get_timestamp(); let mut txs = self.txs.borrow_mut(); let mut tx = txs.get_mut(&tx_id).unwrap(); @@ -449,12 +448,12 @@ impl DatabaseInner { drop(rows); drop(txs); if !log_record.row_versions.is_empty() { - self.storage.log_tx(log_record).await?; + self.storage.log_tx(log_record)?; } Ok(()) } - async fn rollback_tx(&self, tx_id: TxID) { + fn rollback_tx(&self, tx_id: TxID) { let mut txs = self.txs.borrow_mut(); let mut tx = txs.get_mut(&tx_id).unwrap(); assert!(tx.state == TransactionState::Active); @@ -529,8 +528,8 @@ impl DatabaseInner { } } - pub async fn recover(&self) -> Result<()> { - let tx_log = self.storage.read_tx_log().await?; + pub fn recover(&self) -> Result<()> { + let tx_log = self.storage.read_tx_log()?; for record in tx_log { tracing::debug!("RECOVERING {:?}", record); for version in record.row_versions { @@ -617,11 +616,11 @@ mod tests { } #[traced_test] - #[tokio::test] - async fn test_insert_read() { + #[test] + fn test_insert_read() { let db = test_db(); - let tx1 = db.begin_tx().await; + let tx1 = db.begin_tx(); let tx1_row = Row { id: RowID { table_id: 1, @@ -629,7 +628,7 @@ mod tests { }, data: "Hello".to_string(), }; - db.insert(tx1, tx1_row.clone()).await.unwrap(); + db.insert(tx1, tx1_row.clone()).unwrap(); let row = db .read( tx1, @@ -638,13 +637,12 @@ mod tests { row_id: 1, }, ) - .await .unwrap() .unwrap(); assert_eq!(tx1_row, row); - db.commit_tx(tx1).await.unwrap(); + db.commit_tx(tx1).unwrap(); - let tx2 = db.begin_tx().await; + let tx2 = db.begin_tx(); let row = db .read( tx2, @@ -653,35 +651,32 @@ mod tests { row_id: 1, }, ) - .await .unwrap() .unwrap(); assert_eq!(tx1_row, row); } #[traced_test] - #[tokio::test] - async fn test_read_nonexistent() { + #[test] + fn test_read_nonexistent() { let db = test_db(); - let tx = db.begin_tx().await; - let row = db - .read( - tx, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .await; + let tx = db.begin_tx(); + let row = db.read( + tx, + RowID { + table_id: 1, + row_id: 1, + }, + ); assert!(row.unwrap().is_none()); } #[traced_test] - #[tokio::test] - async fn test_delete() { + #[test] + fn test_delete() { let db = test_db(); - let tx1 = db.begin_tx().await; + let tx1 = db.begin_tx(); let tx1_row = Row { id: RowID { table_id: 1, @@ -689,7 +684,7 @@ mod tests { }, data: "Hello".to_string(), }; - db.insert(tx1, tx1_row.clone()).await.unwrap(); + db.insert(tx1, tx1_row.clone()).unwrap(); let row = db .read( tx1, @@ -698,7 +693,6 @@ mod tests { row_id: 1, }, ) - .await .unwrap() .unwrap(); assert_eq!(tx1_row, row); @@ -709,7 +703,6 @@ mod tests { row_id: 1, }, ) - .await .unwrap(); let row = db .read( @@ -719,12 +712,11 @@ mod tests { row_id: 1, }, ) - .await .unwrap(); assert!(row.is_none()); - db.commit_tx(tx1).await.unwrap(); + db.commit_tx(tx1).unwrap(); - let tx2 = db.begin_tx().await; + let tx2 = db.begin_tx(); let row = db .read( tx2, @@ -733,16 +725,15 @@ mod tests { row_id: 1, }, ) - .await .unwrap(); assert!(row.is_none()); } #[traced_test] - #[tokio::test] - async fn test_delete_nonexistent() { + #[test] + fn test_delete_nonexistent() { let db = test_db(); - let tx = db.begin_tx().await; + let tx = db.begin_tx(); assert!(!db .delete( tx, @@ -751,15 +742,14 @@ mod tests { row_id: 1 } ) - .await .unwrap()); } #[traced_test] - #[tokio::test] - async fn test_commit() { + #[test] + fn test_commit() { let db = test_db(); - let tx1 = db.begin_tx().await; + let tx1 = db.begin_tx(); let tx1_row = Row { id: RowID { table_id: 1, @@ -767,7 +757,7 @@ mod tests { }, data: "Hello".to_string(), }; - db.insert(tx1, tx1_row.clone()).await.unwrap(); + db.insert(tx1, tx1_row.clone()).unwrap(); let row = db .read( tx1, @@ -776,7 +766,6 @@ mod tests { row_id: 1, }, ) - .await .unwrap() .unwrap(); assert_eq!(tx1_row, row); @@ -787,7 +776,7 @@ mod tests { }, data: "World".to_string(), }; - db.update(tx1, tx1_updated_row.clone()).await.unwrap(); + db.update(tx1, tx1_updated_row.clone()).unwrap(); let row = db .read( tx1, @@ -796,13 +785,12 @@ mod tests { row_id: 1, }, ) - .await .unwrap() .unwrap(); assert_eq!(tx1_updated_row, row); - db.commit_tx(tx1).await.unwrap(); + db.commit_tx(tx1).unwrap(); - let tx2 = db.begin_tx().await; + let tx2 = db.begin_tx(); let row = db .read( tx2, @@ -811,19 +799,18 @@ mod tests { row_id: 1, }, ) - .await .unwrap() .unwrap(); - db.commit_tx(tx2).await.unwrap(); + db.commit_tx(tx2).unwrap(); assert_eq!(tx1_updated_row, row); - db.drop_unused_row_versions().await; + db.drop_unused_row_versions(); } #[traced_test] - #[tokio::test] - async fn test_rollback() { + #[test] + fn test_rollback() { let db = test_db(); - let tx1 = db.begin_tx().await; + let tx1 = db.begin_tx(); let row1 = Row { id: RowID { table_id: 1, @@ -831,7 +818,7 @@ mod tests { }, data: "Hello".to_string(), }; - db.insert(tx1, row1.clone()).await.unwrap(); + db.insert(tx1, row1.clone()).unwrap(); let row2 = db .read( tx1, @@ -840,7 +827,6 @@ mod tests { row_id: 1, }, ) - .await .unwrap() .unwrap(); assert_eq!(row1, row2); @@ -851,7 +837,7 @@ mod tests { }, data: "World".to_string(), }; - db.update(tx1, row3.clone()).await.unwrap(); + db.update(tx1, row3.clone()).unwrap(); let row4 = db .read( tx1, @@ -860,12 +846,11 @@ mod tests { row_id: 1, }, ) - .await .unwrap() .unwrap(); assert_eq!(row3, row4); - db.rollback_tx(tx1).await; - let tx2 = db.begin_tx().await; + db.rollback_tx(tx1); + let tx2 = db.begin_tx(); let row5 = db .read( tx2, @@ -874,18 +859,17 @@ mod tests { row_id: 1, }, ) - .await .unwrap(); assert_eq!(row5, None); } #[traced_test] - #[tokio::test] - async fn test_dirty_write() { + #[test] + fn test_dirty_write() { let db = test_db(); // T1 inserts a row with ID 1, but does not commit. - let tx1 = db.begin_tx().await; + let tx1 = db.begin_tx(); let tx1_row = Row { id: RowID { table_id: 1, @@ -893,7 +877,7 @@ mod tests { }, data: "Hello".to_string(), }; - db.insert(tx1, tx1_row.clone()).await.unwrap(); + db.insert(tx1, tx1_row.clone()).unwrap(); let row = db .read( tx1, @@ -902,13 +886,12 @@ mod tests { row_id: 1, }, ) - .await .unwrap() .unwrap(); assert_eq!(tx1_row, row); // T2 attempts to delete row with ID 1, but fails because T1 has not committed. - let tx2 = db.begin_tx().await; + let tx2 = db.begin_tx(); let tx2_row = Row { id: RowID { table_id: 1, @@ -916,7 +899,7 @@ mod tests { }, data: "World".to_string(), }; - assert!(!db.update(tx2, tx2_row).await.unwrap()); + assert!(!db.update(tx2, tx2_row).unwrap()); let row = db .read( @@ -926,19 +909,18 @@ mod tests { row_id: 1, }, ) - .await .unwrap() .unwrap(); assert_eq!(tx1_row, row); } #[traced_test] - #[tokio::test] - async fn test_dirty_read() { + #[test] + fn test_dirty_read() { let db = test_db(); // T1 inserts a row with ID 1, but does not commit. - let tx1 = db.begin_tx().await; + let tx1 = db.begin_tx(); let row1 = Row { id: RowID { table_id: 1, @@ -946,10 +928,10 @@ mod tests { }, data: "Hello".to_string(), }; - db.insert(tx1, row1).await.unwrap(); + db.insert(tx1, row1).unwrap(); // T2 attempts to read row with ID 1, but doesn't see one because T1 has not committed. - let tx2 = db.begin_tx().await; + let tx2 = db.begin_tx(); let row2 = db .read( tx2, @@ -958,19 +940,18 @@ mod tests { row_id: 1, }, ) - .await .unwrap(); assert_eq!(row2, None); } #[ignore] #[traced_test] - #[tokio::test] - async fn test_dirty_read_deleted() { + #[test] + fn test_dirty_read_deleted() { let db = test_db(); // T1 inserts a row with ID 1 and commits. - let tx1 = db.begin_tx().await; + let tx1 = db.begin_tx(); let tx1_row = Row { id: RowID { table_id: 1, @@ -978,11 +959,11 @@ mod tests { }, data: "Hello".to_string(), }; - db.insert(tx1, tx1_row.clone()).await.unwrap(); - db.commit_tx(tx1).await.unwrap(); + db.insert(tx1, tx1_row.clone()).unwrap(); + db.commit_tx(tx1).unwrap(); // T2 deletes row with ID 1, but does not commit. - let tx2 = db.begin_tx().await; + let tx2 = db.begin_tx(); assert!(db .delete( tx2, @@ -991,11 +972,10 @@ mod tests { row_id: 1 } ) - .await .unwrap()); // T3 reads row with ID 1, but doesn't see the delete because T2 hasn't committed. - let tx3 = db.begin_tx().await; + let tx3 = db.begin_tx(); let row = db .read( tx3, @@ -1004,19 +984,18 @@ mod tests { row_id: 1, }, ) - .await .unwrap() .unwrap(); assert_eq!(tx1_row, row); } #[traced_test] - #[tokio::test] - async fn test_fuzzy_read() { + #[test] + fn test_fuzzy_read() { let db = test_db(); // T1 inserts a row with ID 1 and commits. - let tx1 = db.begin_tx().await; + let tx1 = db.begin_tx(); let tx1_row = Row { id: RowID { table_id: 1, @@ -1024,7 +1003,7 @@ mod tests { }, data: "Hello".to_string(), }; - db.insert(tx1, tx1_row.clone()).await.unwrap(); + db.insert(tx1, tx1_row.clone()).unwrap(); let row = db .read( tx1, @@ -1033,14 +1012,13 @@ mod tests { row_id: 1, }, ) - .await .unwrap() .unwrap(); assert_eq!(tx1_row, row); - db.commit_tx(tx1).await.unwrap(); + db.commit_tx(tx1).unwrap(); // T2 reads the row with ID 1 within an active transaction. - let tx2 = db.begin_tx().await; + let tx2 = db.begin_tx(); let row = db .read( tx2, @@ -1049,13 +1027,12 @@ mod tests { row_id: 1, }, ) - .await .unwrap() .unwrap(); assert_eq!(tx1_row, row); // T3 updates the row and commits. - let tx3 = db.begin_tx().await; + let tx3 = db.begin_tx(); let tx3_row = Row { id: RowID { table_id: 1, @@ -1063,8 +1040,8 @@ mod tests { }, data: "World".to_string(), }; - db.update(tx3, tx3_row).await.unwrap(); - db.commit_tx(tx3).await.unwrap(); + db.update(tx3, tx3_row).unwrap(); + db.commit_tx(tx3).unwrap(); // T2 still reads the same version of the row as before. let row = db @@ -1075,19 +1052,18 @@ mod tests { row_id: 1, }, ) - .await .unwrap() .unwrap(); assert_eq!(tx1_row, row); } #[traced_test] - #[tokio::test] - async fn test_lost_update() { + #[test] + fn test_lost_update() { let db = test_db(); // T1 inserts a row with ID 1 and commits. - let tx1 = db.begin_tx().await; + let tx1 = db.begin_tx(); let tx1_row = Row { id: RowID { table_id: 1, @@ -1095,7 +1071,7 @@ mod tests { }, data: "Hello".to_string(), }; - db.insert(tx1, tx1_row.clone()).await.unwrap(); + db.insert(tx1, tx1_row.clone()).unwrap(); let row = db .read( tx1, @@ -1104,14 +1080,13 @@ mod tests { row_id: 1, }, ) - .await .unwrap() .unwrap(); assert_eq!(tx1_row, row); - db.commit_tx(tx1).await.unwrap(); + db.commit_tx(tx1).unwrap(); // T2 attempts to update row ID 1 within an active transaction. - let tx2 = db.begin_tx().await; + let tx2 = db.begin_tx(); let tx2_row = Row { id: RowID { table_id: 1, @@ -1119,10 +1094,10 @@ mod tests { }, data: "World".to_string(), }; - assert!(db.update(tx2, tx2_row.clone()).await.unwrap()); + assert!(db.update(tx2, tx2_row.clone()).unwrap()); // T3 also attempts to update row ID 1 within an active transaction. - let tx3 = db.begin_tx().await; + let tx3 = db.begin_tx(); let tx3_row = Row { id: RowID { table_id: 1, @@ -1132,13 +1107,13 @@ mod tests { }; assert_eq!( Err(DatabaseError::WriteWriteConflict), - db.update(tx3, tx3_row).await + db.update(tx3, tx3_row) ); - db.commit_tx(tx2).await.unwrap(); - assert_eq!(Err(DatabaseError::TxTerminated), db.commit_tx(tx3).await); + db.commit_tx(tx2).unwrap(); + assert_eq!(Err(DatabaseError::TxTerminated), db.commit_tx(tx3)); - let tx4 = db.begin_tx().await; + let tx4 = db.begin_tx(); let row = db .read( tx4, @@ -1147,7 +1122,6 @@ mod tests { row_id: 1, }, ) - .await .unwrap() .unwrap(); assert_eq!(tx2_row, row); @@ -1156,12 +1130,12 @@ mod tests { // Test for the visibility to check if a new transaction can see old committed values. // This test checks for the typo present in the paper, explained in https://github.com/penberg/mvcc-rs/issues/15 #[traced_test] - #[tokio::test] - async fn test_committed_visibility() { + #[test] + fn test_committed_visibility() { let db = test_db(); // let's add $10 to my account since I like money - let tx1 = db.begin_tx().await; + let tx1 = db.begin_tx(); let tx1_row = Row { id: RowID { table_id: 1, @@ -1169,11 +1143,11 @@ mod tests { }, data: "10".to_string(), }; - db.insert(tx1, tx1_row.clone()).await.unwrap(); - db.commit_tx(tx1).await.unwrap(); + db.insert(tx1, tx1_row.clone()).unwrap(); + db.commit_tx(tx1).unwrap(); // but I like more money, so let me try adding $10 more - let tx2 = db.begin_tx().await; + let tx2 = db.begin_tx(); let tx2_row = Row { id: RowID { table_id: 1, @@ -1181,7 +1155,7 @@ mod tests { }, data: "20".to_string(), }; - assert!(db.update(tx2, tx2_row.clone()).await.unwrap()); + assert!(db.update(tx2, tx2_row.clone()).unwrap()); let row = db .read( tx2, @@ -1190,13 +1164,12 @@ mod tests { row_id: 1, }, ) - .await .unwrap() .unwrap(); assert_eq!(row, tx2_row); // can I check how much money I have? - let tx3 = db.begin_tx().await; + let tx3 = db.begin_tx(); let row = db .read( tx3, @@ -1205,7 +1178,6 @@ mod tests { row_id: 1, }, ) - .await .unwrap() .unwrap(); assert_eq!(tx1_row, row); @@ -1213,13 +1185,13 @@ mod tests { // Test to check if a older transaction can see (un)committed future rows #[traced_test] - #[tokio::test] - async fn test_future_row() { + #[test] + fn test_future_row() { let db = test_db(); - let tx1 = db.begin_tx().await; + let tx1 = db.begin_tx(); - let tx2 = db.begin_tx().await; + let tx2 = db.begin_tx(); let tx2_row = Row { id: RowID { table_id: 1, @@ -1227,7 +1199,7 @@ mod tests { }, data: "10".to_string(), }; - db.insert(tx2, tx2_row.clone()).await.unwrap(); + db.insert(tx2, tx2_row).unwrap(); // transaction in progress, so tx1 shouldn't be able to see the value let row = db @@ -1238,12 +1210,11 @@ mod tests { row_id: 1, }, ) - .await .unwrap(); assert_eq!(row, None); // lets commit the transaction and check if tx1 can see it - db.commit_tx(tx2).await.unwrap(); + db.commit_tx(tx2).unwrap(); let row = db .read( tx1, @@ -1252,14 +1223,13 @@ mod tests { row_id: 1, }, ) - .await .unwrap(); assert_eq!(row, None); } #[traced_test] - #[tokio::test] - async fn test_storage1() { + #[test] + fn test_storage1() { let clock = LocalClock::new(); let mut path = std::env::temp_dir(); path.push(format!( @@ -1272,9 +1242,9 @@ mod tests { let storage = crate::persistent_storage::Storage::new_json_on_disk(path.clone()); let db = Database::new(clock, storage); - let tx1 = db.begin_tx().await; - let tx2 = db.begin_tx().await; - let tx3 = db.begin_tx().await; + let tx1 = db.begin_tx(); + let tx2 = db.begin_tx(); + let tx3 = db.begin_tx(); db.insert( tx3, @@ -1286,14 +1256,13 @@ mod tests { data: "testme".to_string(), }, ) - .await .unwrap(); - db.commit_tx(tx1).await.unwrap(); - db.rollback_tx(tx2).await; - db.commit_tx(tx3).await.unwrap(); + db.commit_tx(tx1).unwrap(); + db.rollback_tx(tx2); + db.commit_tx(tx3).unwrap(); - let tx4 = db.begin_tx().await; + let tx4 = db.begin_tx(); db.insert( tx4, Row { @@ -1304,7 +1273,6 @@ mod tests { data: "testme2".to_string(), }, ) - .await .unwrap(); db.insert( tx4, @@ -1316,7 +1284,6 @@ mod tests { data: "testme3".to_string(), }, ) - .await .unwrap(); assert_eq!( @@ -1327,7 +1294,6 @@ mod tests { row_id: 1 } ) - .await .unwrap() .unwrap() .data, @@ -1341,7 +1307,6 @@ mod tests { row_id: 2 } ) - .await .unwrap() .unwrap() .data, @@ -1355,21 +1320,20 @@ mod tests { row_id: 3 } ) - .await .unwrap() .unwrap() .data, "testme3" ); - db.commit_tx(tx4).await.unwrap(); + db.commit_tx(tx4).unwrap(); let clock = LocalClock::new(); let storage = crate::persistent_storage::Storage::new_json_on_disk(path); let db = Database::new(clock, storage); - db.recover().await.unwrap(); + db.recover().unwrap(); println!("{:#?}", db); - let tx5 = db.begin_tx().await; + let tx5 = db.begin_tx(); println!( "{:#?}", db.read( @@ -1379,7 +1343,6 @@ mod tests { row_id: 1 } ) - .await ); assert_eq!( db.read( @@ -1389,7 +1352,6 @@ mod tests { row_id: 1 } ) - .await .unwrap() .unwrap() .data, @@ -1403,7 +1365,6 @@ mod tests { row_id: 2 } ) - .await .unwrap() .unwrap() .data, @@ -1417,7 +1378,6 @@ mod tests { row_id: 3 } ) - .await .unwrap() .unwrap() .data, diff --git a/core/mvcc/mvcc-rs/src/persistent_storage/mod.rs b/core/mvcc/mvcc-rs/src/persistent_storage/mod.rs index 1dd72c02a..f927be381 100644 --- a/core/mvcc/mvcc-rs/src/persistent_storage/mod.rs +++ b/core/mvcc/mvcc-rs/src/persistent_storage/mod.rs @@ -20,51 +20,48 @@ impl Storage { Self::JsonOnDisk(path) } - pub async fn new_s3(options: s3::Options) -> Result { - Ok(Self::S3(s3::Replicator::new(options).await?)) + pub fn new_s3(options: s3::Options) -> Result { + let replicator = futures::executor::block_on(s3::Replicator::new(options))?; + Ok(Self::S3(replicator)) } } impl Storage { - pub async fn log_tx(&mut self, m: LogRecord) -> Result<()> { + pub fn log_tx(&mut self, m: LogRecord) -> Result<()> { match self { Self::JsonOnDisk(path) => { - use tokio::io::AsyncWriteExt; + use std::io::Write; let t = serde_json::to_vec(&m).map_err(|e| DatabaseError::Io(e.to_string()))?; - let mut file = tokio::fs::OpenOptions::new() + let mut file = std::fs::OpenOptions::new() .create(true) .append(true) - .open(&path) - .await + .open(path) .map_err(|e| DatabaseError::Io(e.to_string()))?; file.write_all(&t) - .await .map_err(|e| DatabaseError::Io(e.to_string()))?; file.write_all(b"\n") - .await .map_err(|e| DatabaseError::Io(e.to_string()))?; } Self::S3(replicator) => { - replicator.replicate_tx(m).await?; + futures::executor::block_on(replicator.replicate_tx(m))?; } Self::Noop => (), } Ok(()) } - pub async fn read_tx_log(&self) -> Result> { + pub fn read_tx_log(&self) -> Result> { match self { Self::JsonOnDisk(path) => { - use tokio::io::AsyncBufReadExt; - let file = tokio::fs::OpenOptions::new() + use std::io::BufRead; + let file = std::fs::OpenOptions::new() .read(true) - .open(&path) - .await + .open(path) .map_err(|e| DatabaseError::Io(e.to_string()))?; let mut records: Vec = Vec::new(); - let mut lines = tokio::io::BufReader::new(file).lines(); - while let Ok(Some(line)) = lines.next_line().await { + let mut lines = std::io::BufReader::new(file).lines(); + while let Some(Ok(line)) = lines.next() { records.push( serde_json::from_str(&line) .map_err(|e| DatabaseError::Io(e.to_string()))?, @@ -72,7 +69,7 @@ impl Storage { } Ok(records) } - Self::S3(replicator) => replicator.read_tx_log().await, + Self::S3(replicator) => futures::executor::block_on(replicator.read_tx_log()), Self::Noop => Err(crate::errors::DatabaseError::Io( "cannot read from Noop storage".to_string(), )), diff --git a/core/mvcc/mvcc-rs/tests/concurrency_test.rs b/core/mvcc/mvcc-rs/tests/concurrency_test.rs index 4ad81c645..12321aa10 100644 --- a/core/mvcc/mvcc-rs/tests/concurrency_test.rs +++ b/core/mvcc/mvcc-rs/tests/concurrency_test.rs @@ -19,48 +19,44 @@ fn test_non_overlapping_concurrent_inserts() { let db = db.clone(); let ids = ids.clone(); thread::spawn(move || { - shuttle::future::block_on(async move { - let tx = db.begin_tx().await; - let id = ids.fetch_add(1, Ordering::SeqCst); - let id = RowID { - table_id: 1, - row_id: id, - }; - let row = Row { - id, - data: "Hello".to_string(), - }; - db.insert(tx, row.clone()).await.unwrap(); - db.commit_tx(tx).await.unwrap(); - let tx = db.begin_tx().await; - let committed_row = db.read(tx, id).await.unwrap(); - db.commit_tx(tx).await.unwrap(); - assert_eq!(committed_row, Some(row)); - }) + let tx = db.begin_tx(); + let id = ids.fetch_add(1, Ordering::SeqCst); + let id = RowID { + table_id: 1, + row_id: id, + }; + let row = Row { + id, + data: "Hello".to_string(), + }; + db.insert(tx, row.clone()).unwrap(); + db.commit_tx(tx).unwrap(); + let tx = db.begin_tx(); + let committed_row = db.read(tx, id).unwrap(); + db.commit_tx(tx).unwrap(); + assert_eq!(committed_row, Some(row)); }); } { let db = db.clone(); let ids = ids.clone(); thread::spawn(move || { - shuttle::future::block_on(async move { - let tx = db.begin_tx().await; - let id = ids.fetch_add(1, Ordering::SeqCst); - let id = RowID { - table_id: 1, - row_id: id, - }; - let row = Row { - id, - data: "World".to_string(), - }; - db.insert(tx, row.clone()).await.unwrap(); - db.commit_tx(tx).await.unwrap(); - let tx = db.begin_tx().await; - let committed_row = db.read(tx, id).await.unwrap(); - db.commit_tx(tx).await.unwrap(); - assert_eq!(committed_row, Some(row)); - }); + let tx = db.begin_tx(); + let id = ids.fetch_add(1, Ordering::SeqCst); + let id = RowID { + table_id: 1, + row_id: id, + }; + let row = Row { + id, + data: "World".to_string(), + }; + db.insert(tx, row.clone()).unwrap(); + db.commit_tx(tx).unwrap(); + let tx = db.begin_tx(); + let committed_row = db.read(tx, id).unwrap(); + db.commit_tx(tx).unwrap(); + assert_eq!(committed_row, Some(row)); }); } }, From 51f33919d353f28a9c19da1a3d55ae135614c174 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 17 May 2023 16:31:16 +0300 Subject: [PATCH 091/128] Switch to concurrent SkipMap for row versions (#49) Let's switch to concurrent SkipMap as the first small step towards lockless index... --- core/mvcc/mvcc-rs/Cargo.toml | 1 + core/mvcc/mvcc-rs/src/database.rs | 60 ++++++++++++++++++++----------- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/core/mvcc/mvcc-rs/Cargo.toml b/core/mvcc/mvcc-rs/Cargo.toml index f2087c651..21c83167d 100644 --- a/core/mvcc/mvcc-rs/Cargo.toml +++ b/core/mvcc/mvcc-rs/Cargo.toml @@ -15,6 +15,7 @@ aws-sdk-s3 = "0.27.0" aws-config = "0.55.2" parking_lot = "0.12.1" futures = "0.3.28" +crossbeam-skiplist = "0.1.1" [dev-dependencies] criterion = { version = "0.4", features = ["html_reports", "async", "async_futures"] } diff --git a/core/mvcc/mvcc-rs/src/database.rs b/core/mvcc/mvcc-rs/src/database.rs index c69da6c2e..a60ac7663 100644 --- a/core/mvcc/mvcc-rs/src/database.rs +++ b/core/mvcc/mvcc-rs/src/database.rs @@ -2,11 +2,12 @@ use crate::clock::LogicalClock; use crate::errors::DatabaseError; use crate::persistent_storage::Storage; use parking_lot::Mutex; +use crossbeam_skiplist::SkipMap; use serde::{Deserialize, Serialize}; use std::cell::RefCell; use std::collections::{BTreeMap, HashMap, HashSet}; use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::Arc; +use std::sync::{Arc, RwLock}; pub type Result = std::result::Result; @@ -134,7 +135,7 @@ impl Database { /// Creates a new database. pub fn new(clock: Clock, storage: Storage) -> Self { let inner = DatabaseInner { - rows: RefCell::new(BTreeMap::new()), + rows: RefCell::new(SkipMap::new()), txs: RefCell::new(HashMap::new()), tx_timestamps: RefCell::new(BTreeMap::new()), tx_ids: AtomicU64::new(1), // let's reserve transaction 0 for special purposes @@ -289,7 +290,7 @@ impl Database { #[derive(Debug)] pub struct DatabaseInner { - rows: RefCell>>, + rows: RefCell>>>, txs: RefCell>, tx_timestamps: RefCell>, tx_ids: AtomicU64, @@ -310,8 +311,10 @@ impl DatabaseInner { end: None, row, }; - let mut rows = self.rows.borrow_mut(); - rows.entry(id).or_insert_with(Vec::new).push(row_version); + let rows = self.rows.borrow_mut(); + let versions = rows.get_or_insert_with(id, || RwLock::new(Vec::new())); + let mut versions = versions.value().write().unwrap(); + versions.push(row_version); tx.insert_to_write_set(id); Ok(()) } @@ -321,8 +324,10 @@ impl DatabaseInner { // NOTICE: They *are* dropped before an await point!!! But the await is conditional, // so I think clippy is just confused. let mut txs = self.txs.borrow_mut(); - let mut rows = self.rows.borrow_mut(); - if let Some(row_versions) = rows.get_mut(&id) { + let rows = self.rows.borrow_mut(); + let row_versions_opt = rows.get(&id); + if let Some(ref row_versions) = row_versions_opt { + let mut row_versions = row_versions.value().write().unwrap(); for rv in row_versions.iter_mut().rev() { let tx = txs .get(&tx_id) @@ -330,6 +335,8 @@ impl DatabaseInner { assert!(tx.state == TransactionState::Active); if is_write_write_conflict(&txs, tx, rv) { drop(txs); + drop(row_versions); + drop(row_versions_opt); drop(rows); self.rollback_tx(tx_id); return Err(DatabaseError::WriteWriteConflict); @@ -353,6 +360,7 @@ impl DatabaseInner { assert!(tx.state == TransactionState::Active); let rows = self.rows.borrow(); if let Some(row_versions) = rows.get(&id) { + let row_versions = row_versions.value().read().unwrap(); for rv in row_versions.iter().rev() { if is_version_visible(&txs, tx, rv) { tx.insert_to_read_set(id); @@ -365,11 +373,12 @@ impl DatabaseInner { fn scan_row_ids(&self) -> Result> { let rows = self.rows.borrow(); - Ok(rows.keys().cloned().collect()) + let keys = rows.iter().map(|entry| *entry.key()); + Ok(keys.collect()) } fn scan_row_ids_for_table(&self, table_id: u64) -> Result> { - let rows = self.rows.borrow(); + let rows = &self.rows.borrow(); Ok(rows .range( RowID { @@ -380,8 +389,7 @@ impl DatabaseInner { row_id: u64::MAX, }, ) - .map(|(k, _)| k) - .cloned() + .map(|entry| *entry.key()) .collect()) } @@ -407,12 +415,13 @@ impl DatabaseInner { assert!(tx.state == TransactionState::Active); } } - let mut rows = self.rows.borrow_mut(); + let rows = self.rows.borrow_mut(); tx.state = TransactionState::Preparing; tracing::trace!("PREPARE {tx}"); let mut log_record: LogRecord = LogRecord::new(end_ts); for id in &tx.write_set { - if let Some(row_versions) = rows.get_mut(id) { + if let Some(row_versions) = rows.get(id) { + let mut row_versions = row_versions.value().write().unwrap(); for row_version in row_versions.iter_mut() { if let TxTimestampOrID::TxID(id) = row_version.begin { if id == tx_id { @@ -459,9 +468,10 @@ impl DatabaseInner { assert!(tx.state == TransactionState::Active); tx.state = TransactionState::Aborted; tracing::trace!("ABORT {tx}"); - let mut rows = self.rows.borrow_mut(); + let rows = self.rows.borrow_mut(); for id in &tx.write_set { - if let Some(row_versions) = rows.get_mut(id) { + if let Some(row_versions) = rows.get(id) { + let mut row_versions = row_versions.value().write().unwrap(); row_versions.retain(|rv| rv.begin != TxTimestampOrID::TxID(tx_id)); if row_versions.is_empty() { rows.remove(id); @@ -493,9 +503,10 @@ impl DatabaseInner { fn drop_unused_row_versions(&self) { let txs = self.txs.borrow(); let tx_timestamps = self.tx_timestamps.borrow(); - let mut rows = self.rows.borrow_mut(); + let rows = self.rows.borrow_mut(); let mut to_remove = Vec::new(); - for (id, row_versions) in rows.iter_mut() { + for entry in rows.iter() { + let mut row_versions = entry.value().write().unwrap(); row_versions.retain(|rv| { let should_stay = match rv.end { Some(TxTimestampOrID::Timestamp(version_end_ts)) => { @@ -515,12 +526,17 @@ impl DatabaseInner { None => true, }; if !should_stay { - tracing::debug!("Dropping row version {:?} {:?}-{:?}", id, rv.begin, rv.end); + tracing::debug!( + "Dropping row version {:?} {:?}-{:?}", + entry.key(), + rv.begin, + rv.end + ); } should_stay }); if row_versions.is_empty() { - to_remove.push(*id); + to_remove.push(*entry.key()); } } for id in to_remove { @@ -533,8 +549,10 @@ impl DatabaseInner { for record in tx_log { tracing::debug!("RECOVERING {:?}", record); for version in record.row_versions { - let mut rows = self.rows.borrow_mut(); - let row_versions = rows.entry(version.row.id).or_insert_with(Vec::new); + let rows = self.rows.borrow_mut(); + let row_versions = + rows.get_or_insert_with(version.row.id, || RwLock::new(Vec::new())); + let mut row_versions = row_versions.value().write().unwrap(); row_versions.push(version); } self.clock.reset(record.tx_timestamp); From 77e88d3f049032f7f6b662adffe0a53416c03df9 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Mon, 5 Jun 2023 11:47:53 +0200 Subject: [PATCH 092/128] fix clippy --- core/mvcc/mvcc-rs/src/database.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/mvcc/mvcc-rs/src/database.rs b/core/mvcc/mvcc-rs/src/database.rs index a60ac7663..989519be4 100644 --- a/core/mvcc/mvcc-rs/src/database.rs +++ b/core/mvcc/mvcc-rs/src/database.rs @@ -408,7 +408,7 @@ impl DatabaseInner { fn commit_tx(&mut self, tx_id: TxID) -> Result<()> { let end_ts = self.get_timestamp(); let mut txs = self.txs.borrow_mut(); - let mut tx = txs.get_mut(&tx_id).unwrap(); + let tx = txs.get_mut(&tx_id).unwrap(); match tx.state { TransactionState::Terminated => return Err(DatabaseError::TxTerminated), _ => { @@ -464,7 +464,7 @@ impl DatabaseInner { fn rollback_tx(&self, tx_id: TxID) { let mut txs = self.txs.borrow_mut(); - let mut tx = txs.get_mut(&tx_id).unwrap(); + let tx = txs.get_mut(&tx_id).unwrap(); assert!(tx.state == TransactionState::Active); tx.state = TransactionState::Aborted; tracing::trace!("ABORT {tx}"); From 74a7628a0a10de277228b64b96ef28f075ca5206 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Mon, 5 Jun 2023 12:50:35 +0200 Subject: [PATCH 093/128] mvcc-rs: move database tests to a separate file That makes the file more human-readable --- .../src/{database.rs => database/mod.rs} | 788 +----------------- core/mvcc/mvcc-rs/src/database/tests.rs | 780 +++++++++++++++++ 2 files changed, 784 insertions(+), 784 deletions(-) rename core/mvcc/mvcc-rs/src/{database.rs => database/mod.rs} (53%) create mode 100644 core/mvcc/mvcc-rs/src/database/tests.rs diff --git a/core/mvcc/mvcc-rs/src/database.rs b/core/mvcc/mvcc-rs/src/database/mod.rs similarity index 53% rename from core/mvcc/mvcc-rs/src/database.rs rename to core/mvcc/mvcc-rs/src/database/mod.rs index 989519be4..07d532374 100644 --- a/core/mvcc/mvcc-rs/src/database.rs +++ b/core/mvcc/mvcc-rs/src/database/mod.rs @@ -1,8 +1,8 @@ use crate::clock::LogicalClock; use crate::errors::DatabaseError; use crate::persistent_storage::Storage; -use parking_lot::Mutex; use crossbeam_skiplist::SkipMap; +use parking_lot::Mutex; use serde::{Deserialize, Serialize}; use std::cell::RefCell; use std::collections::{BTreeMap, HashMap, HashSet}; @@ -11,6 +11,9 @@ use std::sync::{Arc, RwLock}; pub type Result = std::result::Result; +#[cfg(test)] +mod tests; + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash)] pub struct RowID { pub table_id: u64, @@ -620,786 +623,3 @@ fn is_end_visible(txs: &HashMap, tx: &Transaction, rv: &RowVe None => true, } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::clock::LocalClock; - use tracing_test::traced_test; - - fn test_db() -> Database { - let clock = LocalClock::new(); - let storage = crate::persistent_storage::Storage::new_noop(); - Database::new(clock, storage) - } - - #[traced_test] - #[test] - fn test_insert_read() { - let db = test_db(); - - let tx1 = db.begin_tx(); - let tx1_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "Hello".to_string(), - }; - db.insert(tx1, tx1_row.clone()).unwrap(); - let row = db - .read( - tx1, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(tx1_row, row); - db.commit_tx(tx1).unwrap(); - - let tx2 = db.begin_tx(); - let row = db - .read( - tx2, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(tx1_row, row); - } - - #[traced_test] - #[test] - fn test_read_nonexistent() { - let db = test_db(); - let tx = db.begin_tx(); - let row = db.read( - tx, - RowID { - table_id: 1, - row_id: 1, - }, - ); - assert!(row.unwrap().is_none()); - } - - #[traced_test] - #[test] - fn test_delete() { - let db = test_db(); - - let tx1 = db.begin_tx(); - let tx1_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "Hello".to_string(), - }; - db.insert(tx1, tx1_row.clone()).unwrap(); - let row = db - .read( - tx1, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(tx1_row, row); - db.delete( - tx1, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap(); - let row = db - .read( - tx1, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap(); - assert!(row.is_none()); - db.commit_tx(tx1).unwrap(); - - let tx2 = db.begin_tx(); - let row = db - .read( - tx2, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap(); - assert!(row.is_none()); - } - - #[traced_test] - #[test] - fn test_delete_nonexistent() { - let db = test_db(); - let tx = db.begin_tx(); - assert!(!db - .delete( - tx, - RowID { - table_id: 1, - row_id: 1 - } - ) - .unwrap()); - } - - #[traced_test] - #[test] - fn test_commit() { - let db = test_db(); - let tx1 = db.begin_tx(); - let tx1_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "Hello".to_string(), - }; - db.insert(tx1, tx1_row.clone()).unwrap(); - let row = db - .read( - tx1, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(tx1_row, row); - let tx1_updated_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "World".to_string(), - }; - db.update(tx1, tx1_updated_row.clone()).unwrap(); - let row = db - .read( - tx1, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(tx1_updated_row, row); - db.commit_tx(tx1).unwrap(); - - let tx2 = db.begin_tx(); - let row = db - .read( - tx2, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - db.commit_tx(tx2).unwrap(); - assert_eq!(tx1_updated_row, row); - db.drop_unused_row_versions(); - } - - #[traced_test] - #[test] - fn test_rollback() { - let db = test_db(); - let tx1 = db.begin_tx(); - let row1 = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "Hello".to_string(), - }; - db.insert(tx1, row1.clone()).unwrap(); - let row2 = db - .read( - tx1, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(row1, row2); - let row3 = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "World".to_string(), - }; - db.update(tx1, row3.clone()).unwrap(); - let row4 = db - .read( - tx1, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(row3, row4); - db.rollback_tx(tx1); - let tx2 = db.begin_tx(); - let row5 = db - .read( - tx2, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap(); - assert_eq!(row5, None); - } - - #[traced_test] - #[test] - fn test_dirty_write() { - let db = test_db(); - - // T1 inserts a row with ID 1, but does not commit. - let tx1 = db.begin_tx(); - let tx1_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "Hello".to_string(), - }; - db.insert(tx1, tx1_row.clone()).unwrap(); - let row = db - .read( - tx1, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(tx1_row, row); - - // T2 attempts to delete row with ID 1, but fails because T1 has not committed. - let tx2 = db.begin_tx(); - let tx2_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "World".to_string(), - }; - assert!(!db.update(tx2, tx2_row).unwrap()); - - let row = db - .read( - tx1, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(tx1_row, row); - } - - #[traced_test] - #[test] - fn test_dirty_read() { - let db = test_db(); - - // T1 inserts a row with ID 1, but does not commit. - let tx1 = db.begin_tx(); - let row1 = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "Hello".to_string(), - }; - db.insert(tx1, row1).unwrap(); - - // T2 attempts to read row with ID 1, but doesn't see one because T1 has not committed. - let tx2 = db.begin_tx(); - let row2 = db - .read( - tx2, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap(); - assert_eq!(row2, None); - } - - #[ignore] - #[traced_test] - #[test] - fn test_dirty_read_deleted() { - let db = test_db(); - - // T1 inserts a row with ID 1 and commits. - let tx1 = db.begin_tx(); - let tx1_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "Hello".to_string(), - }; - db.insert(tx1, tx1_row.clone()).unwrap(); - db.commit_tx(tx1).unwrap(); - - // T2 deletes row with ID 1, but does not commit. - let tx2 = db.begin_tx(); - assert!(db - .delete( - tx2, - RowID { - table_id: 1, - row_id: 1 - } - ) - .unwrap()); - - // T3 reads row with ID 1, but doesn't see the delete because T2 hasn't committed. - let tx3 = db.begin_tx(); - let row = db - .read( - tx3, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(tx1_row, row); - } - - #[traced_test] - #[test] - fn test_fuzzy_read() { - let db = test_db(); - - // T1 inserts a row with ID 1 and commits. - let tx1 = db.begin_tx(); - let tx1_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "Hello".to_string(), - }; - db.insert(tx1, tx1_row.clone()).unwrap(); - let row = db - .read( - tx1, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(tx1_row, row); - db.commit_tx(tx1).unwrap(); - - // T2 reads the row with ID 1 within an active transaction. - let tx2 = db.begin_tx(); - let row = db - .read( - tx2, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(tx1_row, row); - - // T3 updates the row and commits. - let tx3 = db.begin_tx(); - let tx3_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "World".to_string(), - }; - db.update(tx3, tx3_row).unwrap(); - db.commit_tx(tx3).unwrap(); - - // T2 still reads the same version of the row as before. - let row = db - .read( - tx2, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(tx1_row, row); - } - - #[traced_test] - #[test] - fn test_lost_update() { - let db = test_db(); - - // T1 inserts a row with ID 1 and commits. - let tx1 = db.begin_tx(); - let tx1_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "Hello".to_string(), - }; - db.insert(tx1, tx1_row.clone()).unwrap(); - let row = db - .read( - tx1, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(tx1_row, row); - db.commit_tx(tx1).unwrap(); - - // T2 attempts to update row ID 1 within an active transaction. - let tx2 = db.begin_tx(); - let tx2_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "World".to_string(), - }; - assert!(db.update(tx2, tx2_row.clone()).unwrap()); - - // T3 also attempts to update row ID 1 within an active transaction. - let tx3 = db.begin_tx(); - let tx3_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "Hello, world!".to_string(), - }; - assert_eq!( - Err(DatabaseError::WriteWriteConflict), - db.update(tx3, tx3_row) - ); - - db.commit_tx(tx2).unwrap(); - assert_eq!(Err(DatabaseError::TxTerminated), db.commit_tx(tx3)); - - let tx4 = db.begin_tx(); - let row = db - .read( - tx4, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(tx2_row, row); - } - - // Test for the visibility to check if a new transaction can see old committed values. - // This test checks for the typo present in the paper, explained in https://github.com/penberg/mvcc-rs/issues/15 - #[traced_test] - #[test] - fn test_committed_visibility() { - let db = test_db(); - - // let's add $10 to my account since I like money - let tx1 = db.begin_tx(); - let tx1_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "10".to_string(), - }; - db.insert(tx1, tx1_row.clone()).unwrap(); - db.commit_tx(tx1).unwrap(); - - // but I like more money, so let me try adding $10 more - let tx2 = db.begin_tx(); - let tx2_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "20".to_string(), - }; - assert!(db.update(tx2, tx2_row.clone()).unwrap()); - let row = db - .read( - tx2, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(row, tx2_row); - - // can I check how much money I have? - let tx3 = db.begin_tx(); - let row = db - .read( - tx3, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap() - .unwrap(); - assert_eq!(tx1_row, row); - } - - // Test to check if a older transaction can see (un)committed future rows - #[traced_test] - #[test] - fn test_future_row() { - let db = test_db(); - - let tx1 = db.begin_tx(); - - let tx2 = db.begin_tx(); - let tx2_row = Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "10".to_string(), - }; - db.insert(tx2, tx2_row).unwrap(); - - // transaction in progress, so tx1 shouldn't be able to see the value - let row = db - .read( - tx1, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap(); - assert_eq!(row, None); - - // lets commit the transaction and check if tx1 can see it - db.commit_tx(tx2).unwrap(); - let row = db - .read( - tx1, - RowID { - table_id: 1, - row_id: 1, - }, - ) - .unwrap(); - assert_eq!(row, None); - } - - #[traced_test] - #[test] - fn test_storage1() { - let clock = LocalClock::new(); - let mut path = std::env::temp_dir(); - path.push(format!( - "mvcc-rs-storage-test-{}", - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos(), - )); - let storage = crate::persistent_storage::Storage::new_json_on_disk(path.clone()); - let db = Database::new(clock, storage); - - let tx1 = db.begin_tx(); - let tx2 = db.begin_tx(); - let tx3 = db.begin_tx(); - - db.insert( - tx3, - Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "testme".to_string(), - }, - ) - .unwrap(); - - db.commit_tx(tx1).unwrap(); - db.rollback_tx(tx2); - db.commit_tx(tx3).unwrap(); - - let tx4 = db.begin_tx(); - db.insert( - tx4, - Row { - id: RowID { - table_id: 1, - row_id: 2, - }, - data: "testme2".to_string(), - }, - ) - .unwrap(); - db.insert( - tx4, - Row { - id: RowID { - table_id: 1, - row_id: 3, - }, - data: "testme3".to_string(), - }, - ) - .unwrap(); - - assert_eq!( - db.read( - tx4, - RowID { - table_id: 1, - row_id: 1 - } - ) - .unwrap() - .unwrap() - .data, - "testme" - ); - assert_eq!( - db.read( - tx4, - RowID { - table_id: 1, - row_id: 2 - } - ) - .unwrap() - .unwrap() - .data, - "testme2" - ); - assert_eq!( - db.read( - tx4, - RowID { - table_id: 1, - row_id: 3 - } - ) - .unwrap() - .unwrap() - .data, - "testme3" - ); - db.commit_tx(tx4).unwrap(); - - let clock = LocalClock::new(); - let storage = crate::persistent_storage::Storage::new_json_on_disk(path); - let db = Database::new(clock, storage); - db.recover().unwrap(); - println!("{:#?}", db); - - let tx5 = db.begin_tx(); - println!( - "{:#?}", - db.read( - tx5, - RowID { - table_id: 1, - row_id: 1 - } - ) - ); - assert_eq!( - db.read( - tx5, - RowID { - table_id: 1, - row_id: 1 - } - ) - .unwrap() - .unwrap() - .data, - "testme" - ); - assert_eq!( - db.read( - tx5, - RowID { - table_id: 1, - row_id: 2 - } - ) - .unwrap() - .unwrap() - .data, - "testme2" - ); - assert_eq!( - db.read( - tx5, - RowID { - table_id: 1, - row_id: 3 - } - ) - .unwrap() - .unwrap() - .data, - "testme3" - ); - } -} diff --git a/core/mvcc/mvcc-rs/src/database/tests.rs b/core/mvcc/mvcc-rs/src/database/tests.rs new file mode 100644 index 000000000..adb29856e --- /dev/null +++ b/core/mvcc/mvcc-rs/src/database/tests.rs @@ -0,0 +1,780 @@ + +use super::*; +use crate::clock::LocalClock; +use tracing_test::traced_test; + +fn test_db() -> Database { + let clock = LocalClock::new(); + let storage = crate::persistent_storage::Storage::new_noop(); + Database::new(clock, storage) +} + +#[traced_test] +#[test] +fn test_insert_read() { + let db = test_db(); + + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()).unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(tx1_row, row); + db.commit_tx(tx1).unwrap(); + + let tx2 = db.begin_tx(); + let row = db + .read( + tx2, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(tx1_row, row); +} + +#[traced_test] +#[test] +fn test_read_nonexistent() { + let db = test_db(); + let tx = db.begin_tx(); + let row = db.read( + tx, + RowID { + table_id: 1, + row_id: 1, + }, + ); + assert!(row.unwrap().is_none()); +} + +#[traced_test] +#[test] +fn test_delete() { + let db = test_db(); + + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()).unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(tx1_row, row); + db.delete( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap(); + assert!(row.is_none()); + db.commit_tx(tx1).unwrap(); + + let tx2 = db.begin_tx(); + let row = db + .read( + tx2, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap(); + assert!(row.is_none()); +} + +#[traced_test] +#[test] +fn test_delete_nonexistent() { + let db = test_db(); + let tx = db.begin_tx(); + assert!(!db + .delete( + tx, + RowID { + table_id: 1, + row_id: 1 + } + ) + .unwrap()); +} + +#[traced_test] +#[test] +fn test_commit() { + let db = test_db(); + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()).unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(tx1_row, row); + let tx1_updated_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "World".to_string(), + }; + db.update(tx1, tx1_updated_row.clone()).unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(tx1_updated_row, row); + db.commit_tx(tx1).unwrap(); + + let tx2 = db.begin_tx(); + let row = db + .read( + tx2, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + db.commit_tx(tx2).unwrap(); + assert_eq!(tx1_updated_row, row); + db.drop_unused_row_versions(); +} + +#[traced_test] +#[test] +fn test_rollback() { + let db = test_db(); + let tx1 = db.begin_tx(); + let row1 = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "Hello".to_string(), + }; + db.insert(tx1, row1.clone()).unwrap(); + let row2 = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(row1, row2); + let row3 = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "World".to_string(), + }; + db.update(tx1, row3.clone()).unwrap(); + let row4 = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(row3, row4); + db.rollback_tx(tx1); + let tx2 = db.begin_tx(); + let row5 = db + .read( + tx2, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap(); + assert_eq!(row5, None); +} + +#[traced_test] +#[test] +fn test_dirty_write() { + let db = test_db(); + + // T1 inserts a row with ID 1, but does not commit. + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()).unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(tx1_row, row); + + // T2 attempts to delete row with ID 1, but fails because T1 has not committed. + let tx2 = db.begin_tx(); + let tx2_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "World".to_string(), + }; + assert!(!db.update(tx2, tx2_row).unwrap()); + + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(tx1_row, row); +} + +#[traced_test] +#[test] +fn test_dirty_read() { + let db = test_db(); + + // T1 inserts a row with ID 1, but does not commit. + let tx1 = db.begin_tx(); + let row1 = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "Hello".to_string(), + }; + db.insert(tx1, row1).unwrap(); + + // T2 attempts to read row with ID 1, but doesn't see one because T1 has not committed. + let tx2 = db.begin_tx(); + let row2 = db + .read( + tx2, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap(); + assert_eq!(row2, None); +} + +#[ignore] +#[traced_test] +#[test] +fn test_dirty_read_deleted() { + let db = test_db(); + + // T1 inserts a row with ID 1 and commits. + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()).unwrap(); + db.commit_tx(tx1).unwrap(); + + // T2 deletes row with ID 1, but does not commit. + let tx2 = db.begin_tx(); + assert!(db + .delete( + tx2, + RowID { + table_id: 1, + row_id: 1 + } + ) + .unwrap()); + + // T3 reads row with ID 1, but doesn't see the delete because T2 hasn't committed. + let tx3 = db.begin_tx(); + let row = db + .read( + tx3, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(tx1_row, row); +} + +#[traced_test] +#[test] +fn test_fuzzy_read() { + let db = test_db(); + + // T1 inserts a row with ID 1 and commits. + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()).unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(tx1_row, row); + db.commit_tx(tx1).unwrap(); + + // T2 reads the row with ID 1 within an active transaction. + let tx2 = db.begin_tx(); + let row = db + .read( + tx2, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(tx1_row, row); + + // T3 updates the row and commits. + let tx3 = db.begin_tx(); + let tx3_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "World".to_string(), + }; + db.update(tx3, tx3_row).unwrap(); + db.commit_tx(tx3).unwrap(); + + // T2 still reads the same version of the row as before. + let row = db + .read( + tx2, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(tx1_row, row); +} + +#[traced_test] +#[test] +fn test_lost_update() { + let db = test_db(); + + // T1 inserts a row with ID 1 and commits. + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "Hello".to_string(), + }; + db.insert(tx1, tx1_row.clone()).unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(tx1_row, row); + db.commit_tx(tx1).unwrap(); + + // T2 attempts to update row ID 1 within an active transaction. + let tx2 = db.begin_tx(); + let tx2_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "World".to_string(), + }; + assert!(db.update(tx2, tx2_row.clone()).unwrap()); + + // T3 also attempts to update row ID 1 within an active transaction. + let tx3 = db.begin_tx(); + let tx3_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "Hello, world!".to_string(), + }; + assert_eq!( + Err(DatabaseError::WriteWriteConflict), + db.update(tx3, tx3_row) + ); + + db.commit_tx(tx2).unwrap(); + assert_eq!(Err(DatabaseError::TxTerminated), db.commit_tx(tx3)); + + let tx4 = db.begin_tx(); + let row = db + .read( + tx4, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(tx2_row, row); +} + +// Test for the visibility to check if a new transaction can see old committed values. +// This test checks for the typo present in the paper, explained in https://github.com/penberg/mvcc-rs/issues/15 +#[traced_test] +#[test] +fn test_committed_visibility() { + let db = test_db(); + + // let's add $10 to my account since I like money + let tx1 = db.begin_tx(); + let tx1_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "10".to_string(), + }; + db.insert(tx1, tx1_row.clone()).unwrap(); + db.commit_tx(tx1).unwrap(); + + // but I like more money, so let me try adding $10 more + let tx2 = db.begin_tx(); + let tx2_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "20".to_string(), + }; + assert!(db.update(tx2, tx2_row.clone()).unwrap()); + let row = db + .read( + tx2, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(row, tx2_row); + + // can I check how much money I have? + let tx3 = db.begin_tx(); + let row = db + .read( + tx3, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(tx1_row, row); +} + +// Test to check if a older transaction can see (un)committed future rows +#[traced_test] +#[test] +fn test_future_row() { + let db = test_db(); + + let tx1 = db.begin_tx(); + + let tx2 = db.begin_tx(); + let tx2_row = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "10".to_string(), + }; + db.insert(tx2, tx2_row).unwrap(); + + // transaction in progress, so tx1 shouldn't be able to see the value + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap(); + assert_eq!(row, None); + + // lets commit the transaction and check if tx1 can see it + db.commit_tx(tx2).unwrap(); + let row = db + .read( + tx1, + RowID { + table_id: 1, + row_id: 1, + }, + ) + .unwrap(); + assert_eq!(row, None); +} + +#[traced_test] +#[test] +fn test_storage1() { + let clock = LocalClock::new(); + let mut path = std::env::temp_dir(); + path.push(format!( + "mvcc-rs-storage-test-{}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos(), + )); + let storage = crate::persistent_storage::Storage::new_json_on_disk(path.clone()); + let db = Database::new(clock, storage); + + let tx1 = db.begin_tx(); + let tx2 = db.begin_tx(); + let tx3 = db.begin_tx(); + + db.insert( + tx3, + Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "testme".to_string(), + }, + ) + .unwrap(); + + db.commit_tx(tx1).unwrap(); + db.rollback_tx(tx2); + db.commit_tx(tx3).unwrap(); + + let tx4 = db.begin_tx(); + db.insert( + tx4, + Row { + id: RowID { + table_id: 1, + row_id: 2, + }, + data: "testme2".to_string(), + }, + ) + .unwrap(); + db.insert( + tx4, + Row { + id: RowID { + table_id: 1, + row_id: 3, + }, + data: "testme3".to_string(), + }, + ) + .unwrap(); + + assert_eq!( + db.read( + tx4, + RowID { + table_id: 1, + row_id: 1 + } + ) + .unwrap() + .unwrap() + .data, + "testme" + ); + assert_eq!( + db.read( + tx4, + RowID { + table_id: 1, + row_id: 2 + } + ) + .unwrap() + .unwrap() + .data, + "testme2" + ); + assert_eq!( + db.read( + tx4, + RowID { + table_id: 1, + row_id: 3 + } + ) + .unwrap() + .unwrap() + .data, + "testme3" + ); + db.commit_tx(tx4).unwrap(); + + let clock = LocalClock::new(); + let storage = crate::persistent_storage::Storage::new_json_on_disk(path); + let db = Database::new(clock, storage); + db.recover().unwrap(); + println!("{:#?}", db); + + let tx5 = db.begin_tx(); + println!( + "{:#?}", + db.read( + tx5, + RowID { + table_id: 1, + row_id: 1 + } + ) + ); + assert_eq!( + db.read( + tx5, + RowID { + table_id: 1, + row_id: 1 + } + ) + .unwrap() + .unwrap() + .data, + "testme" + ); + assert_eq!( + db.read( + tx5, + RowID { + table_id: 1, + row_id: 2 + } + ) + .unwrap() + .unwrap() + .data, + "testme2" + ); + assert_eq!( + db.read( + tx5, + RowID { + table_id: 1, + row_id: 3 + } + ) + .unwrap() + .unwrap() + .data, + "testme3" + ); +} From fdfc4fd5b4312408fc5eaa13c2ac2bd860f50964 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Mon, 5 Jun 2023 13:58:24 +0200 Subject: [PATCH 094/128] database: drop RefCell from SkipMap not needed, the structure is already Send&Sync --- core/mvcc/mvcc-rs/src/database/mod.rs | 41 ++++++++++--------------- core/mvcc/mvcc-rs/src/database/tests.rs | 1 - 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/core/mvcc/mvcc-rs/src/database/mod.rs b/core/mvcc/mvcc-rs/src/database/mod.rs index 07d532374..df9cc7bb1 100644 --- a/core/mvcc/mvcc-rs/src/database/mod.rs +++ b/core/mvcc/mvcc-rs/src/database/mod.rs @@ -138,7 +138,7 @@ impl Database { /// Creates a new database. pub fn new(clock: Clock, storage: Storage) -> Self { let inner = DatabaseInner { - rows: RefCell::new(SkipMap::new()), + rows: SkipMap::new(), txs: RefCell::new(HashMap::new()), tx_timestamps: RefCell::new(BTreeMap::new()), tx_ids: AtomicU64::new(1), // let's reserve transaction 0 for special purposes @@ -293,7 +293,7 @@ impl Database { #[derive(Debug)] pub struct DatabaseInner { - rows: RefCell>>>, + rows: SkipMap>>, txs: RefCell>, tx_timestamps: RefCell>, tx_ids: AtomicU64, @@ -314,8 +314,7 @@ impl DatabaseInner { end: None, row, }; - let rows = self.rows.borrow_mut(); - let versions = rows.get_or_insert_with(id, || RwLock::new(Vec::new())); + let versions = self.rows.get_or_insert_with(id, || RwLock::new(Vec::new())); let mut versions = versions.value().write().unwrap(); versions.push(row_version); tx.insert_to_write_set(id); @@ -327,8 +326,7 @@ impl DatabaseInner { // NOTICE: They *are* dropped before an await point!!! But the await is conditional, // so I think clippy is just confused. let mut txs = self.txs.borrow_mut(); - let rows = self.rows.borrow_mut(); - let row_versions_opt = rows.get(&id); + let row_versions_opt = self.rows.get(&id); if let Some(ref row_versions) = row_versions_opt { let mut row_versions = row_versions.value().write().unwrap(); for rv in row_versions.iter_mut().rev() { @@ -340,7 +338,6 @@ impl DatabaseInner { drop(txs); drop(row_versions); drop(row_versions_opt); - drop(rows); self.rollback_tx(tx_id); return Err(DatabaseError::WriteWriteConflict); } @@ -361,8 +358,7 @@ impl DatabaseInner { let txs = self.txs.borrow_mut(); let tx = txs.get(&tx_id).unwrap(); assert!(tx.state == TransactionState::Active); - let rows = self.rows.borrow(); - if let Some(row_versions) = rows.get(&id) { + if let Some(row_versions) = self.rows.get(&id) { let row_versions = row_versions.value().read().unwrap(); for rv in row_versions.iter().rev() { if is_version_visible(&txs, tx, rv) { @@ -375,14 +371,13 @@ impl DatabaseInner { } fn scan_row_ids(&self) -> Result> { - let rows = self.rows.borrow(); - let keys = rows.iter().map(|entry| *entry.key()); + let keys = self.rows.iter().map(|entry| *entry.key()); Ok(keys.collect()) } fn scan_row_ids_for_table(&self, table_id: u64) -> Result> { - let rows = &self.rows.borrow(); - Ok(rows + Ok(self + .rows .range( RowID { table_id, @@ -418,12 +413,11 @@ impl DatabaseInner { assert!(tx.state == TransactionState::Active); } } - let rows = self.rows.borrow_mut(); tx.state = TransactionState::Preparing; tracing::trace!("PREPARE {tx}"); let mut log_record: LogRecord = LogRecord::new(end_ts); for id in &tx.write_set { - if let Some(row_versions) = rows.get(id) { + if let Some(row_versions) = self.rows.get(id) { let mut row_versions = row_versions.value().write().unwrap(); for row_version in row_versions.iter_mut() { if let TxTimestampOrID::TxID(id) = row_version.begin { @@ -457,7 +451,6 @@ impl DatabaseInner { } } txs.remove(&tx_id); - drop(rows); drop(txs); if !log_record.row_versions.is_empty() { self.storage.log_tx(log_record)?; @@ -471,13 +464,12 @@ impl DatabaseInner { assert!(tx.state == TransactionState::Active); tx.state = TransactionState::Aborted; tracing::trace!("ABORT {tx}"); - let rows = self.rows.borrow_mut(); for id in &tx.write_set { - if let Some(row_versions) = rows.get(id) { + if let Some(row_versions) = self.rows.get(id) { let mut row_versions = row_versions.value().write().unwrap(); row_versions.retain(|rv| rv.begin != TxTimestampOrID::TxID(tx_id)); if row_versions.is_empty() { - rows.remove(id); + self.rows.remove(id); } } } @@ -506,9 +498,8 @@ impl DatabaseInner { fn drop_unused_row_versions(&self) { let txs = self.txs.borrow(); let tx_timestamps = self.tx_timestamps.borrow(); - let rows = self.rows.borrow_mut(); let mut to_remove = Vec::new(); - for entry in rows.iter() { + for entry in self.rows.iter() { let mut row_versions = entry.value().write().unwrap(); row_versions.retain(|rv| { let should_stay = match rv.end { @@ -543,7 +534,7 @@ impl DatabaseInner { } } for id in to_remove { - rows.remove(&id); + self.rows.remove(&id); } } @@ -552,9 +543,9 @@ impl DatabaseInner { for record in tx_log { tracing::debug!("RECOVERING {:?}", record); for version in record.row_versions { - let rows = self.rows.borrow_mut(); - let row_versions = - rows.get_or_insert_with(version.row.id, || RwLock::new(Vec::new())); + let row_versions = self + .rows + .get_or_insert_with(version.row.id, || RwLock::new(Vec::new())); let mut row_versions = row_versions.value().write().unwrap(); row_versions.push(version); } diff --git a/core/mvcc/mvcc-rs/src/database/tests.rs b/core/mvcc/mvcc-rs/src/database/tests.rs index adb29856e..29bf1c4f7 100644 --- a/core/mvcc/mvcc-rs/src/database/tests.rs +++ b/core/mvcc/mvcc-rs/src/database/tests.rs @@ -1,4 +1,3 @@ - use super::*; use crate::clock::LocalClock; use tracing_test::traced_test; From a8faffa9f59cf33628e51cf3214161c2a70937c0 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Mon, 5 Jun 2023 14:25:31 +0200 Subject: [PATCH 095/128] database: migrate txs to SkipMap --- core/mvcc/mvcc-rs/src/database/mod.rs | 170 ++++++++++++++++++-------- 1 file changed, 117 insertions(+), 53 deletions(-) diff --git a/core/mvcc/mvcc-rs/src/database/mod.rs b/core/mvcc/mvcc-rs/src/database/mod.rs index df9cc7bb1..66d82e54d 100644 --- a/core/mvcc/mvcc-rs/src/database/mod.rs +++ b/core/mvcc/mvcc-rs/src/database/mod.rs @@ -1,11 +1,11 @@ use crate::clock::LogicalClock; use crate::errors::DatabaseError; use crate::persistent_storage::Storage; -use crossbeam_skiplist::SkipMap; +use crossbeam_skiplist::{SkipMap, SkipSet}; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; use std::cell::RefCell; -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::BTreeMap; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, RwLock}; @@ -66,7 +66,7 @@ enum TxTimestampOrID { } /// Transaction -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct Transaction { /// The state of the transaction. state: TransactionState, @@ -75,9 +75,55 @@ pub struct Transaction { /// The transaction begin timestamp. begin_ts: u64, /// The transaction write set. - write_set: HashSet, + #[serde(with = "skipset_rowid")] + write_set: SkipSet, /// The transaction read set. - read_set: RefCell>, + #[serde(with = "skipset_rowid")] + read_set: SkipSet, +} + +mod skipset_rowid { + use super::*; + use serde::{de, ser, ser::SerializeSeq}; + + struct SkipSetDeserializer; + + impl<'de> serde::de::Visitor<'de> for SkipSetDeserializer { + type Value = SkipSet; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("SkipSet key value sequence.") + } + + fn visit_seq
(self, mut seq: A) -> std::result::Result + where + A: serde::de::SeqAccess<'de>, + { + let new_skipset = SkipSet::new(); + while let Some(elem) = seq.next_element()? { + new_skipset.insert(elem); + } + + Ok(new_skipset) + } + } + + pub fn serialize( + value: &SkipSet, + ser: S, + ) -> std::result::Result { + let mut set = ser.serialize_seq(Some(value.len()))?; + for v in value { + set.serialize_element(v.value())?; + } + set.end() + } + + pub fn deserialize<'de, D: de::Deserializer<'de>>( + de: D, + ) -> std::result::Result, D::Error> { + de.deserialize_seq(SkipSetDeserializer) + } } impl Transaction { @@ -86,14 +132,13 @@ impl Transaction { state: TransactionState::Active, tx_id, begin_ts, - write_set: HashSet::new(), - read_set: RefCell::new(HashSet::new()), + write_set: SkipSet::new(), + read_set: SkipSet::new(), } } fn insert_to_read_set(&self, id: RowID) { - let mut read_set = self.read_set.borrow_mut(); - read_set.insert(id); + self.read_set.insert(id); } fn insert_to_write_set(&mut self, id: RowID) { @@ -103,18 +148,21 @@ impl Transaction { impl std::fmt::Display for Transaction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - match self.read_set.try_borrow() { - Ok(read_set) => write!( - f, - "{{ id: {}, begin_ts: {}, write_set: {:?}, read_set: {:?} }}", - self.tx_id, self.begin_ts, self.write_set, read_set - ), - Err(_) => write!( - f, - "{{ id: {}, begin_ts: {}, write_set: {:?}, read_set: }}", - self.tx_id, self.begin_ts, self.write_set - ), - } + write!( + f, + "{{ id: {}, begin_ts: {}, write_set: {:?}, read_set: {:?}", + self.tx_id, + self.begin_ts, + // FIXME: I'm sorry, we obviously shouldn't be cloning here. + self.write_set + .iter() + .map(|v| *v.value()) + .collect::>(), + self.read_set + .iter() + .map(|v| *v.value()) + .collect::>() + ) } } @@ -139,7 +187,7 @@ impl Database { pub fn new(clock: Clock, storage: Storage) -> Self { let inner = DatabaseInner { rows: SkipMap::new(), - txs: RefCell::new(HashMap::new()), + txs: SkipMap::new(), tx_timestamps: RefCell::new(BTreeMap::new()), tx_ids: AtomicU64::new(1), // let's reserve transaction 0 for special purposes clock, @@ -294,7 +342,7 @@ impl Database { #[derive(Debug)] pub struct DatabaseInner { rows: SkipMap>>, - txs: RefCell>, + txs: SkipMap>, tx_timestamps: RefCell>, tx_ids: AtomicU64, clock: Clock, @@ -303,10 +351,11 @@ pub struct DatabaseInner { impl DatabaseInner { fn insert(&self, tx_id: TxID, row: Row) -> Result<()> { - let mut txs = self.txs.borrow_mut(); - let tx = txs - .get_mut(&tx_id) + let tx = self + .txs + .get(&tx_id) .ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; + let mut tx = tx.value().write().unwrap(); assert!(tx.state == TransactionState::Active); let id = row.id; let row_version = RowVersion { @@ -321,31 +370,32 @@ impl DatabaseInner { Ok(()) } - #[allow(clippy::await_holding_refcell_ref)] fn delete(&self, tx_id: TxID, id: RowID) -> Result { - // NOTICE: They *are* dropped before an await point!!! But the await is conditional, - // so I think clippy is just confused. - let mut txs = self.txs.borrow_mut(); let row_versions_opt = self.rows.get(&id); if let Some(ref row_versions) = row_versions_opt { let mut row_versions = row_versions.value().write().unwrap(); for rv in row_versions.iter_mut().rev() { - let tx = txs + let tx = self + .txs .get(&tx_id) .ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; + let tx = tx.value().read().unwrap(); assert!(tx.state == TransactionState::Active); - if is_write_write_conflict(&txs, tx, rv) { - drop(txs); + if is_write_write_conflict(&self.txs, &tx, rv) { drop(row_versions); drop(row_versions_opt); + drop(tx); self.rollback_tx(tx_id); return Err(DatabaseError::WriteWriteConflict); } - if is_version_visible(&txs, tx, rv) { + if is_version_visible(&self.txs, &tx, rv) { rv.end = Some(TxTimestampOrID::TxID(tx.tx_id)); - let tx = txs - .get_mut(&tx_id) + drop(tx); // FIXME: maybe just grab the write lock above? Do we ever expect conflicts? + let tx = self + .txs + .get(&tx_id) .ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; + let mut tx = tx.value().write().unwrap(); tx.insert_to_write_set(id); return Ok(true); } @@ -355,13 +405,13 @@ impl DatabaseInner { } fn read(&self, tx_id: TxID, id: RowID) -> Result> { - let txs = self.txs.borrow_mut(); - let tx = txs.get(&tx_id).unwrap(); + let tx = self.txs.get(&tx_id).unwrap(); + let tx = tx.value().read().unwrap(); assert!(tx.state == TransactionState::Active); if let Some(row_versions) = self.rows.get(&id) { let row_versions = row_versions.value().read().unwrap(); for rv in row_versions.iter().rev() { - if is_version_visible(&txs, tx, rv) { + if is_version_visible(&self.txs, &tx, rv) { tx.insert_to_read_set(id); return Ok(Some(rv.row.clone())); } @@ -396,17 +446,16 @@ impl DatabaseInner { let begin_ts = self.get_timestamp(); let tx = Transaction::new(tx_id, begin_ts); tracing::trace!("BEGIN {tx}"); - let mut txs = self.txs.borrow_mut(); let mut tx_timestamps = self.tx_timestamps.borrow_mut(); - txs.insert(tx_id, tx); + self.txs.insert(tx_id, RwLock::new(tx)); *tx_timestamps.entry(begin_ts).or_insert(0) += 1; tx_id } fn commit_tx(&mut self, tx_id: TxID) -> Result<()> { let end_ts = self.get_timestamp(); - let mut txs = self.txs.borrow_mut(); - let tx = txs.get_mut(&tx_id).unwrap(); + let tx = self.txs.get(&tx_id).unwrap(); + let mut tx = tx.value().write().unwrap(); match tx.state { TransactionState::Terminated => return Err(DatabaseError::TxTerminated), _ => { @@ -417,6 +466,7 @@ impl DatabaseInner { tracing::trace!("PREPARE {tx}"); let mut log_record: LogRecord = LogRecord::new(end_ts); for id in &tx.write_set { + let id = id.value(); if let Some(row_versions) = self.rows.get(id) { let mut row_versions = row_versions.value().write().unwrap(); for row_version in row_versions.iter_mut() { @@ -450,8 +500,7 @@ impl DatabaseInner { tx_timestamps.remove(&tx.begin_ts); } } - txs.remove(&tx_id); - drop(txs); + self.txs.remove(&tx_id); if !log_record.row_versions.is_empty() { self.storage.log_tx(log_record)?; } @@ -459,12 +508,13 @@ impl DatabaseInner { } fn rollback_tx(&self, tx_id: TxID) { - let mut txs = self.txs.borrow_mut(); - let tx = txs.get_mut(&tx_id).unwrap(); + let tx = self.txs.get(&tx_id).unwrap(); + let mut tx = tx.value().write().unwrap(); assert!(tx.state == TransactionState::Active); tx.state = TransactionState::Aborted; tracing::trace!("ABORT {tx}"); for id in &tx.write_set { + let id = id.value(); if let Some(row_versions) = self.rows.get(id) { let mut row_versions = row_versions.value().write().unwrap(); row_versions.retain(|rv| rv.begin != TxTimestampOrID::TxID(tx_id)); @@ -496,7 +546,6 @@ impl DatabaseInner { /// We can do better by keeping an index of row versions ordered /// by their end timestamps. fn drop_unused_row_versions(&self) { - let txs = self.txs.borrow(); let tx_timestamps = self.tx_timestamps.borrow(); let mut to_remove = Vec::new(); for entry in self.rows.iter() { @@ -515,7 +564,7 @@ impl DatabaseInner { // Let's skip potentially complex logic if the transaction is still // active/tracked. We will drop the row version when the transaction // gets garbage-collected itself, it will always happen eventually. - Some(TxTimestampOrID::TxID(tx_id)) => !txs.contains_key(&tx_id), + Some(TxTimestampOrID::TxID(tx_id)) => !self.txs.contains_key(&tx_id), // this row version is current, ergo visible None => true, }; @@ -558,13 +607,14 @@ impl DatabaseInner { /// A write-write conflict happens when transaction T_m attempts to update a /// row version that is currently being updated by an active transaction T_n. fn is_write_write_conflict( - txs: &HashMap, + txs: &SkipMap>, tx: &Transaction, rv: &RowVersion, ) -> bool { match rv.end { Some(TxTimestampOrID::TxID(rv_end)) => { let te = txs.get(&rv_end).unwrap(); + let te = te.value().read().unwrap(); match te.state { TransactionState::Active => tx.tx_id != te.tx_id, TransactionState::Preparing => todo!(), @@ -578,15 +628,24 @@ fn is_write_write_conflict( } } -fn is_version_visible(txs: &HashMap, tx: &Transaction, rv: &RowVersion) -> bool { +fn is_version_visible( + txs: &SkipMap>, + tx: &Transaction, + rv: &RowVersion, +) -> bool { is_begin_visible(txs, tx, rv) && is_end_visible(txs, tx, rv) } -fn is_begin_visible(txs: &HashMap, tx: &Transaction, rv: &RowVersion) -> bool { +fn is_begin_visible( + txs: &SkipMap>, + tx: &Transaction, + rv: &RowVersion, +) -> bool { match rv.begin { TxTimestampOrID::Timestamp(rv_begin_ts) => tx.begin_ts >= rv_begin_ts, TxTimestampOrID::TxID(rv_begin) => { let tb = txs.get(&rv_begin).unwrap(); + let tb = tb.value().read().unwrap(); match tb.state { TransactionState::Active => tx.tx_id == tb.tx_id && rv.end.is_none(), TransactionState::Preparing => todo!(), @@ -598,11 +657,16 @@ fn is_begin_visible(txs: &HashMap, tx: &Transaction, rv: &Row } } -fn is_end_visible(txs: &HashMap, tx: &Transaction, rv: &RowVersion) -> bool { +fn is_end_visible( + txs: &SkipMap>, + tx: &Transaction, + rv: &RowVersion, +) -> bool { match rv.end { Some(TxTimestampOrID::Timestamp(rv_end_ts)) => tx.begin_ts < rv_end_ts, Some(TxTimestampOrID::TxID(rv_end)) => { let te = txs.get(&rv_end).unwrap(); + let te = te.value().read().unwrap(); match te.state { TransactionState::Active => tx.tx_id != te.tx_id, TransactionState::Preparing => todo!(), From 47eb149214d982f3557dd44e5004018745fdfd3d Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Tue, 6 Jun 2023 11:21:10 +0200 Subject: [PATCH 096/128] database: drop the mutex Without a critical section, we naturally hit a few unimplemented paths when handling concurrent transactions, which is great news! Visiting previously impossible paths already proves that lock-free is able to handle concurrency > 1. Now, the easy part - fixing all the unimplemented paths and making the Hekaton implementation 100% foolproof. --- core/mvcc/mvcc-rs/src/database/mod.rs | 282 +++++------------- .../mvcc-rs/src/persistent_storage/mod.rs | 2 +- 2 files changed, 78 insertions(+), 206 deletions(-) diff --git a/core/mvcc/mvcc-rs/src/database/mod.rs b/core/mvcc/mvcc-rs/src/database/mod.rs index 66d82e54d..cfdd43c4c 100644 --- a/core/mvcc/mvcc-rs/src/database/mod.rs +++ b/core/mvcc/mvcc-rs/src/database/mod.rs @@ -2,12 +2,9 @@ use crate::clock::LogicalClock; use crate::errors::DatabaseError; use crate::persistent_storage::Storage; use crossbeam_skiplist::{SkipMap, SkipSet}; -use parking_lot::Mutex; use serde::{Deserialize, Serialize}; -use std::cell::RefCell; -use std::collections::BTreeMap; use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::{Arc, RwLock}; +use std::sync::RwLock; pub type Result = std::result::Result; @@ -175,29 +172,28 @@ enum TransactionState { Aborted, Terminated, } - -/// A database with MVCC. #[derive(Debug)] pub struct Database { - inner: Arc>>, + rows: SkipMap>>, + txs: SkipMap>, + tx_ids: AtomicU64, + clock: Clock, + storage: Storage, } impl Database { + /// Creates a new database. pub fn new(clock: Clock, storage: Storage) -> Self { - let inner = DatabaseInner { + Self { rows: SkipMap::new(), txs: SkipMap::new(), - tx_timestamps: RefCell::new(BTreeMap::new()), tx_ids: AtomicU64::new(1), // let's reserve transaction 0 for special purposes clock, storage, - }; - Self { - inner: Arc::new(Mutex::new(inner)), } } - + /// Inserts a new row into the database. /// /// This function inserts a new `row` into the database within the context @@ -209,8 +205,23 @@ impl Database { /// * `row` - the row object containing the values to be inserted. /// pub fn insert(&self, tx_id: TxID, row: Row) -> Result<()> { - let inner = self.inner.lock(); - inner.insert(tx_id, row) + let tx = self + .txs + .get(&tx_id) + .ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; + let mut tx = tx.value().write().unwrap(); + assert!(tx.state == TransactionState::Active); + let id = row.id; + let row_version = RowVersion { + begin: TxTimestampOrID::TxID(tx.tx_id), + end: None, + row, + }; + let versions = self.rows.get_or_insert_with(id, || RwLock::new(Vec::new())); + let mut versions = versions.value().write().unwrap(); + versions.push(row_version); + tx.insert_to_write_set(id); + Ok(()) } /// Updates a row in the database with new values. @@ -254,123 +265,6 @@ impl Database { /// Returns `true` if the row was successfully deleted, and `false` otherwise. /// pub fn delete(&self, tx_id: TxID, id: RowID) -> Result { - let inner = self.inner.lock(); - inner.delete(tx_id, id) - } - - /// Retrieves a row from the table with the given `id`. - /// - /// This operation is performed within the scope of the transaction identified - /// by `tx_id`. - /// - /// # Arguments - /// - /// * `tx_id` - The ID of the transaction to perform the read operation in. - /// * `id` - The ID of the row to retrieve. - /// - /// # Returns - /// - /// Returns `Some(row)` with the row data if the row with the given `id` exists, - /// and `None` otherwise. - pub fn read(&self, tx_id: TxID, id: RowID) -> Result> { - let inner = self.inner.lock(); - inner.read(tx_id, id) - } - - pub fn scan_row_ids(&self) -> Result> { - let inner = self.inner.lock(); - inner.scan_row_ids() - } - - pub fn scan_row_ids_for_table(&self, table_id: u64) -> Result> { - let inner = self.inner.lock(); - inner.scan_row_ids_for_table(table_id) - } - - /// Begins a new transaction in the database. - /// - /// This function starts a new transaction in the database and returns a `TxID` value - /// that you can use to perform operations within the transaction. All changes made within the - /// transaction are isolated from other transactions until you commit the transaction. - pub fn begin_tx(&self) -> TxID { - let mut inner = self.inner.lock(); - inner.begin_tx() - } - - /// Commits a transaction with the specified transaction ID. - /// - /// This function commits the changes made within the specified transaction and finalizes the - /// transaction. Once a transaction has been committed, all changes made within the transaction - /// are visible to other transactions that access the same data. - /// - /// # Arguments - /// - /// * `tx_id` - The ID of the transaction to commit. - pub fn commit_tx(&self, tx_id: TxID) -> Result<()> { - let mut inner = self.inner.lock(); - inner.commit_tx(tx_id) - } - - /// Rolls back a transaction with the specified ID. - /// - /// This function rolls back a transaction with the specified `tx_id` by - /// discarding any changes made by the transaction. - /// - /// # Arguments - /// - /// * `tx_id` - The ID of the transaction to abort. - pub fn rollback_tx(&self, tx_id: TxID) { - let inner = self.inner.lock(); - inner.rollback_tx(tx_id); - } - - /// Drops all unused row versions from the database. - /// - /// A version is considered unused if it is not visible to any active transaction - /// and it is not the most recent version of the row. - pub fn drop_unused_row_versions(&self) { - let inner = self.inner.lock(); - inner.drop_unused_row_versions(); - } - - pub fn recover(&self) -> Result<()> { - let inner = self.inner.lock(); - inner.recover() - } -} - -#[derive(Debug)] -pub struct DatabaseInner { - rows: SkipMap>>, - txs: SkipMap>, - tx_timestamps: RefCell>, - tx_ids: AtomicU64, - clock: Clock, - storage: Storage, -} - -impl DatabaseInner { - fn insert(&self, tx_id: TxID, row: Row) -> Result<()> { - let tx = self - .txs - .get(&tx_id) - .ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; - let mut tx = tx.value().write().unwrap(); - assert!(tx.state == TransactionState::Active); - let id = row.id; - let row_version = RowVersion { - begin: TxTimestampOrID::TxID(tx.tx_id), - end: None, - row, - }; - let versions = self.rows.get_or_insert_with(id, || RwLock::new(Vec::new())); - let mut versions = versions.value().write().unwrap(); - versions.push(row_version); - tx.insert_to_write_set(id); - Ok(()) - } - - fn delete(&self, tx_id: TxID, id: RowID) -> Result { let row_versions_opt = self.rows.get(&id); if let Some(ref row_versions) = row_versions_opt { let mut row_versions = row_versions.value().write().unwrap(); @@ -404,7 +298,21 @@ impl DatabaseInner { Ok(false) } - fn read(&self, tx_id: TxID, id: RowID) -> Result> { + /// Retrieves a row from the table with the given `id`. + /// + /// This operation is performed within the scope of the transaction identified + /// by `tx_id`. + /// + /// # Arguments + /// + /// * `tx_id` - The ID of the transaction to perform the read operation in. + /// * `id` - The ID of the row to retrieve. + /// + /// # Returns + /// + /// Returns `Some(row)` with the row data if the row with the given `id` exists, + /// and `None` otherwise. + pub fn read(&self, tx_id: TxID, id: RowID) -> Result> { let tx = self.txs.get(&tx_id).unwrap(); let tx = tx.value().read().unwrap(); assert!(tx.state == TransactionState::Active); @@ -420,12 +328,14 @@ impl DatabaseInner { Ok(None) } - fn scan_row_ids(&self) -> Result> { + /// Gets all row ids in the database. + pub fn scan_row_ids(&self) -> Result> { let keys = self.rows.iter().map(|entry| *entry.key()); Ok(keys.collect()) } - fn scan_row_ids_for_table(&self, table_id: u64) -> Result> { + /// Gets all row ids in the database for a given table. + pub fn scan_row_ids_for_table(&self, table_id: u64) -> Result> { Ok(self .rows .range( @@ -441,18 +351,30 @@ impl DatabaseInner { .collect()) } - fn begin_tx(&mut self) -> TxID { + /// Begins a new transaction in the database. + /// + /// This function starts a new transaction in the database and returns a `TxID` value + /// that you can use to perform operations within the transaction. All changes made within the + /// transaction are isolated from other transactions until you commit the transaction. + pub fn begin_tx(&self) -> TxID { let tx_id = self.get_tx_id(); let begin_ts = self.get_timestamp(); let tx = Transaction::new(tx_id, begin_ts); tracing::trace!("BEGIN {tx}"); - let mut tx_timestamps = self.tx_timestamps.borrow_mut(); self.txs.insert(tx_id, RwLock::new(tx)); - *tx_timestamps.entry(begin_ts).or_insert(0) += 1; tx_id } - fn commit_tx(&mut self, tx_id: TxID) -> Result<()> { + /// Commits a transaction with the specified transaction ID. + /// + /// This function commits the changes made within the specified transaction and finalizes the + /// transaction. Once a transaction has been committed, all changes made within the transaction + /// are visible to other transactions that access the same data. + /// + /// # Arguments + /// + /// * `tx_id` - The ID of the transaction to commit. + pub fn commit_tx(&self, tx_id: TxID) -> Result<()> { let end_ts = self.get_timestamp(); let tx = self.txs.get(&tx_id).unwrap(); let mut tx = tx.value().write().unwrap(); @@ -487,19 +409,6 @@ impl DatabaseInner { } tx.state = TransactionState::Committed; tracing::trace!("COMMIT {tx}"); - // We have now updated all the versions with a reference to the - // transaction ID to a timestamp and can, therefore, remove the - // transaction. Please note that when we move to lockless, the - // invariant doesn't necessarily hold anymore because another thread - // might have speculatively read a version that we want to remove. - // But that's a problem for another day. - let mut tx_timestamps = self.tx_timestamps.borrow_mut(); - if let Some(timestamp_entry) = tx_timestamps.get_mut(&tx.begin_ts) { - *timestamp_entry -= 1; - if timestamp_entry == &0 { - tx_timestamps.remove(&tx.begin_ts); - } - } self.txs.remove(&tx_id); if !log_record.row_versions.is_empty() { self.storage.log_tx(log_record)?; @@ -507,7 +416,15 @@ impl DatabaseInner { Ok(()) } - fn rollback_tx(&self, tx_id: TxID) { + /// Rolls back a transaction with the specified ID. + /// + /// This function rolls back a transaction with the specified `tx_id` by + /// discarding any changes made by the transaction. + /// + /// # Arguments + /// + /// * `tx_id` - The ID of the transaction to abort. + pub fn rollback_tx(&self, tx_id: TxID) { let tx = self.txs.get(&tx_id).unwrap(); let mut tx = tx.value().write().unwrap(); assert!(tx.state == TransactionState::Active); @@ -527,64 +444,19 @@ impl DatabaseInner { tracing::trace!("TERMINATE {tx}"); } - fn get_tx_id(&mut self) -> u64 { + /// Generates next unique transaction id + pub fn get_tx_id(&self) -> u64 { self.tx_ids.fetch_add(1, Ordering::SeqCst) } - fn get_timestamp(&mut self) -> u64 { + /// Gets current timestamp + pub fn get_timestamp(&self) -> u64 { self.clock.get_timestamp() } - /// Drops all rows that are not visible to any transaction. - /// The logic is as follows. If a row version has an end marker - /// which denotes a transaction that is not active, then we can - /// drop the row version -- it is not visible to any transaction. - /// If a row version has an end marker that denotes a timestamp T_END, - /// then we can drop the row version only if all active transactions - /// have a begin timestamp that is greater than timestamp T_END. - /// FIXME: this function is a full scan over all rows and row versions. - /// We can do better by keeping an index of row versions ordered - /// by their end timestamps. - fn drop_unused_row_versions(&self) { - let tx_timestamps = self.tx_timestamps.borrow(); - let mut to_remove = Vec::new(); - for entry in self.rows.iter() { - let mut row_versions = entry.value().write().unwrap(); - row_versions.retain(|rv| { - let should_stay = match rv.end { - Some(TxTimestampOrID::Timestamp(version_end_ts)) => { - match tx_timestamps.first_key_value() { - // a transaction started before this row version ended, - // ergo row version is needed - Some((begin_ts, _)) => version_end_ts >= *begin_ts, - // no transaction => row version is not needed - None => false, - } - } - // Let's skip potentially complex logic if the transaction is still - // active/tracked. We will drop the row version when the transaction - // gets garbage-collected itself, it will always happen eventually. - Some(TxTimestampOrID::TxID(tx_id)) => !self.txs.contains_key(&tx_id), - // this row version is current, ergo visible - None => true, - }; - if !should_stay { - tracing::debug!( - "Dropping row version {:?} {:?}-{:?}", - entry.key(), - rv.begin, - rv.end - ); - } - should_stay - }); - if row_versions.is_empty() { - to_remove.push(*entry.key()); - } - } - for id in to_remove { - self.rows.remove(&id); - } + /// FIXME: implement in a lock-free manner + pub fn drop_unused_row_versions(&self) { + tracing::error!("Unused rows are not dropped at the moment. Will do!"); } pub fn recover(&self) -> Result<()> { diff --git a/core/mvcc/mvcc-rs/src/persistent_storage/mod.rs b/core/mvcc/mvcc-rs/src/persistent_storage/mod.rs index f927be381..185a432ee 100644 --- a/core/mvcc/mvcc-rs/src/persistent_storage/mod.rs +++ b/core/mvcc/mvcc-rs/src/persistent_storage/mod.rs @@ -27,7 +27,7 @@ impl Storage { } impl Storage { - pub fn log_tx(&mut self, m: LogRecord) -> Result<()> { + pub fn log_tx(&self, m: LogRecord) -> Result<()> { match self { Self::JsonOnDisk(path) => { use std::io::Write; From 625394000e71e102e1120b35a629c2e94b30a5fb Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Tue, 6 Jun 2023 11:34:34 +0200 Subject: [PATCH 097/128] unignore test_dirty_read_deleted --- core/mvcc/mvcc-rs/src/database/tests.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/core/mvcc/mvcc-rs/src/database/tests.rs b/core/mvcc/mvcc-rs/src/database/tests.rs index 29bf1c4f7..57e08b881 100644 --- a/core/mvcc/mvcc-rs/src/database/tests.rs +++ b/core/mvcc/mvcc-rs/src/database/tests.rs @@ -337,7 +337,6 @@ fn test_dirty_read() { assert_eq!(row2, None); } -#[ignore] #[traced_test] #[test] fn test_dirty_read_deleted() { From ddbcd9be792a19347fd31b8b88b0018f4138e3a2 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Tue, 6 Jun 2023 12:47:40 +0200 Subject: [PATCH 098/128] database: bring back dropping unused row versions --- core/mvcc/mvcc-rs/src/database/mod.rs | 41 +++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/core/mvcc/mvcc-rs/src/database/mod.rs b/core/mvcc/mvcc-rs/src/database/mod.rs index cfdd43c4c..3f6049a94 100644 --- a/core/mvcc/mvcc-rs/src/database/mod.rs +++ b/core/mvcc/mvcc-rs/src/database/mod.rs @@ -182,7 +182,6 @@ pub struct Database { } impl Database { - /// Creates a new database. pub fn new(clock: Clock, storage: Storage) -> Self { Self { @@ -193,7 +192,7 @@ impl Database { storage, } } - + /// Inserts a new row into the database. /// /// This function inserts a new `row` into the database within the context @@ -456,7 +455,43 @@ impl Database { /// FIXME: implement in a lock-free manner pub fn drop_unused_row_versions(&self) { - tracing::error!("Unused rows are not dropped at the moment. Will do!"); + let mut to_remove = Vec::new(); + for entry in self.rows.iter() { + let mut row_versions = entry.value().write().unwrap(); + row_versions.retain(|rv| { + let should_stay = match rv.end { + Some(TxTimestampOrID::Timestamp(version_end_ts)) => { + // a transaction started before this row version ended, + // ergo row version is needed + // NOTICE: O(row_versions x transactions), but also lock-free, so sounds acceptable + self.txs + .iter() + .any(|tx| version_end_ts >= tx.value().read().unwrap().begin_ts) + } + // Let's skip potentially complex logic if the transaction is still + // active/tracked. We will drop the row version when the transaction + // gets garbage-collected itself, it will always happen eventually. + Some(TxTimestampOrID::TxID(tx_id)) => !self.txs.contains_key(&tx_id), + // this row version is current, ergo visible + None => true, + }; + if !should_stay { + tracing::debug!( + "Dropping row version {:?} {:?}-{:?}", + entry.key(), + rv.begin, + rv.end + ); + } + should_stay + }); + if row_versions.is_empty() { + to_remove.push(*entry.key()); + } + } + for id in to_remove { + self.rows.remove(&id); + } } pub fn recover(&self) -> Result<()> { From 6d829733593cbdd85b1c8f717a59acafb166bc80 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Tue, 6 Jun 2023 15:31:56 +0200 Subject: [PATCH 099/128] database: restore a CRUCIAL comment about dropping a tx ... which stops being correct after lock-free! --- core/mvcc/mvcc-rs/src/database/mod.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/mvcc/mvcc-rs/src/database/mod.rs b/core/mvcc/mvcc-rs/src/database/mod.rs index 3f6049a94..28f9d8839 100644 --- a/core/mvcc/mvcc-rs/src/database/mod.rs +++ b/core/mvcc/mvcc-rs/src/database/mod.rs @@ -408,6 +408,14 @@ impl Database { } tx.state = TransactionState::Committed; tracing::trace!("COMMIT {tx}"); + // We have now updated all the versions with a reference to the + // transaction ID to a timestamp and can, therefore, remove the + // transaction. Please note that when we move to lockless, the + // invariant doesn't necessarily hold anymore because another thread + // might have speculatively read a version that we want to remove. + // But that's a problem for another day. + // FIXME: it actually just become a problem for today!!! + // TODO: test that reproduces this failure, and then a fix self.txs.remove(&tx_id); if !log_record.row_versions.is_empty() { self.storage.log_tx(log_record)?; From b4932340f4197e31f35be94448fa6cda04f5dae3 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Wed, 7 Jun 2023 11:20:00 +0200 Subject: [PATCH 100/128] database: add a juicy comment about serializability And specifically, the amount of things we don't have implemented to even think of that. It's mostly about tracking commit dependencies which allow speculative reads/ignores of certain versions, as well as making sure that in the commit phase, we validate visibility of all versions read, as well as that our scans took into account all data. If some version appeared after the transaction began, and it was not taken into account during its scans, it is considered a "phantom", and it invalidates the transaction if we strive for serializability. --- core/mvcc/mvcc-rs/src/database/mod.rs | 74 +++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/core/mvcc/mvcc-rs/src/database/mod.rs b/core/mvcc/mvcc-rs/src/database/mod.rs index 28f9d8839..49756f6cd 100644 --- a/core/mvcc/mvcc-rs/src/database/mod.rs +++ b/core/mvcc/mvcc-rs/src/database/mod.rs @@ -385,6 +385,80 @@ impl Database { } tx.state = TransactionState::Preparing; tracing::trace!("PREPARE {tx}"); + + /* TODO: The code we have here is sufficient for snapshot isolation. + ** In order to implement serializability, we need the following steps: + ** + ** 1. Validate if all read versions are still visible by inspecting the read_set + ** 2. Validate if there are no phantoms by walking the scans from scan_set (which we don't even have yet) + ** - a phantom is a version that became visible in the middle of our transaction, + ** but wasn't taken into account during one of the scans from the scan_set + ** 3. Wait for commit dependencies, which we don't even track yet... + ** Excerpt from what's a commit dependency and how it's tracked in the original paper: + ** """ + A transaction T1 has a commit dependency on another transaction + T2, if T1 is allowed to commit only if T2 commits. If T2 aborts, + T1 must also abort, so cascading aborts are possible. T1 acquires a + commit dependency either by speculatively reading or speculatively ignoring a version, + instead of waiting for T2 to commit. + We implement commit dependencies by a register-and-report + approach: T1 registers its dependency with T2 and T2 informs T1 + when it has committed or aborted. Each transaction T contains a + counter, CommitDepCounter, that counts how many unresolved + commit dependencies it still has. A transaction cannot commit + until this counter is zero. In addition, T has a Boolean variable + AbortNow that other transactions can set to tell T to abort. Each + transaction T also has a set, CommitDepSet, that stores transaction IDs + of the transactions that depend on T. + To take a commit dependency on a transaction T2, T1 increments + its CommitDepCounter and adds its transaction ID to T2’s CommitDepSet. + When T2 has committed, it locates each transaction in + its CommitDepSet and decrements their CommitDepCounter. If + T2 aborted, it tells the dependent transactions to also abort by + setting their AbortNow flags. If a dependent transaction is not + found, this means that it has already aborted. + Note that a transaction with commit dependencies may not have to + wait at all - the dependencies may have been resolved before it is + ready to commit. Commit dependencies consolidate all waits into + a single wait and postpone the wait to just before commit. + Some transactions may have to wait before commit. + Waiting raises a concern of deadlocks. + However, deadlocks cannot occur because an older transaction never + waits on a younger transaction. In + a wait-for graph the direction of edges would always be from a + younger transaction (higher end timestamp) to an older transaction + (lower end timestamp) so cycles are impossible. + """ + ** If you're wondering when a speculative read happens, here you go: + ** Case 1: speculative read of TB: + """ + If transaction TB is in the Preparing state, it has acquired an end + timestamp TS which will be V’s begin timestamp if TB commits. + A safe approach in this situation would be to have transaction T + wait until transaction TB commits. However, we want to avoid all + blocking during normal processing so instead we continue with + the visibility test and, if the test returns true, allow T to + speculatively read V. Transaction T acquires a commit dependency on + TB, restricting the serialization order of the two transactions. That + is, T is allowed to commit only if TB commits. + """ + ** Case 2: speculative ignore of TE: + """ + If TE’s state is Preparing, it has an end timestamp TS that will become + the end timestamp of V if TE does commit. If TS is greater than the read + time RT, it is obvious that V will be visible if TE commits. If TE + aborts, V will still be visible, because any transaction that updates + V after TE has aborted will obtain an end timestamp greater than + TS. If TS is less than RT, we have a more complicated situation: + if TE commits, V will not be visible to T but if TE aborts, it will + be visible. We could handle this by forcing T to wait until TE + commits or aborts but we want to avoid all blocking during normal processing. + Instead we allow T to speculatively ignore V and + proceed with its processing. Transaction T acquires a commit + dependency (see Section 2.7) on TE, that is, T is allowed to commit + only if TE commits. + """ + */ let mut log_record: LogRecord = LogRecord::new(end_ts); for id in &tx.write_set { let id = id.value(); From 983544dbfd7414d1f62a1c8fcfc66e56d3f44bc4 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Wed, 7 Jun 2023 13:58:34 +0200 Subject: [PATCH 101/128] database: implement missing cases for is_version_visible + tests Following the Hekaton paper tables, but also taking into account that in iteration 0 we're only interested in snapshot isolation, not serializability. --- core/mvcc/mvcc-rs/src/database/mod.rs | 72 ++++++++--- core/mvcc/mvcc-rs/src/database/tests.rs | 154 ++++++++++++++++++++++++ 2 files changed, 207 insertions(+), 19 deletions(-) diff --git a/core/mvcc/mvcc-rs/src/database/mod.rs b/core/mvcc/mvcc-rs/src/database/mod.rs index 49756f6cd..3307b831b 100644 --- a/core/mvcc/mvcc-rs/src/database/mod.rs +++ b/core/mvcc/mvcc-rs/src/database/mod.rs @@ -147,7 +147,8 @@ impl std::fmt::Display for Transaction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { write!( f, - "{{ id: {}, begin_ts: {}, write_set: {:?}, read_set: {:?}", + "{{ state: {}, id: {}, begin_ts: {}, write_set: {:?}, read_set: {:?}", + self.state, self.tx_id, self.begin_ts, // FIXME: I'm sorry, we obviously shouldn't be cloning here. @@ -168,10 +169,23 @@ impl std::fmt::Display for Transaction { enum TransactionState { Active, Preparing, - Committed, + Committed(u64), Aborted, Terminated, } + +impl std::fmt::Display for TransactionState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + match self { + TransactionState::Active => write!(f, "Active"), + TransactionState::Preparing => write!(f, "Preparing"), + TransactionState::Committed(ts) => write!(f, "Committed({ts})"), + TransactionState::Aborted => write!(f, "Aborted"), + TransactionState::Terminated => write!(f, "Terminated"), + } + } +} + #[derive(Debug)] pub struct Database { rows: SkipMap>>, @@ -459,6 +473,10 @@ impl Database { only if TE commits. """ */ + tx.state = TransactionState::Committed(end_ts); + tracing::trace!("COMMIT {tx}"); + // Postprocessing: inserting row versions and logging the transaction to persistent storage. + // TODO: we should probably save to persistent storage first, and only then update the in-memory structures. let mut log_record: LogRecord = LogRecord::new(end_ts); for id in &tx.write_set { let id = id.value(); @@ -480,8 +498,6 @@ impl Database { } } } - tx.state = TransactionState::Committed; - tracing::trace!("COMMIT {tx}"); // We have now updated all the versions with a reference to the // transaction ID to a timestamp and can, therefore, remove the // transaction. Please note that when we move to lockless, the @@ -595,7 +611,7 @@ impl Database { /// A write-write conflict happens when transaction T_m attempts to update a /// row version that is currently being updated by an active transaction T_n. -fn is_write_write_conflict( +pub(crate) fn is_write_write_conflict( txs: &SkipMap>, tx: &Transaction, rv: &RowVersion, @@ -607,7 +623,7 @@ fn is_write_write_conflict( match te.state { TransactionState::Active => tx.tx_id != te.tx_id, TransactionState::Preparing => todo!(), - TransactionState::Committed => todo!(), + TransactionState::Committed(_end_ts) => todo!(), TransactionState::Aborted => todo!(), TransactionState::Terminated => todo!(), } @@ -617,7 +633,7 @@ fn is_write_write_conflict( } } -fn is_version_visible( +pub(crate) fn is_version_visible( txs: &SkipMap>, tx: &Transaction, rv: &RowVersion, @@ -635,13 +651,22 @@ fn is_begin_visible( TxTimestampOrID::TxID(rv_begin) => { let tb = txs.get(&rv_begin).unwrap(); let tb = tb.value().read().unwrap(); - match tb.state { + let visible = match tb.state { TransactionState::Active => tx.tx_id == tb.tx_id && rv.end.is_none(), - TransactionState::Preparing => todo!(), - TransactionState::Committed => todo!(), - TransactionState::Aborted => todo!(), - TransactionState::Terminated => todo!(), - } + TransactionState::Preparing => false, // NOTICE: makes sense for snapshot isolation, not so much for serializable! + TransactionState::Committed(committed_ts) => tx.begin_ts >= committed_ts, + TransactionState::Aborted => false, + TransactionState::Terminated => { + tracing::debug!("TODO: should reread rv's end field - it should have updated the timestamp in the row version by now"); + false + } + }; + tracing::trace!( + "is_begin_visible: tx={tx}, tb={tb} rv = {:?}-{:?} visible = {visible}", + rv.begin, + rv.end + ); + visible } } } @@ -656,13 +681,22 @@ fn is_end_visible( Some(TxTimestampOrID::TxID(rv_end)) => { let te = txs.get(&rv_end).unwrap(); let te = te.value().read().unwrap(); - match te.state { + let visible = match te.state { TransactionState::Active => tx.tx_id != te.tx_id, - TransactionState::Preparing => todo!(), - TransactionState::Committed => todo!(), - TransactionState::Aborted => todo!(), - TransactionState::Terminated => todo!(), - } + TransactionState::Preparing => false, // NOTICE: makes sense for snapshot isolation, not so much for serializable! + TransactionState::Committed(committed_ts) => tx.begin_ts < committed_ts, + TransactionState::Aborted => false, + TransactionState::Terminated => { + tracing::debug!("TODO: should reread rv's end field - it should have updated the timestamp in the row version by now"); + false + } + }; + tracing::trace!( + "is_end_visible: tx={tx}, te={te} rv = {:?}-{:?} visible = {visible}", + rv.begin, + rv.end + ); + visible } None => true, } diff --git a/core/mvcc/mvcc-rs/src/database/tests.rs b/core/mvcc/mvcc-rs/src/database/tests.rs index 57e08b881..ada842218 100644 --- a/core/mvcc/mvcc-rs/src/database/tests.rs +++ b/core/mvcc/mvcc-rs/src/database/tests.rs @@ -776,3 +776,157 @@ fn test_storage1() { "testme3" ); } + +/* States described in the Hekaton paper *for serializability*: + +Table 1: Case analysis of action to take when version V’s +Begin field contains the ID of transaction TB +------------------------------------------------------------------------------------------------------ +TB’s state | TB’s end timestamp | Action to take when transaction T checks visibility of version V. +------------------------------------------------------------------------------------------------------ +Active | Not set | V is visible only if TB=T and V’s end timestamp equals infinity. +------------------------------------------------------------------------------------------------------ +Preparing | TS | V’s begin timestamp will be TS ut V is not yet committed. Use TS + | as V’s begin time when testing visibility. If the test is true, + | allow T to speculatively read V. Committed TS V’s begin timestamp + | will be TS and V is committed. Use TS as V’s begin time to test + | visibility. +------------------------------------------------------------------------------------------------------ +Committed | TS | V’s begin timestamp will be TS and V is committed. Use TS as V’s + | begin time to test visibility. +------------------------------------------------------------------------------------------------------ +Aborted | Irrelevant | Ignore V; it’s a garbage version. +------------------------------------------------------------------------------------------------------ +Terminated | Irrelevant | Reread V’s Begin field. TB has terminated so it must have finalized +or not found | | the timestamp. +------------------------------------------------------------------------------------------------------ + +Table 2: Case analysis of action to take when V's End field +contains a transaction ID TE. +------------------------------------------------------------------------------------------------------ +TE’s state | TE’s end timestamp | Action to take when transaction T checks visibility of a version V + | | as of read time RT. +------------------------------------------------------------------------------------------------------ +Active | Not set | V is visible only if TE is not T. +------------------------------------------------------------------------------------------------------ +Preparing | TS | V’s end timestamp will be TS provided that TE commits. If TS > RT, + | V is visible to T. If TS < RT, T speculatively ignores V. +------------------------------------------------------------------------------------------------------ +Committed | TS | V’s end timestamp will be TS and V is committed. Use TS as V’s end + | timestamp when testing visibility. +------------------------------------------------------------------------------------------------------ +Aborted | Irrelevant | V is visible. +------------------------------------------------------------------------------------------------------ +Terminated | Irrelevant | Reread V’s End field. TE has terminated so it must have finalized +or not found | | the timestamp. +*/ + +fn new_tx(tx_id: TxID, begin_ts: u64, state: TransactionState) -> RwLock { + RwLock::new(Transaction { + state, + tx_id, + begin_ts, + write_set: SkipSet::new(), + read_set: SkipSet::new(), + }) +} + +#[traced_test] +#[test] +fn test_snapshot_isolation_tx_visible1() { + let txs: SkipMap> = SkipMap::from_iter([ + (1, new_tx(1, 1, TransactionState::Committed(2))), + (2, new_tx(2, 2, TransactionState::Committed(5))), + (3, new_tx(3, 3, TransactionState::Aborted)), + (5, new_tx(5, 5, TransactionState::Preparing)), + (6, new_tx(6, 6, TransactionState::Committed(10))), + (7, new_tx(7, 7, TransactionState::Active)), + ]); + + let current_tx = new_tx(4, 4, TransactionState::Preparing); + let current_tx = current_tx.read().unwrap(); + + let rv_visible = |begin: TxTimestampOrID, end: Option| { + let row_version = RowVersion { + begin, + end, + row: Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "testme".to_string(), + }, + }; + tracing::debug!("Testing visibility of {row_version:?}"); + is_version_visible(&txs, ¤t_tx, &row_version) + }; + + // begin visible: transaction committed with ts < current_tx.begin_ts + // end visible: inf + assert!(rv_visible(TxTimestampOrID::TxID(1), None)); + + // begin invisible: transaction committed with ts > current_tx.begin_ts + assert!(!rv_visible(TxTimestampOrID::TxID(2), None)); + + // begin invisible: transaction aborted + assert!(!rv_visible(TxTimestampOrID::TxID(3), None)); + + // begin visible: timestamp < current_tx.begin_ts + // end invisible: transaction committed with ts > current_tx.begin_ts + assert!(!rv_visible( + TxTimestampOrID::Timestamp(0), + Some(TxTimestampOrID::TxID(1)) + )); + + // begin visible: timestamp < current_tx.begin_ts + // end visible: transaction committed with ts < current_tx.begin_ts + assert!(rv_visible( + TxTimestampOrID::Timestamp(0), + Some(TxTimestampOrID::TxID(2)) + )); + + // begin visible: timestamp < current_tx.begin_ts + // end invisible: transaction aborted + assert!(!rv_visible( + TxTimestampOrID::Timestamp(0), + Some(TxTimestampOrID::TxID(3)) + )); + + // begin invisible: transaction preparing + assert!(!rv_visible(TxTimestampOrID::TxID(5), None)); + + // begin invisible: transaction committed with ts > current_tx.begin_ts + assert!(!rv_visible(TxTimestampOrID::TxID(6), None)); + + // begin invisible: transaction active + assert!(!rv_visible(TxTimestampOrID::TxID(7), None)); + + // begin invisible: transaction committed with ts > current_tx.begin_ts + assert!(!rv_visible(TxTimestampOrID::TxID(6), None)); + + // begin invisible: transaction active + assert!(!rv_visible(TxTimestampOrID::TxID(7), None)); + + // begin visible: timestamp < current_tx.begin_ts + // end invisible: transaction preparing + assert!(!rv_visible( + TxTimestampOrID::Timestamp(0), + Some(TxTimestampOrID::TxID(5)) + )); + + // begin invisible: timestamp > current_tx.begin_ts + assert!(!rv_visible( + TxTimestampOrID::Timestamp(6), + Some(TxTimestampOrID::TxID(6)) + )); + + // begin visible: timestamp < current_tx.begin_ts + // end visible: some active transaction will eventually overwrite this version, + // but that hasn't happened + // (this is the https://avi.im/blag/2023/hekaton-paper-typo/ case, I believe!) + assert!(rv_visible( + TxTimestampOrID::Timestamp(0), + Some(TxTimestampOrID::TxID(7)) + )); +} From a93fcdcbcf3f5f7532c3d2bd8fffb3045a71bb05 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Mon, 12 Jun 2023 13:01:21 +0200 Subject: [PATCH 102/128] database: make transaction state atomic Without atomic access, we're subject to races when inspecting whether a transaction just changed its state, e.g. from Preparing to Committed. --- core/mvcc/mvcc-rs/src/database/mod.rs | 95 +++++++++++++++++++++---- core/mvcc/mvcc-rs/src/database/tests.rs | 1 + 2 files changed, 82 insertions(+), 14 deletions(-) diff --git a/core/mvcc/mvcc-rs/src/database/mod.rs b/core/mvcc/mvcc-rs/src/database/mod.rs index 3307b831b..01e0c2dce 100644 --- a/core/mvcc/mvcc-rs/src/database/mod.rs +++ b/core/mvcc/mvcc-rs/src/database/mod.rs @@ -66,7 +66,7 @@ enum TxTimestampOrID { #[derive(Debug, Serialize, Deserialize)] pub struct Transaction { /// The state of the transaction. - state: TransactionState, + state: AtomicTransactionState, /// The transaction ID. tx_id: u64, /// The transaction begin timestamp. @@ -126,7 +126,7 @@ mod skipset_rowid { impl Transaction { fn new(tx_id: u64, begin_ts: u64) -> Transaction { Transaction { - state: TransactionState::Active, + state: TransactionState::Active.into(), tx_id, begin_ts, write_set: SkipSet::new(), @@ -148,7 +148,7 @@ impl std::fmt::Display for Transaction { write!( f, "{{ state: {}, id: {}, begin_ts: {}, write_set: {:?}, read_set: {:?}", - self.state, + self.state.load(), self.tx_id, self.begin_ts, // FIXME: I'm sorry, we obviously shouldn't be cloning here. @@ -169,9 +169,66 @@ impl std::fmt::Display for Transaction { enum TransactionState { Active, Preparing, - Committed(u64), Aborted, Terminated, + Committed(u64), +} + +impl TransactionState { + pub fn encode(&self) -> u64 { + match self { + TransactionState::Active => 0, + TransactionState::Preparing => 1, + TransactionState::Aborted => 2, + TransactionState::Terminated => 3, + TransactionState::Committed(ts) => { + // We only support 2*62 - 1 timestamps, because the extra bit + // is used to encode the type. + assert!(ts & 0x8000_0000_0000_0000 == 0); + 0x8000_0000_0000_0000 | ts + } + } + } + + pub fn decode(v: u64) -> Self { + match v { + 0 => TransactionState::Active, + 1 => TransactionState::Preparing, + 2 => TransactionState::Aborted, + 3 => TransactionState::Terminated, + v if v & 0x8000_0000_0000_0000 != 0 => { + TransactionState::Committed(v & 0x7fff_ffff_ffff_ffff) + } + _ => panic!("Invalid transaction state"), + } + } +} + +// Transaction state encoded into a single 64-bit atomic. +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct AtomicTransactionState { + pub(crate) state: AtomicU64, +} + +impl From for AtomicTransactionState { + fn from(state: TransactionState) -> Self { + Self { + state: AtomicU64::new(state.encode()), + } + } +} + +impl From for TransactionState { + fn from(state: AtomicTransactionState) -> Self { + let encoded = state.state.load(Ordering::Acquire); + TransactionState::decode(encoded) + } +} + +impl std::cmp::PartialEq for AtomicTransactionState { + fn eq(&self, other: &TransactionState) -> bool { + &self.load() == other + } } impl std::fmt::Display for TransactionState { @@ -186,6 +243,16 @@ impl std::fmt::Display for TransactionState { } } +impl AtomicTransactionState { + fn store(&self, state: TransactionState) { + self.state.store(state.encode(), Ordering::Release); + } + + fn load(&self) -> TransactionState { + TransactionState::decode(self.state.load(Ordering::Acquire)) + } +} + #[derive(Debug)] pub struct Database { rows: SkipMap>>, @@ -390,14 +457,14 @@ impl Database { pub fn commit_tx(&self, tx_id: TxID) -> Result<()> { let end_ts = self.get_timestamp(); let tx = self.txs.get(&tx_id).unwrap(); - let mut tx = tx.value().write().unwrap(); - match tx.state { + let tx = tx.value().write().unwrap(); + match tx.state.load() { TransactionState::Terminated => return Err(DatabaseError::TxTerminated), _ => { assert!(tx.state == TransactionState::Active); } } - tx.state = TransactionState::Preparing; + tx.state.store(TransactionState::Preparing); tracing::trace!("PREPARE {tx}"); /* TODO: The code we have here is sufficient for snapshot isolation. @@ -473,7 +540,7 @@ impl Database { only if TE commits. """ */ - tx.state = TransactionState::Committed(end_ts); + tx.state.store(TransactionState::Committed(end_ts)); tracing::trace!("COMMIT {tx}"); // Postprocessing: inserting row versions and logging the transaction to persistent storage. // TODO: we should probably save to persistent storage first, and only then update the in-memory structures. @@ -523,9 +590,9 @@ impl Database { /// * `tx_id` - The ID of the transaction to abort. pub fn rollback_tx(&self, tx_id: TxID) { let tx = self.txs.get(&tx_id).unwrap(); - let mut tx = tx.value().write().unwrap(); + let tx = tx.value().write().unwrap(); assert!(tx.state == TransactionState::Active); - tx.state = TransactionState::Aborted; + tx.state.store(TransactionState::Aborted); tracing::trace!("ABORT {tx}"); for id in &tx.write_set { let id = id.value(); @@ -537,7 +604,7 @@ impl Database { } } } - tx.state = TransactionState::Terminated; + tx.state.store(TransactionState::Terminated); tracing::trace!("TERMINATE {tx}"); } @@ -620,7 +687,7 @@ pub(crate) fn is_write_write_conflict( Some(TxTimestampOrID::TxID(rv_end)) => { let te = txs.get(&rv_end).unwrap(); let te = te.value().read().unwrap(); - match te.state { + match te.state.load() { TransactionState::Active => tx.tx_id != te.tx_id, TransactionState::Preparing => todo!(), TransactionState::Committed(_end_ts) => todo!(), @@ -651,7 +718,7 @@ fn is_begin_visible( TxTimestampOrID::TxID(rv_begin) => { let tb = txs.get(&rv_begin).unwrap(); let tb = tb.value().read().unwrap(); - let visible = match tb.state { + let visible = match tb.state.load() { TransactionState::Active => tx.tx_id == tb.tx_id && rv.end.is_none(), TransactionState::Preparing => false, // NOTICE: makes sense for snapshot isolation, not so much for serializable! TransactionState::Committed(committed_ts) => tx.begin_ts >= committed_ts, @@ -681,7 +748,7 @@ fn is_end_visible( Some(TxTimestampOrID::TxID(rv_end)) => { let te = txs.get(&rv_end).unwrap(); let te = te.value().read().unwrap(); - let visible = match te.state { + let visible = match te.state.load() { TransactionState::Active => tx.tx_id != te.tx_id, TransactionState::Preparing => false, // NOTICE: makes sense for snapshot isolation, not so much for serializable! TransactionState::Committed(committed_ts) => tx.begin_ts < committed_ts, diff --git a/core/mvcc/mvcc-rs/src/database/tests.rs b/core/mvcc/mvcc-rs/src/database/tests.rs index ada842218..e9023c76a 100644 --- a/core/mvcc/mvcc-rs/src/database/tests.rs +++ b/core/mvcc/mvcc-rs/src/database/tests.rs @@ -822,6 +822,7 @@ or not found | | the timestamp. */ fn new_tx(tx_id: TxID, begin_ts: u64, state: TransactionState) -> RwLock { + let state = state.into(); RwLock::new(Transaction { state, tx_id, From 57249f2c94361fefda30336f7d5d98e2d64ccff2 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Mon, 12 Jun 2023 13:33:29 +0200 Subject: [PATCH 103/128] concurrency test: port to OS threads Without mutexes, it makes no sense anymore to use shuttle. Instead, the test cases just spawn OS threads. Also, a case with overlapping ids is added, to test whether transactions read their own writes within the same transaction. --- core/mvcc/mvcc-rs/Cargo.toml | 3 +- core/mvcc/mvcc-rs/src/database/mod.rs | 4 +- core/mvcc/mvcc-rs/tests/concurrency_test.rs | 164 ++++++++++++++------ 3 files changed, 117 insertions(+), 54 deletions(-) diff --git a/core/mvcc/mvcc-rs/Cargo.toml b/core/mvcc/mvcc-rs/Cargo.toml index 21c83167d..27f030a73 100644 --- a/core/mvcc/mvcc-rs/Cargo.toml +++ b/core/mvcc/mvcc-rs/Cargo.toml @@ -16,13 +16,12 @@ aws-config = "0.55.2" parking_lot = "0.12.1" futures = "0.3.28" crossbeam-skiplist = "0.1.1" +tracing-test = "0" [dev-dependencies] criterion = { version = "0.4", features = ["html_reports", "async", "async_futures"] } pprof = { version = "0.11.1", features = ["criterion", "flamegraph"] } -shuttle = "0.6.0" tracing-subscriber = "0" -tracing-test = "0" mvcc-rs = { path = "." } [[bench]] diff --git a/core/mvcc/mvcc-rs/src/database/mod.rs b/core/mvcc/mvcc-rs/src/database/mod.rs index 01e0c2dce..29bfbdf73 100644 --- a/core/mvcc/mvcc-rs/src/database/mod.rs +++ b/core/mvcc/mvcc-rs/src/database/mod.rs @@ -440,7 +440,7 @@ impl Database { let tx_id = self.get_tx_id(); let begin_ts = self.get_timestamp(); let tx = Transaction::new(tx_id, begin_ts); - tracing::trace!("BEGIN {tx}"); + tracing::trace!("BEGIN {tx}"); self.txs.insert(tx_id, RwLock::new(tx)); tx_id } @@ -565,6 +565,7 @@ impl Database { } } } + tracing::trace!("UPDATED {tx}"); // We have now updated all the versions with a reference to the // transaction ID to a timestamp and can, therefore, remove the // transaction. Please note that when we move to lockless, the @@ -577,6 +578,7 @@ impl Database { if !log_record.row_versions.is_empty() { self.storage.log_tx(log_record)?; } + tracing::trace!("LOGGED {tx}"); Ok(()) } diff --git a/core/mvcc/mvcc-rs/tests/concurrency_test.rs b/core/mvcc/mvcc-rs/tests/concurrency_test.rs index 12321aa10..e284dd6da 100644 --- a/core/mvcc/mvcc-rs/tests/concurrency_test.rs +++ b/core/mvcc/mvcc-rs/tests/concurrency_test.rs @@ -1,65 +1,127 @@ use mvcc_rs::clock::LocalClock; use mvcc_rs::database::{Database, Row, RowID}; -use shuttle::sync::atomic::AtomicU64; -use shuttle::sync::Arc; -use shuttle::thread; +use std::sync::atomic::AtomicU64; use std::sync::atomic::Ordering; +use std::sync::Arc; +static IDS: AtomicU64 = AtomicU64::new(1); + +#[tracing_test::traced_test] #[test] fn test_non_overlapping_concurrent_inserts() { + tracing_subscriber::fmt::init(); // Two threads insert to the database concurrently using non-overlapping // row IDs. let clock = LocalClock::default(); let storage = mvcc_rs::persistent_storage::Storage::new_noop(); let db = Arc::new(Database::new(clock, storage)); - let ids = Arc::new(AtomicU64::new(0)); - shuttle::check_random( - move || { - { - let db = db.clone(); - let ids = ids.clone(); - thread::spawn(move || { - let tx = db.begin_tx(); - let id = ids.fetch_add(1, Ordering::SeqCst); - let id = RowID { - table_id: 1, - row_id: id, - }; - let row = Row { - id, - data: "Hello".to_string(), - }; - db.insert(tx, row.clone()).unwrap(); - db.commit_tx(tx).unwrap(); - let tx = db.begin_tx(); - let committed_row = db.read(tx, id).unwrap(); - db.commit_tx(tx).unwrap(); - assert_eq!(committed_row, Some(row)); - }); + let iterations = 100000; + + let th1 = { + let db = db.clone(); + std::thread::spawn(move || { + for _ in 0..iterations { + let tx = db.begin_tx(); + let id = IDS.fetch_add(1, Ordering::SeqCst); + let id = RowID { + table_id: 1, + row_id: id, + }; + let row = Row { + id, + data: "Hello".to_string(), + }; + db.insert(tx, row.clone()).unwrap(); + db.commit_tx(tx).unwrap(); + let tx = db.begin_tx(); + let committed_row = db.read(tx, id).unwrap(); + db.commit_tx(tx).unwrap(); + assert_eq!(committed_row, Some(row)); } - { - let db = db.clone(); - let ids = ids.clone(); - thread::spawn(move || { - let tx = db.begin_tx(); - let id = ids.fetch_add(1, Ordering::SeqCst); - let id = RowID { - table_id: 1, - row_id: id, - }; - let row = Row { - id, - data: "World".to_string(), - }; - db.insert(tx, row.clone()).unwrap(); - db.commit_tx(tx).unwrap(); - let tx = db.begin_tx(); - let committed_row = db.read(tx, id).unwrap(); - db.commit_tx(tx).unwrap(); - assert_eq!(committed_row, Some(row)); - }); + }) + }; + let th2 = { + std::thread::spawn(move || { + for _ in 0..iterations { + let tx = db.begin_tx(); + let id = IDS.fetch_add(1, Ordering::SeqCst); + let id = RowID { + table_id: 1, + row_id: id, + }; + let row = Row { + id, + data: "World".to_string(), + }; + db.insert(tx, row.clone()).unwrap(); + db.commit_tx(tx).unwrap(); + let tx = db.begin_tx(); + let committed_row = db.read(tx, id).unwrap(); + db.commit_tx(tx).unwrap(); + assert_eq!(committed_row, Some(row)); } - }, - 100, - ); + }) + }; + th1.join().unwrap(); + th2.join().unwrap(); +} + +#[test] +fn test_overlapping_concurrent_inserts_read_your_writes() { + tracing_subscriber::fmt::init(); + // Two threads insert to the database concurrently using overlapping row IDs. + let clock = LocalClock::default(); + let storage = mvcc_rs::persistent_storage::Storage::new_noop(); + let db = Arc::new(Database::new(clock, storage)); + let iterations = 100000; + + let th1 = { + let db = db.clone(); + std::thread::spawn(move || { + for i in 0..iterations { + if i % 1000 == 0 { + tracing::debug!("{i}"); + } + let tx = db.begin_tx(); + let id = i % 16; + let id = RowID { + table_id: 1, + row_id: id, + }; + let row = Row { + id, + data: format!("Hello @{tx}"), + }; + db.insert(tx, row.clone()).unwrap(); + let committed_row = db.read(tx, id).unwrap(); + db.commit_tx(tx).unwrap(); + assert_eq!(committed_row, Some(row)); + } + }) + }; + let th2 = { + std::thread::spawn(move || { + for i in 0..iterations { + if i % 1000 == 0 { + tracing::debug!("{i}"); + } + let tx = db.begin_tx(); + let id = i % 16; + let id = RowID { + table_id: 1, + row_id: id, + }; + let row = Row { + id, + data: format!("World @{tx}"), + }; + db.insert(tx, row.clone()).unwrap(); + let committed_row = db.read(tx, id).unwrap(); + db.commit_tx(tx).unwrap(); + assert_eq!(committed_row, Some(row)); + } + }) + }; + th1.join().unwrap(); + th2.join().unwrap(); } From 95ed29e6cbe2729c8af520dac3ef6fac176768cc Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Mon, 12 Jun 2023 15:48:31 +0200 Subject: [PATCH 104/128] database: fix the locking order in transactions Before this commit, deadlocks were possible (and detected), because some functions took row_versions lock first, and then individual transaction locks, while other functions took the locks in opposite order. --- core/mvcc/.github/workflows/smoke_test.yml | 1 + core/mvcc/mvcc-rs/src/database/mod.rs | 31 +++++++++++++-------- core/mvcc/mvcc-rs/tests/concurrency_test.rs | 14 ++++++---- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/core/mvcc/.github/workflows/smoke_test.yml b/core/mvcc/.github/workflows/smoke_test.yml index c776a1635..3a00d72b4 100644 --- a/core/mvcc/.github/workflows/smoke_test.yml +++ b/core/mvcc/.github/workflows/smoke_test.yml @@ -8,6 +8,7 @@ on: env: CARGO_TERM_COLOR: always + RUST_LOG: info,mvcc_rs=trace jobs: build: diff --git a/core/mvcc/mvcc-rs/src/database/mod.rs b/core/mvcc/mvcc-rs/src/database/mod.rs index 29bfbdf73..890c98f62 100644 --- a/core/mvcc/mvcc-rs/src/database/mod.rs +++ b/core/mvcc/mvcc-rs/src/database/mod.rs @@ -297,10 +297,11 @@ impl Database { end: None, row, }; + tx.insert_to_write_set(id); + drop(tx); let versions = self.rows.get_or_insert_with(id, || RwLock::new(Vec::new())); let mut versions = versions.value().write().unwrap(); versions.push(row_version); - tx.insert_to_write_set(id); Ok(()) } @@ -364,7 +365,9 @@ impl Database { } if is_version_visible(&self.txs, &tx, rv) { rv.end = Some(TxTimestampOrID::TxID(tx.tx_id)); - drop(tx); // FIXME: maybe just grab the write lock above? Do we ever expect conflicts? + drop(row_versions); + drop(row_versions_opt); + drop(tx); let tx = self .txs .get(&tx_id) @@ -456,6 +459,8 @@ impl Database { /// * `tx_id` - The ID of the transaction to commit. pub fn commit_tx(&self, tx_id: TxID) -> Result<()> { let end_ts = self.get_timestamp(); + // NOTICE: the first shadowed tx keeps the entry alive in the map + // for the duration of this whole function, which is important for correctness! let tx = self.txs.get(&tx_id).unwrap(); let tx = tx.value().write().unwrap(); match tx.state.load() { @@ -542,17 +547,19 @@ impl Database { */ tx.state.store(TransactionState::Committed(end_ts)); tracing::trace!("COMMIT {tx}"); + let tx_begin_ts = tx.begin_ts; + let write_set: Vec = tx.write_set.iter().map(|v| *v.value()).collect(); + drop(tx); // Postprocessing: inserting row versions and logging the transaction to persistent storage. // TODO: we should probably save to persistent storage first, and only then update the in-memory structures. let mut log_record: LogRecord = LogRecord::new(end_ts); - for id in &tx.write_set { - let id = id.value(); + for ref id in write_set { if let Some(row_versions) = self.rows.get(id) { let mut row_versions = row_versions.value().write().unwrap(); for row_version in row_versions.iter_mut() { if let TxTimestampOrID::TxID(id) = row_version.begin { if id == tx_id { - row_version.begin = TxTimestampOrID::Timestamp(tx.begin_ts); + row_version.begin = TxTimestampOrID::Timestamp(tx_begin_ts); log_record.row_versions.push(row_version.clone()); // FIXME: optimize cloning out } } @@ -565,7 +572,7 @@ impl Database { } } } - tracing::trace!("UPDATED {tx}"); + tracing::trace!("UPDATED TX{tx_id}"); // We have now updated all the versions with a reference to the // transaction ID to a timestamp and can, therefore, remove the // transaction. Please note that when we move to lockless, the @@ -578,7 +585,7 @@ impl Database { if !log_record.row_versions.is_empty() { self.storage.log_tx(log_record)?; } - tracing::trace!("LOGGED {tx}"); + tracing::trace!("LOGGED {tx_id}"); Ok(()) } @@ -591,13 +598,14 @@ impl Database { /// /// * `tx_id` - The ID of the transaction to abort. pub fn rollback_tx(&self, tx_id: TxID) { - let tx = self.txs.get(&tx_id).unwrap(); - let tx = tx.value().write().unwrap(); + let tx_unlocked = self.txs.get(&tx_id).unwrap(); + let tx = tx_unlocked.value().write().unwrap(); assert!(tx.state == TransactionState::Active); tx.state.store(TransactionState::Aborted); tracing::trace!("ABORT {tx}"); - for id in &tx.write_set { - let id = id.value(); + let write_set: Vec = tx.write_set.iter().map(|v| *v.value()).collect(); + drop(tx); + for ref id in write_set { if let Some(row_versions) = self.rows.get(id) { let mut row_versions = row_versions.value().write().unwrap(); row_versions.retain(|rv| rv.begin != TxTimestampOrID::TxID(tx_id)); @@ -606,6 +614,7 @@ impl Database { } } } + let tx = tx_unlocked.value().write().unwrap(); tx.state.store(TransactionState::Terminated); tracing::trace!("TERMINATE {tx}"); } diff --git a/core/mvcc/mvcc-rs/tests/concurrency_test.rs b/core/mvcc/mvcc-rs/tests/concurrency_test.rs index e284dd6da..3c8085ea0 100644 --- a/core/mvcc/mvcc-rs/tests/concurrency_test.rs +++ b/core/mvcc/mvcc-rs/tests/concurrency_test.rs @@ -2,14 +2,17 @@ use mvcc_rs::clock::LocalClock; use mvcc_rs::database::{Database, Row, RowID}; use std::sync::atomic::AtomicU64; use std::sync::atomic::Ordering; -use std::sync::Arc; +use std::sync::{Arc, Once}; static IDS: AtomicU64 = AtomicU64::new(1); -#[tracing_test::traced_test] +static START: Once = Once::new(); + #[test] fn test_non_overlapping_concurrent_inserts() { - tracing_subscriber::fmt::init(); + START.call_once(|| { + tracing_subscriber::fmt::init(); + }); // Two threads insert to the database concurrently using non-overlapping // row IDs. let clock = LocalClock::default(); @@ -68,8 +71,9 @@ fn test_non_overlapping_concurrent_inserts() { #[test] fn test_overlapping_concurrent_inserts_read_your_writes() { - tracing_subscriber::fmt::init(); - // Two threads insert to the database concurrently using overlapping row IDs. + START.call_once(|| { + tracing_subscriber::fmt::init(); + }); // Two threads insert to the database concurrently using overlapping row IDs. let clock = LocalClock::default(); let storage = mvcc_rs::persistent_storage::Storage::new_noop(); let db = Arc::new(Database::new(clock, storage)); From 7a6ca27986ea418603bd49ce57e649aad81cffb5 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Tue, 13 Jun 2023 11:24:19 +0200 Subject: [PATCH 105/128] database: make sure row versions are inserted in a sorted order For the time being, we still assume that the row versions vector is *nearly* sorted, so we just perform a linear reverse search and insert the version at an appropriate place. During concurrency tests, the error was at most 1 offset, and as long as we empirically prove it to be below a reasonable constant, we're fine. Otherwise we should consider switching to either a data structure that keeps elements ordered, or at least a list that gives us constant insertion. --- core/mvcc/mvcc-rs/src/database/mod.rs | 48 +++++++++++++++++++-------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/core/mvcc/mvcc-rs/src/database/mod.rs b/core/mvcc/mvcc-rs/src/database/mod.rs index 890c98f62..131ff4345 100644 --- a/core/mvcc/mvcc-rs/src/database/mod.rs +++ b/core/mvcc/mvcc-rs/src/database/mod.rs @@ -17,7 +17,7 @@ pub struct RowID { pub row_id: u64, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct Row { pub id: RowID, @@ -25,7 +25,7 @@ pub struct Row { } /// A row version. -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, PartialOrd)] pub struct RowVersion { begin: TxTimestampOrID, end: Option, @@ -56,7 +56,7 @@ impl LogRecord { /// phase of the transaction. During the active phase, new versions track the /// transaction ID in the `begin` and `end` fields. After a transaction commits, /// versions switch to tracking timestamps. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, PartialOrd, Serialize, Deserialize)] enum TxTimestampOrID { Timestamp(u64), TxID(TxID), @@ -274,6 +274,28 @@ impl Database { } } + /// Inserts a new row version into the database, while making sure that + /// the row version is inserted in the correct order. + fn insert_version(&self, id: RowID, row_version: RowVersion) { + let versions = self.rows.get_or_insert_with(id, || RwLock::new(Vec::new())); + let mut versions = versions.value().write().unwrap(); + self.insert_version_raw(&mut versions, row_version) + } + + /// Inserts a new row version into the internal data structure for versions, + /// while making sure that the row version is inserted in the correct order. + fn insert_version_raw(&self, versions: &mut Vec, row_version: RowVersion) { + // NOTICE: this is an insert a'la insertion sort, with pessimistic linear complexity. + // However, we expect the number of versions to be nearly sorted, so we deem it worthy + // to search linearly for the insertion point instead of paying the price of using + // another data structure, e.g. a BTreeSet. + let position = versions.iter().rposition(|v| v >= &row_version); + match position { + Some(position) => versions.insert(position, row_version), + None => versions.push(row_version), + }; + } + /// Inserts a new row into the database. /// /// This function inserts a new `row` into the database within the context @@ -299,9 +321,7 @@ impl Database { }; tx.insert_to_write_set(id); drop(tx); - let versions = self.rows.get_or_insert_with(id, || RwLock::new(Vec::new())); - let mut versions = versions.value().write().unwrap(); - versions.push(row_version); + self.insert_version(id, row_version); Ok(()) } @@ -560,13 +580,19 @@ impl Database { if let TxTimestampOrID::TxID(id) = row_version.begin { if id == tx_id { row_version.begin = TxTimestampOrID::Timestamp(tx_begin_ts); - log_record.row_versions.push(row_version.clone()); // FIXME: optimize cloning out + self.insert_version_raw( + &mut log_record.row_versions, + row_version.clone(), + ); // FIXME: optimize cloning out } } if let Some(TxTimestampOrID::TxID(id)) = row_version.end { if id == tx_id { row_version.end = Some(TxTimestampOrID::Timestamp(end_ts)); - log_record.row_versions.push(row_version.clone()); // FIXME: optimize cloning out + self.insert_version_raw( + &mut log_record.row_versions, + row_version.clone(), + ); // FIXME: optimize cloning out } } } @@ -675,11 +701,7 @@ impl Database { for record in tx_log { tracing::debug!("RECOVERING {:?}", record); for version in record.row_versions { - let row_versions = self - .rows - .get_or_insert_with(version.row.id, || RwLock::new(Vec::new())); - let mut row_versions = row_versions.value().write().unwrap(); - row_versions.push(version); + self.insert_version(version.row.id, version); } self.clock.reset(record.tx_timestamp); } From 1a50e12102144b2910a1eb1d808b3fabc1a2f5e5 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Tue, 13 Jun 2023 11:33:23 +0200 Subject: [PATCH 106/128] tests: make concurrency test run 4 threads --- core/mvcc/mvcc-rs/tests/concurrency_test.rs | 36 +++++---------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/core/mvcc/mvcc-rs/tests/concurrency_test.rs b/core/mvcc/mvcc-rs/tests/concurrency_test.rs index 3c8085ea0..fced575d7 100644 --- a/core/mvcc/mvcc-rs/tests/concurrency_test.rs +++ b/core/mvcc/mvcc-rs/tests/concurrency_test.rs @@ -79,12 +79,12 @@ fn test_overlapping_concurrent_inserts_read_your_writes() { let db = Arc::new(Database::new(clock, storage)); let iterations = 100000; - let th1 = { + let work = |prefix: &'static str| { let db = db.clone(); std::thread::spawn(move || { for i in 0..iterations { if i % 1000 == 0 { - tracing::debug!("{i}"); + tracing::debug!("{prefix}: {i}"); } let tx = db.begin_tx(); let id = i % 16; @@ -94,7 +94,7 @@ fn test_overlapping_concurrent_inserts_read_your_writes() { }; let row = Row { id, - data: format!("Hello @{tx}"), + data: format!("{prefix} @{tx}"), }; db.insert(tx, row.clone()).unwrap(); let committed_row = db.read(tx, id).unwrap(); @@ -103,29 +103,9 @@ fn test_overlapping_concurrent_inserts_read_your_writes() { } }) }; - let th2 = { - std::thread::spawn(move || { - for i in 0..iterations { - if i % 1000 == 0 { - tracing::debug!("{i}"); - } - let tx = db.begin_tx(); - let id = i % 16; - let id = RowID { - table_id: 1, - row_id: id, - }; - let row = Row { - id, - data: format!("World @{tx}"), - }; - db.insert(tx, row.clone()).unwrap(); - let committed_row = db.read(tx, id).unwrap(); - db.commit_tx(tx).unwrap(); - assert_eq!(committed_row, Some(row)); - } - }) - }; - th1.join().unwrap(); - th2.join().unwrap(); + + let threads = vec![work("A"), work("B"), work("C"), work("D")]; + for th in threads { + th.join().unwrap(); + } } From 36d989babb9310f2e24ab48742c696eed4773be7 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Tue, 13 Jun 2023 12:51:46 +0200 Subject: [PATCH 107/128] database: properly compare row versions Previous commit was incorrect in two manners: 1. It *only* worked if the version was either pushed as the most recent or 1 behind the most recent - that's fixed. 2. Comparing row versions incorrectly compared either timestamps or transaction ids, while we *need* to only compare timestamps. That's done by looking up the transaction and extracting its timestamp - potentially expensive, and maybe we need to rework the algorithm and/or consult the Hekaton paper. --- core/mvcc/mvcc-rs/src/database/mod.rs | 35 +++++++++++++++++++++------ 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/core/mvcc/mvcc-rs/src/database/mod.rs b/core/mvcc/mvcc-rs/src/database/mod.rs index 131ff4345..4941b5305 100644 --- a/core/mvcc/mvcc-rs/src/database/mod.rs +++ b/core/mvcc/mvcc-rs/src/database/mod.rs @@ -25,7 +25,7 @@ pub struct Row { } /// A row version. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, PartialOrd)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct RowVersion { begin: TxTimestampOrID, end: Option, @@ -274,6 +274,22 @@ impl Database { } } + // Extracts the begin timestamp from a transaction + fn get_begin_timestamp(&self, ts_or_id: &TxTimestampOrID) -> u64 { + match ts_or_id { + TxTimestampOrID::Timestamp(ts) => *ts, + TxTimestampOrID::TxID(tx_id) => { + self.txs + .get(tx_id) + .unwrap() + .value() + .read() + .unwrap() + .begin_ts + } + } + } + /// Inserts a new row version into the database, while making sure that /// the row version is inserted in the correct order. fn insert_version(&self, id: RowID, row_version: RowVersion) { @@ -288,12 +304,17 @@ impl Database { // NOTICE: this is an insert a'la insertion sort, with pessimistic linear complexity. // However, we expect the number of versions to be nearly sorted, so we deem it worthy // to search linearly for the insertion point instead of paying the price of using - // another data structure, e.g. a BTreeSet. - let position = versions.iter().rposition(|v| v >= &row_version); - match position { - Some(position) => versions.insert(position, row_version), - None => versions.push(row_version), - }; + // another data structure, e.g. a BTreeSet. If it proves to be too quadratic empirically, + // we can either switch to a tree-like structure, or at least use partition_point() + // which performs a binary search for the insertion point. + let position = versions + .iter() + .rposition(|v| { + self.get_begin_timestamp(&v.begin) < self.get_begin_timestamp(&row_version.begin) + }) + .map(|p| p + 1) + .unwrap_or(0); + versions.insert(position, row_version); } /// Inserts a new row into the database. From 0338e14814875235f63e40901d9be6fa69b9cdc6 Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Tue, 13 Jun 2023 13:47:38 +0200 Subject: [PATCH 108/128] database: change insert to upsert in concurrency tests Using insert() was a violation of our API, kind of, because inserts are not expected to be called twice on the same id. Instead, update or upsert should delete the version first, and that's what's done in this patch. At the same time, write-write conflict detection needed to be implemented, because we started hitting it with rollback(). Finally, garbage collection is modified to actually work and garbage-collect row versions. Without it, the number of tracked row versions very quickly goes out of hand. --- core/mvcc/mvcc-rs/src/database/mod.rs | 30 ++++++++++++++++++--- core/mvcc/mvcc-rs/tests/concurrency_test.rs | 6 ++++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/core/mvcc/mvcc-rs/src/database/mod.rs b/core/mvcc/mvcc-rs/src/database/mod.rs index 4941b5305..064864a7c 100644 --- a/core/mvcc/mvcc-rs/src/database/mod.rs +++ b/core/mvcc/mvcc-rs/src/database/mod.rs @@ -314,6 +314,12 @@ impl Database { }) .map(|p| p + 1) .unwrap_or(0); + if versions.len() - position > 3 { + tracing::debug!( + "Inserting an element {} positions from the end", + versions.len() - position + ); + } versions.insert(position, row_version); } @@ -372,6 +378,13 @@ impl Database { Ok(true) } + /// Inserts a row in the database with new values, previously deleting + /// any old data if it existed. + pub fn upsert(&self, tx_id: TxID, row: Row) -> Result<()> { + self.delete(tx_id, row.id).ok(); + self.insert(tx_id, row) + } + /// Deletes a row from the table with the given `id`. /// /// This function deletes an existing row `id` in the database within the @@ -676,12 +689,21 @@ impl Database { self.clock.get_timestamp() } - /// FIXME: implement in a lock-free manner - pub fn drop_unused_row_versions(&self) { + /// Removes unused row versions with very loose heuristics, + /// which sometimes leaves versions intact for too long. + /// Returns the number of removed versions. + pub fn drop_unused_row_versions(&self) -> usize { + tracing::debug!( + "transactions: {}; rows: {}", + self.txs.len(), + self.rows.len() + ); let mut to_remove = Vec::new(); for entry in self.rows.iter() { let mut row_versions = entry.value().write().unwrap(); + tracing::debug!("versions: {}", row_versions.len()); row_versions.retain(|rv| { + tracing::debug!("inspecting {rv:?}"); let should_stay = match rv.end { Some(TxTimestampOrID::Timestamp(version_end_ts)) => { // a transaction started before this row version ended, @@ -699,7 +721,7 @@ impl Database { None => true, }; if !should_stay { - tracing::debug!( + tracing::trace!( "Dropping row version {:?} {:?}-{:?}", entry.key(), rv.begin, @@ -712,9 +734,11 @@ impl Database { to_remove.push(*entry.key()); } } + let dropped = to_remove.len(); for id in to_remove { self.rows.remove(&id); } + dropped } pub fn recover(&self) -> Result<()> { diff --git a/core/mvcc/mvcc-rs/tests/concurrency_test.rs b/core/mvcc/mvcc-rs/tests/concurrency_test.rs index fced575d7..5cf7ccbd1 100644 --- a/core/mvcc/mvcc-rs/tests/concurrency_test.rs +++ b/core/mvcc/mvcc-rs/tests/concurrency_test.rs @@ -86,6 +86,10 @@ fn test_overlapping_concurrent_inserts_read_your_writes() { if i % 1000 == 0 { tracing::debug!("{prefix}: {i}"); } + if i % 10000 == 0 { + let dropped = db.drop_unused_row_versions(); + tracing::debug!("garbage collected {dropped} versions"); + } let tx = db.begin_tx(); let id = i % 16; let id = RowID { @@ -96,7 +100,7 @@ fn test_overlapping_concurrent_inserts_read_your_writes() { id, data: format!("{prefix} @{tx}"), }; - db.insert(tx, row.clone()).unwrap(); + db.upsert(tx, row.clone()).unwrap(); let committed_row = db.read(tx, id).unwrap(); db.commit_tx(tx).unwrap(); assert_eq!(committed_row, Some(row)); From 6daaa79f6391e6ef16e37030e4b0080a2715f8fc Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Tue, 13 Jun 2023 19:32:38 +0200 Subject: [PATCH 109/128] database: actually implement upserts Fixes #55 - it was the code that should have been there in the first place, but I forgot to `git add`... --- core/mvcc/mvcc-rs/src/database/mod.rs | 54 ++++++++++++--------- core/mvcc/mvcc-rs/tests/concurrency_test.rs | 11 ++++- 2 files changed, 40 insertions(+), 25 deletions(-) diff --git a/core/mvcc/mvcc-rs/src/database/mod.rs b/core/mvcc/mvcc-rs/src/database/mod.rs index 064864a7c..4bdde3b12 100644 --- a/core/mvcc/mvcc-rs/src/database/mod.rs +++ b/core/mvcc/mvcc-rs/src/database/mod.rs @@ -316,7 +316,7 @@ impl Database { .unwrap_or(0); if versions.len() - position > 3 { tracing::debug!( - "Inserting an element {} positions from the end", + "Inserting a row version {} positions from the end", versions.len() - position ); } @@ -339,7 +339,7 @@ impl Database { .get(&tx_id) .ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; let mut tx = tx.value().write().unwrap(); - assert!(tx.state == TransactionState::Active); + assert_eq!(tx.state, TransactionState::Active); let id = row.id; let row_version = RowVersion { begin: TxTimestampOrID::TxID(tx.tx_id), @@ -379,9 +379,9 @@ impl Database { } /// Inserts a row in the database with new values, previously deleting - /// any old data if it existed. + /// any old data if it existed. Bails on a delete error, e.g. write-write conflict. pub fn upsert(&self, tx_id: TxID, row: Row) -> Result<()> { - self.delete(tx_id, row.id).ok(); + self.delete(tx_id, row.id)?; self.insert(tx_id, row) } @@ -409,7 +409,7 @@ impl Database { .get(&tx_id) .ok_or(DatabaseError::NoSuchTransactionID(tx_id))?; let tx = tx.value().read().unwrap(); - assert!(tx.state == TransactionState::Active); + assert_eq!(tx.state, TransactionState::Active); if is_write_write_conflict(&self.txs, &tx, rv) { drop(row_versions); drop(row_versions_opt); @@ -452,7 +452,7 @@ impl Database { pub fn read(&self, tx_id: TxID, id: RowID) -> Result> { let tx = self.txs.get(&tx_id).unwrap(); let tx = tx.value().read().unwrap(); - assert!(tx.state == TransactionState::Active); + assert_eq!(tx.state, TransactionState::Active); if let Some(row_versions) = self.rows.get(&id) { let row_versions = row_versions.value().read().unwrap(); for rv in row_versions.iter().rev() { @@ -520,7 +520,7 @@ impl Database { match tx.state.load() { TransactionState::Terminated => return Err(DatabaseError::TxTerminated), _ => { - assert!(tx.state == TransactionState::Active); + assert_eq!(tx.state, TransactionState::Active); } } tx.state.store(TransactionState::Preparing); @@ -660,7 +660,7 @@ impl Database { pub fn rollback_tx(&self, tx_id: TxID) { let tx_unlocked = self.txs.get(&tx_id).unwrap(); let tx = tx_unlocked.value().write().unwrap(); - assert!(tx.state == TransactionState::Active); + assert_eq!(tx.state, TransactionState::Active); tx.state.store(TransactionState::Aborted); tracing::trace!("ABORT {tx}"); let write_set: Vec = tx.write_set.iter().map(|v| *v.value()).collect(); @@ -677,6 +677,9 @@ impl Database { let tx = tx_unlocked.value().write().unwrap(); tx.state.store(TransactionState::Terminated); tracing::trace!("TERMINATE {tx}"); + // FIXME: verify that we can already remove the transaction here! + // Maybe it's fine for snapshot isolation, but too early for serializable? + self.txs.remove(&tx_id); } /// Generates next unique transaction id @@ -693,27 +696,33 @@ impl Database { /// which sometimes leaves versions intact for too long. /// Returns the number of removed versions. pub fn drop_unused_row_versions(&self) -> usize { - tracing::debug!( - "transactions: {}; rows: {}", + tracing::trace!( + "Dropping unused row versions. Database stats: transactions: {}; rows: {}", self.txs.len(), self.rows.len() ); + let mut dropped = 0; let mut to_remove = Vec::new(); for entry in self.rows.iter() { let mut row_versions = entry.value().write().unwrap(); - tracing::debug!("versions: {}", row_versions.len()); row_versions.retain(|rv| { - tracing::debug!("inspecting {rv:?}"); + // FIXME: should take rv.begin into account as well let should_stay = match rv.end { Some(TxTimestampOrID::Timestamp(version_end_ts)) => { - // a transaction started before this row version ended, - // ergo row version is needed + // a transaction started before this row version ended, ergo row version is needed // NOTICE: O(row_versions x transactions), but also lock-free, so sounds acceptable - self.txs - .iter() - .any(|tx| version_end_ts >= tx.value().read().unwrap().begin_ts) + self.txs.iter().any(|tx| { + let tx = tx.value().read().unwrap(); + // FIXME: verify! + match tx.state.load() { + TransactionState::Active | TransactionState::Preparing => { + version_end_ts > tx.begin_ts + } + _ => false, + } + }) } - // Let's skip potentially complex logic if the transaction is still + // Let's skip potentially complex logic if the transafction is still // active/tracked. We will drop the row version when the transaction // gets garbage-collected itself, it will always happen eventually. Some(TxTimestampOrID::TxID(tx_id)) => !self.txs.contains_key(&tx_id), @@ -721,6 +730,7 @@ impl Database { None => true, }; if !should_stay { + dropped += 1; tracing::trace!( "Dropping row version {:?} {:?}-{:?}", entry.key(), @@ -734,7 +744,6 @@ impl Database { to_remove.push(*entry.key()); } } - let dropped = to_remove.len(); for id in to_remove { self.rows.remove(&id); } @@ -766,11 +775,8 @@ pub(crate) fn is_write_write_conflict( let te = txs.get(&rv_end).unwrap(); let te = te.value().read().unwrap(); match te.state.load() { - TransactionState::Active => tx.tx_id != te.tx_id, - TransactionState::Preparing => todo!(), - TransactionState::Committed(_end_ts) => todo!(), - TransactionState::Aborted => todo!(), - TransactionState::Terminated => todo!(), + TransactionState::Active | TransactionState::Preparing => tx.tx_id != te.tx_id, + _ => false, } } Some(TxTimestampOrID::Timestamp(_)) => false, diff --git a/core/mvcc/mvcc-rs/tests/concurrency_test.rs b/core/mvcc/mvcc-rs/tests/concurrency_test.rs index 5cf7ccbd1..f7d77893e 100644 --- a/core/mvcc/mvcc-rs/tests/concurrency_test.rs +++ b/core/mvcc/mvcc-rs/tests/concurrency_test.rs @@ -82,6 +82,7 @@ fn test_overlapping_concurrent_inserts_read_your_writes() { let work = |prefix: &'static str| { let db = db.clone(); std::thread::spawn(move || { + let mut failed_upserts = 0; for i in 0..iterations { if i % 1000 == 0 { tracing::debug!("{prefix}: {i}"); @@ -100,11 +101,19 @@ fn test_overlapping_concurrent_inserts_read_your_writes() { id, data: format!("{prefix} @{tx}"), }; - db.upsert(tx, row.clone()).unwrap(); + if let Err(e) = db.upsert(tx, row.clone()) { + tracing::trace!("upsert failed: {e}"); + failed_upserts += 1; + continue; + } let committed_row = db.read(tx, id).unwrap(); db.commit_tx(tx).unwrap(); assert_eq!(committed_row, Some(row)); } + tracing::info!( + "{prefix}'s failed upserts: {failed_upserts}/{iterations} {:.2}%", + (failed_upserts * 100) as f64 / iterations as f64 + ); }) }; From 9bb09d005f63413544513e071218e373c75c38ff Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Sun, 18 Jun 2023 11:17:01 +0300 Subject: [PATCH 110/128] Rename project to Tihku The "mvcc-rs" name is not great for a lot of reasons. Let's rename the project to (Iku-)Tihku to give t a proper name --- core/mvcc/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/mvcc/README.md b/core/mvcc/README.md index e48c3ac45..1dce4fc55 100644 --- a/core/mvcc/README.md +++ b/core/mvcc/README.md @@ -1,7 +1,7 @@ -# MVCC for Rust +# Tihku -This is a _work-in-progress_ the Hekaton optimistic multiversion concurrency control library in Rust. -The aim of the project is to provide a building block for implementing database management systems. +Tihku is an _work-in-progress_, open-source implementation of the Hekaton multi-version concurrency control (MVCC) written in Rust. +The project aims to provide a foundational building block for implementing database management systems. ## Features From 4888a0639caf30c83f57dd1a39a3483455b8c2a3 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Mon, 19 Jun 2023 11:15:20 +0300 Subject: [PATCH 111/128] Mention libsql with MVCC --- core/mvcc/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/mvcc/README.md b/core/mvcc/README.md index 1dce4fc55..7dcc797d9 100644 --- a/core/mvcc/README.md +++ b/core/mvcc/README.md @@ -3,6 +3,8 @@ Tihku is an _work-in-progress_, open-source implementation of the Hekaton multi-version concurrency control (MVCC) written in Rust. The project aims to provide a foundational building block for implementing database management systems. +One of the projects using Tihku is an experimental [libSQL branch with MVCC](https://github.com/penberg/libsql/tree/mvcc) that aims to implement `BEGIN CONCURRENT` with Tihku improve SQLite write concurrency. + ## Features * Main memory architecture, rows are accessed via an index From ce6a6ceba2cfb471a8aea943a42819f103eb8d4f Mon Sep 17 00:00:00 2001 From: Tommi Pisto Date: Fri, 7 Jul 2023 18:14:31 +0300 Subject: [PATCH 112/128] Row --- core/mvcc/bindings/c/src/lib.rs | 6 +- core/mvcc/mvcc-rs/src/cursor.rs | 16 +++-- core/mvcc/mvcc-rs/src/database/mod.rs | 67 +++++++++++-------- core/mvcc/mvcc-rs/src/database/tests.rs | 4 +- .../mvcc-rs/src/persistent_storage/mod.rs | 10 ++- .../mvcc/mvcc-rs/src/persistent_storage/s3.rs | 11 +-- 6 files changed, 70 insertions(+), 44 deletions(-) diff --git a/core/mvcc/bindings/c/src/lib.rs b/core/mvcc/bindings/c/src/lib.rs index 509fa94cf..7d222a8d2 100644 --- a/core/mvcc/bindings/c/src/lib.rs +++ b/core/mvcc/bindings/c/src/lib.rs @@ -13,10 +13,12 @@ use types::{DbContext, MVCCDatabaseRef, MVCCScanCursorRef, ScanCursorContext}; type Clock = clock::LocalClock; /// cbindgen:ignore -type Db = database::Database; +/// Note - We use String type in C bindings as Row type. Type is generic. +type Db = database::Database; /// cbindgen:ignore -type ScanCursor = cursor::ScanCursor<'static, Clock>; +/// Note - We use String type in C bindings as Row type. Type is generic. +type ScanCursor = cursor::ScanCursor<'static, Clock, String>; static INIT_RUST_LOG: std::sync::Once = std::sync::Once::new(); diff --git a/core/mvcc/mvcc-rs/src/cursor.rs b/core/mvcc/mvcc-rs/src/cursor.rs index 7042c090f..d02e49541 100644 --- a/core/mvcc/mvcc-rs/src/cursor.rs +++ b/core/mvcc/mvcc-rs/src/cursor.rs @@ -1,20 +1,24 @@ +use serde::de::DeserializeOwned; +use serde::Serialize; + use crate::clock::LogicalClock; use crate::database::{Database, Result, Row, RowID}; +use std::fmt::Debug; #[derive(Debug)] -pub struct ScanCursor<'a, Clock: LogicalClock> { - pub db: &'a Database, +pub struct ScanCursor<'a, Clock: LogicalClock, T: Sync + Send + Clone + Serialize + DeserializeOwned + Debug> { + pub db: &'a Database, pub row_ids: Vec, pub index: usize, tx_id: u64, } -impl<'a, Clock: LogicalClock> ScanCursor<'a, Clock> { +impl<'a, Clock: LogicalClock, T: Sync + Send + Clone + Serialize + DeserializeOwned + Debug> ScanCursor<'a, Clock, T> { pub fn new( - db: &'a Database, + db: &'a Database, tx_id: u64, table_id: u64, - ) -> Result> { + ) -> Result> { let row_ids = db.scan_row_ids_for_table(table_id)?; Ok(Self { db, @@ -31,7 +35,7 @@ impl<'a, Clock: LogicalClock> ScanCursor<'a, Clock> { Some(self.row_ids[self.index]) } - pub fn current_row(&self) -> Result> { + pub fn current_row(&self) -> Result>> { if self.index >= self.row_ids.len() { return Ok(None); } diff --git a/core/mvcc/mvcc-rs/src/database/mod.rs b/core/mvcc/mvcc-rs/src/database/mod.rs index 4bdde3b12..9bdade7fd 100644 --- a/core/mvcc/mvcc-rs/src/database/mod.rs +++ b/core/mvcc/mvcc-rs/src/database/mod.rs @@ -2,7 +2,9 @@ use crate::clock::LogicalClock; use crate::errors::DatabaseError; use crate::persistent_storage::Storage; use crossbeam_skiplist::{SkipMap, SkipSet}; +use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; +use std::fmt::Debug; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::RwLock; @@ -19,29 +21,29 @@ pub struct RowID { #[derive(Clone, Debug, PartialEq, PartialOrd, Serialize, Deserialize)] -pub struct Row { +pub struct Row { pub id: RowID, - pub data: String, + pub data: T, } /// A row version. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct RowVersion { +pub struct RowVersion { begin: TxTimestampOrID, end: Option, - row: Row, + row: Row, } pub type TxID = u64; /// A log record contains all the versions inserted and deleted by a transaction. #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct LogRecord { +pub struct LogRecord { pub(crate) tx_timestamp: TxID, - row_versions: Vec, + row_versions: Vec>, } -impl LogRecord { +impl LogRecord { fn new(tx_timestamp: TxID) -> Self { Self { tx_timestamp, @@ -254,15 +256,22 @@ impl AtomicTransactionState { } #[derive(Debug)] -pub struct Database { - rows: SkipMap>>, +pub struct Database< + Clock: LogicalClock, + T: Sync + Send + Clone + Serialize + Debug + DeserializeOwned, +> { + rows: SkipMap>>>, txs: SkipMap>, tx_ids: AtomicU64, clock: Clock, storage: Storage, } -impl Database { +impl< + Clock: LogicalClock, + T: Sync + Send + Clone + Serialize + Debug + DeserializeOwned, + > Database +{ /// Creates a new database. pub fn new(clock: Clock, storage: Storage) -> Self { Self { @@ -292,7 +301,7 @@ impl Database { /// Inserts a new row version into the database, while making sure that /// the row version is inserted in the correct order. - fn insert_version(&self, id: RowID, row_version: RowVersion) { + fn insert_version(&self, id: RowID, row_version: RowVersion) { let versions = self.rows.get_or_insert_with(id, || RwLock::new(Vec::new())); let mut versions = versions.value().write().unwrap(); self.insert_version_raw(&mut versions, row_version) @@ -300,7 +309,7 @@ impl Database { /// Inserts a new row version into the internal data structure for versions, /// while making sure that the row version is inserted in the correct order. - fn insert_version_raw(&self, versions: &mut Vec, row_version: RowVersion) { + fn insert_version_raw(&self, versions: &mut Vec>, row_version: RowVersion) { // NOTICE: this is an insert a'la insertion sort, with pessimistic linear complexity. // However, we expect the number of versions to be nearly sorted, so we deem it worthy // to search linearly for the insertion point instead of paying the price of using @@ -333,7 +342,7 @@ impl Database { /// * `tx_id` - the ID of the transaction in which to insert the new row. /// * `row` - the row object containing the values to be inserted. /// - pub fn insert(&self, tx_id: TxID, row: Row) -> Result<()> { + pub fn insert(&self, tx_id: TxID, row: Row) -> Result<()> { let tx = self .txs .get(&tx_id) @@ -370,7 +379,7 @@ impl Database { /// # Returns /// /// Returns `true` if the row was successfully updated, and `false` otherwise. - pub fn update(&self, tx_id: TxID, row: Row) -> Result { + pub fn update(&self, tx_id: TxID, row: Row) -> Result { if !self.delete(tx_id, row.id)? { return Ok(false); } @@ -380,7 +389,7 @@ impl Database { /// Inserts a row in the database with new values, previously deleting /// any old data if it existed. Bails on a delete error, e.g. write-write conflict. - pub fn upsert(&self, tx_id: TxID, row: Row) -> Result<()> { + pub fn upsert(&self, tx_id: TxID, row: Row) -> Result<()> { self.delete(tx_id, row.id)?; self.insert(tx_id, row) } @@ -449,7 +458,7 @@ impl Database { /// /// Returns `Some(row)` with the row data if the row with the given `id` exists, /// and `None` otherwise. - pub fn read(&self, tx_id: TxID, id: RowID) -> Result> { + pub fn read(&self, tx_id: TxID, id: RowID) -> Result>> { let tx = self.txs.get(&tx_id).unwrap(); let tx = tx.value().read().unwrap(); assert_eq!(tx.state, TransactionState::Active); @@ -606,7 +615,7 @@ impl Database { drop(tx); // Postprocessing: inserting row versions and logging the transaction to persistent storage. // TODO: we should probably save to persistent storage first, and only then update the in-memory structures. - let mut log_record: LogRecord = LogRecord::new(end_ts); + let mut log_record: LogRecord = LogRecord::new(end_ts); for ref id in write_set { if let Some(row_versions) = self.rows.get(id) { let mut row_versions = row_versions.value().write().unwrap(); @@ -665,15 +674,18 @@ impl Database { tracing::trace!("ABORT {tx}"); let write_set: Vec = tx.write_set.iter().map(|v| *v.value()).collect(); drop(tx); + for ref id in write_set { if let Some(row_versions) = self.rows.get(id) { let mut row_versions = row_versions.value().write().unwrap(); row_versions.retain(|rv| rv.begin != TxTimestampOrID::TxID(tx_id)); if row_versions.is_empty() { - self.rows.remove(id); + // !TODO! FIXME! This is a bug, because we can't remove the row here! + // self.rows.remove(id); } } } + let tx = tx_unlocked.value().write().unwrap(); tx.state.store(TransactionState::Terminated); tracing::trace!("TERMINATE {tx}"); @@ -745,7 +757,8 @@ impl Database { } } for id in to_remove { - self.rows.remove(&id); + // !TODO! FIXME! This is a bug, because we can't remove the row here! + // self.rows.remove(&id); } dropped } @@ -765,10 +778,10 @@ impl Database { /// A write-write conflict happens when transaction T_m attempts to update a /// row version that is currently being updated by an active transaction T_n. -pub(crate) fn is_write_write_conflict( +pub(crate) fn is_write_write_conflict( txs: &SkipMap>, tx: &Transaction, - rv: &RowVersion, + rv: &RowVersion, ) -> bool { match rv.end { Some(TxTimestampOrID::TxID(rv_end)) => { @@ -784,18 +797,18 @@ pub(crate) fn is_write_write_conflict( } } -pub(crate) fn is_version_visible( +pub(crate) fn is_version_visible( txs: &SkipMap>, tx: &Transaction, - rv: &RowVersion, + rv: &RowVersion, ) -> bool { is_begin_visible(txs, tx, rv) && is_end_visible(txs, tx, rv) } -fn is_begin_visible( +fn is_begin_visible( txs: &SkipMap>, tx: &Transaction, - rv: &RowVersion, + rv: &RowVersion, ) -> bool { match rv.begin { TxTimestampOrID::Timestamp(rv_begin_ts) => tx.begin_ts >= rv_begin_ts, @@ -822,10 +835,10 @@ fn is_begin_visible( } } -fn is_end_visible( +fn is_end_visible( txs: &SkipMap>, tx: &Transaction, - rv: &RowVersion, + rv: &RowVersion, ) -> bool { match rv.end { Some(TxTimestampOrID::Timestamp(rv_end_ts)) => tx.begin_ts < rv_end_ts, diff --git a/core/mvcc/mvcc-rs/src/database/tests.rs b/core/mvcc/mvcc-rs/src/database/tests.rs index e9023c76a..225c34a0e 100644 --- a/core/mvcc/mvcc-rs/src/database/tests.rs +++ b/core/mvcc/mvcc-rs/src/database/tests.rs @@ -2,7 +2,7 @@ use super::*; use crate::clock::LocalClock; use tracing_test::traced_test; -fn test_db() -> Database { +fn test_db() -> Database { let clock = LocalClock::new(); let storage = crate::persistent_storage::Storage::new_noop(); Database::new(clock, storage) @@ -721,7 +721,7 @@ fn test_storage1() { let clock = LocalClock::new(); let storage = crate::persistent_storage::Storage::new_json_on_disk(path); - let db = Database::new(clock, storage); + let db: Database = Database::new(clock, storage); db.recover().unwrap(); println!("{:#?}", db); diff --git a/core/mvcc/mvcc-rs/src/persistent_storage/mod.rs b/core/mvcc/mvcc-rs/src/persistent_storage/mod.rs index 185a432ee..0cac45259 100644 --- a/core/mvcc/mvcc-rs/src/persistent_storage/mod.rs +++ b/core/mvcc/mvcc-rs/src/persistent_storage/mod.rs @@ -1,3 +1,7 @@ +use serde::Serialize; +use serde::de::DeserializeOwned; +use std::fmt::Debug; + use crate::database::{LogRecord, Result}; use crate::errors::DatabaseError; @@ -27,7 +31,7 @@ impl Storage { } impl Storage { - pub fn log_tx(&self, m: LogRecord) -> Result<()> { + pub fn log_tx(&self, m: LogRecord) -> Result<()> { match self { Self::JsonOnDisk(path) => { use std::io::Write; @@ -50,7 +54,7 @@ impl Storage { Ok(()) } - pub fn read_tx_log(&self) -> Result> { + pub fn read_tx_log(&self) -> Result>> { match self { Self::JsonOnDisk(path) => { use std::io::BufRead; @@ -59,7 +63,7 @@ impl Storage { .open(path) .map_err(|e| DatabaseError::Io(e.to_string()))?; - let mut records: Vec = Vec::new(); + let mut records: Vec> = Vec::new(); let mut lines = std::io::BufReader::new(file).lines(); while let Some(Ok(line)) = lines.next() { records.push( diff --git a/core/mvcc/mvcc-rs/src/persistent_storage/s3.rs b/core/mvcc/mvcc-rs/src/persistent_storage/s3.rs index 836c35363..cda65fd5e 100644 --- a/core/mvcc/mvcc-rs/src/persistent_storage/s3.rs +++ b/core/mvcc/mvcc-rs/src/persistent_storage/s3.rs @@ -1,6 +1,9 @@ use crate::database::{LogRecord, Result}; use crate::errors::DatabaseError; use aws_sdk_s3::Client; +use serde::Serialize; +use serde::de::DeserializeOwned; +use std::fmt::Debug; #[derive(Clone, Copy, Debug)] #[non_exhaustive] @@ -66,7 +69,7 @@ impl Replicator { }) } - pub async fn replicate_tx(&self, record: LogRecord) -> Result<()> { + pub async fn replicate_tx(&self, record: LogRecord) -> Result<()> { let key = format!("{}-{:020}", self.prefix, record.tx_timestamp); tracing::trace!("Replicating {key}"); let body = serde_json::to_vec(&record).map_err(|e| DatabaseError::Io(e.to_string()))?; @@ -83,8 +86,8 @@ impl Replicator { Ok(()) } - pub async fn read_tx_log(&self) -> Result> { - let mut records: Vec = Vec::new(); + pub async fn read_tx_log(&self) -> Result>> { + let mut records: Vec> = Vec::new(); // Read all objects from the bucket, one log record is stored in one object let mut next_token = None; loop { @@ -120,7 +123,7 @@ impl Replicator { .collect() .await .map_err(|e| DatabaseError::Io(e.to_string()))?; - let record: LogRecord = serde_json::from_slice(&body.into_bytes()) + let record: LogRecord = serde_json::from_slice(&body.into_bytes()) .map_err(|e| DatabaseError::Io(e.to_string()))?; records.push(record); } From b42cbe52d058f88f09c551eaf8d05050ccafd8be Mon Sep 17 00:00:00 2001 From: Tommi Pisto Date: Sat, 8 Jul 2023 12:28:27 +0300 Subject: [PATCH 113/128] Added 'static bounds to T --- core/mvcc/mvcc-rs/src/cursor.rs | 2 +- core/mvcc/mvcc-rs/src/database/mod.rs | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/core/mvcc/mvcc-rs/src/cursor.rs b/core/mvcc/mvcc-rs/src/cursor.rs index d02e49541..93ec3e2bd 100644 --- a/core/mvcc/mvcc-rs/src/cursor.rs +++ b/core/mvcc/mvcc-rs/src/cursor.rs @@ -13,7 +13,7 @@ pub struct ScanCursor<'a, Clock: LogicalClock, T: Sync + Send + Clone + Serializ tx_id: u64, } -impl<'a, Clock: LogicalClock, T: Sync + Send + Clone + Serialize + DeserializeOwned + Debug> ScanCursor<'a, Clock, T> { +impl<'a, Clock: LogicalClock, T: Sync + Send + Clone + Serialize + DeserializeOwned + Debug + 'static> ScanCursor<'a, Clock, T> { pub fn new( db: &'a Database, tx_id: u64, diff --git a/core/mvcc/mvcc-rs/src/database/mod.rs b/core/mvcc/mvcc-rs/src/database/mod.rs index 9bdade7fd..7f039110b 100644 --- a/core/mvcc/mvcc-rs/src/database/mod.rs +++ b/core/mvcc/mvcc-rs/src/database/mod.rs @@ -267,10 +267,8 @@ pub struct Database< storage: Storage, } -impl< - Clock: LogicalClock, - T: Sync + Send + Clone + Serialize + Debug + DeserializeOwned, - > Database +impl + Database { /// Creates a new database. pub fn new(clock: Clock, storage: Storage) -> Self { @@ -680,8 +678,7 @@ impl< let mut row_versions = row_versions.value().write().unwrap(); row_versions.retain(|rv| rv.begin != TxTimestampOrID::TxID(tx_id)); if row_versions.is_empty() { - // !TODO! FIXME! This is a bug, because we can't remove the row here! - // self.rows.remove(id); + self.rows.remove(id); } } } @@ -757,8 +754,7 @@ impl< } } for id in to_remove { - // !TODO! FIXME! This is a bug, because we can't remove the row here! - // self.rows.remove(&id); + self.rows.remove(&id); } dropped } From 6d2a4150aaf436e7c298c2f81b135743158436fb Mon Sep 17 00:00:00 2001 From: Piotr Sarna Date: Sun, 9 Jul 2023 11:15:48 +0200 Subject: [PATCH 114/128] database: fix an unwrap() in tx_commit It was a legit error -> the transaction doesn't have to be active when commit() is called on it, and the right behavior in that case is to return a TxTerminated error. Fixes https://github.com/penberg/tihku/issues/59 --- core/mvcc/mvcc-rs/src/database/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/mvcc/mvcc-rs/src/database/mod.rs b/core/mvcc/mvcc-rs/src/database/mod.rs index 4bdde3b12..f425e01b1 100644 --- a/core/mvcc/mvcc-rs/src/database/mod.rs +++ b/core/mvcc/mvcc-rs/src/database/mod.rs @@ -515,7 +515,7 @@ impl Database { let end_ts = self.get_timestamp(); // NOTICE: the first shadowed tx keeps the entry alive in the map // for the duration of this whole function, which is important for correctness! - let tx = self.txs.get(&tx_id).unwrap(); + let tx = self.txs.get(&tx_id).ok_or(DatabaseError::TxTerminated)?; let tx = tx.value().write().unwrap(); match tx.state.load() { TransactionState::Terminated => return Err(DatabaseError::TxTerminated), From 5d240e731fae3ec03442be14d4a7e90782954609 Mon Sep 17 00:00:00 2001 From: Tommi Pisto Date: Tue, 11 Jul 2023 18:03:05 +0300 Subject: [PATCH 115/128] Fixed bench build --- core/mvcc/mvcc-rs/benches/my_benchmark.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/mvcc/mvcc-rs/benches/my_benchmark.rs b/core/mvcc/mvcc-rs/benches/my_benchmark.rs index 4a9e3d122..9cb998ca6 100644 --- a/core/mvcc/mvcc-rs/benches/my_benchmark.rs +++ b/core/mvcc/mvcc-rs/benches/my_benchmark.rs @@ -4,7 +4,7 @@ use mvcc_rs::clock::LocalClock; use mvcc_rs::database::{Database, Row, RowID}; use pprof::criterion::{Output, PProfProfiler}; -fn bench_db() -> Database { +fn bench_db() -> Database { let clock = LocalClock::default(); let storage = mvcc_rs::persistent_storage::Storage::new_noop(); Database::new(clock, storage) From fcb4c7e46ad838e2b1c2bc0ab9848dd0572a7a4a Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 5 Feb 2025 12:40:06 +0200 Subject: [PATCH 116/128] core/mvcc: Remove Git metadata files --- core/mvcc/.github/workflows/smoke_test.yml | 25 ---------------------- core/mvcc/.gitignore | 2 -- 2 files changed, 27 deletions(-) delete mode 100644 core/mvcc/.github/workflows/smoke_test.yml delete mode 100644 core/mvcc/.gitignore diff --git a/core/mvcc/.github/workflows/smoke_test.yml b/core/mvcc/.github/workflows/smoke_test.yml deleted file mode 100644 index 3a00d72b4..000000000 --- a/core/mvcc/.github/workflows/smoke_test.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Rust - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -env: - CARGO_TERM_COLOR: always - RUST_LOG: info,mvcc_rs=trace - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Check - run: cargo check --all-targets --all-features - - name: Clippy - run: cargo clippy --all-targets --all-features -- -D warnings - - name: Run tests - run: cargo test --verbose diff --git a/core/mvcc/.gitignore b/core/mvcc/.gitignore deleted file mode 100644 index 1e7caa9ea..000000000 --- a/core/mvcc/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -Cargo.lock -target/ From df20213a4b964f21b695854af0c18eb509b09b86 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 5 Feb 2025 12:40:28 +0200 Subject: [PATCH 117/128] core/mvcc: Remove C bindings We won't need them because we just use the Rust APIs in the core. --- core/mvcc/bindings/c/Cargo.toml | 22 -- core/mvcc/bindings/c/build.rs | 8 - core/mvcc/bindings/c/cbindgen.toml | 7 - core/mvcc/bindings/c/include/mvcc.h | 64 ------ core/mvcc/bindings/c/src/errors.rs | 6 - core/mvcc/bindings/c/src/lib.rs | 298 ---------------------------- core/mvcc/bindings/c/src/types.rs | 79 -------- 7 files changed, 484 deletions(-) delete mode 100644 core/mvcc/bindings/c/Cargo.toml delete mode 100644 core/mvcc/bindings/c/build.rs delete mode 100644 core/mvcc/bindings/c/cbindgen.toml delete mode 100644 core/mvcc/bindings/c/include/mvcc.h delete mode 100644 core/mvcc/bindings/c/src/errors.rs delete mode 100644 core/mvcc/bindings/c/src/lib.rs delete mode 100644 core/mvcc/bindings/c/src/types.rs diff --git a/core/mvcc/bindings/c/Cargo.toml b/core/mvcc/bindings/c/Cargo.toml deleted file mode 100644 index be23a9a1b..000000000 --- a/core/mvcc/bindings/c/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "mvcc-c" -version = "0.0.0" -edition = "2021" - -[lib] -crate-type = ["cdylib", "staticlib"] -doc = false - -[build-dependencies] -cbindgen = "0.24.0" - -[dependencies] -base64 = "0.21.0" -mvcc-rs = { path = "../../mvcc-rs" } -tracing = "0.1.37" -tracing-subscriber = { version = "0" } - -[features] -default = [] -json_on_disk_storage = [] -s3_storage = [] diff --git a/core/mvcc/bindings/c/build.rs b/core/mvcc/bindings/c/build.rs deleted file mode 100644 index f418d0a9a..000000000 --- a/core/mvcc/bindings/c/build.rs +++ /dev/null @@ -1,8 +0,0 @@ -use std::path::Path; - -fn main() { - let header_file = Path::new("include").join("mvcc.h"); - cbindgen::generate(".") - .expect("Failed to generate C bindings") - .write_to_file(header_file); -} diff --git a/core/mvcc/bindings/c/cbindgen.toml b/core/mvcc/bindings/c/cbindgen.toml deleted file mode 100644 index 1b5ac2f31..000000000 --- a/core/mvcc/bindings/c/cbindgen.toml +++ /dev/null @@ -1,7 +0,0 @@ -language = "C" -cpp_compat = true -include_guard = "MVCC_H" -line_length = 120 -no_includes = true -style = "type" -sys_includes = ["stdint.h"] diff --git a/core/mvcc/bindings/c/include/mvcc.h b/core/mvcc/bindings/c/include/mvcc.h deleted file mode 100644 index eead91b01..000000000 --- a/core/mvcc/bindings/c/include/mvcc.h +++ /dev/null @@ -1,64 +0,0 @@ -#ifndef MVCC_H -#define MVCC_H - -#include - -typedef enum { - MVCC_OK = 0, - MVCC_IO_ERROR_READ = 266, - MVCC_IO_ERROR_WRITE = 778, -} MVCCError; - -typedef struct DbContext DbContext; - -typedef struct ScanCursorContext ScanCursorContext; - -typedef const DbContext *MVCCDatabaseRef; - -typedef ScanCursorContext *MVCCScanCursorRef; - -#ifdef __cplusplus -extern "C" { -#endif // __cplusplus - -MVCCDatabaseRef MVCCDatabaseOpen(const char *path); - -void MVCCDatabaseClose(MVCCDatabaseRef db); - -uint64_t MVCCTransactionBegin(MVCCDatabaseRef db); - -MVCCError MVCCTransactionCommit(MVCCDatabaseRef db, uint64_t tx_id); - -MVCCError MVCCTransactionRollback(MVCCDatabaseRef db, uint64_t tx_id); - -MVCCError MVCCDatabaseInsert(MVCCDatabaseRef db, - uint64_t tx_id, - uint64_t table_id, - uint64_t row_id, - const void *value_ptr, - uintptr_t value_len); - -MVCCError MVCCDatabaseRead(MVCCDatabaseRef db, - uint64_t tx_id, - uint64_t table_id, - uint64_t row_id, - uint8_t **value_ptr, - int64_t *value_len); - -void MVCCFreeStr(void *ptr); - -MVCCScanCursorRef MVCCScanCursorOpen(MVCCDatabaseRef db, uint64_t tx_id, uint64_t table_id); - -void MVCCScanCursorClose(MVCCScanCursorRef cursor); - -MVCCError MVCCScanCursorRead(MVCCScanCursorRef cursor, uint8_t **value_ptr, int64_t *value_len); - -int MVCCScanCursorNext(MVCCScanCursorRef cursor); - -uint64_t MVCCScanCursorPosition(MVCCScanCursorRef cursor); - -#ifdef __cplusplus -} // extern "C" -#endif // __cplusplus - -#endif /* MVCC_H */ diff --git a/core/mvcc/bindings/c/src/errors.rs b/core/mvcc/bindings/c/src/errors.rs deleted file mode 100644 index 65a174b50..000000000 --- a/core/mvcc/bindings/c/src/errors.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[repr(C)] -pub enum MVCCError { - MVCC_OK = 0, - MVCC_IO_ERROR_READ = 266, - MVCC_IO_ERROR_WRITE = 778, -} diff --git a/core/mvcc/bindings/c/src/lib.rs b/core/mvcc/bindings/c/src/lib.rs deleted file mode 100644 index 7d222a8d2..000000000 --- a/core/mvcc/bindings/c/src/lib.rs +++ /dev/null @@ -1,298 +0,0 @@ -#![allow(non_camel_case_types)] -#![allow(clippy::missing_safety_doc)] - -mod errors; -mod types; - -use errors::MVCCError; -use mvcc_rs::persistent_storage::{s3, Storage}; -use mvcc_rs::*; -use types::{DbContext, MVCCDatabaseRef, MVCCScanCursorRef, ScanCursorContext}; - -/// cbindgen:ignore -type Clock = clock::LocalClock; - -/// cbindgen:ignore -/// Note - We use String type in C bindings as Row type. Type is generic. -type Db = database::Database; - -/// cbindgen:ignore -/// Note - We use String type in C bindings as Row type. Type is generic. -type ScanCursor = cursor::ScanCursor<'static, Clock, String>; - -static INIT_RUST_LOG: std::sync::Once = std::sync::Once::new(); - -fn storage_for(main_db_path: &str) -> database::Result { - // TODO: let's accept an URL instead of main_db_path here, so we can - // pass custom S3 endpoints, options, etc. - if cfg!(feature = "json_on_disk_storage") { - tracing::info!("JSONonDisk storage stored in {main_db_path}-mvcc"); - return Ok(Storage::new_json_on_disk(format!("{main_db_path}-mvcc"))); - } - if cfg!(feature = "s3_storage") { - tracing::info!("S3 storage for {main_db_path}"); - let options = s3::Options::with_create_bucket_if_not_exists(true); - return Storage::new_s3(options); - } - tracing::info!("No persistent storage for {main_db_path}"); - Ok(Storage::new_noop()) -} - -#[no_mangle] -pub unsafe extern "C" fn MVCCDatabaseOpen(path: *const std::ffi::c_char) -> MVCCDatabaseRef { - INIT_RUST_LOG.call_once(|| { - tracing_subscriber::fmt::init(); - }); - - tracing::debug!("MVCCDatabaseOpen"); - - let clock = clock::LocalClock::new(); - let main_db_path = unsafe { std::ffi::CStr::from_ptr(path) }; - let main_db_path = match main_db_path.to_str() { - Ok(path) => path, - Err(_) => { - tracing::error!("Invalid UTF-8 path"); - return MVCCDatabaseRef::null(); - } - }; - - tracing::debug!("mvccrs: opening persistent storage for {main_db_path}"); - let storage = match storage_for(main_db_path) { - Ok(storage) => storage, - Err(e) => { - tracing::error!("Failed to open persistent storage: {e}"); - return MVCCDatabaseRef::null(); - } - }; - let db = Db::new(clock, storage); - - db.recover().ok(); - - let db = Box::leak(Box::new(DbContext { db })); - MVCCDatabaseRef::from(db) -} - -#[no_mangle] -pub unsafe extern "C" fn MVCCDatabaseClose(db: MVCCDatabaseRef) { - tracing::debug!("MVCCDatabaseClose"); - if db.is_null() { - tracing::debug!("warning: `db` is null in MVCCDatabaseClose()"); - return; - } - let _ = unsafe { Box::from_raw(db.get_ref_mut()) }; -} - -#[no_mangle] -pub unsafe extern "C" fn MVCCTransactionBegin(db: MVCCDatabaseRef) -> u64 { - let db = db.get_ref(); - let tx_id = db.begin_tx(); - tracing::debug!("MVCCTransactionBegin: {tx_id}"); - tx_id -} - -#[no_mangle] -pub unsafe extern "C" fn MVCCTransactionCommit(db: MVCCDatabaseRef, tx_id: u64) -> MVCCError { - let db = db.get_ref(); - tracing::debug!("MVCCTransactionCommit: {tx_id}"); - match db.commit_tx(tx_id) { - Ok(()) => MVCCError::MVCC_OK, - Err(e) => { - tracing::error!("MVCCTransactionCommit: {e}"); - MVCCError::MVCC_IO_ERROR_WRITE - } - } -} - -#[no_mangle] -pub unsafe extern "C" fn MVCCTransactionRollback(db: MVCCDatabaseRef, tx_id: u64) -> MVCCError { - let db = db.get_ref(); - tracing::debug!("MVCCTransactionRollback: {tx_id}"); - db.rollback_tx(tx_id); - MVCCError::MVCC_OK -} - -#[no_mangle] -pub unsafe extern "C" fn MVCCDatabaseInsert( - db: MVCCDatabaseRef, - tx_id: u64, - table_id: u64, - row_id: u64, - value_ptr: *const std::ffi::c_void, - value_len: usize, -) -> MVCCError { - let db = db.get_ref(); - let value = std::slice::from_raw_parts(value_ptr as *const u8, value_len); - let data = match std::str::from_utf8(value) { - Ok(value) => value.to_string(), - Err(_) => { - tracing::info!("Invalid UTF-8, let's base64 this fellow"); - use base64::{engine::general_purpose, Engine as _}; - general_purpose::STANDARD.encode(value) - } - }; - let id = database::RowID { table_id, row_id }; - let row = database::Row { id, data }; - tracing::debug!("MVCCDatabaseInsert: {row:?}"); - match db.insert(tx_id, row) { - Ok(_) => { - tracing::debug!("MVCCDatabaseInsert: success"); - MVCCError::MVCC_OK - } - Err(e) => { - tracing::error!("MVCCDatabaseInsert: {e}"); - MVCCError::MVCC_IO_ERROR_WRITE - } - } -} - -#[no_mangle] -pub unsafe extern "C" fn MVCCDatabaseRead( - db: MVCCDatabaseRef, - tx_id: u64, - table_id: u64, - row_id: u64, - value_ptr: *mut *mut u8, - value_len: *mut i64, -) -> MVCCError { - let db = db.get_ref(); - - match { - let id = database::RowID { table_id, row_id }; - let maybe_row = db.read(tx_id, id); - match maybe_row { - Ok(Some(row)) => { - tracing::debug!("Found row {row:?}"); - let str_len = row.data.len() + 1; - let value = std::ffi::CString::new(row.data.as_bytes()).unwrap_or_default(); - unsafe { - *value_ptr = value.into_raw() as *mut u8; - *value_len = str_len as i64; - } - } - _ => unsafe { *value_len = -1 }, - }; - Ok::<(), mvcc_rs::errors::DatabaseError>(()) - } { - Ok(_) => { - tracing::debug!("MVCCDatabaseRead: success"); - MVCCError::MVCC_OK - } - Err(e) => { - tracing::error!("MVCCDatabaseRead: {e}"); - MVCCError::MVCC_IO_ERROR_READ - } - } -} - -#[no_mangle] -pub unsafe extern "C" fn MVCCFreeStr(ptr: *mut std::ffi::c_void) { - if ptr.is_null() { - return; - } - let _ = std::ffi::CString::from_raw(ptr as *mut std::ffi::c_char); -} - -#[no_mangle] -pub unsafe extern "C" fn MVCCScanCursorOpen( - db: MVCCDatabaseRef, - tx_id: u64, - table_id: u64, -) -> MVCCScanCursorRef { - tracing::debug!("MVCCScanCursorOpen()"); - // Reference is transmuted to &'static in order to be able to pass the cursor back to C. - // The contract with C is to never use a cursor after MVCCDatabaseClose() has been called. - let db = unsafe { std::mem::transmute::<&Db, &'static Db>(db.get_ref()) }; - match mvcc_rs::cursor::ScanCursor::new(db, tx_id, table_id) { - Ok(cursor) => { - if cursor.is_empty() { - tracing::debug!("Cursor is empty"); - return MVCCScanCursorRef { - ptr: std::ptr::null_mut(), - }; - } - tracing::debug!("Cursor open: {cursor:?}"); - MVCCScanCursorRef { - ptr: Box::into_raw(Box::new(ScanCursorContext { cursor })), - } - } - Err(e) => { - tracing::error!("MVCCScanCursorOpen: {e}"); - MVCCScanCursorRef { - ptr: std::ptr::null_mut(), - } - } - } -} - -#[no_mangle] -pub unsafe extern "C" fn MVCCScanCursorClose(cursor: MVCCScanCursorRef) { - tracing::debug!("MVCCScanCursorClose()"); - if cursor.ptr.is_null() { - tracing::debug!("warning: `cursor` is null in MVCCScanCursorClose()"); - return; - } - let cursor = unsafe { Box::from_raw(cursor.ptr) }.cursor; - cursor.close().ok(); -} - -#[no_mangle] -pub unsafe extern "C" fn MVCCScanCursorRead( - cursor: MVCCScanCursorRef, - value_ptr: *mut *mut u8, - value_len: *mut i64, -) -> MVCCError { - tracing::debug!("MVCCScanCursorRead()"); - if cursor.ptr.is_null() { - tracing::debug!("warning: `cursor` is null in MVCCScanCursorRead()"); - return MVCCError::MVCC_IO_ERROR_READ; - } - let cursor = cursor.get_ref(); - - match { - let maybe_row = cursor.current_row(); - match maybe_row { - Ok(Some(row)) => { - tracing::debug!("Found row {row:?}"); - let str_len = row.data.len() + 1; - let value = std::ffi::CString::new(row.data.as_bytes()).unwrap_or_default(); - unsafe { - *value_ptr = value.into_raw() as *mut u8; - *value_len = str_len as i64; - } - } - _ => unsafe { *value_len = -1 }, - }; - Ok::<(), mvcc_rs::errors::DatabaseError>(()) - } { - Ok(_) => { - tracing::debug!("MVCCDatabaseRead: success"); - MVCCError::MVCC_OK - } - Err(e) => { - tracing::error!("MVCCDatabaseRead: {e}"); - MVCCError::MVCC_IO_ERROR_READ - } - } -} - -#[no_mangle] -pub unsafe extern "C" fn MVCCScanCursorNext(cursor: MVCCScanCursorRef) -> std::ffi::c_int { - let cursor = cursor.get_ref_mut(); - tracing::debug!("MVCCScanCursorNext(): {}", cursor.index); - if cursor.forward() { - tracing::debug!("Forwarded to {}", cursor.index); - 1 - } else { - tracing::debug!("Forwarded to end"); - 0 - } -} - -#[no_mangle] -pub unsafe extern "C" fn MVCCScanCursorPosition(cursor: MVCCScanCursorRef) -> u64 { - let cursor = cursor.get_ref(); - cursor - .current_row_id() - .map(|row_id| row_id.row_id) - .unwrap_or(0) -} diff --git a/core/mvcc/bindings/c/src/types.rs b/core/mvcc/bindings/c/src/types.rs deleted file mode 100644 index 52c21951d..000000000 --- a/core/mvcc/bindings/c/src/types.rs +++ /dev/null @@ -1,79 +0,0 @@ -use crate::Db; - -#[derive(Clone, Debug)] -#[repr(transparent)] -pub struct MVCCDatabaseRef { - ptr: *const DbContext, -} - -impl MVCCDatabaseRef { - pub fn null() -> MVCCDatabaseRef { - MVCCDatabaseRef { - ptr: std::ptr::null(), - } - } - - pub fn is_null(&self) -> bool { - self.ptr.is_null() - } - - pub fn get_ref(&self) -> &Db { - &unsafe { &*(self.ptr) }.db - } - - #[allow(clippy::mut_from_ref)] - pub fn get_ref_mut(&self) -> &mut Db { - let ptr_mut = self.ptr as *mut DbContext; - &mut unsafe { &mut (*ptr_mut) }.db - } -} - -#[allow(clippy::from_over_into)] -impl From<&DbContext> for MVCCDatabaseRef { - fn from(value: &DbContext) -> Self { - Self { ptr: value } - } -} - -#[allow(clippy::from_over_into)] -impl From<&mut DbContext> for MVCCDatabaseRef { - fn from(value: &mut DbContext) -> Self { - Self { ptr: value } - } -} - -pub struct DbContext { - pub(crate) db: Db, -} - -pub struct ScanCursorContext { - pub(crate) cursor: crate::ScanCursor, -} - -#[derive(Clone, Debug)] -#[repr(transparent)] -pub struct MVCCScanCursorRef { - pub ptr: *mut ScanCursorContext, -} - -impl MVCCScanCursorRef { - pub fn null() -> MVCCScanCursorRef { - MVCCScanCursorRef { - ptr: std::ptr::null_mut(), - } - } - - pub fn is_null(&self) -> bool { - self.ptr.is_null() - } - - pub fn get_ref(&self) -> &crate::ScanCursor { - &unsafe { &*(self.ptr) }.cursor - } - - #[allow(clippy::mut_from_ref)] - pub fn get_ref_mut(&self) -> &mut crate::ScanCursor { - let ptr_mut = self.ptr as *mut ScanCursorContext; - &mut unsafe { &mut (*ptr_mut) }.cursor - } -} From 7d998942691bf8f33933c20d6aeac1c4fcb46957 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 5 Feb 2025 12:41:34 +0200 Subject: [PATCH 118/128] Move MVCC docs to top-level docs directory --- {core/mvcc/docs => docs/internals/mvcc}/DESIGN.md | 0 .../internals/mvcc}/figures/transactions.excalidraw | 0 .../internals/mvcc}/figures/transactions.png | Bin 3 files changed, 0 insertions(+), 0 deletions(-) rename {core/mvcc/docs => docs/internals/mvcc}/DESIGN.md (100%) rename {core/mvcc/docs => docs/internals/mvcc}/figures/transactions.excalidraw (100%) rename {core/mvcc/docs => docs/internals/mvcc}/figures/transactions.png (100%) diff --git a/core/mvcc/docs/DESIGN.md b/docs/internals/mvcc/DESIGN.md similarity index 100% rename from core/mvcc/docs/DESIGN.md rename to docs/internals/mvcc/DESIGN.md diff --git a/core/mvcc/docs/figures/transactions.excalidraw b/docs/internals/mvcc/figures/transactions.excalidraw similarity index 100% rename from core/mvcc/docs/figures/transactions.excalidraw rename to docs/internals/mvcc/figures/transactions.excalidraw diff --git a/core/mvcc/docs/figures/transactions.png b/docs/internals/mvcc/figures/transactions.png similarity index 100% rename from core/mvcc/docs/figures/transactions.png rename to docs/internals/mvcc/figures/transactions.png From 5e282c00bc4ab44556b8edcc8caf36ef82a78035 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 5 Feb 2025 12:42:15 +0200 Subject: [PATCH 119/128] Remove duplicate MIT license --- core/mvcc/LICENSE.md | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 core/mvcc/LICENSE.md diff --git a/core/mvcc/LICENSE.md b/core/mvcc/LICENSE.md deleted file mode 100644 index 0c99a0831..000000000 --- a/core/mvcc/LICENSE.md +++ /dev/null @@ -1,20 +0,0 @@ -MIT License - -Copyright 2023 Pekka Enberg - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. From 5c9bb4bddd1e0c6e548377f7b071cb9b394dfd0c Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 5 Feb 2025 12:42:39 +0200 Subject: [PATCH 120/128] core/mvcc: Remove duplicate Cargo workspace config --- core/mvcc/Cargo.toml | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 core/mvcc/Cargo.toml diff --git a/core/mvcc/Cargo.toml b/core/mvcc/Cargo.toml deleted file mode 100644 index 7ebb0ebff..000000000 --- a/core/mvcc/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[workspace] -resolver = "2" -members = [ - "mvcc-rs", - "bindings/c", -] - -[profile.release] -codegen-units = 1 -panic = "abort" -strip = true From 9f0b33a8efb6061aa741f2f05a5ab0a17f4bf1a2 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 5 Feb 2025 12:44:36 +0200 Subject: [PATCH 121/128] core/mvcc: Remove README.md --- core/mvcc/README.md | 58 --------------------------------------------- 1 file changed, 58 deletions(-) delete mode 100644 core/mvcc/README.md diff --git a/core/mvcc/README.md b/core/mvcc/README.md deleted file mode 100644 index 7dcc797d9..000000000 --- a/core/mvcc/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# Tihku - -Tihku is an _work-in-progress_, open-source implementation of the Hekaton multi-version concurrency control (MVCC) written in Rust. -The project aims to provide a foundational building block for implementing database management systems. - -One of the projects using Tihku is an experimental [libSQL branch with MVCC](https://github.com/penberg/libsql/tree/mvcc) that aims to implement `BEGIN CONCURRENT` with Tihku improve SQLite write concurrency. - -## Features - -* Main memory architecture, rows are accessed via an index -* Optimistic multi-version concurrency control -* Rust and C APIs - -## Experimental Evaluation - -**Single-threaded micro-benchmarks** - -Operations | Throughput ------------------------------------|------------ -`begin_tx`, `read`, and `commit` | 2.2M ops/second -`begin_tx`, `update`, and `commit` | 2.2M ops/second -`read` | 12.9M ops/second -`update` | 6.2M ops/second - -(The `cargo bench` was run on a AMD Ryzen 9 3900XT 2.2 GHz CPU.) - -## Development - -Run tests: - -```console -cargo test -``` - -Test coverage report: - -```console -cargo tarpaulin -o html -``` - -Run benchmarks: - -```console -cargo bench -``` - -Run benchmarks and generate flamegraphs: - -```console -echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid -cargo bench --bench my_benchmark -- --profile-time=5 -``` - -## References - -Larson et al. [High-Performance Concurrency Control Mechanisms for Main-Memory Databases](https://vldb.org/pvldb/vol5/p298_per-akelarson_vldb2012.pdf). VLDB '11 - -Paper errata: The visibility check in Table 2 is wrong and causes uncommitted delete to become visible to transactions (fixed in [commit 6ca3773]( https://github.com/penberg/mvcc-rs/commit/6ca377320bb59b52ecc0430b9e5e422e8d61658d)). From e923a2352eae5fee2bc13cc9aef80a901c6474ad Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 5 Feb 2025 12:44:57 +0200 Subject: [PATCH 122/128] core/mvcc: Kill `mvcc-rs` crate We'll just integrate everything in the core. --- core/mvcc/mvcc-rs/Cargo.toml | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 core/mvcc/mvcc-rs/Cargo.toml diff --git a/core/mvcc/mvcc-rs/Cargo.toml b/core/mvcc/mvcc-rs/Cargo.toml deleted file mode 100644 index 27f030a73..000000000 --- a/core/mvcc/mvcc-rs/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ -[package] -name = "mvcc-rs" -version = "0.0.0" -edition = "2021" - -[dependencies] -anyhow = "1.0.70" -thiserror = "1.0.40" -tracing = "0.1.37" -serde = { version = "1.0.160", features = ["derive"] } -serde_json = "1.0.96" -tracing-subscriber = { version = "0", optional = true } -base64 = "0.21.0" -aws-sdk-s3 = "0.27.0" -aws-config = "0.55.2" -parking_lot = "0.12.1" -futures = "0.3.28" -crossbeam-skiplist = "0.1.1" -tracing-test = "0" - -[dev-dependencies] -criterion = { version = "0.4", features = ["html_reports", "async", "async_futures"] } -pprof = { version = "0.11.1", features = ["criterion", "flamegraph"] } -tracing-subscriber = "0" -mvcc-rs = { path = "." } - -[[bench]] -name = "my_benchmark" -harness = false - -[features] -default = [] -c_bindings = ["dep:tracing-subscriber"] From a585b8114880f9717075bae8af4cb7411d4fda37 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 5 Feb 2025 12:51:58 +0200 Subject: [PATCH 123/128] mvcc/core: Kill S3 persistent storage --- .../mvcc-rs/src/persistent_storage/mod.rs | 11 -- .../mvcc/mvcc-rs/src/persistent_storage/s3.rs | 139 ------------------ 2 files changed, 150 deletions(-) delete mode 100644 core/mvcc/mvcc-rs/src/persistent_storage/s3.rs diff --git a/core/mvcc/mvcc-rs/src/persistent_storage/mod.rs b/core/mvcc/mvcc-rs/src/persistent_storage/mod.rs index 0cac45259..e7d17923b 100644 --- a/core/mvcc/mvcc-rs/src/persistent_storage/mod.rs +++ b/core/mvcc/mvcc-rs/src/persistent_storage/mod.rs @@ -5,13 +5,10 @@ use std::fmt::Debug; use crate::database::{LogRecord, Result}; use crate::errors::DatabaseError; -pub mod s3; - #[derive(Debug)] pub enum Storage { Noop, JsonOnDisk(std::path::PathBuf), - S3(s3::Replicator), } impl Storage { @@ -23,11 +20,6 @@ impl Storage { let path = path.into(); Self::JsonOnDisk(path) } - - pub fn new_s3(options: s3::Options) -> Result { - let replicator = futures::executor::block_on(s3::Replicator::new(options))?; - Ok(Self::S3(replicator)) - } } impl Storage { @@ -46,9 +38,6 @@ impl Storage { file.write_all(b"\n") .map_err(|e| DatabaseError::Io(e.to_string()))?; } - Self::S3(replicator) => { - futures::executor::block_on(replicator.replicate_tx(m))?; - } Self::Noop => (), } Ok(()) diff --git a/core/mvcc/mvcc-rs/src/persistent_storage/s3.rs b/core/mvcc/mvcc-rs/src/persistent_storage/s3.rs deleted file mode 100644 index cda65fd5e..000000000 --- a/core/mvcc/mvcc-rs/src/persistent_storage/s3.rs +++ /dev/null @@ -1,139 +0,0 @@ -use crate::database::{LogRecord, Result}; -use crate::errors::DatabaseError; -use aws_sdk_s3::Client; -use serde::Serialize; -use serde::de::DeserializeOwned; -use std::fmt::Debug; - -#[derive(Clone, Copy, Debug)] -#[non_exhaustive] -pub struct Options { - pub create_bucket_if_not_exists: bool, -} - -impl Options { - pub fn with_create_bucket_if_not_exists(create_bucket_if_not_exists: bool) -> Self { - Self { - create_bucket_if_not_exists, - } - } -} - -#[derive(Debug)] -pub struct Replicator { - pub client: Client, - pub bucket: String, - pub prefix: String, -} - -impl Replicator { - pub async fn new(options: Options) -> Result { - let mut loader = aws_config::from_env(); - if let Ok(endpoint) = std::env::var("MVCCRS_ENDPOINT") { - loader = loader.endpoint_url(endpoint); - } - let sdk_config = loader.load().await; - let config = aws_sdk_s3::config::Builder::from(&sdk_config) - .force_path_style(true) - .build(); - let bucket = std::env::var("MVCCRS_BUCKET").unwrap_or_else(|_| "mvccrs".to_string()); - let prefix = std::env::var("MVCCRS_PREFIX").unwrap_or_else(|_| "tx".to_string()); - let client = Client::from_conf(config); - - match client.head_bucket().bucket(&bucket).send().await { - Ok(_) => tracing::info!("Bucket {bucket} exists and is accessible"), - Err(aws_sdk_s3::error::SdkError::ServiceError(err)) if err.err().is_not_found() => { - if options.create_bucket_if_not_exists { - tracing::info!("Bucket {bucket} not found, recreating"); - client - .create_bucket() - .bucket(&bucket) - .send() - .await - .map_err(|e| DatabaseError::Io(e.to_string()))?; - } else { - tracing::error!("Bucket {bucket} does not exist"); - return Err(DatabaseError::Io(err.err().to_string())); - } - } - Err(e) => { - tracing::error!("Bucket checking error: {e}"); - return Err(DatabaseError::Io(e.to_string())); - } - } - - Ok(Self { - client, - bucket, - prefix, - }) - } - - pub async fn replicate_tx(&self, record: LogRecord) -> Result<()> { - let key = format!("{}-{:020}", self.prefix, record.tx_timestamp); - tracing::trace!("Replicating {key}"); - let body = serde_json::to_vec(&record).map_err(|e| DatabaseError::Io(e.to_string()))?; - let resp = self - .client - .put_object() - .bucket(&self.bucket) - .key(&key) - .body(body.into()) - .send() - .await - .map_err(|e| DatabaseError::Io(e.to_string()))?; - tracing::trace!("Replicator response: {:?}", resp); - Ok(()) - } - - pub async fn read_tx_log(&self) -> Result>> { - let mut records: Vec> = Vec::new(); - // Read all objects from the bucket, one log record is stored in one object - let mut next_token = None; - loop { - let mut req = self - .client - .list_objects_v2() - .bucket(&self.bucket) - .prefix(&self.prefix); - if let Some(next_token) = next_token { - req = req.continuation_token(next_token); - } - let resp = req - .send() - .await - .map_err(|e| DatabaseError::Io(e.to_string()))?; - tracing::trace!("List objects response: {:?}", resp); - if let Some(contents) = resp.contents { - // read the record from s3 based on the object metadata (`contents`) - // and store it in the `records` vector - for object in contents { - let key = object.key.unwrap(); - let resp = self - .client - .get_object() - .bucket(&self.bucket) - .key(&key) - .send() - .await - .map_err(|e| DatabaseError::Io(e.to_string()))?; - tracing::trace!("Get object response: {:?}", resp); - let body = resp - .body - .collect() - .await - .map_err(|e| DatabaseError::Io(e.to_string()))?; - let record: LogRecord = serde_json::from_slice(&body.into_bytes()) - .map_err(|e| DatabaseError::Io(e.to_string()))?; - records.push(record); - } - } - if resp.next_continuation_token.is_none() { - break; - } - next_token = resp.next_continuation_token; - } - tracing::trace!("Records: {records:?}"); - Ok(records) - } -} From fad479ac59c2b3af1b7048f77bb1334ec09b105d Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 5 Feb 2025 13:02:04 +0200 Subject: [PATCH 124/128] core/mvcc: Move source code to module --- Cargo.lock | 27 +++ core/Cargo.toml | 2 + .../mvcc_benchmark.rs} | 6 +- core/lib.rs | 1 + core/mvcc/{mvcc-rs/src => }/clock.rs | 0 core/mvcc/{mvcc-rs/src => }/cursor.rs | 17 +- core/mvcc/{mvcc-rs/src => }/database/mod.rs | 81 ++------ core/mvcc/{mvcc-rs/src => }/database/tests.rs | 177 +----------------- core/mvcc/{mvcc-rs/src => }/errors.rs | 0 core/mvcc/mod.rs | 160 ++++++++++++++++ core/mvcc/mvcc-rs/src/lib.rs | 38 ---- .../mvcc-rs/src/persistent_storage/mod.rs | 71 ------- core/mvcc/mvcc-rs/tests/concurrency_test.rs | 124 ------------ core/mvcc/persistent_storage/mod.rs | 32 ++++ 14 files changed, 254 insertions(+), 482 deletions(-) rename core/{mvcc/mvcc-rs/benches/my_benchmark.rs => benches/mvcc_benchmark.rs} (95%) rename core/mvcc/{mvcc-rs/src => }/clock.rs (100%) rename core/mvcc/{mvcc-rs/src => }/cursor.rs (75%) rename core/mvcc/{mvcc-rs/src => }/database/mod.rs (93%) rename core/mvcc/{mvcc-rs/src => }/database/tests.rs (85%) rename core/mvcc/{mvcc-rs/src => }/errors.rs (100%) create mode 100644 core/mvcc/mod.rs delete mode 100644 core/mvcc/mvcc-rs/src/lib.rs delete mode 100644 core/mvcc/mvcc-rs/src/persistent_storage/mod.rs delete mode 100644 core/mvcc/mvcc-rs/tests/concurrency_test.rs create mode 100644 core/mvcc/persistent_storage/mod.rs diff --git a/Cargo.lock b/Cargo.lock index dc49a85f0..cb6dda8d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -554,6 +554,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-skiplist" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df29de440c58ca2cc6e587ec3d22347551a32435fbde9d2bff64e78a9ffa151b" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -1570,6 +1580,7 @@ dependencies = [ "cfg_block", "chrono", "criterion", + "crossbeam-skiplist", "fallible-iterator 0.3.0", "getrandom 0.2.15", "hex", @@ -1607,6 +1618,7 @@ dependencies = [ "strum", "tempfile", "thiserror 1.0.69", + "tracing", ] [[package]] @@ -3043,14 +3055,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "tracing-core" version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] [[package]] name = "typenum" diff --git a/core/Cargo.toml b/core/Cargo.toml index f2d43a156..e4db1ee95 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -70,6 +70,8 @@ limbo_time = { path = "../extensions/time", optional = true, features = ["static miette = "7.4.0" strum = "0.26" parking_lot = "0.12.3" +tracing = "0.1.41" +crossbeam-skiplist = "0.1.3" [build-dependencies] chrono = "0.4.38" diff --git a/core/mvcc/mvcc-rs/benches/my_benchmark.rs b/core/benches/mvcc_benchmark.rs similarity index 95% rename from core/mvcc/mvcc-rs/benches/my_benchmark.rs rename to core/benches/mvcc_benchmark.rs index 9cb998ca6..14fc2d61e 100644 --- a/core/mvcc/mvcc-rs/benches/my_benchmark.rs +++ b/core/benches/mvcc_benchmark.rs @@ -1,12 +1,12 @@ use criterion::async_executor::FuturesExecutor; use criterion::{criterion_group, criterion_main, Criterion, Throughput}; -use mvcc_rs::clock::LocalClock; -use mvcc_rs::database::{Database, Row, RowID}; +use limbo_core::mvcc::clock::LocalClock; +use limbo_core::mvcc::database::{Database, Row, RowID}; use pprof::criterion::{Output, PProfProfiler}; fn bench_db() -> Database { let clock = LocalClock::default(); - let storage = mvcc_rs::persistent_storage::Storage::new_noop(); + let storage = limbo_core::mvcc::persistent_storage::Storage::new_noop(); Database::new(clock, storage) } diff --git a/core/lib.rs b/core/lib.rs index f1c78f9f2..c2e0f22c6 100644 --- a/core/lib.rs +++ b/core/lib.rs @@ -5,6 +5,7 @@ mod info; mod io; #[cfg(feature = "json")] mod json; +pub mod mvcc; mod parameters; mod pseudo; mod result; diff --git a/core/mvcc/mvcc-rs/src/clock.rs b/core/mvcc/clock.rs similarity index 100% rename from core/mvcc/mvcc-rs/src/clock.rs rename to core/mvcc/clock.rs diff --git a/core/mvcc/mvcc-rs/src/cursor.rs b/core/mvcc/cursor.rs similarity index 75% rename from core/mvcc/mvcc-rs/src/cursor.rs rename to core/mvcc/cursor.rs index 93ec3e2bd..4d120e214 100644 --- a/core/mvcc/mvcc-rs/src/cursor.rs +++ b/core/mvcc/cursor.rs @@ -1,19 +1,28 @@ use serde::de::DeserializeOwned; use serde::Serialize; -use crate::clock::LogicalClock; -use crate::database::{Database, Result, Row, RowID}; +use crate::mvcc::clock::LogicalClock; +use crate::mvcc::database::{Database, Result, Row, RowID}; use std::fmt::Debug; #[derive(Debug)] -pub struct ScanCursor<'a, Clock: LogicalClock, T: Sync + Send + Clone + Serialize + DeserializeOwned + Debug> { +pub struct ScanCursor< + 'a, + Clock: LogicalClock, + T: Sync + Send + Clone + Serialize + DeserializeOwned + Debug, +> { pub db: &'a Database, pub row_ids: Vec, pub index: usize, tx_id: u64, } -impl<'a, Clock: LogicalClock, T: Sync + Send + Clone + Serialize + DeserializeOwned + Debug + 'static> ScanCursor<'a, Clock, T> { +impl< + 'a, + Clock: LogicalClock, + T: Sync + Send + Clone + Serialize + DeserializeOwned + Debug + 'static, + > ScanCursor<'a, Clock, T> +{ pub fn new( db: &'a Database, tx_id: u64, diff --git a/core/mvcc/mvcc-rs/src/database/mod.rs b/core/mvcc/database/mod.rs similarity index 93% rename from core/mvcc/mvcc-rs/src/database/mod.rs rename to core/mvcc/database/mod.rs index d434722ea..30e42c303 100644 --- a/core/mvcc/mvcc-rs/src/database/mod.rs +++ b/core/mvcc/database/mod.rs @@ -1,9 +1,7 @@ -use crate::clock::LogicalClock; -use crate::errors::DatabaseError; -use crate::persistent_storage::Storage; +use crate::mvcc::clock::LogicalClock; +use crate::mvcc::errors::DatabaseError; +use crate::mvcc::persistent_storage::Storage; use crossbeam_skiplist::{SkipMap, SkipSet}; -use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; use std::fmt::Debug; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::RwLock; @@ -13,13 +11,13 @@ pub type Result = std::result::Result; #[cfg(test)] mod tests; -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct RowID { pub table_id: u64, pub row_id: u64, } -#[derive(Clone, Debug, PartialEq, PartialOrd, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, PartialOrd)] pub struct Row { pub id: RowID, @@ -27,7 +25,7 @@ pub struct Row { } /// A row version. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct RowVersion { begin: TxTimestampOrID, end: Option, @@ -37,7 +35,7 @@ pub struct RowVersion { pub type TxID = u64; /// A log record contains all the versions inserted and deleted by a transaction. -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug)] pub struct LogRecord { pub(crate) tx_timestamp: TxID, row_versions: Vec>, @@ -58,14 +56,14 @@ impl LogRecord { /// phase of the transaction. During the active phase, new versions track the /// transaction ID in the `begin` and `end` fields. After a transaction commits, /// versions switch to tracking timestamps. -#[derive(Clone, Debug, PartialEq, PartialOrd, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, PartialOrd)] enum TxTimestampOrID { Timestamp(u64), TxID(TxID), } /// Transaction -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug)] pub struct Transaction { /// The state of the transaction. state: AtomicTransactionState, @@ -74,57 +72,11 @@ pub struct Transaction { /// The transaction begin timestamp. begin_ts: u64, /// The transaction write set. - #[serde(with = "skipset_rowid")] write_set: SkipSet, /// The transaction read set. - #[serde(with = "skipset_rowid")] read_set: SkipSet, } -mod skipset_rowid { - use super::*; - use serde::{de, ser, ser::SerializeSeq}; - - struct SkipSetDeserializer; - - impl<'de> serde::de::Visitor<'de> for SkipSetDeserializer { - type Value = SkipSet; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("SkipSet key value sequence.") - } - - fn visit_seq(self, mut seq: A) -> std::result::Result - where - A: serde::de::SeqAccess<'de>, - { - let new_skipset = SkipSet::new(); - while let Some(elem) = seq.next_element()? { - new_skipset.insert(elem); - } - - Ok(new_skipset) - } - } - - pub fn serialize( - value: &SkipSet, - ser: S, - ) -> std::result::Result { - let mut set = ser.serialize_seq(Some(value.len()))?; - for v in value { - set.serialize_element(v.value())?; - } - set.end() - } - - pub fn deserialize<'de, D: de::Deserializer<'de>>( - de: D, - ) -> std::result::Result, D::Error> { - de.deserialize_seq(SkipSetDeserializer) - } -} - impl Transaction { fn new(tx_id: u64, begin_ts: u64) -> Transaction { Transaction { @@ -167,7 +119,7 @@ impl std::fmt::Display for Transaction { } /// Transaction state. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq)] enum TransactionState { Active, Preparing, @@ -207,7 +159,7 @@ impl TransactionState { } // Transaction state encoded into a single 64-bit atomic. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug)] pub(crate) struct AtomicTransactionState { pub(crate) state: AtomicU64, } @@ -256,10 +208,7 @@ impl AtomicTransactionState { } #[derive(Debug)] -pub struct Database< - Clock: LogicalClock, - T: Sync + Send + Clone + Serialize + Debug + DeserializeOwned, -> { +pub struct Database { rows: SkipMap>>>, txs: SkipMap>, tx_ids: AtomicU64, @@ -267,9 +216,7 @@ pub struct Database< storage: Storage, } -impl - Database -{ +impl Database { /// Creates a new database. pub fn new(clock: Clock, storage: Storage) -> Self { Self { @@ -672,7 +619,7 @@ impl = tx.write_set.iter().map(|v| *v.value()).collect(); drop(tx); - + for ref id in write_set { if let Some(row_versions) = self.rows.get(id) { let mut row_versions = row_versions.value().write().unwrap(); diff --git a/core/mvcc/mvcc-rs/src/database/tests.rs b/core/mvcc/database/tests.rs similarity index 85% rename from core/mvcc/mvcc-rs/src/database/tests.rs rename to core/mvcc/database/tests.rs index 225c34a0e..741ada4cb 100644 --- a/core/mvcc/mvcc-rs/src/database/tests.rs +++ b/core/mvcc/database/tests.rs @@ -1,14 +1,12 @@ use super::*; -use crate::clock::LocalClock; -use tracing_test::traced_test; +use crate::mvcc::clock::LocalClock; fn test_db() -> Database { let clock = LocalClock::new(); - let storage = crate::persistent_storage::Storage::new_noop(); + let storage = crate::mvcc::persistent_storage::Storage::new_noop(); Database::new(clock, storage) } -#[traced_test] #[test] fn test_insert_read() { let db = test_db(); @@ -49,7 +47,6 @@ fn test_insert_read() { assert_eq!(tx1_row, row); } -#[traced_test] #[test] fn test_read_nonexistent() { let db = test_db(); @@ -64,7 +61,6 @@ fn test_read_nonexistent() { assert!(row.unwrap().is_none()); } -#[traced_test] #[test] fn test_delete() { let db = test_db(); @@ -122,7 +118,6 @@ fn test_delete() { assert!(row.is_none()); } -#[traced_test] #[test] fn test_delete_nonexistent() { let db = test_db(); @@ -138,7 +133,6 @@ fn test_delete_nonexistent() { .unwrap()); } -#[traced_test] #[test] fn test_commit() { let db = test_db(); @@ -199,7 +193,6 @@ fn test_commit() { db.drop_unused_row_versions(); } -#[traced_test] #[test] fn test_rollback() { let db = test_db(); @@ -256,7 +249,6 @@ fn test_rollback() { assert_eq!(row5, None); } -#[traced_test] #[test] fn test_dirty_write() { let db = test_db(); @@ -307,7 +299,6 @@ fn test_dirty_write() { assert_eq!(tx1_row, row); } -#[traced_test] #[test] fn test_dirty_read() { let db = test_db(); @@ -337,7 +328,6 @@ fn test_dirty_read() { assert_eq!(row2, None); } -#[traced_test] #[test] fn test_dirty_read_deleted() { let db = test_db(); @@ -381,7 +371,6 @@ fn test_dirty_read_deleted() { assert_eq!(tx1_row, row); } -#[traced_test] #[test] fn test_fuzzy_read() { let db = test_db(); @@ -449,7 +438,6 @@ fn test_fuzzy_read() { assert_eq!(tx1_row, row); } -#[traced_test] #[test] fn test_lost_update() { let db = test_db(); @@ -521,7 +509,6 @@ fn test_lost_update() { // Test for the visibility to check if a new transaction can see old committed values. // This test checks for the typo present in the paper, explained in https://github.com/penberg/mvcc-rs/issues/15 -#[traced_test] #[test] fn test_committed_visibility() { let db = test_db(); @@ -576,7 +563,6 @@ fn test_committed_visibility() { } // Test to check if a older transaction can see (un)committed future rows -#[traced_test] #[test] fn test_future_row() { let db = test_db(); @@ -619,164 +605,6 @@ fn test_future_row() { assert_eq!(row, None); } -#[traced_test] -#[test] -fn test_storage1() { - let clock = LocalClock::new(); - let mut path = std::env::temp_dir(); - path.push(format!( - "mvcc-rs-storage-test-{}", - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos(), - )); - let storage = crate::persistent_storage::Storage::new_json_on_disk(path.clone()); - let db = Database::new(clock, storage); - - let tx1 = db.begin_tx(); - let tx2 = db.begin_tx(); - let tx3 = db.begin_tx(); - - db.insert( - tx3, - Row { - id: RowID { - table_id: 1, - row_id: 1, - }, - data: "testme".to_string(), - }, - ) - .unwrap(); - - db.commit_tx(tx1).unwrap(); - db.rollback_tx(tx2); - db.commit_tx(tx3).unwrap(); - - let tx4 = db.begin_tx(); - db.insert( - tx4, - Row { - id: RowID { - table_id: 1, - row_id: 2, - }, - data: "testme2".to_string(), - }, - ) - .unwrap(); - db.insert( - tx4, - Row { - id: RowID { - table_id: 1, - row_id: 3, - }, - data: "testme3".to_string(), - }, - ) - .unwrap(); - - assert_eq!( - db.read( - tx4, - RowID { - table_id: 1, - row_id: 1 - } - ) - .unwrap() - .unwrap() - .data, - "testme" - ); - assert_eq!( - db.read( - tx4, - RowID { - table_id: 1, - row_id: 2 - } - ) - .unwrap() - .unwrap() - .data, - "testme2" - ); - assert_eq!( - db.read( - tx4, - RowID { - table_id: 1, - row_id: 3 - } - ) - .unwrap() - .unwrap() - .data, - "testme3" - ); - db.commit_tx(tx4).unwrap(); - - let clock = LocalClock::new(); - let storage = crate::persistent_storage::Storage::new_json_on_disk(path); - let db: Database = Database::new(clock, storage); - db.recover().unwrap(); - println!("{:#?}", db); - - let tx5 = db.begin_tx(); - println!( - "{:#?}", - db.read( - tx5, - RowID { - table_id: 1, - row_id: 1 - } - ) - ); - assert_eq!( - db.read( - tx5, - RowID { - table_id: 1, - row_id: 1 - } - ) - .unwrap() - .unwrap() - .data, - "testme" - ); - assert_eq!( - db.read( - tx5, - RowID { - table_id: 1, - row_id: 2 - } - ) - .unwrap() - .unwrap() - .data, - "testme2" - ); - assert_eq!( - db.read( - tx5, - RowID { - table_id: 1, - row_id: 3 - } - ) - .unwrap() - .unwrap() - .data, - "testme3" - ); -} - /* States described in the Hekaton paper *for serializability*: Table 1: Case analysis of action to take when version V’s @@ -832,7 +660,6 @@ fn new_tx(tx_id: TxID, begin_ts: u64, state: TransactionState) -> RwLock> = SkipMap::from_iter([ diff --git a/core/mvcc/mvcc-rs/src/errors.rs b/core/mvcc/errors.rs similarity index 100% rename from core/mvcc/mvcc-rs/src/errors.rs rename to core/mvcc/errors.rs diff --git a/core/mvcc/mod.rs b/core/mvcc/mod.rs new file mode 100644 index 000000000..abcba7926 --- /dev/null +++ b/core/mvcc/mod.rs @@ -0,0 +1,160 @@ +//! Multiversion concurrency control (MVCC) for Rust. +//! +//! This module implements the main memory MVCC method outlined in the paper +//! "High-Performance Concurrency Control Mechanisms for Main-Memory Databases" +//! by Per-Åke Larson et al (VLDB, 2011). +//! +//! ## Data anomalies +//! +//! * A *dirty write* occurs when transaction T_m updates a value that is written by +//! transaction T_n but not yet committed. The MVCC algorithm prevents dirty +//! writes by validating that a row version is visible to transaction T_m before +//! allowing update to it. +//! +//! * A *dirty read* occurs when transaction T_m reads a value that was written by +//! transaction T_n but not yet committed. The MVCC algorithm prevents dirty +//! reads by validating that a row version is visible to transaction T_m. +//! +//! * A *fuzzy read* (non-repeatable read) occurs when transaction T_m reads a +//! different value in the course of the transaction because another +//! transaction T_n has updated the value. +//! +//! * A *lost update* occurs when transactions T_m and T_n both attempt to update +//! the same value, resulting in one of the updates being lost. The MVCC algorithm +//! prevents lost updates by detecting the write-write conflict and letting the +//! first-writer win by aborting the later transaction. +//! +//! TODO: phantom reads, cursor lost updates, read skew, write skew. +//! +//! ## TODO +//! +//! * Optimistic reads and writes +//! * Garbage collection + +pub mod clock; +pub mod cursor; +pub mod database; +pub mod errors; +pub mod persistent_storage; + +#[cfg(test)] +mod tests { + use crate::mvcc::clock::LocalClock; + use crate::mvcc::database::{Database, Row, RowID}; + use std::sync::atomic::AtomicU64; + use std::sync::atomic::Ordering; + use std::sync::{Arc, Once}; + + static IDS: AtomicU64 = AtomicU64::new(1); + + static START: Once = Once::new(); + + #[test] + fn test_non_overlapping_concurrent_inserts() { + // Two threads insert to the database concurrently using non-overlapping + // row IDs. + let clock = LocalClock::default(); + let storage = crate::mvcc::persistent_storage::Storage::new_noop(); + let db = Arc::new(Database::new(clock, storage)); + let iterations = 100000; + + let th1 = { + let db = db.clone(); + std::thread::spawn(move || { + for _ in 0..iterations { + let tx = db.begin_tx(); + let id = IDS.fetch_add(1, Ordering::SeqCst); + let id = RowID { + table_id: 1, + row_id: id, + }; + let row = Row { + id, + data: "Hello".to_string(), + }; + db.insert(tx, row.clone()).unwrap(); + db.commit_tx(tx).unwrap(); + let tx = db.begin_tx(); + let committed_row = db.read(tx, id).unwrap(); + db.commit_tx(tx).unwrap(); + assert_eq!(committed_row, Some(row)); + } + }) + }; + let th2 = { + std::thread::spawn(move || { + for _ in 0..iterations { + let tx = db.begin_tx(); + let id = IDS.fetch_add(1, Ordering::SeqCst); + let id = RowID { + table_id: 1, + row_id: id, + }; + let row = Row { + id, + data: "World".to_string(), + }; + db.insert(tx, row.clone()).unwrap(); + db.commit_tx(tx).unwrap(); + let tx = db.begin_tx(); + let committed_row = db.read(tx, id).unwrap(); + db.commit_tx(tx).unwrap(); + assert_eq!(committed_row, Some(row)); + } + }) + }; + th1.join().unwrap(); + th2.join().unwrap(); + } + + #[test] + fn test_overlapping_concurrent_inserts_read_your_writes() { + let clock = LocalClock::default(); + let storage = crate::mvcc::persistent_storage::Storage::new_noop(); + let db = Arc::new(Database::new(clock, storage)); + let iterations = 100000; + + let work = |prefix: &'static str| { + let db = db.clone(); + std::thread::spawn(move || { + let mut failed_upserts = 0; + for i in 0..iterations { + if i % 1000 == 0 { + tracing::debug!("{prefix}: {i}"); + } + if i % 10000 == 0 { + let dropped = db.drop_unused_row_versions(); + tracing::debug!("garbage collected {dropped} versions"); + } + let tx = db.begin_tx(); + let id = i % 16; + let id = RowID { + table_id: 1, + row_id: id, + }; + let row = Row { + id, + data: format!("{prefix} @{tx}"), + }; + if let Err(e) = db.upsert(tx, row.clone()) { + tracing::trace!("upsert failed: {e}"); + failed_upserts += 1; + continue; + } + let committed_row = db.read(tx, id).unwrap(); + db.commit_tx(tx).unwrap(); + assert_eq!(committed_row, Some(row)); + } + tracing::info!( + "{prefix}'s failed upserts: {failed_upserts}/{iterations} {:.2}%", + (failed_upserts * 100) as f64 / iterations as f64 + ); + }) + }; + + let threads = vec![work("A"), work("B"), work("C"), work("D")]; + for th in threads { + th.join().unwrap(); + } + } +} diff --git a/core/mvcc/mvcc-rs/src/lib.rs b/core/mvcc/mvcc-rs/src/lib.rs deleted file mode 100644 index 00eaee336..000000000 --- a/core/mvcc/mvcc-rs/src/lib.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! Multiversion concurrency control (MVCC) for Rust. -//! -//! This module implements the main memory MVCC method outlined in the paper -//! "High-Performance Concurrency Control Mechanisms for Main-Memory Databases" -//! by Per-Åke Larson et al (VLDB, 2011). -//! -//! ## Data anomalies -//! -//! * A *dirty write* occurs when transaction T_m updates a value that is written by -//! transaction T_n but not yet committed. The MVCC algorithm prevents dirty -//! writes by validating that a row version is visible to transaction T_m before -//! allowing update to it. -//! -//! * A *dirty read* occurs when transaction T_m reads a value that was written by -//! transaction T_n but not yet committed. The MVCC algorithm prevents dirty -//! reads by validating that a row version is visible to transaction T_m. -//! -//! * A *fuzzy read* (non-repeatable read) occurs when transaction T_m reads a -//! different value in the course of the transaction because another -//! transaction T_n has updated the value. -//! -//! * A *lost update* occurs when transactions T_m and T_n both attempt to update -//! the same value, resulting in one of the updates being lost. The MVCC algorithm -//! prevents lost updates by detecting the write-write conflict and letting the -//! first-writer win by aborting the later transaction. -//! -//! TODO: phantom reads, cursor lost updates, read skew, write skew. -//! -//! ## TODO -//! -//! * Optimistic reads and writes -//! * Garbage collection - -pub mod clock; -pub mod cursor; -pub mod database; -pub mod errors; -pub mod persistent_storage; diff --git a/core/mvcc/mvcc-rs/src/persistent_storage/mod.rs b/core/mvcc/mvcc-rs/src/persistent_storage/mod.rs deleted file mode 100644 index e7d17923b..000000000 --- a/core/mvcc/mvcc-rs/src/persistent_storage/mod.rs +++ /dev/null @@ -1,71 +0,0 @@ -use serde::Serialize; -use serde::de::DeserializeOwned; -use std::fmt::Debug; - -use crate::database::{LogRecord, Result}; -use crate::errors::DatabaseError; - -#[derive(Debug)] -pub enum Storage { - Noop, - JsonOnDisk(std::path::PathBuf), -} - -impl Storage { - pub fn new_noop() -> Self { - Self::Noop - } - - pub fn new_json_on_disk(path: impl Into) -> Self { - let path = path.into(); - Self::JsonOnDisk(path) - } -} - -impl Storage { - pub fn log_tx(&self, m: LogRecord) -> Result<()> { - match self { - Self::JsonOnDisk(path) => { - use std::io::Write; - let t = serde_json::to_vec(&m).map_err(|e| DatabaseError::Io(e.to_string()))?; - let mut file = std::fs::OpenOptions::new() - .create(true) - .append(true) - .open(path) - .map_err(|e| DatabaseError::Io(e.to_string()))?; - file.write_all(&t) - .map_err(|e| DatabaseError::Io(e.to_string()))?; - file.write_all(b"\n") - .map_err(|e| DatabaseError::Io(e.to_string()))?; - } - Self::Noop => (), - } - Ok(()) - } - - pub fn read_tx_log(&self) -> Result>> { - match self { - Self::JsonOnDisk(path) => { - use std::io::BufRead; - let file = std::fs::OpenOptions::new() - .read(true) - .open(path) - .map_err(|e| DatabaseError::Io(e.to_string()))?; - - let mut records: Vec> = Vec::new(); - let mut lines = std::io::BufReader::new(file).lines(); - while let Some(Ok(line)) = lines.next() { - records.push( - serde_json::from_str(&line) - .map_err(|e| DatabaseError::Io(e.to_string()))?, - ) - } - Ok(records) - } - Self::S3(replicator) => futures::executor::block_on(replicator.read_tx_log()), - Self::Noop => Err(crate::errors::DatabaseError::Io( - "cannot read from Noop storage".to_string(), - )), - } - } -} diff --git a/core/mvcc/mvcc-rs/tests/concurrency_test.rs b/core/mvcc/mvcc-rs/tests/concurrency_test.rs deleted file mode 100644 index f7d77893e..000000000 --- a/core/mvcc/mvcc-rs/tests/concurrency_test.rs +++ /dev/null @@ -1,124 +0,0 @@ -use mvcc_rs::clock::LocalClock; -use mvcc_rs::database::{Database, Row, RowID}; -use std::sync::atomic::AtomicU64; -use std::sync::atomic::Ordering; -use std::sync::{Arc, Once}; - -static IDS: AtomicU64 = AtomicU64::new(1); - -static START: Once = Once::new(); - -#[test] -fn test_non_overlapping_concurrent_inserts() { - START.call_once(|| { - tracing_subscriber::fmt::init(); - }); - // Two threads insert to the database concurrently using non-overlapping - // row IDs. - let clock = LocalClock::default(); - let storage = mvcc_rs::persistent_storage::Storage::new_noop(); - let db = Arc::new(Database::new(clock, storage)); - let iterations = 100000; - - let th1 = { - let db = db.clone(); - std::thread::spawn(move || { - for _ in 0..iterations { - let tx = db.begin_tx(); - let id = IDS.fetch_add(1, Ordering::SeqCst); - let id = RowID { - table_id: 1, - row_id: id, - }; - let row = Row { - id, - data: "Hello".to_string(), - }; - db.insert(tx, row.clone()).unwrap(); - db.commit_tx(tx).unwrap(); - let tx = db.begin_tx(); - let committed_row = db.read(tx, id).unwrap(); - db.commit_tx(tx).unwrap(); - assert_eq!(committed_row, Some(row)); - } - }) - }; - let th2 = { - std::thread::spawn(move || { - for _ in 0..iterations { - let tx = db.begin_tx(); - let id = IDS.fetch_add(1, Ordering::SeqCst); - let id = RowID { - table_id: 1, - row_id: id, - }; - let row = Row { - id, - data: "World".to_string(), - }; - db.insert(tx, row.clone()).unwrap(); - db.commit_tx(tx).unwrap(); - let tx = db.begin_tx(); - let committed_row = db.read(tx, id).unwrap(); - db.commit_tx(tx).unwrap(); - assert_eq!(committed_row, Some(row)); - } - }) - }; - th1.join().unwrap(); - th2.join().unwrap(); -} - -#[test] -fn test_overlapping_concurrent_inserts_read_your_writes() { - START.call_once(|| { - tracing_subscriber::fmt::init(); - }); // Two threads insert to the database concurrently using overlapping row IDs. - let clock = LocalClock::default(); - let storage = mvcc_rs::persistent_storage::Storage::new_noop(); - let db = Arc::new(Database::new(clock, storage)); - let iterations = 100000; - - let work = |prefix: &'static str| { - let db = db.clone(); - std::thread::spawn(move || { - let mut failed_upserts = 0; - for i in 0..iterations { - if i % 1000 == 0 { - tracing::debug!("{prefix}: {i}"); - } - if i % 10000 == 0 { - let dropped = db.drop_unused_row_versions(); - tracing::debug!("garbage collected {dropped} versions"); - } - let tx = db.begin_tx(); - let id = i % 16; - let id = RowID { - table_id: 1, - row_id: id, - }; - let row = Row { - id, - data: format!("{prefix} @{tx}"), - }; - if let Err(e) = db.upsert(tx, row.clone()) { - tracing::trace!("upsert failed: {e}"); - failed_upserts += 1; - continue; - } - let committed_row = db.read(tx, id).unwrap(); - db.commit_tx(tx).unwrap(); - assert_eq!(committed_row, Some(row)); - } - tracing::info!( - "{prefix}'s failed upserts: {failed_upserts}/{iterations} {:.2}%", - (failed_upserts * 100) as f64 / iterations as f64 - ); - }) - }; - - let threads = vec![work("A"), work("B"), work("C"), work("D")]; - for th in threads { - th.join().unwrap(); - } -} diff --git a/core/mvcc/persistent_storage/mod.rs b/core/mvcc/persistent_storage/mod.rs new file mode 100644 index 000000000..3f9ff2171 --- /dev/null +++ b/core/mvcc/persistent_storage/mod.rs @@ -0,0 +1,32 @@ +use std::fmt::Debug; + +use crate::mvcc::database::{LogRecord, Result}; +use crate::mvcc::errors::DatabaseError; + +#[derive(Debug)] +pub enum Storage { + Noop, +} + +impl Storage { + pub fn new_noop() -> Self { + Self::Noop + } +} + +impl Storage { + pub fn log_tx(&self, _m: LogRecord) -> Result<()> { + match self { + Self::Noop => (), + } + Ok(()) + } + + pub fn read_tx_log(&self) -> Result>> { + match self { + Self::Noop => Err(DatabaseError::Io( + "cannot read from Noop storage".to_string(), + )), + } + } +} From 44ca85e1214c64cb03452d90fa265b34b74f2bac Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 5 Feb 2025 13:26:05 +0200 Subject: [PATCH 125/128] core: Enable MVCC benchmark --- core/Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/Cargo.toml b/core/Cargo.toml index e4db1ee95..f2bba5f59 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -94,3 +94,7 @@ tempfile = "3.8.0" [[bench]] name = "benchmark" harness = false + +[[bench]] +name = "mvcc_benchmark" +harness = false From 5870c92e9e895aafbfc252a403036bd2b916f6b3 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 5 Feb 2025 13:33:38 +0200 Subject: [PATCH 126/128] core/mvcc: Fix MVCC benchmark SIGKILL The `begin_tx` benchmark makes no sense because it just fills up memory with transaction metadata, eventually killing the process... --- core/benches/mvcc_benchmark.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/core/benches/mvcc_benchmark.rs b/core/benches/mvcc_benchmark.rs index 14fc2d61e..899c8b82d 100644 --- a/core/benches/mvcc_benchmark.rs +++ b/core/benches/mvcc_benchmark.rs @@ -14,13 +14,6 @@ fn bench(c: &mut Criterion) { let mut group = c.benchmark_group("mvcc-ops-throughput"); group.throughput(Throughput::Elements(1)); - let db = bench_db(); - group.bench_function("begin_tx", |b| { - b.to_async(FuturesExecutor).iter(|| async { - db.begin_tx(); - }) - }); - let db = bench_db(); group.bench_function("begin_tx + rollback_tx", |b| { b.to_async(FuturesExecutor).iter(|| async { From 36b487d281d0fe6e613e91237a60fb42aa670cf4 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 5 Feb 2025 13:41:20 +0200 Subject: [PATCH 127/128] core/mvcc: Make Clippy happy --- core/mvcc/database/mod.rs | 2 +- core/mvcc/mod.rs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/core/mvcc/database/mod.rs b/core/mvcc/database/mod.rs index 30e42c303..6fa8420f3 100644 --- a/core/mvcc/database/mod.rs +++ b/core/mvcc/database/mod.rs @@ -630,7 +630,7 @@ impl Database Date: Wed, 5 Feb 2025 13:44:55 +0200 Subject: [PATCH 128/128] core/mvcc: Thanks Clippy... --- core/mvcc/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/mvcc/mod.rs b/core/mvcc/mod.rs index 1389b4d76..57e3c717e 100644 --- a/core/mvcc/mod.rs +++ b/core/mvcc/mod.rs @@ -43,7 +43,7 @@ mod tests { use crate::mvcc::database::{Database, Row, RowID}; use std::sync::atomic::AtomicU64; use std::sync::atomic::Ordering; - use std::sync::{Arc, Once}; + use std::sync::Arc; static IDS: AtomicU64 = AtomicU64::new(1);