From 1036ca6ac1bb205e2cbd025eb8a458b8b85240aa Mon Sep 17 00:00:00 2001 From: ardocrat Date: Thu, 16 Apr 2026 12:12:34 +0300 Subject: [PATCH 01/37] db: migrate from lmdb-zero to heed --- Cargo.lock | 280 ++++++++++++++-------- chain/src/chain.rs | 49 ++-- chain/src/linked_list.rs | 39 ++- chain/src/pipe.rs | 35 +-- chain/src/store.rs | 40 ++-- chain/src/txhashset/desegmenter.rs | 14 +- chain/src/txhashset/txhashset.rs | 52 ++-- chain/tests/store_indices.rs | 2 +- chain/tests/store_kernel_pos_index.rs | 106 ++++----- chain/tests/test_block_known.rs | 2 +- p2p/src/peers.rs | 64 ++--- p2p/src/store.rs | 92 +++---- servers/Cargo.toml | 1 - store/Cargo.toml | 2 +- store/src/lmdb.rs | 330 +++++++++++--------------- store/tests/lmdb.rs | 8 +- 16 files changed, 589 insertions(+), 527 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 429a99f9bb..74db18b779 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,16 +187,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] -name = "bit-vec" -version = "0.6.3" +name = "bincode" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] [[package]] -name = "bitflags" -version = "0.9.1" +name = "bit-vec" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bitflags" @@ -206,9 +209,12 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] [[package]] name = "blake2-rfc" @@ -273,10 +279,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.16" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -422,6 +429,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.20" @@ -471,8 +487,6 @@ dependencies = [ "lazy_static", "libc", "log", - "maplit", - "pancurses", "signal-hook", "unicode-segmentation", "unicode-width", @@ -626,6 +640,15 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "doxygen-rs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "415b6ec780d34dcf624666747194393603d0373b7141eef01d12ee58881507d9" +dependencies = [ + "phf", +] + [[package]] name = "easy-jsonrpc-mw" version = "0.5.4" @@ -748,6 +771,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "flate2" version = "1.0.32" @@ -884,12 +913,6 @@ dependencies = [ "slab", ] -[[package]] -name = "gcc" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" - [[package]] name = "generic-array" version = "0.14.7" @@ -923,7 +946,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "libc", "libgit2-sys", "log", @@ -1054,7 +1077,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "siphasher", + "siphasher 0.3.11", "thiserror", "zeroize", ] @@ -1153,7 +1176,6 @@ dependencies = [ "http", "hyper", "hyper-rustls", - "lmdb-zero", "log", "rand 0.6.5", "rustls", @@ -1176,8 +1198,8 @@ dependencies = [ "filetime", "grin_core", "grin_util", + "heed", "libc", - "lmdb-zero", "log", "memmap", "rand 0.6.5", @@ -1242,6 +1264,44 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heed" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad82d6598ccf1dac15c8b758a1bd282b755b6776be600429176757190a1b0202" +dependencies = [ + "bitflags 2.11.1", + "byteorder", + "heed-traits", + "heed-types", + "libc", + "lmdb-master-sys", + "once_cell", + "page_size", + "serde", + "synchronoise", + "url", +] + +[[package]] +name = "heed-traits" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3130048d404c57ce5a1ac61a903696e8fcde7e8c2991e9fcfc1f27c3ef74ff" + +[[package]] +name = "heed-types" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c255bdf46e07fb840d120a36dcc81f385140d7191c76a7391672675c01a55d" +dependencies = [ + "bincode", + "byteorder", + "heed-traits", + "serde", + "serde_json", +] + [[package]] name = "hermit-abi" version = "0.1.19" @@ -1559,9 +1619,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.158" +version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "libgit2-sys" @@ -1575,23 +1635,13 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "liblmdb-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feed38a3a580f60bf61aaa067b0ff4123395966839adeaf67258a9e50c4d2e49" -dependencies = [ - "gcc", - "libc", -] - [[package]] name = "libredox" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "libc", "redox_syscall 0.5.3", ] @@ -1627,15 +1677,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] -name = "lmdb-zero" -version = "0.4.4" +name = "lmdb-master-sys" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13416eee745b087c22934f35f1f24da22da41ba2a5ce197143d168ce055cc58d" +checksum = "aaeb9bd22e73bd1babffff614994b341e9b2008de7bb73bf1f7e9154f1978f8b" dependencies = [ - "bitflags 0.9.1", + "cc", + "doxygen-rs", "libc", - "liblmdb-sys", - "supercow", ] [[package]] @@ -1710,12 +1759,6 @@ dependencies = [ "linked-hash-map", ] -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - [[package]] name = "memchr" version = "2.7.4" @@ -1762,24 +1805,13 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "ncurses" -version = "5.101.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2c5d34d72657dc4b638a1c25d40aae81e4f1c699062f72f467237920752032" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - [[package]] name = "nix" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "cfg-if 1.0.0", "cfg_aliases", "libc", @@ -1934,9 +1966,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "opaque-debug" @@ -1969,16 +2001,13 @@ dependencies = [ ] [[package]] -name = "pancurses" -version = "0.17.0" +name = "page_size" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0352975c36cbacb9ee99bfb709b9db818bed43af57751797f8633649759d13db" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" dependencies = [ "libc", - "log", - "ncurses", - "pdcurses-sys", - "winreg", + "winapi", ] [[package]] @@ -2053,20 +2082,52 @@ dependencies = [ ] [[package]] -name = "pdcurses-sys" -version = "0.7.1" +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084dd22796ff60f1225d4eb6329f33afaf4c85419d51d440ab6b8c6f4529166b" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ - "cc", - "libc", + "phf_macros", + "phf_shared", ] [[package]] -name = "percent-encoding" -version = "2.3.2" +name = "phf_generator" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2 1.0.86", + "quote 1.0.36", + "syn 2.0.87", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.2", +] [[package]] name = "pin-project-lite" @@ -2334,7 +2395,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", ] [[package]] @@ -2429,7 +2490,7 @@ version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys", @@ -2521,7 +2582,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "core-foundation", "core-foundation-sys", "libc", @@ -2540,10 +2601,11 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.208" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] @@ -2557,11 +2619,20 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + [[package]] name = "serde_derive" -version = "1.0.208" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", @@ -2570,14 +2641,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.125" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] @@ -2637,6 +2709,12 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + [[package]] name = "slab" version = "0.4.9" @@ -2692,12 +2770,6 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "supercow" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171758edb47aa306a78dfa4ab9aeb5167405bd4e3dc2b64e88f6a84bbe98bd63" - [[package]] name = "syn" version = "0.15.44" @@ -2731,6 +2803,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synchronoise" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dbc01390fc626ce8d1cffe3376ded2b72a11bb70e1c75f404a210e4daa4def2" +dependencies = [ + "crossbeam-queue", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -3283,15 +3364,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winreg" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27a759395c1195c4cc5cda607ef6f8f6498f64e78f7900f5de0a127a424704a" -dependencies = [ - "winapi", -] - [[package]] name = "writeable" version = "0.6.2" @@ -3438,3 +3510,9 @@ dependencies = [ "crc32fast", "thiserror", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/chain/src/chain.rs b/chain/src/chain.rs index 273a44153a..aabadc3224 100644 --- a/chain/src/chain.rs +++ b/chain/src/chain.rs @@ -150,8 +150,8 @@ pub struct Chain { store: Arc, adapter: Arc, orphans: Arc, - txhashset: Arc>, - header_pmmr: Arc>>, + txhashset: Arc>, + header_pmmr: Arc>>, pibd_segmenter: Arc>>, pibd_desegmenter: Arc>>, // POW verification function @@ -189,9 +189,9 @@ impl Chain { // Initialize the output_pos index based on UTXO set // and NRD kernel_pos index based recent kernel history. { - let batch = store.batch()?; - txhashset.init_output_pos_index(&header_pmmr, &batch)?; - txhashset.init_recent_kernel_pos_index(&header_pmmr, &batch)?; + let mut batch = store.batch()?; + txhashset.init_output_pos_index(&header_pmmr, &mut batch)?; + txhashset.init_recent_kernel_pos_index(&header_pmmr, &mut batch)?; batch.commit()?; } @@ -275,7 +275,7 @@ impl Chain { pub fn reset_chain_head_to_genesis(&self) -> Result<(), Error> { let mut header_pmmr = self.header_pmmr.write(); let mut txhashset = self.txhashset.write(); - let batch = self.store.batch()?; + let mut batch = self.store.batch()?; // Change head back to genesis { @@ -314,7 +314,7 @@ impl Chain { /// Reset PIBD head pub fn reset_pibd_head(&self) -> Result<(), Error> { - let batch = self.store.batch()?; + let mut batch = self.store.batch()?; batch.save_pibd_head(&self.genesis().into())?; Ok(()) } @@ -530,9 +530,9 @@ impl Chain { pub fn new_ctx<'a>( &self, opts: Options, - batch: store::Batch<'a>, - header_pmmr: &'a mut txhashset::PMMRHandle, - txhashset: &'a mut txhashset::TxHashSet, + batch: Batch<'a>, + header_pmmr: &'a mut PMMRHandle, + txhashset: &'a mut TxHashSet, ) -> Result, Error> { let denylist = self.denylist.read().clone(); Ok(pipe::BlockContext { @@ -832,7 +832,7 @@ impl Chain { &self, header: &BlockHeader, ext: &mut ExtensionPair, - batch: &Batch, + batch: &mut Batch, ) -> Result { let denylist = self.denylist.read().clone(); pipe::rewind_and_apply_fork(header, ext, batch, &|header| { @@ -846,7 +846,7 @@ impl Chain { &self, header: &BlockHeader, ext: &mut HeaderExtension, - batch: &Batch, + batch: &mut Batch, ) -> Result<(), Error> { let denylist = self.denylist.read().clone(); pipe::rewind_and_apply_header_fork(header, ext, batch, &|header| { @@ -1015,7 +1015,7 @@ impl Chain { fn validate_kernel_history( &self, header: &BlockHeader, - txhashset: &txhashset::TxHashSet, + txhashset: &TxHashSet, ) -> Result<(), Error> { debug!("validate_kernel_history: rewinding and validating kernel history (readonly)"); @@ -1151,11 +1151,11 @@ impl Chain { self.validate_kernel_history(&header, &txhashset)?; let header_pmmr = self.header_pmmr.read(); - let batch = self.store.batch()?; + let mut batch = self.store.batch()?; txhashset.verify_kernel_pos_index( &self.genesis.header, &header_pmmr, - &batch, + &mut batch, None, None, )?; @@ -1213,10 +1213,10 @@ impl Chain { } // Rebuild our output_pos index in the db based on fresh UTXO set. - txhashset.init_output_pos_index(&header_pmmr, &batch)?; + txhashset.init_output_pos_index(&header_pmmr, &mut batch)?; // Rebuild our NRD kernel_pos index based on recent kernel history. - txhashset.init_recent_kernel_pos_index(&header_pmmr, &batch)?; + txhashset.init_recent_kernel_pos_index(&header_pmmr, &mut batch)?; // Commit all the changes to the db. batch.commit()?; @@ -1257,9 +1257,9 @@ impl Chain { /// *Only* runs if we are not in archive mode. fn remove_historical_blocks( &self, - header_pmmr: &txhashset::PMMRHandle, + header_pmmr: &PMMRHandle, archive_header: BlockHeader, - batch: &store::Batch<'_>, + batch: &mut Batch<'_>, ) -> Result<(), Error> { if self.archive_mode() { return Ok(()); @@ -1345,7 +1345,7 @@ impl Chain { // Take a write lock on the txhashet and start a new writeable db batch. let header_pmmr = self.header_pmmr.read(); let mut txhashset = self.txhashset.write(); - let batch = self.store.batch()?; + let mut batch = self.store.batch()?; // Compact the txhashset itself (rewriting the pruned backend files). { @@ -1361,15 +1361,15 @@ impl Chain { // If we are not in archival mode remove historical blocks from the db. if !self.archive_mode() { - self.remove_historical_blocks(&header_pmmr, archive_header, &batch)?; + self.remove_historical_blocks(&header_pmmr, archive_header, &mut batch)?; } // Make sure our output_pos index is consistent with the UTXO set. - txhashset.init_output_pos_index(&header_pmmr, &batch)?; + txhashset.init_output_pos_index(&header_pmmr, &mut batch)?; // TODO - Why is this part of chain compaction? // Rebuild our NRD kernel_pos index based on recent kernel history. - txhashset.init_recent_kernel_pos_index(&header_pmmr, &batch)?; + txhashset.init_recent_kernel_pos_index(&header_pmmr, &mut batch)?; // Commit all the above db changes. batch.commit()?; @@ -1439,7 +1439,8 @@ impl Chain { 0 } else { self.get_header_by_height(start_block_height - 1)? - .output_mmr_size + 1 + .output_mmr_size + + 1 }; let end_mmr_size = self.get_header_by_height(end_block_height)?.output_mmr_size; Ok((start_mmr_size, end_mmr_size)) diff --git a/chain/src/linked_list.rs b/chain/src/linked_list.rs index 14039b6626..79d22564fe 100644 --- a/chain/src/linked_list.rs +++ b/chain/src/linked_list.rs @@ -108,7 +108,7 @@ pub trait ListIndex { /// Push a pos onto the list for the specified commitment. fn push_pos( &self, - batch: &Batch<'_>, + batch: &mut Batch<'_>, commit: Commitment, new_pos: ::Pos, ) -> Result<(), Error>; @@ -116,7 +116,7 @@ pub trait ListIndex { /// Pop a pos off the list for the specified commitment. fn pop_pos( &self, - batch: &Batch<'_>, + batch: &mut Batch<'_>, commit: Commitment, ) -> Result::Pos>, Error>; } @@ -124,7 +124,12 @@ pub trait ListIndex { /// Supports "rewind" given the provided commit and a pos to rewind back to. pub trait RewindableListIndex { /// Rewind the index for the given commitment to the specified position. - fn rewind(&self, batch: &Batch<'_>, commit: Commitment, rewind_pos: u64) -> Result<(), Error>; + fn rewind( + &self, + batch: &mut Batch<'_>, + commit: Commitment, + rewind_pos: u64, + ) -> Result<(), Error>; } /// A pruneable list index supports pruning of old data from the index lists. @@ -133,15 +138,20 @@ pub trait RewindableListIndex { pub trait PruneableListIndex: ListIndex { /// Clear all data from the index. /// Used when rebuilding the index. - fn clear(&self, batch: &Batch<'_>) -> Result<(), Error>; + fn clear(&self, batch: &mut Batch<'_>) -> Result<(), Error>; /// Prune old data. - fn prune(&self, batch: &Batch<'_>, commit: Commitment, cutoff_pos: u64) -> Result<(), Error>; + fn prune( + &self, + batch: &mut Batch<'_>, + commit: Commitment, + cutoff_pos: u64, + ) -> Result<(), Error>; /// Pop a pos off the back of the list (used for pruning old data). fn pop_pos_back( &self, - batch: &Batch<'_>, + batch: &mut Batch<'_>, commit: Commitment, ) -> Result::Pos>, Error>; } @@ -255,7 +265,7 @@ where } } - fn push_pos(&self, batch: &Batch<'_>, commit: Commitment, new_pos: T) -> Result<(), Error> { + fn push_pos(&self, batch: &mut Batch<'_>, commit: Commitment, new_pos: T) -> Result<(), Error> { match self.get_list(batch, commit)? { None => { let list = ListWrapper::Single { pos: new_pos }; @@ -327,7 +337,7 @@ where /// Pop the head of the list. /// Returns the output_pos. /// Returns None if list was empty. - fn pop_pos(&self, batch: &Batch<'_>, commit: Commitment) -> Result, Error> { + fn pop_pos(&self, batch: &mut Batch<'_>, commit: Commitment) -> Result, Error> { match self.get_list(batch, commit)? { None => Ok(None), Some(ListWrapper::Single { pos }) => { @@ -373,7 +383,12 @@ where /// List index that supports rewind. impl RewindableListIndex for MultiIndex { - fn rewind(&self, batch: &Batch<'_>, commit: Commitment, rewind_pos: u64) -> Result<(), Error> { + fn rewind( + &self, + batch: &mut Batch<'_>, + commit: Commitment, + rewind_pos: u64, + ) -> Result<(), Error> { while self .peek_pos(batch, commit)? .map(|x| x.pos() > rewind_pos) @@ -386,7 +401,7 @@ impl RewindableListIndex for MultiIndex { } impl PruneableListIndex for MultiIndex { - fn clear(&self, batch: &Batch<'_>) -> Result<(), Error> { + fn clear(&self, batch: &mut Batch<'_>) -> Result<(), Error> { let mut list_count = 0; let mut entry_count = 0; let prefix = to_key(self.list_prefix, ""); @@ -409,7 +424,7 @@ impl PruneableListIndex for MultiIndex { /// Pruning will be more performant than full rebuild but not yet necessary. fn prune( &self, - _batch: &Batch<'_>, + _batch: &mut Batch<'_>, _commit: Commitment, _cutoff_pos: u64, ) -> Result<(), Error> { @@ -420,7 +435,7 @@ impl PruneableListIndex for MultiIndex { /// Pop off the back/tail of the linked list. /// Used when pruning old data. - fn pop_pos_back(&self, batch: &Batch<'_>, commit: Commitment) -> Result, Error> { + fn pop_pos_back(&self, batch: &mut Batch<'_>, commit: Commitment) -> Result, Error> { match self.get_list(batch, commit)? { None => Ok(None), Some(ListWrapper::Single { pos }) => { diff --git a/chain/src/pipe.rs b/chain/src/pipe.rs index e49ea57924..820b3a07c4 100644 --- a/chain/src/pipe.rs +++ b/chain/src/pipe.rs @@ -161,11 +161,11 @@ pub fn process_block( // Note we do this in the outer batch, not the child batch from the extension // as we only commit the child batch if the extension increases total work. // We want to save the block to the db regardless. - add_block(b, &ctx.batch)?; + add_block(b, &mut ctx.batch)?; // If we have no "tail" then set it now. if ctx.batch.tail().is_err() { - update_body_tail(&b.header, &ctx.batch)?; + update_body_tail(&b.header, &mut ctx.batch)?; } if has_more_work(&b.header, &head) { @@ -198,13 +198,13 @@ pub fn process_block_headers( // Note: This batch may later be committed even if the MMR itself is rollbacked. for header in headers { validate_header(header, ctx)?; - add_block_header(header, &ctx.batch)?; + add_block_header(header, &mut ctx.batch)?; } let ctx_specific_validation = &ctx.header_allowed; // Now apply this entire chunk of headers to the header MMR. - txhashset::header_extending(&mut ctx.header_pmmr, &mut ctx.batch, |ext, batch| { + txhashset::header_extending(&mut ctx.header_pmmr, &mut ctx.batch, |ext, mut batch| { rewind_and_apply_header_fork(&last_header, ext, batch, ctx_specific_validation)?; // If previous sync_head is not on the "current" chain then @@ -216,7 +216,7 @@ pub fn process_block_headers( // Note the outer batch may still be committed to db assuming no errors occur in the extension. if has_more_work(last_header, &head) { let header_head = last_header.into(); - update_header_head(&header_head, &batch)?; + update_header_head(&header_head, &mut batch)?; } else { ext.force_rollback(); }; @@ -275,7 +275,7 @@ pub fn process_block_header(header: &BlockHeader, ctx: &mut BlockContext<'_>) -> })?; // Add this new block header to the db. - add_block_header(header, &ctx.batch)?; + add_block_header(header, &mut ctx.batch)?; if has_more_work(header, &header_head) { update_header_head(&Tip::from_header(header), &mut ctx.batch)?; @@ -478,7 +478,7 @@ fn verify_coinbase_maturity( /// Verify kernel sums across the full utxo and kernel sets based on block_sums /// of previous block accounting for the inputs|outputs|kernels of the new block. /// Saves the new block_sums to the db via the current batch if successful. -fn verify_block_sums(b: &Block, batch: &store::Batch<'_>) -> Result<(), Error> { +fn verify_block_sums(b: &Block, batch: &mut store::Batch<'_>) -> Result<(), Error> { // Retrieve the block_sums for the previous block. let block_sums = batch.get_block_sums(&b.header.prev_hash)?; @@ -509,7 +509,7 @@ fn verify_block_sums(b: &Block, batch: &store::Batch<'_>) -> Result<(), Error> { fn apply_block_to_txhashset( block: &Block, ext: &mut txhashset::ExtensionPair<'_>, - batch: &store::Batch<'_>, + batch: &mut store::Batch<'_>, ) -> Result<(), Error> { ext.extension .apply_block(block, ext.header_extension, batch)?; @@ -520,13 +520,13 @@ fn apply_block_to_txhashset( /// Officially adds the block to our chain (possibly on a losing fork). /// Header must be added separately (assume this has been done previously). -fn add_block(b: &Block, batch: &store::Batch<'_>) -> Result<(), Error> { +fn add_block(b: &Block, batch: &mut store::Batch<'_>) -> Result<(), Error> { batch.save_block(b)?; Ok(()) } /// Update the block chain tail so we can know the exact tail of full blocks in this node -fn update_body_tail(bh: &BlockHeader, batch: &store::Batch<'_>) -> Result<(), Error> { +fn update_body_tail(bh: &BlockHeader, batch: &mut store::Batch<'_>) -> Result<(), Error> { let tip = Tip::from_header(bh); batch .save_body_tail(&tip) @@ -536,27 +536,28 @@ fn update_body_tail(bh: &BlockHeader, batch: &store::Batch<'_>) -> Result<(), Er } /// Officially adds the block header to our header chain. -fn add_block_header(bh: &BlockHeader, batch: &store::Batch<'_>) -> Result<(), Error> { +fn add_block_header(bh: &BlockHeader, batch: &mut store::Batch<'_>) -> Result<(), Error> { batch .save_block_header(bh) .map_err(|e| Error::StoreErr(e, "pipe save header".to_owned()))?; Ok(()) } -fn update_header_head(head: &Tip, batch: &store::Batch<'_>) -> Result<(), Error> { +fn update_header_head(head: &Tip, batch: &mut store::Batch<'_>) -> Result<(), Error> { batch .save_header_head(&head) .map_err(|e| Error::StoreErr(e, "pipe save header head".to_owned()))?; - debug!( + trace!( "header head updated to {} at {}", - head.last_block_h, head.height + head.last_block_h, + head.height ); Ok(()) } -fn update_head(head: &Tip, batch: &store::Batch<'_>) -> Result<(), Error> { +fn update_head(head: &Tip, batch: &mut store::Batch<'_>) -> Result<(), Error> { batch .save_body_head(&head) .map_err(|e| Error::StoreErr(e, "pipe save body".to_owned()))?; @@ -575,7 +576,7 @@ fn has_more_work(header: &BlockHeader, head: &Tip) -> bool { pub fn rewind_and_apply_header_fork( header: &BlockHeader, ext: &mut txhashset::HeaderExtension<'_>, - batch: &store::Batch<'_>, + batch: &mut store::Batch<'_>, ctx_specific_validation: &dyn Fn(&BlockHeader) -> Result<(), Error>, ) -> Result<(), Error> { let mut fork_hashes = vec![]; @@ -616,7 +617,7 @@ pub fn rewind_and_apply_header_fork( pub fn rewind_and_apply_fork( header: &BlockHeader, ext: &mut txhashset::ExtensionPair<'_>, - batch: &store::Batch<'_>, + batch: &mut store::Batch<'_>, ctx_specific_validation: &dyn Fn(&BlockHeader) -> Result<(), Error>, ) -> Result { let extension = &mut ext.extension; diff --git a/chain/src/store.rs b/chain/src/store.rs index e11d1f4863..56fb706018 100644 --- a/chain/src/store.rs +++ b/chain/src/store.rs @@ -201,22 +201,22 @@ impl<'a> Batch<'a> { } /// Save body head to db. - pub fn save_body_head(&self, t: &Tip) -> Result<(), Error> { + pub fn save_body_head(&mut self, t: &Tip) -> Result<(), Error> { self.db.put_ser(&[HEAD_PREFIX], t) } /// Save body "tail" to db. - pub fn save_body_tail(&self, t: &Tip) -> Result<(), Error> { + pub fn save_body_tail(&mut self, t: &Tip) -> Result<(), Error> { self.db.put_ser(&[TAIL_PREFIX], t) } /// Save header head to db. - pub fn save_header_head(&self, t: &Tip) -> Result<(), Error> { + pub fn save_header_head(&mut self, t: &Tip) -> Result<(), Error> { self.db.put_ser(&[HEADER_HEAD_PREFIX], t) } /// Save PIBD head to db. - pub fn save_pibd_head(&self, t: &Tip) -> Result<(), Error> { + pub fn save_pibd_head(&mut self, t: &Tip) -> Result<(), Error> { self.db.put_ser(&[PIBD_HEAD_PREFIX], t) } @@ -234,7 +234,7 @@ impl<'a> Batch<'a> { /// Save the block to the db. /// Note: the block header is not saved to the db here, assumes this has already been done. - pub fn save_block(&self, b: &Block) -> Result<(), Error> { + pub fn save_block(&mut self, b: &Block) -> Result<(), Error> { debug!( "save_block: {} at {} ({} -> v{})", b.header.hash(), @@ -248,20 +248,20 @@ impl<'a> Batch<'a> { /// We maintain a "spent" index for each full block to allow the output_pos /// to be easily reverted during rewind. - pub fn save_spent_index(&self, h: &Hash, spent: &[CommitPos]) -> Result<(), Error> { + pub fn save_spent_index(&mut self, h: &Hash, spent: &[CommitPos]) -> Result<(), Error> { self.db .put_ser(&to_key(BLOCK_SPENT_PREFIX, h)[..], &spent.to_vec())?; Ok(()) } /// Low level function to delete directly by raw key. - pub fn delete(&self, key: &[u8]) -> Result<(), Error> { + pub fn delete(&mut self, key: &[u8]) -> Result<(), Error> { self.db.delete(key) } /// Delete a full block. Does not delete any record associated with a block /// header. - pub fn delete_block(&self, bh: &Hash) -> Result<(), Error> { + pub fn delete_block(&mut self, bh: &Hash) -> Result<(), Error> { self.db.delete(&to_key(BLOCK_PREFIX, bh)[..])?; // Best effort at deleting associated data for this block. @@ -275,7 +275,7 @@ impl<'a> Batch<'a> { } /// Save block header to db. - pub fn save_block_header(&self, header: &BlockHeader) -> Result<(), Error> { + pub fn save_block_header(&mut self, header: &BlockHeader) -> Result<(), Error> { let hash = header.hash(); // Store the header itself indexed by hash. @@ -286,13 +286,17 @@ impl<'a> Batch<'a> { } /// Save output_pos and block height to index. - pub fn save_output_pos_height(&self, commit: &Commitment, pos: CommitPos) -> Result<(), Error> { + pub fn save_output_pos_height( + &mut self, + commit: &Commitment, + pos: CommitPos, + ) -> Result<(), Error> { self.db .put_ser(&to_key(OUTPUT_POS_PREFIX, commit)[..], &pos) } /// Delete the output_pos index entry for a spent output. - pub fn delete_output_pos_height(&self, commit: &Commitment) -> Result<(), Error> { + pub fn delete_output_pos_height(&mut self, commit: &Commitment) -> Result<(), Error> { self.db.delete(&to_key(OUTPUT_POS_PREFIX, commit)) } @@ -305,7 +309,9 @@ impl<'a> Batch<'a> { } /// Iterator over the output_pos index. - pub fn output_pos_iter(&self) -> Result, CommitPos)>, Error> { + pub fn output_pos_iter( + &self, + ) -> Result, CommitPos)> + 'a, Error> { let key = to_key(OUTPUT_POS_PREFIX, ""); let protocol_version = self.db.protocol_version(); self.db.iter(&key, move |k, mut v| { @@ -366,12 +372,12 @@ impl<'a> Batch<'a> { } /// Delete the block spent index. - fn delete_spent_index(&self, bh: &Hash) -> Result<(), Error> { + fn delete_spent_index(&mut self, bh: &Hash) -> Result<(), Error> { self.db.delete(&to_key(BLOCK_SPENT_PREFIX, bh)) } /// Save block_sums for the block. - pub fn save_block_sums(&self, h: &Hash, sums: BlockSums) -> Result<(), Error> { + pub fn save_block_sums(&mut self, h: &Hash, sums: BlockSums) -> Result<(), Error> { self.db.put_ser(&to_key(BLOCK_SUMS_PREFIX, h)[..], &sums) } @@ -383,7 +389,7 @@ impl<'a> Batch<'a> { } /// Delete the block_sums for the block. - fn delete_block_sums(&self, bh: &Hash) -> Result<(), Error> { + fn delete_block_sums(&mut self, bh: &Hash) -> Result<(), Error> { self.db.delete(&to_key(BLOCK_SUMS_PREFIX, bh)) } @@ -423,7 +429,7 @@ impl<'a> Batch<'a> { /// Iterator over all full blocks in the db. /// Uses default db serialization strategy via db protocol version. - pub fn blocks_iter(&self) -> Result, Error> { + pub fn blocks_iter(&self) -> Result + 'a, Error> { let key = to_key(BLOCK_PREFIX, ""); let protocol_version = self.db.protocol_version(); self.db.iter(&key, move |_, mut v| { @@ -434,7 +440,7 @@ impl<'a> Batch<'a> { /// Iterator over raw data for full blocks in the db. /// Used during block migration (we need flexibility around deserialization). - pub fn blocks_raw_iter(&self) -> Result, Vec)>, Error> { + pub fn blocks_raw_iter(&self) -> Result, Vec)> + 'a, Error> { let key = to_key(BLOCK_PREFIX, ""); self.db.iter(&key, |k, v| Ok((k.to_vec(), v.to_vec()))) } diff --git a/chain/src/txhashset/desegmenter.rs b/chain/src/txhashset/desegmenter.rs index ca391397a8..2efbfee3c2 100644 --- a/chain/src/txhashset/desegmenter.rs +++ b/chain/src/txhashset/desegmenter.rs @@ -189,7 +189,7 @@ impl Desegmenter { // TODO: Unwraps let tip = Tip::from_header(&h); - let batch = self.store.batch()?; + let mut batch = self.store.batch()?; batch.save_pibd_head(&tip)?; batch.commit()?; @@ -283,11 +283,11 @@ impl Desegmenter { { let header_pmmr = self.header_pmmr.read(); let txhashset = self.txhashset.read(); - let batch = self.store.batch()?; + let mut batch = self.store.batch()?; txhashset.verify_kernel_pos_index( &self.genesis, &header_pmmr, - &batch, + &mut batch, Some(status.clone()), Some(stop_state.clone()), )?; @@ -308,9 +308,9 @@ impl Desegmenter { &mut header_pmmr, &mut txhashset, &mut batch, - |ext, batch| { + |ext, mut batch| { let extension = &mut ext.extension; - extension.rewind(&self.archive_header, batch)?; + extension.rewind(&self.archive_header, &mut batch)?; // Validate the extension, generating the utxo_sum and kernel_sum. // Full validation, including rangeproofs and kernel signature verification. @@ -359,10 +359,10 @@ impl Desegmenter { } // Rebuild our output_pos index in the db based on fresh UTXO set. - txhashset.init_output_pos_index(&header_pmmr, &batch)?; + txhashset.init_output_pos_index(&header_pmmr, &mut batch)?; // Rebuild our NRD kernel_pos index based on recent kernel history. - txhashset.init_recent_kernel_pos_index(&header_pmmr, &batch)?; + txhashset.init_recent_kernel_pos_index(&header_pmmr, &mut batch)?; // Commit all the changes to the db. batch.commit()?; diff --git a/chain/src/txhashset/txhashset.rs b/chain/src/txhashset/txhashset.rs index a0d6bc64ca..cbdb899f38 100644 --- a/chain/src/txhashset/txhashset.rs +++ b/chain/src/txhashset/txhashset.rs @@ -538,7 +538,7 @@ impl TxHashSet { pub fn init_recent_kernel_pos_index( &self, header_pmmr: &PMMRHandle, - batch: &Batch<'_>, + batch: &mut Batch<'_>, ) -> Result<(), Error> { let head = batch.head()?; let cutoff = head.height.saturating_sub(WEEK_HEIGHT * 2); @@ -552,7 +552,7 @@ impl TxHashSet { &self, from_header: &BlockHeader, header_pmmr: &PMMRHandle, - batch: &Batch<'_>, + batch: &mut Batch<'_>, status: Option>, stop_state: Option>, ) -> Result<(), Error> { @@ -635,7 +635,7 @@ impl TxHashSet { pub fn init_output_pos_index( &self, header_pmmr: &PMMRHandle, - batch: &Batch<'_>, + batch: &mut Batch<'_>, ) -> Result<(), Error> { let now = Instant::now(); @@ -733,10 +733,10 @@ pub fn extending_readonly( inner: F, ) -> Result where - F: FnOnce(&mut ExtensionPair<'_>, &Batch<'_>) -> Result, + F: FnOnce(&mut ExtensionPair<'_>, &mut Batch<'_>) -> Result, { let commit_index = trees.commit_index.clone(); - let batch = commit_index.batch()?; + let mut batch = commit_index.batch()?; trace!("Starting new txhashset (readonly) extension."); @@ -751,7 +751,7 @@ where header_extension: &mut header_extension, extension: &mut extension, }; - inner(&mut extension_pair, &batch) + inner(&mut extension_pair, &mut batch) }; trace!("Rollbacking txhashset (readonly) extension."); @@ -830,7 +830,7 @@ pub fn extending<'a, F, T>( inner: F, ) -> Result where - F: FnOnce(&mut ExtensionPair<'_>, &Batch<'_>) -> Result, + F: FnOnce(&mut ExtensionPair<'_>, &mut Batch<'_>) -> Result, { let sizes: (u64, u64, u64); let res: Result; @@ -842,7 +842,7 @@ where // create a child transaction so if the state is rolled back by itself, all // index saving can be undone - let child_batch = batch.child()?; + let mut child_batch = batch.child()?; { trace!("Starting new txhashset extension."); @@ -853,7 +853,7 @@ where header_extension: &mut header_extension, extension: &mut extension, }; - res = inner(&mut extension_pair, &child_batch); + res = inner(&mut extension_pair, &mut child_batch); rollback = extension_pair.extension.rollback; sizes = extension_pair.extension.sizes(); @@ -901,15 +901,15 @@ where /// Start a new readonly header MMR extension. /// This MMR can be extended individually beyond the other (output, rangeproof and kernel) MMRs /// to allow headers to be validated before we receive the full block data. -pub fn header_extending_readonly<'a, F, T>( - handle: &'a mut PMMRHandle, +pub fn header_extending_readonly( + handle: &mut PMMRHandle, store: &ChainStore, inner: F, ) -> Result where - F: FnOnce(&mut HeaderExtension<'_>, &Batch<'_>) -> Result, + F: FnOnce(&mut HeaderExtension<'_>, &mut Batch<'_>) -> Result, { - let batch = store.batch()?; + let mut batch = store.batch()?; let head = match handle.head_hash() { Ok(hash) => { @@ -921,7 +921,7 @@ where let pmmr = PMMR::at(&mut handle.backend, handle.size); let mut extension = HeaderExtension::new(pmmr, head); - let res = inner(&mut extension, &batch); + let res = inner(&mut extension, &mut batch); handle.backend.discard(); @@ -937,7 +937,7 @@ pub fn header_extending<'a, F, T>( inner: F, ) -> Result where - F: FnOnce(&mut HeaderExtension<'_>, &Batch<'_>) -> Result, + F: FnOnce(&mut HeaderExtension<'_>, &mut Batch<'_>) -> Result, { let size: u64; let res: Result; @@ -945,7 +945,7 @@ where // create a child transaction so if the state is rolled back by itself, all // index saving can be undone - let child_batch = batch.child()?; + let mut child_batch = batch.child()?; let head = match handle.head_hash() { Ok(hash) => { @@ -958,7 +958,7 @@ where { let pmmr = PMMR::at(&mut handle.backend, handle.size); let mut extension = HeaderExtension::new(pmmr, head); - res = inner(&mut extension, &child_batch); + res = inner(&mut extension, &mut child_batch); rollback = extension.rollback; size = extension.size(); @@ -1233,7 +1233,7 @@ impl<'a> Extension<'a> { &mut self, b: &Block, header_ext: &HeaderExtension<'_>, - batch: &Batch<'_>, + batch: &mut Batch<'_>, ) -> Result<(), Error> { let mut affected_pos = vec![]; @@ -1499,7 +1499,7 @@ impl<'a> Extension<'a> { &mut self, kernels: &[TxKernel], height: u64, - batch: &Batch<'_>, + batch: &mut Batch<'_>, ) -> Result<(), Error> { for kernel in kernels { let pos = self.apply_kernel(kernel)?; @@ -1579,7 +1579,7 @@ impl<'a> Extension<'a> { /// Rewinds the MMRs to the provided block, rewinding to the last output pos /// and last kernel pos of that block. If `updated_bitmap` is supplied, the /// bitmap accumulator will be replaced with its contents - pub fn rewind(&mut self, header: &BlockHeader, batch: &Batch<'_>) -> Result<(), Error> { + pub fn rewind(&mut self, header: &BlockHeader, batch: &mut Batch) -> Result<(), Error> { debug!( "Rewind extension to {} at {} from {} at {}", header.hash(), @@ -1622,7 +1622,11 @@ impl<'a> Extension<'a> { // Rewind the MMRs and the output_pos index. // Returns a vec of "affected_pos" so we can apply the necessary updates to the bitmap // accumulator in a single pass for all rewound blocks. - fn rewind_single_block(&mut self, block: &Block, batch: &Batch<'_>) -> Result, Error> { + fn rewind_single_block( + &mut self, + block: &Block, + batch: &mut Batch<'_>, + ) -> Result, Error> { let header = &block.header; let prev_header = batch.get_previous_header(&header)?; @@ -2206,7 +2210,11 @@ fn input_pos_to_rewind( } /// If NRD enabled then enforce NRD relative height rules. -fn apply_kernel_rules(kernel: &TxKernel, pos: CommitPos, batch: &Batch<'_>) -> Result<(), Error> { +fn apply_kernel_rules( + kernel: &TxKernel, + pos: CommitPos, + batch: &mut Batch<'_>, +) -> Result<(), Error> { if !global::is_nrd_enabled() { return Ok(()); } diff --git a/chain/tests/store_indices.rs b/chain/tests/store_indices.rs index 5cc193565e..c011ed6c64 100644 --- a/chain/tests/store_indices.rs +++ b/chain/tests/store_indices.rs @@ -50,7 +50,7 @@ fn test_store_indices() { { // Start a new batch and delete the block. let store = chain.store(); - let batch = store.batch().unwrap(); + let mut batch = store.batch().unwrap(); assert!(batch.delete_block(&block_hash).is_ok()); // Block is deleted within this batch. diff --git a/chain/tests/store_kernel_pos_index.rs b/chain/tests/store_kernel_pos_index.rs index 664e2818c4..d7cf6d30d0 100644 --- a/chain/tests/store_kernel_pos_index.rs +++ b/chain/tests/store_kernel_pos_index.rs @@ -39,14 +39,14 @@ fn test_store_kernel_idx() { let commit = Commitment::from_vec(vec![]); let store = ChainStore::new(chain_dir).unwrap(); - let batch = store.batch().unwrap(); + let mut batch = store.batch().unwrap(); let index = store::nrd_recent_kernel_index(); assert_eq!(index.peek_pos(&batch, commit), Ok(None)); assert_eq!(index.get_list(&batch, commit), Ok(None)); assert_eq!( - index.push_pos(&batch, commit, CommitPos { pos: 1, height: 1 }), + index.push_pos(&mut batch, commit, CommitPos { pos: 1, height: 1 }), Ok(()), ); @@ -63,7 +63,7 @@ fn test_store_kernel_idx() { ); assert_eq!( - index.push_pos(&batch, commit, CommitPos { pos: 2, height: 2 }), + index.push_pos(&mut batch, commit, CommitPos { pos: 2, height: 2 }), Ok(()), ); @@ -79,17 +79,17 @@ fn test_store_kernel_idx() { // Pos must always increase. assert_eq!( - index.push_pos(&batch, commit, CommitPos { pos: 1, height: 1 }), + index.push_pos(&mut batch, commit, CommitPos { pos: 1, height: 1 }), Err(Error::OtherErr("pos must be increasing".into())), ); assert_eq!( - index.push_pos(&batch, commit, CommitPos { pos: 2, height: 2 }), + index.push_pos(&mut batch, commit, CommitPos { pos: 2, height: 2 }), Err(Error::OtherErr("pos must be increasing".into())), ); assert_eq!( - index.push_pos(&batch, commit, CommitPos { pos: 3, height: 3 }), + index.push_pos(&mut batch, commit, CommitPos { pos: 3, height: 3 }), Ok(()), ); @@ -104,7 +104,7 @@ fn test_store_kernel_idx() { ); assert_eq!( - index.pop_pos(&batch, commit), + index.pop_pos(&mut batch, commit), Ok(Some(CommitPos { pos: 3, height: 3 })), ); @@ -119,7 +119,7 @@ fn test_store_kernel_idx() { ); assert_eq!( - index.push_pos(&batch, commit, CommitPos { pos: 3, height: 3 }), + index.push_pos(&mut batch, commit, CommitPos { pos: 3, height: 3 }), Ok(()), ); @@ -134,7 +134,7 @@ fn test_store_kernel_idx() { ); assert_eq!( - index.pop_pos(&batch, commit), + index.pop_pos(&mut batch, commit), Ok(Some(CommitPos { pos: 3, height: 3 })), ); @@ -149,7 +149,7 @@ fn test_store_kernel_idx() { ); assert_eq!( - index.pop_pos(&batch, commit), + index.pop_pos(&mut batch, commit), Ok(Some(CommitPos { pos: 2, height: 2 })), ); @@ -166,7 +166,7 @@ fn test_store_kernel_idx() { ); assert_eq!( - index.pop_pos(&batch, commit), + index.pop_pos(&mut batch, commit), Ok(Some(CommitPos { pos: 1, height: 1 })), ); @@ -186,14 +186,14 @@ fn test_store_kernel_idx_pop_back() { let commit = Commitment::from_vec(vec![]); let store = ChainStore::new(chain_dir).unwrap(); - let batch = store.batch().unwrap(); + let mut batch = store.batch().unwrap(); let index = store::nrd_recent_kernel_index(); assert_eq!(index.peek_pos(&batch, commit), Ok(None)); assert_eq!(index.get_list(&batch, commit), Ok(None)); assert_eq!( - index.push_pos(&batch, commit, CommitPos { pos: 1, height: 1 }), + index.push_pos(&mut batch, commit, CommitPos { pos: 1, height: 1 }), Ok(()), ); @@ -210,7 +210,7 @@ fn test_store_kernel_idx_pop_back() { ); assert_eq!( - index.pop_pos_back(&batch, commit), + index.pop_pos_back(&mut batch, commit), Ok(Some(CommitPos { pos: 1, height: 1 })), ); @@ -218,32 +218,32 @@ fn test_store_kernel_idx_pop_back() { assert_eq!(index.get_list(&batch, commit), Ok(None)); assert_eq!( - index.push_pos(&batch, commit, CommitPos { pos: 1, height: 1 }), + index.push_pos(&mut batch, commit, CommitPos { pos: 1, height: 1 }), Ok(()), ); assert_eq!( - index.push_pos(&batch, commit, CommitPos { pos: 2, height: 2 }), + index.push_pos(&mut batch, commit, CommitPos { pos: 2, height: 2 }), Ok(()), ); assert_eq!( - index.push_pos(&batch, commit, CommitPos { pos: 3, height: 3 }), + index.push_pos(&mut batch, commit, CommitPos { pos: 3, height: 3 }), Ok(()), ); assert_eq!( - index.peek_pos(&batch, commit), + index.peek_pos(&mut batch, commit), Ok(Some(CommitPos { pos: 3, height: 3 })), ); assert_eq!( - index.get_list(&batch, commit), + index.get_list(&mut batch, commit), Ok(Some(ListWrapper::Multi { head: 3, tail: 1 })), ); assert_eq!( - index.pop_pos_back(&batch, commit), + index.pop_pos_back(&mut batch, commit), Ok(Some(CommitPos { pos: 1, height: 1 })), ); @@ -258,7 +258,7 @@ fn test_store_kernel_idx_pop_back() { ); assert_eq!( - index.pop_pos_back(&batch, commit), + index.pop_pos_back(&mut batch, commit), Ok(Some(CommitPos { pos: 2, height: 2 })), ); @@ -275,7 +275,7 @@ fn test_store_kernel_idx_pop_back() { ); assert_eq!( - index.pop_pos_back(&batch, commit), + index.pop_pos_back(&mut batch, commit), Ok(Some(CommitPos { pos: 3, height: 3 })), ); @@ -294,21 +294,21 @@ fn test_store_kernel_idx_rewind() { let commit = Commitment::from_vec(vec![]); let store = ChainStore::new(chain_dir).unwrap(); - let batch = store.batch().unwrap(); + let mut batch = store.batch().unwrap(); let index = store::nrd_recent_kernel_index(); assert_eq!( - index.push_pos(&batch, commit, CommitPos { pos: 1, height: 1 }), + index.push_pos(&mut batch, commit, CommitPos { pos: 1, height: 1 }), Ok(()), ); assert_eq!( - index.push_pos(&batch, commit, CommitPos { pos: 2, height: 2 }), + index.push_pos(&mut batch, commit, CommitPos { pos: 2, height: 2 }), Ok(()), ); assert_eq!( - index.push_pos(&batch, commit, CommitPos { pos: 3, height: 3 }), + index.push_pos(&mut batch, commit, CommitPos { pos: 3, height: 3 }), Ok(()), ); @@ -317,7 +317,7 @@ fn test_store_kernel_idx_rewind() { Ok(Some(ListWrapper::Multi { head: 3, tail: 1 })), ); - assert_eq!(index.rewind(&batch, commit, 1), Ok(()),); + assert_eq!(index.rewind(&mut batch, commit, 1), Ok(()),); assert_eq!( index.get_list(&batch, commit), @@ -327,7 +327,7 @@ fn test_store_kernel_idx_rewind() { ); // Check we can safely noop rewind. - assert_eq!(index.rewind(&batch, commit, 2), Ok(()),); + assert_eq!(index.rewind(&mut batch, commit, 2), Ok(()),); assert_eq!( index.get_list(&batch, commit), @@ -336,7 +336,7 @@ fn test_store_kernel_idx_rewind() { })), ); - assert_eq!(index.rewind(&batch, commit, 1), Ok(()),); + assert_eq!(index.rewind(&mut batch, commit, 1), Ok(()),); assert_eq!( index.get_list(&batch, commit), @@ -346,42 +346,42 @@ fn test_store_kernel_idx_rewind() { ); // Check we can rewind back to 0. - assert_eq!(index.rewind(&batch, commit, 0), Ok(()),); + assert_eq!(index.rewind(&mut batch, commit, 0), Ok(()),); assert_eq!(index.get_list(&batch, commit), Ok(None),); - assert_eq!(index.rewind(&batch, commit, 0), Ok(()),); + assert_eq!(index.rewind(&mut batch, commit, 0), Ok(()),); // Now check we can rewind past the end of a list safely. assert_eq!( - index.push_pos(&batch, commit, CommitPos { pos: 1, height: 1 }), + index.push_pos(&mut batch, commit, CommitPos { pos: 1, height: 1 }), Ok(()), ); assert_eq!( - index.push_pos(&batch, commit, CommitPos { pos: 2, height: 2 }), + index.push_pos(&mut batch, commit, CommitPos { pos: 2, height: 2 }), Ok(()), ); assert_eq!( - index.push_pos(&batch, commit, CommitPos { pos: 3, height: 3 }), + index.push_pos(&mut batch, commit, CommitPos { pos: 3, height: 3 }), Ok(()), ); assert_eq!( - index.pop_pos_back(&batch, commit), + index.pop_pos_back(&mut batch, commit), Ok(Some(CommitPos { pos: 1, height: 1 })), ); assert_eq!( - index.get_list(&batch, commit), + index.get_list(&mut batch, commit), Ok(Some(ListWrapper::Multi { head: 3, tail: 2 })), ); - assert_eq!(index.rewind(&batch, commit, 1), Ok(()),); + assert_eq!(index.rewind(&mut batch, commit, 1), Ok(()),); - assert_eq!(index.get_list(&batch, commit), Ok(None),); + assert_eq!(index.get_list(&mut batch, commit), Ok(None),); clean_output_dir(chain_dir); } @@ -396,14 +396,14 @@ fn test_store_kernel_idx_multiple_commits() { let commit2 = Commitment::from_vec(vec![1]); let store = ChainStore::new(chain_dir).unwrap(); - let batch = store.batch().unwrap(); + let mut batch = store.batch().unwrap(); let index = store::nrd_recent_kernel_index(); assert_eq!(index.get_list(&batch, commit), Ok(None)); assert_eq!(index.get_list(&batch, commit2), Ok(None)); assert_eq!( - index.push_pos(&batch, commit, CommitPos { pos: 1, height: 1 }), + index.push_pos(&mut batch, commit, CommitPos { pos: 1, height: 1 }), Ok(()), ); @@ -417,7 +417,7 @@ fn test_store_kernel_idx_multiple_commits() { assert_eq!(index.get_list(&batch, commit2), Ok(None)); assert_eq!( - index.push_pos(&batch, commit2, CommitPos { pos: 2, height: 2 }), + index.push_pos(&mut batch, commit2, CommitPos { pos: 2, height: 2 }), Ok(()), ); @@ -436,7 +436,7 @@ fn test_store_kernel_idx_multiple_commits() { ); assert_eq!( - index.push_pos(&batch, commit, CommitPos { pos: 3, height: 3 }), + index.push_pos(&mut batch, commit, CommitPos { pos: 3, height: 3 }), Ok(()), ); @@ -453,7 +453,7 @@ fn test_store_kernel_idx_multiple_commits() { ); assert_eq!( - index.pop_pos(&batch, commit), + index.pop_pos(&mut batch, commit), Ok(Some(CommitPos { pos: 3, height: 3 })), ); @@ -488,18 +488,18 @@ fn test_store_kernel_idx_clear() -> Result<(), Error> { // Add a couple of single entries to the index and commit the batch. { - let batch = store.batch()?; + let mut batch = store.batch()?; assert_eq!(index.peek_pos(&batch, commit), Ok(None)); assert_eq!(index.get_list(&batch, commit), Ok(None)); assert_eq!( - index.push_pos(&batch, commit, CommitPos { pos: 1, height: 1 }), + index.push_pos(&mut batch, commit, CommitPos { pos: 1, height: 1 }), Ok(()), ); assert_eq!( index.push_pos( - &batch, + &mut batch, commit2, CommitPos { pos: 10, @@ -544,8 +544,8 @@ fn test_store_kernel_idx_clear() -> Result<(), Error> { // Clear the index and confirm everything was deleted as expected. { - let batch = store.batch()?; - assert_eq!(index.clear(&batch), Ok(())); + let mut batch = store.batch()?; + assert_eq!(index.clear(&mut batch), Ok(())); assert_eq!(index.peek_pos(&batch, commit), Ok(None)); assert_eq!(index.get_list(&batch, commit), Ok(None)); assert_eq!(index.peek_pos(&batch, commit2), Ok(None)); @@ -555,13 +555,13 @@ fn test_store_kernel_idx_clear() -> Result<(), Error> { // Add multiple entries to the index, commit the batch. { - let batch = store.batch()?; + let mut batch = store.batch()?; assert_eq!( - index.push_pos(&batch, commit, CommitPos { pos: 1, height: 1 }), + index.push_pos(&mut batch, commit, CommitPos { pos: 1, height: 1 }), Ok(()), ); assert_eq!( - index.push_pos(&batch, commit, CommitPos { pos: 2, height: 2 }), + index.push_pos(&mut batch, commit, CommitPos { pos: 2, height: 2 }), Ok(()), ); assert_eq!( @@ -577,8 +577,8 @@ fn test_store_kernel_idx_clear() -> Result<(), Error> { // Clear the index and confirm everything was deleted as expected. { - let batch = store.batch()?; - assert_eq!(index.clear(&batch), Ok(())); + let mut batch = store.batch()?; + assert_eq!(index.clear(&mut batch), Ok(())); assert_eq!(index.peek_pos(&batch, commit), Ok(None)); assert_eq!(index.get_list(&batch, commit), Ok(None)); batch.commit()?; diff --git a/chain/tests/test_block_known.rs b/chain/tests/test_block_known.rs index bdab3a818f..d5c0694aec 100644 --- a/chain/tests/test_block_known.rs +++ b/chain/tests/test_block_known.rs @@ -61,7 +61,7 @@ fn check_known() { { let chain = init_chain(chain_dir, genesis.clone()); let store = chain.store(); - let batch = store.batch().unwrap(); + let mut batch = store.batch().unwrap(); let head_header = chain.head_header().unwrap(); let prev = batch.get_previous_header(&head_header).unwrap(); batch.save_body_head(&Tip::from_header(&prev)).unwrap(); diff --git a/p2p/src/peers.rs b/p2p/src/peers.rs index 0f10a84eca..21340b1008 100644 --- a/p2p/src/peers.rs +++ b/p2p/src/peers.rs @@ -275,22 +275,30 @@ impl Peers { } } - /// Iterator over all peers we know about (stored in our db). - pub fn peer_data_iter(&self) -> Result, Error> { - self.store.peers_iter().map_err(From::from) - } - /// Convenience for reading all peer data from the db. pub fn all_peer_data(&self) -> Vec { - self.peer_data_iter() - .map(|peers| peers.collect()) - .unwrap_or(vec![]) + match self.store.iter_batch() { + Ok(batch) => match batch.peers_iter() { + Ok(iter) => iter.collect(), + Err(e) => { + error!("failed to get all peer data: {:?}", e); + vec![] + } + }, + Err(e) => { + error!("failed to get all peer data: {:?}", e); + vec![] + } + } } /// Find peers in store (not necessarily connected) and return their data pub fn find_peers(&self, state: State, cap: Capabilities, count: usize) -> Vec { - match self.store.find_peers(state, cap, count) { - Ok(peers) => peers, + match self.store.iter_batch() { + Ok(batch) => batch.find_peers(state, cap, count).unwrap_or_else(|e| { + error!("failed to find peers: {:?}", e); + vec![] + }), Err(e) => { error!("failed to find peers: {:?}", e); vec![] @@ -445,24 +453,26 @@ impl Peers { let now = Utc::now(); // Delete defunct peers from storage - let _ = self.store.delete_peers(|peer| { - let diff = now - Utc.timestamp_opt(peer.last_connected, 0).unwrap(); - - let should_remove = peer.flags == State::Defunct - && diff > Duration::seconds(global::PEER_EXPIRATION_REMOVE_TIME); - - if should_remove { - debug!( - "removing peer {:?}: last connected {} days {} hours {} minutes ago.", - peer.addr, - diff.num_days(), - diff.num_hours(), - diff.num_minutes() - ); - } + if let Ok(batch) = self.store.iter_batch() { + let _ = batch.delete_peers(|peer| { + let diff = now - Utc.timestamp_opt(peer.last_connected, 0).unwrap(); + + let should_remove = peer.flags == State::Defunct + && diff > Duration::seconds(global::PEER_EXPIRATION_REMOVE_TIME); + + if should_remove { + debug!( + "removing peer {:?}: last connected {} days {} hours {} minutes ago.", + peer.addr, + diff.num_days(), + diff.num_hours(), + diff.num_minutes() + ); + } - should_remove - }); + should_remove + }); + } } } diff --git a/p2p/src/store.rs b/p2p/src/store.rs index 4b4dde9fac..85b30437a0 100644 --- a/p2p/src/store.rs +++ b/p2p/src/store.rs @@ -119,19 +119,19 @@ impl PeerStore { /// Instantiates a new peer store under the provided root path. pub fn new(db_root: &str) -> Result { let db = grin_store::Store::new(db_root, Some(DB_NAME), Some(STORE_SUBPATH), None)?; - Ok(PeerStore { db: db }) + Ok(PeerStore { db }) } pub fn save_peer(&self, p: &PeerData) -> Result<(), Error> { debug!("save_peer: {:?} marked {:?}", p.addr, p.flags); - let batch = self.db.batch()?; + let mut batch = self.db.batch()?; batch.put_ser(&peer_key(p.addr)[..], p)?; batch.commit() } pub fn save_peers(&self, p: Vec) -> Result<(), Error> { - let batch = self.db.batch()?; + let mut batch = self.db.batch()?; for pd in p { debug!("save_peers: {:?} marked {:?}", pd.addr, pd.flags); batch.put_ser(&peer_key(pd.addr)[..], &pd)?; @@ -149,14 +149,50 @@ impl PeerStore { self.db.exists(&peer_key(peer_addr)[..]) } - /// TODO - allow below added to avoid github issue reports - #[allow(dead_code)] - pub fn delete_peer(&self, peer_addr: PeerAddr) -> Result<(), Error> { - let batch = self.db.batch()?; - batch.delete(&peer_key(peer_addr)[..])?; + /// Convenience method to load a peer data, update its status and save it + /// back. If new state is Banned its last banned time will be updated too. + /// If new state is Defunct last connection attempt will be updated too. + pub fn update_state(&self, peer_addr: PeerAddr, new_state: State) -> Result<(), Error> { + let mut batch = self.db.batch()?; + + let mut peer = option_to_not_found( + batch.get_ser::(&peer_key(peer_addr)[..], None), + || format!("Peer at address: {}", peer_addr), + )?; + peer.flags = new_state; + if new_state == State::Banned { + peer.last_banned = Utc::now().timestamp(); + } else { + peer.last_attempt = Utc::now().timestamp(); + } + + batch.put_ser(&peer_key(peer_addr)[..], &peer)?; batch.commit() } + /// Builds a new iterator batch to be used with this store. + pub fn iter_batch(&self) -> Result, Error> { + Ok(PeersIterBatch { + db: self.db.batch()?, + }) + } +} + +pub struct PeersIterBatch<'a> { + db: grin_store::Batch<'a>, +} + +impl<'a> PeersIterBatch<'a> { + /// Iterator over all known peers. + pub fn peers_iter(&self) -> Result + 'a, Error> { + let key = to_key(PEER_PREFIX, ""); + let protocol_version = self.db.protocol_version(); + self.db.iter(&key, move |_, mut v| { + ser::deserialize(&mut v, protocol_version, DeserializationMode::default()) + .map_err(From::from) + }) + } + /// Find some peers in our local db. pub fn find_peers( &self, @@ -171,16 +207,6 @@ impl PeerStore { Ok(peers) } - /// Iterator over all known peers. - pub fn peers_iter(&self) -> Result, Error> { - let key = to_key(PEER_PREFIX, ""); - let protocol_version = self.db.protocol_version(); - self.db.iter(&key, move |_, mut v| { - ser::deserialize(&mut v, protocol_version, DeserializationMode::default()) - .map_err(From::from) - }) - } - /// List all known peers /// Used for /v1/peers/all api endpoint pub fn all_peers(&self) -> Result, Error> { @@ -188,29 +214,8 @@ impl PeerStore { Ok(peers) } - /// Convenience method to load a peer data, update its status and save it - /// back. If new state is Banned its last banned time will be updated too. - /// If new state is Defunct last connection attempt will be updated too. - pub fn update_state(&self, peer_addr: PeerAddr, new_state: State) -> Result<(), Error> { - let batch = self.db.batch()?; - - let mut peer = option_to_not_found( - batch.get_ser::(&peer_key(peer_addr)[..], None), - || format!("Peer at address: {}", peer_addr), - )?; - peer.flags = new_state; - if new_state == State::Banned { - peer.last_banned = Utc::now().timestamp(); - } else { - peer.last_attempt = Utc::now().timestamp(); - } - - batch.put_ser(&peer_key(peer_addr)[..], &peer)?; - batch.commit() - } - /// Deletes peers from the storage that satisfy some condition `predicate` - pub fn delete_peers(&self, predicate: F) -> Result<(), Error> + pub fn delete_peers(mut self, predicate: F) -> Result<(), Error> where F: Fn(&PeerData) -> bool, { @@ -224,13 +229,10 @@ impl PeerStore { // Delete peers in single batch if !to_remove.is_empty() { - let batch = self.db.batch()?; - for peer in to_remove { - batch.delete(&peer_key(peer.addr)[..])?; + self.db.delete(&peer_key(peer.addr)[..])?; } - - batch.commit()?; + self.db.commit()?; } Ok(()) diff --git a/servers/Cargo.toml b/servers/Cargo.toml index cb16277195..a9a6978a2e 100644 --- a/servers/Cargo.toml +++ b/servers/Cargo.toml @@ -15,7 +15,6 @@ hyper-rustls = "0.23" fs2 = "0.4" futures = "0.3" http = "0.2" -lmdb-zero = "0.4.4" rand = "0.6" serde = "1" log = "0.4" diff --git a/store/Cargo.toml b/store/Cargo.toml index 72f30ca84f..9654cc1fc5 100644 --- a/store/Cargo.toml +++ b/store/Cargo.toml @@ -13,7 +13,7 @@ edition = "2018" byteorder = "1" croaring = "1.0.1" libc = "0.2" -lmdb-zero = "0.4.4" +heed = "0.22.1" memmap = "0.7" tempfile = "3.1" serde = "1" diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index b1173fbcde..f2e0faacca 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -17,9 +17,8 @@ use std::fs; use std::sync::Arc; -use lmdb_zero as lmdb; -use lmdb_zero::traits::CreateCursor; -use lmdb_zero::LmdbResultExt; +use heed::types::Bytes; +use heed::{Database, Env, EnvOpenOptions, RoTxn, RwTxn, WithoutTls}; use crate::grin_core::global; use crate::grin_core::ser::{self, DeserializationMode, ProtocolVersion}; @@ -42,7 +41,7 @@ pub enum Error { NotFoundErr(String), /// Wraps an error originating from LMDB #[error("LMDB error: {0}")] - LmdbErr(lmdb::error::Error), + LmdbErr(String), /// Wraps a serialization error for Writeable or Readable #[error("Serialization Error: {0}")] SerErr(ser::Error), @@ -54,9 +53,9 @@ pub enum Error { OtherErr(String), } -impl From for Error { - fn from(e: lmdb::error::Error) -> Error { - Error::LmdbErr(e) +impl From for Error { + fn from(e: heed::Error) -> Error { + Error::LmdbErr(e.to_string()) } } @@ -83,8 +82,8 @@ const DEFAULT_DB_VERSION: ProtocolVersion = ProtocolVersion(3); /// LMDB-backed store facilitating data access and serialization. All writes /// are done through a Batch abstraction providing atomicity. pub struct Store { - env: Arc, - db: Arc>>>>, + env: Arc>, + db: Arc>>>>, name: String, version: ProtocolVersion, alloc_chunk_size: usize, @@ -117,53 +116,53 @@ impl Store { )) })?; - let mut env_builder = lmdb::EnvBuilder::new()?; - env_builder.set_maxdbs(8)?; - - if let Some(max_readers) = max_readers { - env_builder.set_maxreaders(max_readers)?; - } - let alloc_chunk_size = match global::is_production_mode() { true => ALLOC_CHUNK_SIZE_DEFAULT, false => ALLOC_CHUNK_SIZE_DEFAULT_TEST, }; - let env = unsafe { env_builder.open(&full_path, lmdb::open::NOTLS, 0o600)? }; - - debug!("DB Mapsize for {} is {}", full_path, env.info()?.mapsize); - let res = Store { + let env = unsafe { + let mut options = EnvOpenOptions::new().read_txn_without_tls(); + let mut env_options = options.map_size(alloc_chunk_size).max_dbs(8); + if let Some(max_readers) = max_readers { + env_options = env_options.max_readers(max_readers); + } + env_options.open(&full_path)? + }; + let (resize, new_size) = Self::needs_resize(Arc::new(env.clone()), alloc_chunk_size); + if resize { + unsafe { + env.resize(new_size)?; + }; + } + let s = Store { env: Arc::new(env), db: Arc::new(RwLock::new(None)), name: db_name, version: DEFAULT_DB_VERSION, alloc_chunk_size, }; - { - let mut w = res.db.write(); - *w = Some(Arc::new(lmdb::Database::open( - res.env.clone(), - Some(&res.name), - &lmdb::DatabaseOptions::new(lmdb::db::CREATE), - )?)); + let mut write = s.env.write_txn()?; + let db = s.env.create_database(&mut write, Some(&s.name))?; + write.commit()?; + let mut w_db = s.db.write(); + *w_db = Some(Arc::new(db)); } - Ok(res) + Ok(s) + + // debug!("DB Mapsize for {} is {}", full_path, env.info().map_size); } /// Construct a new store using a specific protocol version. /// Permits access to the db with legacy protocol versions for db migrations. pub fn with_version(&self, version: ProtocolVersion) -> Store { - let alloc_chunk_size = match global::is_production_mode() { - true => ALLOC_CHUNK_SIZE_DEFAULT, - false => ALLOC_CHUNK_SIZE_DEFAULT_TEST, - }; Store { env: self.env.clone(), db: self.db.clone(), name: self.name.clone(), version, - alloc_chunk_size, + alloc_chunk_size: self.alloc_chunk_size, } } @@ -172,97 +171,50 @@ impl Store { self.version } - /// Opens the database environment - pub fn open(&self) -> Result<(), Error> { - let mut w = self.db.write(); - *w = Some(Arc::new(lmdb::Database::open( - self.env.clone(), - Some(&self.name), - &lmdb::DatabaseOptions::new(lmdb::db::CREATE), - )?)); - Ok(()) - } - - /// Determines whether the environment needs a resize based on a simple percentage threshold - pub fn needs_resize(&self) -> Result { - let env_info = self.env.info()?; - let stat = self.env.stat()?; - - let size_used = stat.psize as usize * env_info.last_pgno; - trace!("DB map size: {}", env_info.mapsize); + /// Determines whether the environment needs a resize based on a simple percentage threshold. + pub fn needs_resize(env: Arc>, alloc_chunk_size: usize) -> (bool, usize) { + let env_info = env.info(); + let stat = env.stat(); + let size_used = stat.page_size as usize * env_info.last_page_number; + trace!("DB map size: {}", env_info.map_size); trace!("Space used: {}", size_used); - trace!("Space remaining: {}", env_info.mapsize - size_used); + trace!("Space remaining: {}", env_info.map_size - size_used); let resize_percent = RESIZE_PERCENT; trace!( "Percent used: {:.*} Percent threshold: {:.*}", 4, - size_used as f64 / env_info.mapsize as f64, + size_used as f64 / env_info.map_size as f64, 4, resize_percent ); - - if size_used as f32 / env_info.mapsize as f32 > resize_percent - || env_info.mapsize < self.alloc_chunk_size + let resize = if size_used as f32 / env_info.map_size as f32 > resize_percent + || env_info.map_size < alloc_chunk_size { trace!("Resize threshold met (percent-based)"); - Ok(true) + true } else { trace!("Resize threshold not met (percent-based)"); - Ok(false) - } - } - - /// Increments the database size by as many ALLOC_CHUNK_SIZES - /// to give a minimum threshold of free space - pub fn do_resize(&self) -> Result<(), Error> { - let env_info = self.env.info()?; - let stat = self.env.stat()?; - let size_used = stat.psize as usize * env_info.last_pgno; + false + }; - let new_mapsize = if env_info.mapsize < self.alloc_chunk_size { - self.alloc_chunk_size - } else { - let mut tot = env_info.mapsize; + let new_size = { + let mut tot = env_info.map_size; while size_used as f32 / tot as f32 > RESIZE_MIN_TARGET_PERCENT { - tot += self.alloc_chunk_size; + tot += alloc_chunk_size; } tot }; - - // close - let mut w = self.db.write(); - *w = None; - - unsafe { - self.env.set_mapsize(new_mapsize)?; - } - - *w = Some(Arc::new(lmdb::Database::open( - self.env.clone(), - Some(&self.name), - &lmdb::DatabaseOptions::new(lmdb::db::CREATE), - )?)); - - info!( - "Resized database from {} to {}", - env_info.mapsize, new_mapsize - ); - Ok(()) + (resize, new_size) } /// Gets a value from the db, provided its key. /// Deserializes the retrieved data using the provided function. - pub fn get_with( - &self, - key: &[u8], - access: &lmdb::ConstAccessor<'_>, - db: &lmdb::Database<'_>, - deserialize: F, - ) -> Result, Error> + fn get_with(&self, key: &[u8], read: &RoTxn, deserialize: F) -> Result, Error> where F: Fn(&[u8], &[u8]) -> Result, { - let res: Option<&[u8]> = access.get(db, key).to_opt()?; + let db = self.db.read(); + let res: Option<&[u8]> = db.as_ref().unwrap().get(read, key)?; match res { None => Ok(None), Some(res) => deserialize(key, res).map(Some), @@ -276,81 +228,73 @@ impl Store { key: &[u8], deser_mode: Option, ) -> Result, Error> { - let lock = self.db.read(); - let db = lock - .as_ref() - .ok_or_else(|| Error::NotFoundErr("chain db is None".to_string()))?; - let txn = lmdb::ReadTransaction::new(self.env.clone())?; - let access = txn.access(); let d = match deser_mode { Some(d) => d, _ => DeserializationMode::default(), }; - self.get_with(key, &access, &db, |_, mut data| { + let read = self.env.read_txn()?; + self.get_with(key, &read, |_, mut data| { ser::deserialize(&mut data, self.protocol_version(), d).map_err(From::from) }) } - /// Whether the provided key exists + /// Whether the provided key exists. pub fn exists(&self, key: &[u8]) -> Result { - let lock = self.db.read(); - let db = lock - .as_ref() - .ok_or_else(|| Error::NotFoundErr("chain db is None".to_string()))?; - let txn = lmdb::ReadTransaction::new(self.env.clone())?; - let access = txn.access(); - - let res: Option<&lmdb::Ignore> = access.get(db, key).to_opt()?; + let db = self.db.read(); + let read = self.env.read_txn()?; + let res = db.as_ref().unwrap().get(&read, key)?; Ok(res.is_some()) } /// Produces an iterator from the provided key prefix. - pub fn iter(&self, prefix: &[u8], deserialize: F) -> Result, Error> + pub fn iter( + &'_ self, + prefix: &[u8], + deserialize: F, + ) -> Result, Error> where F: Fn(&[u8], &[u8]) -> Result, { - let lock = self.db.read(); - let db = lock - .as_ref() - .ok_or_else(|| Error::NotFoundErr("chain db is None".to_string()))?; - let tx = Arc::new(lmdb::ReadTransaction::new(self.env.clone())?); - let cursor = Arc::new(tx.cursor(db.clone())?); - Ok(PrefixIterator::new(tx, cursor, prefix, deserialize)) + let db = Arc::new(self.db.read().clone()); + let read = self.env.read_txn()?; + Ok(PrefixIterator::new(db.clone(), read, prefix, deserialize)) } /// Builds a new batch to be used with this store. pub fn batch(&self) -> Result, Error> { - // check if the db needs resizing before returning the batch - if self.needs_resize()? { - self.do_resize()?; + let (resize, new_size) = Self::needs_resize(self.env.clone(), self.alloc_chunk_size); + if resize { + unsafe { + self.env.resize(new_size)?; + } } - let tx = lmdb::WriteTransaction::new(self.env.clone())?; - Ok(Batch { store: self, tx }) + Ok(Batch::new(self)?) } } /// Batch to write multiple Writeables to db in an atomic manner. pub struct Batch<'a> { store: &'a Store, - tx: lmdb::WriteTransaction<'a>, + write: RwTxn<'a>, } impl<'a> Batch<'a> { - /// Writes a single key/value pair to the db - pub fn put(&self, key: &[u8], value: &[u8]) -> Result<(), Error> { - let lock = self.store.db.read(); - let db = lock - .as_ref() - .ok_or_else(|| Error::NotFoundErr("chain db is None".to_string()))?; - self.tx - .access() - .put(db, key, value, lmdb::put::Flags::empty())?; + /// Creates a new batch for provided db. + pub fn new(store: &'a Store) -> Result, Error> { + let write = store.env.write_txn()?; + Ok(Batch { store, write }) + } + + /// Writes a single key/value pair to the db. + pub fn put(&mut self, key: &[u8], value: &[u8]) -> Result<(), Error> { + let db = self.store.db.read(); + db.as_ref().unwrap().put(&mut self.write, key, value)?; Ok(()) } /// Writes a single key and its `Writeable` value to the db. /// Encapsulates serialization using the (default) version configured on the store instance. - pub fn put_ser(&self, key: &[u8], value: &W) -> Result<(), Error> { + pub fn put_ser(&mut self, key: &[u8], value: &W) -> Result<(), Error> { self.put_ser_with_version(key, value, self.store.protocol_version()) } @@ -362,7 +306,7 @@ impl<'a> Batch<'a> { /// Writes a single key and its `Writeable` value to the db. /// Encapsulates serialization using the specified protocol version. pub fn put_ser_with_version( - &self, + &mut self, key: &[u8], value: &W, version: ProtocolVersion, @@ -376,33 +320,26 @@ impl<'a> Batch<'a> { /// Low-level access for retrieving data by key. /// Takes a function for flexible deserialization. - pub fn get_with(&self, key: &[u8], deserialize: F) -> Result, Error> + fn get_with(&self, key: &[u8], deserialize: F) -> Result, Error> where F: Fn(&[u8], &[u8]) -> Result, { - let access = self.tx.access(); - let lock = self.store.db.read(); - let db = lock - .as_ref() - .ok_or_else(|| Error::NotFoundErr("chain db is None".to_string()))?; - - self.store.get_with(key, &access, &db, deserialize) + let read = self.write.nested_read_txn()?; + self.store.get_with(key, &read, deserialize) } /// Whether the provided key exists. /// This is in the context of the current write transaction. pub fn exists(&self, key: &[u8]) -> Result { - let access = self.tx.access(); - let lock = self.store.db.read(); - let db = lock - .as_ref() - .ok_or_else(|| Error::NotFoundErr("chain db is None".to_string()))?; - let res: Option<&lmdb::Ignore> = access.get(db, key).to_opt()?; - Ok(res.is_some()) + Ok(self.store.exists(key)?) } /// Produces an iterator from the provided key prefix. - pub fn iter(&self, prefix: &[u8], deserialize: F) -> Result, Error> + pub fn iter( + &self, + prefix: &[u8], + deserialize: F, + ) -> Result, Error> where F: Fn(&[u8], &[u8]) -> Result, { @@ -427,85 +364,90 @@ impl<'a> Batch<'a> { }) } - /// Deletes a key/value pair from the db - pub fn delete(&self, key: &[u8]) -> Result<(), Error> { - let lock = self.store.db.read(); - let db = lock - .as_ref() - .ok_or_else(|| Error::NotFoundErr("chain db is None".to_string()))?; - self.tx.access().del_key(db, key)?; + /// Deletes a key/value pair from the db. + pub fn delete(&mut self, key: &[u8]) -> Result<(), Error> { + let db = self.store.db.read(); + db.as_ref().unwrap().delete(&mut self.write, key)?; Ok(()) } - /// Writes the batch to db + /// Writes the batch to db. pub fn commit(self) -> Result<(), Error> { - self.tx.commit()?; + self.write.commit()?; Ok(()) } /// Creates a child of this batch. It will be merged with its parent on /// commit, abandoned otherwise. pub fn child(&mut self) -> Result, Error> { + let write = self.store.env.nested_write_txn(&mut self.write)?; Ok(Batch { store: self.store, - tx: self.tx.child_tx()?, + write, }) } } /// An iterator based on key prefix. /// Caller is responsible for deserialization of the data. -pub struct PrefixIterator +pub struct PrefixIterator<'a, F, T> where F: Fn(&[u8], &[u8]) -> Result, { - tx: Arc>, - cursor: Arc>, - seek: bool, + db: Arc>>>, + read: Arc>, + skip: usize, prefix: Vec, deserialize: F, } -impl Iterator for PrefixIterator +impl<'a, F, T> Iterator for PrefixIterator<'a, F, T> where F: Fn(&[u8], &[u8]) -> Result, { type Item = T; fn next(&mut self) -> Option { - let access = self.tx.access(); - let cursor = Arc::get_mut(&mut self.cursor).expect("failed to get cursor"); - let kv: Result<(&[u8], &[u8]), _> = if self.seek { - cursor.next(&access) - } else { - self.seek = true; - cursor.seek_range_k(&access, &self.prefix[..]) - }; - kv.ok() - .filter(|(k, _)| k.starts_with(self.prefix.as_slice())) - .map(|(k, v)| match (self.deserialize)(k, v) { - Ok(v) => Some(v), - Err(_) => None, - }) - .flatten() + let db = self.db.as_ref(); + if let Ok(iter) = db.as_ref().unwrap().iter(&self.read) { + let kv = iter + .filter(|i| { + if let Ok(i) = i { + return i.0.starts_with(&self.prefix); + } + false + }) + .skip(self.skip) + .next() + .transpose() + .unwrap_or(None); + self.skip += 1; + if let Some((k, v)) = kv { + return match (self.deserialize)(k, v) { + Ok(v) => Some(v), + Err(_) => None, + }; + } + } + None } } -impl PrefixIterator +impl<'a, F, T> PrefixIterator<'a, F, T> where F: Fn(&[u8], &[u8]) -> Result, { /// Initialize a new prefix iterator. pub fn new( - tx: Arc>, - cursor: Arc>, + db: Arc>>>, + read: RoTxn<'a, WithoutTls>, prefix: &[u8], deserialize: F, - ) -> PrefixIterator { + ) -> PrefixIterator<'a, F, T> { PrefixIterator { - tx, - cursor, - seek: false, + db, + read: Arc::new(read), + skip: 0, prefix: prefix.to_vec(), deserialize, } diff --git a/store/tests/lmdb.rs b/store/tests/lmdb.rs index dc3d2329fd..c9fe6ceb1a 100644 --- a/store/tests/lmdb.rs +++ b/store/tests/lmdb.rs @@ -76,7 +76,7 @@ fn test_exists() -> Result<(), store::Error> { let value = [1, 1, 1, 1]; // Start new batch and insert a new key/value entry. - let batch = store.batch()?; + let mut batch = store.batch()?; batch.put(&key, &value)?; // Check we can see the new entry in uncommitted batch. @@ -105,7 +105,7 @@ fn test_iter() -> Result<(), store::Error> { let value = [1, 1, 1, 1]; // Start new batch and insert a new key/value entry. - let batch = store.batch()?; + let mut batch = store.batch()?; batch.put(&key, &value)?; // TODO - This is not currently possible (and we need to be aware of this). @@ -144,7 +144,7 @@ fn lmdb_allocate() -> Result<(), store::Error> { println!("Allocating chunk: {}", i); let chunk = PhatChunkStruct::new(); let key_val = format!("phat_chunk_set_1_{}", i); - let batch = store.batch()?; + let mut batch = store.batch()?; let key = store::to_key(b'P', &key_val); batch.put_ser(&key, &chunk)?; batch.commit()?; @@ -160,7 +160,7 @@ fn lmdb_allocate() -> Result<(), store::Error> { println!("Allocating chunk: {}", i); let chunk = PhatChunkStruct::new(); let key_val = format!("phat_chunk_set_2_{}", i); - let batch = store.batch()?; + let mut batch = store.batch()?; let key = store::to_key(b'P', &key_val); batch.put_ser(&key, &chunk)?; batch.commit()?; From 02cce567411c9a9aa483ed154d14d13420436f21 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Thu, 16 Apr 2026 14:30:36 +0300 Subject: [PATCH 02/37] fix: check resizing operation and wait to avoid crash with multiple batches, fix exists check at batch --- store/src/lmdb.rs | 44 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index f2e0faacca..031250c4ce 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -14,18 +14,19 @@ //! Storage of core types using LMDB. -use std::fs; -use std::sync::Arc; - use heed::types::Bytes; use heed::{Database, Env, EnvOpenOptions, RoTxn, RwTxn, WithoutTls}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::Duration; +use std::{fs, thread}; use crate::grin_core::global; use crate::grin_core::ser::{self, DeserializationMode, ProtocolVersion}; use crate::util::RwLock; /// number of bytes to grow the database by when needed -pub const ALLOC_CHUNK_SIZE_DEFAULT: usize = 134_217_728; //128 MB +pub const ALLOC_CHUNK_SIZE_DEFAULT: usize = 134_217_728 / 32; //128 MB /// And for test mode, to avoid too much disk allocation on windows pub const ALLOC_CHUNK_SIZE_DEFAULT_TEST: usize = 1_048_576; //1 MB const RESIZE_PERCENT: f32 = 0.9; @@ -87,6 +88,7 @@ pub struct Store { name: String, version: ProtocolVersion, alloc_chunk_size: usize, + resizing: Arc, } impl Store { @@ -141,6 +143,7 @@ impl Store { name: db_name, version: DEFAULT_DB_VERSION, alloc_chunk_size, + resizing: Default::default(), }; { let mut write = s.env.write_txn()?; @@ -163,6 +166,7 @@ impl Store { name: self.name.clone(), version, alloc_chunk_size: self.alloc_chunk_size, + resizing: self.resizing.clone(), } } @@ -187,6 +191,7 @@ impl Store { 4, resize_percent ); + let resize = if size_used as f32 / env_info.map_size as f32 > resize_percent || env_info.map_size < alloc_chunk_size { @@ -197,13 +202,20 @@ impl Store { false }; - let new_size = { - let mut tot = env_info.map_size; - while size_used as f32 / tot as f32 > RESIZE_MIN_TARGET_PERCENT { - tot += alloc_chunk_size; + let new_size = if resize { + if env_info.map_size < alloc_chunk_size { + alloc_chunk_size + } else { + let mut tot = env_info.map_size; + while size_used as f32 / tot as f32 > RESIZE_MIN_TARGET_PERCENT { + tot += alloc_chunk_size; + } + tot } - tot + } else { + env_info.map_size }; + (resize, new_size) } @@ -262,11 +274,20 @@ impl Store { /// Builds a new batch to be used with this store. pub fn batch(&self) -> Result, Error> { + while self.resizing.load(Ordering::SeqCst) { + debug!("Wait resizing {} DB", self.name); + thread::sleep(Duration::from_millis(100)); + } let (resize, new_size) = Self::needs_resize(self.env.clone(), self.alloc_chunk_size); if resize { + self.resizing.store(true, Ordering::SeqCst); + debug!("Start resizing {} DB", self.name); unsafe { + thread::sleep(Duration::from_millis(3000)); self.env.resize(new_size)?; } + self.resizing.store(false, Ordering::SeqCst); + debug!("End resizing {} DB", self.name); } Ok(Batch::new(self)?) } @@ -331,7 +352,10 @@ impl<'a> Batch<'a> { /// Whether the provided key exists. /// This is in the context of the current write transaction. pub fn exists(&self, key: &[u8]) -> Result { - Ok(self.store.exists(key)?) + let db = self.store.db.read(); + let read = self.write.nested_read_txn()?; + let res = db.as_ref().unwrap().get(&read, key)?; + Ok(res.is_some()) } /// Produces an iterator from the provided key prefix. From 593f4c420a8d25dba0c8db8e8427f4e1c86eeb80 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Thu, 16 Apr 2026 22:50:53 +0300 Subject: [PATCH 03/37] build: fix missing deps at Cargo.lock --- Cargo.lock | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 74db18b779..2b8bc7c4a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -487,6 +487,8 @@ dependencies = [ "lazy_static", "libc", "log", + "maplit", + "pancurses", "signal-hook", "unicode-segmentation", "unicode-width", @@ -1759,6 +1761,12 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + [[package]] name = "memchr" version = "2.7.4" @@ -1805,6 +1813,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ncurses" +version = "5.101.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5d34d72657dc4b638a1c25d40aae81e4f1c699062f72f467237920752032" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "nix" version = "0.29.0" @@ -2010,6 +2029,19 @@ dependencies = [ "winapi", ] +[[package]] +name = "pancurses" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0352975c36cbacb9ee99bfb709b9db818bed43af57751797f8633649759d13db" +dependencies = [ + "libc", + "log", + "ncurses", + "pdcurses-sys", + "winreg", +] + [[package]] name = "parking_lot" version = "0.10.2" @@ -2081,6 +2113,16 @@ dependencies = [ "sha2", ] +[[package]] +name = "pdcurses-sys" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "084dd22796ff60f1225d4eb6329f33afaf4c85419d51d440ab6b8c6f4529166b" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -3364,6 +3406,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winreg" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27a759395c1195c4cc5cda607ef6f8f6498f64e78f7900f5de0a127a424704a" +dependencies = [ + "winapi", +] + [[package]] name = "writeable" version = "0.6.2" From fcf0884b343686bd1719bb98355aaecb2518e2bc Mon Sep 17 00:00:00 2001 From: ardocrat Date: Fri, 17 Apr 2026 00:44:09 +0300 Subject: [PATCH 04/37] lmdb: single environment, migrate existing databases with provided non-default environment name --- Cargo.lock | 1 + store/Cargo.toml | 1 + store/src/lmdb.rs | 117 +++++++++++++++++++++++++++++++++++----------- 3 files changed, 93 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2b8bc7c4a2..3bb392e394 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1201,6 +1201,7 @@ dependencies = [ "grin_core", "grin_util", "heed", + "lazy_static", "libc", "log", "memmap", diff --git a/store/Cargo.toml b/store/Cargo.toml index 9654cc1fc5..5154b91b57 100644 --- a/store/Cargo.toml +++ b/store/Cargo.toml @@ -20,6 +20,7 @@ serde = "1" serde_derive = "1" thiserror = "1" log = "0.4" +lazy_static = "1.5.0" grin_core = { path = "../core", version = "5.4.0" } grin_util = { path = "../util", version = "5.4.0" } diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index 031250c4ce..0315fdde27 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -16,6 +16,8 @@ use heed::types::Bytes; use heed::{Database, Env, EnvOpenOptions, RoTxn, RwTxn, WithoutTls}; +use lazy_static::lazy_static; +use std::path::Path; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; @@ -34,6 +36,11 @@ const RESIZE_PERCENT: f32 = 0.9; /// of total space free const RESIZE_MIN_TARGET_PERCENT: f32 = 0.65; +lazy_static! { + /// Shared environment. + static ref ENV: Arc>>> = Arc::new(RwLock::new(None)); +} + /// Main error type for this lmdb #[derive(Clone, Eq, PartialEq, Debug, thiserror::Error)] pub enum Error { @@ -80,11 +87,13 @@ where const DEFAULT_DB_VERSION: ProtocolVersion = ProtocolVersion(3); +const ENV_NAME: &'static str = "lmdb"; + /// LMDB-backed store facilitating data access and serialization. All writes /// are done through a Batch abstraction providing atomicity. pub struct Store { - env: Arc>, db: Arc>>>>, + env: Arc>, name: String, version: ProtocolVersion, alloc_chunk_size: usize, @@ -96,25 +105,26 @@ impl Store { /// By default creates an environment named "lmdb". /// Be aware of transactional semantics in lmdb /// (transactions are per environment, not per database). + /// db with non-default `env_name` will be migrated into default environment. pub fn new( root_path: &str, env_name: Option<&str>, db_name: Option<&str>, max_readers: Option, ) -> Result { - let name = match env_name { - Some(n) => n.to_owned(), - None => "lmdb".to_owned(), - }; - let db_name = match db_name { - Some(n) => n.to_owned(), - None => "lmdb".to_owned(), - }; - let full_path = [root_path.to_owned(), name].join("/"); + let name = env_name.unwrap_or_else(|| ENV_NAME); + let db_name = db_name.unwrap_or_else(|| ENV_NAME); + let mut full_path = Path::new(root_path).join(name); + + // Fix path to use default environment. + if name != ENV_NAME { + full_path = Path::new(root_path).join(ENV_NAME); + } + fs::create_dir_all(&full_path).map_err(|e| { Error::FileErr(format!( - "Unable to create directory 'db_root' to store chain_data: {:?}", - e + "Unable to create {:?} to store data: {:?}", + full_path, e )) })?; @@ -123,24 +133,42 @@ impl Store { false => ALLOC_CHUNK_SIZE_DEFAULT_TEST, }; - let env = unsafe { - let mut options = EnvOpenOptions::new().read_txn_without_tls(); - let mut env_options = options.map_size(alloc_chunk_size).max_dbs(8); - if let Some(max_readers) = max_readers { - env_options = env_options.max_readers(max_readers); - } - env_options.open(&full_path)? + // Environment setup. + let has_env = { + let r_env = ENV.read(); + r_env.is_some() }; - let (resize, new_size) = Self::needs_resize(Arc::new(env.clone()), alloc_chunk_size); - if resize { - unsafe { - env.resize(new_size)?; + let env = if !has_env { + let env = unsafe { + let mut options = EnvOpenOptions::new().read_txn_without_tls(); + let mut env_options = options.map_size(alloc_chunk_size).max_dbs(8); + if let Some(max_readers) = max_readers { + env_options = env_options.max_readers(max_readers); + } + env_options.open(&full_path)? }; - } + let (resize, new_size) = Self::needs_resize(Arc::new(env.clone()), alloc_chunk_size); + if resize { + unsafe { + env.resize(new_size)?; + }; + } + { + let mut w_env = ENV.write(); + *w_env = Some(env.clone()); + } + debug!("DB Mapsize for {:?} is {}", full_path, env.info().map_size); + env + } else { + let r_env = ENV.read(); + r_env.clone().unwrap() + }; + + // Database setup. let s = Store { env: Arc::new(env), db: Arc::new(RwLock::new(None)), - name: db_name, + name: db_name.to_string(), version: DEFAULT_DB_VERSION, alloc_chunk_size, resizing: Default::default(), @@ -152,9 +180,46 @@ impl Store { let mut w_db = s.db.write(); *w_db = Some(Arc::new(db)); } + + // Migrate to default environment if needed. + let migrate_from = Path::new(root_path).join(name); + if name != ENV_NAME && migrate_from.exists() { + debug!("Migrating {} to {:?}", name, full_path); + if Self::migrate_to_default_env(&s, &migrate_from, db_name).is_ok() { + let _ = fs::remove_dir_all(&migrate_from); + } else { + error!("Migrating {} failed", name); + } + } Ok(s) + } - // debug!("DB Mapsize for {} is {}", full_path, env.info().map_size); + /// Migrate db from provided path to default environment. + fn migrate_to_default_env(s: &Store, path: &Path, db_name: &str) -> Result<(), Error> { + let env = unsafe { + let mut options = EnvOpenOptions::new().read_txn_without_tls(); + let env_options = options.map_size(s.alloc_chunk_size).max_dbs(1); + env_options.open(path)? + }; + let db_from = { + let mut write = env.write_txn()?; + let db: Database = env.create_database(&mut write, Some(db_name))?; + write.commit()?; + db + }; + let db_to = s.db.read(); + let mut write_to = s.env.write_txn()?; + let read_from = env.read_txn()?; + let mut count = 0; + for kv in db_from.iter(&read_from)? { + count += 1; + if let Ok((k, v)) = kv { + db_to.as_ref().unwrap().put(&mut write_to, &k, &v)?; + } + } + write_to.commit()?; + debug!("Migrated {} records of {}", count, db_name); + Ok(()) } /// Construct a new store using a specific protocol version. From 08e95ce197c86cdd6c84773263712d59c8cec8b2 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Fri, 17 Apr 2026 01:26:45 +0300 Subject: [PATCH 05/37] fix: revert chunk size to 128mb --- store/src/lmdb.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index 0315fdde27..279608c1f2 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -28,7 +28,7 @@ use crate::grin_core::ser::{self, DeserializationMode, ProtocolVersion}; use crate::util::RwLock; /// number of bytes to grow the database by when needed -pub const ALLOC_CHUNK_SIZE_DEFAULT: usize = 134_217_728 / 32; //128 MB +pub const ALLOC_CHUNK_SIZE_DEFAULT: usize = 134_217_728; //128 MB /// And for test mode, to avoid too much disk allocation on windows pub const ALLOC_CHUNK_SIZE_DEFAULT_TEST: usize = 1_048_576; //1 MB const RESIZE_PERCENT: f32 = 0.9; @@ -185,7 +185,7 @@ impl Store { let migrate_from = Path::new(root_path).join(name); if name != ENV_NAME && migrate_from.exists() { debug!("Migrating {} to {:?}", name, full_path); - if Self::migrate_to_default_env(&s, &migrate_from, db_name).is_ok() { + if s.migrate_to_default_env(&migrate_from, db_name).is_ok() { let _ = fs::remove_dir_all(&migrate_from); } else { error!("Migrating {} failed", name); @@ -194,11 +194,11 @@ impl Store { Ok(s) } - /// Migrate db from provided path to default environment. - fn migrate_to_default_env(s: &Store, path: &Path, db_name: &str) -> Result<(), Error> { + /// Migrate db from provided path to store environment. + fn migrate_to_default_env(&self, path: &Path, db_name: &str) -> Result<(), Error> { let env = unsafe { let mut options = EnvOpenOptions::new().read_txn_without_tls(); - let env_options = options.map_size(s.alloc_chunk_size).max_dbs(1); + let env_options = options.map_size(self.alloc_chunk_size).max_dbs(1); env_options.open(path)? }; let db_from = { @@ -207,8 +207,8 @@ impl Store { write.commit()?; db }; - let db_to = s.db.read(); - let mut write_to = s.env.write_txn()?; + let db_to = self.db.read(); + let mut write_to = self.env.write_txn()?; let read_from = env.read_txn()?; let mut count = 0; for kv in db_from.iter(&read_from)? { From f9a04ff100de31be9938080866f4ea04ea6d4d19 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Mon, 20 Apr 2026 21:08:26 +0300 Subject: [PATCH 06/37] lmdb: ability to use multiple shared environments --- store/src/lmdb.rs | 205 ++++++++++++++++++++++++++-------------------- 1 file changed, 116 insertions(+), 89 deletions(-) diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index 279608c1f2..f4f21b1576 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -16,10 +16,9 @@ use heed::types::Bytes; use heed::{Database, Env, EnvOpenOptions, RoTxn, RwTxn, WithoutTls}; -use lazy_static::lazy_static; +use std::collections::HashMap; use std::path::Path; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; +use std::sync::{Arc, OnceLock}; use std::time::Duration; use std::{fs, thread}; @@ -36,11 +35,6 @@ const RESIZE_PERCENT: f32 = 0.9; /// of total space free const RESIZE_MIN_TARGET_PERCENT: f32 = 0.65; -lazy_static! { - /// Shared environment. - static ref ENV: Arc>>> = Arc::new(RwLock::new(None)); -} - /// Main error type for this lmdb #[derive(Clone, Eq, PartialEq, Debug, thiserror::Error)] pub enum Error { @@ -87,17 +81,22 @@ where const DEFAULT_DB_VERSION: ProtocolVersion = ProtocolVersion(3); -const ENV_NAME: &'static str = "lmdb"; +const DEFAULT_ENV_NAME: &'static str = "lmdb"; + +/// Mapping of database path to environment. +static ENV_MAP: OnceLock>>>> = OnceLock::new(); +/// Mapping of database path to check if database is resizing. +static ENV_RESIZING: OnceLock>>> = OnceLock::new(); /// LMDB-backed store facilitating data access and serialization. All writes /// are done through a Batch abstraction providing atomicity. pub struct Store { - db: Arc>>>>, - env: Arc>, + env: Env, + env_path: String, + db: Arc>, name: String, version: ProtocolVersion, alloc_chunk_size: usize, - resizing: Arc, } impl Store { @@ -112,15 +111,23 @@ impl Store { db_name: Option<&str>, max_readers: Option, ) -> Result { - let name = env_name.unwrap_or_else(|| ENV_NAME); - let db_name = db_name.unwrap_or_else(|| ENV_NAME); - let mut full_path = Path::new(root_path).join(name); - - // Fix path to use default environment. - if name != ENV_NAME { - full_path = Path::new(root_path).join(ENV_NAME); - } - + let name = env_name + .map(|n| { + if n != DEFAULT_ENV_NAME { + DEFAULT_ENV_NAME + } else { + n + } + }) + .unwrap_or_else(|| DEFAULT_ENV_NAME); + let db_name = db_name.unwrap_or_else(|| DEFAULT_ENV_NAME); + + // Database path setup. + let full_path = Path::new(root_path) + .join(name) + .to_str() + .unwrap() + .to_string(); fs::create_dir_all(&full_path).map_err(|e| { Error::FileErr(format!( "Unable to create {:?} to store data: {:?}", @@ -134,11 +141,12 @@ impl Store { }; // Environment setup. + let env_map = ENV_MAP.get_or_init(|| Arc::new(RwLock::new(HashMap::new()))); let has_env = { - let r_env = ENV.read(); - r_env.is_some() + let r_env_map = env_map.read(); + r_env_map.contains_key(&full_path) }; - let env = if !has_env { + if !has_env { let env = unsafe { let mut options = EnvOpenOptions::new().read_txn_without_tls(); let mut env_options = options.map_size(alloc_chunk_size).max_dbs(8); @@ -147,78 +155,77 @@ impl Store { } env_options.open(&full_path)? }; - let (resize, new_size) = Self::needs_resize(Arc::new(env.clone()), alloc_chunk_size); + let (resize, new_size) = Self::needs_resize(&env, alloc_chunk_size); if resize { unsafe { env.resize(new_size)?; }; } - { - let mut w_env = ENV.write(); - *w_env = Some(env.clone()); - } - debug!("DB Mapsize for {:?} is {}", full_path, env.info().map_size); - env - } else { - let r_env = ENV.read(); - r_env.clone().unwrap() - }; + debug!("DB Mapsize for {} is {}", db_name, env.info().map_size); + let mut w_env_map = env_map.write(); + w_env_map.insert(full_path.clone(), env); + } // Database setup. + let r_env_map = env_map.read(); + let env = r_env_map.get(&full_path).unwrap(); + let mut write = env.write_txn()?; + let db = env.create_database(&mut write, Some(db_name))?; + write.commit()?; + let s = Store { - env: Arc::new(env), - db: Arc::new(RwLock::new(None)), + env: env.clone(), + env_path: full_path.clone(), + db: Arc::new(db), name: db_name.to_string(), version: DEFAULT_DB_VERSION, alloc_chunk_size, - resizing: Default::default(), }; - { - let mut write = s.env.write_txn()?; - let db = s.env.create_database(&mut write, Some(&s.name))?; - write.commit()?; - let mut w_db = s.db.write(); - *w_db = Some(Arc::new(db)); - } // Migrate to default environment if needed. - let migrate_from = Path::new(root_path).join(name); - if name != ENV_NAME && migrate_from.exists() { - debug!("Migrating {} to {:?}", name, full_path); - if s.migrate_to_default_env(&migrate_from, db_name).is_ok() { - let _ = fs::remove_dir_all(&migrate_from); - } else { - error!("Migrating {} failed", name); + if let Some(env_name) = env_name { + if env_name != DEFAULT_ENV_NAME { + let migrate_from = Path::new(root_path).join(env_name); + if s.migrate_to_default_env(&migrate_from).is_ok() { + let _ = fs::remove_dir_all(&migrate_from); + } else { + error!("Migrating {} failed", name); + } } } + Ok(s) } /// Migrate db from provided path to store environment. - fn migrate_to_default_env(&self, path: &Path, db_name: &str) -> Result<(), Error> { - let env = unsafe { + fn migrate_to_default_env(&self, from_path: &Path) -> Result<(), Error> { + if !from_path.exists() { + return Ok(()); + }; + debug!("Migrating {} to {:?}", self.name, self.env_path); + let from_env = unsafe { let mut options = EnvOpenOptions::new().read_txn_without_tls(); let env_options = options.map_size(self.alloc_chunk_size).max_dbs(1); - env_options.open(path)? + env_options.open(from_path)? }; let db_from = { - let mut write = env.write_txn()?; - let db: Database = env.create_database(&mut write, Some(db_name))?; + let mut write = from_env.write_txn()?; + let db_name = self.name.as_str(); + let db: Database = from_env.create_database(&mut write, Some(db_name))?; write.commit()?; db }; - let db_to = self.db.read(); let mut write_to = self.env.write_txn()?; - let read_from = env.read_txn()?; + let read_from = from_env.read_txn()?; let mut count = 0; for kv in db_from.iter(&read_from)? { count += 1; if let Ok((k, v)) = kv { - db_to.as_ref().unwrap().put(&mut write_to, &k, &v)?; + self.db.put(&mut write_to, &k, &v)?; } } write_to.commit()?; - debug!("Migrated {} records of {}", count, db_name); + debug!("Migrated {} records from {}", count, self.name); Ok(()) } @@ -227,11 +234,11 @@ impl Store { pub fn with_version(&self, version: ProtocolVersion) -> Store { Store { env: self.env.clone(), + env_path: self.env_path.clone(), db: self.db.clone(), name: self.name.clone(), version, alloc_chunk_size: self.alloc_chunk_size, - resizing: self.resizing.clone(), } } @@ -241,7 +248,7 @@ impl Store { } /// Determines whether the environment needs a resize based on a simple percentage threshold. - pub fn needs_resize(env: Arc>, alloc_chunk_size: usize) -> (bool, usize) { + pub fn needs_resize(env: &Env, alloc_chunk_size: usize) -> (bool, usize) { let env_info = env.info(); let stat = env.stat(); let size_used = stat.page_size as usize * env_info.last_page_number; @@ -290,8 +297,7 @@ impl Store { where F: Fn(&[u8], &[u8]) -> Result, { - let db = self.db.read(); - let res: Option<&[u8]> = db.as_ref().unwrap().get(read, key)?; + let res: Option<&[u8]> = self.db.get(read, key)?; match res { None => Ok(None), Some(res) => deserialize(key, res).map(Some), @@ -317,9 +323,8 @@ impl Store { /// Whether the provided key exists. pub fn exists(&self, key: &[u8]) -> Result { - let db = self.db.read(); let read = self.env.read_txn()?; - let res = db.as_ref().unwrap().get(&read, key)?; + let res = self.db.get(&read, key)?; Ok(res.is_some()) } @@ -332,28 +337,53 @@ impl Store { where F: Fn(&[u8], &[u8]) -> Result, { - let db = Arc::new(self.db.read().clone()); let read = self.env.read_txn()?; - Ok(PrefixIterator::new(db.clone(), read, prefix, deserialize)) + Ok(PrefixIterator::new( + self.db.clone(), + read, + prefix, + deserialize, + )) } - /// Builds a new batch to be used with this store. - pub fn batch(&self) -> Result, Error> { - while self.resizing.load(Ordering::SeqCst) { - debug!("Wait resizing {} DB", self.name); - thread::sleep(Duration::from_millis(100)); + /// Resize database environment if needed. + fn maybe_resize(&self) -> Result<(), Error> { + loop { + let resizing = { + let res_map = ENV_RESIZING.get_or_init(|| Arc::new(RwLock::new(HashMap::new()))); + let r_res_map = res_map.read(); + r_res_map.get(&self.env_path).map(|r| *r).unwrap_or(false) + }; + if !resizing { + break; + } + trace!("Wait resizing DB"); + thread::sleep(Duration::from_millis(500)); } - let (resize, new_size) = Self::needs_resize(self.env.clone(), self.alloc_chunk_size); + let (resize, new_size) = Self::needs_resize(&self.env, self.alloc_chunk_size); if resize { - self.resizing.store(true, Ordering::SeqCst); - debug!("Start resizing {} DB", self.name); + let res_map = ENV_RESIZING.get().unwrap(); + { + let mut w_res_map = res_map.write(); + w_res_map.insert(self.env_path.clone(), true); + } + trace!("Start resizing {} DB", self.name); unsafe { thread::sleep(Duration::from_millis(3000)); self.env.resize(new_size)?; } - self.resizing.store(false, Ordering::SeqCst); - debug!("End resizing {} DB", self.name); + { + let mut w_res_map = res_map.write(); + w_res_map.insert(self.env_path.clone(), false); + } + trace!("End resizing {} DB", self.name); } + Ok(()) + } + + /// Builds a new batch to be used with this store. + pub fn batch(&self) -> Result, Error> { + self.maybe_resize()?; Ok(Batch::new(self)?) } } @@ -373,8 +403,7 @@ impl<'a> Batch<'a> { /// Writes a single key/value pair to the db. pub fn put(&mut self, key: &[u8], value: &[u8]) -> Result<(), Error> { - let db = self.store.db.read(); - db.as_ref().unwrap().put(&mut self.write, key, value)?; + self.store.db.put(&mut self.write, key, value)?; Ok(()) } @@ -417,9 +446,8 @@ impl<'a> Batch<'a> { /// Whether the provided key exists. /// This is in the context of the current write transaction. pub fn exists(&self, key: &[u8]) -> Result { - let db = self.store.db.read(); let read = self.write.nested_read_txn()?; - let res = db.as_ref().unwrap().get(&read, key)?; + let res = self.store.db.get(&read, key)?; Ok(res.is_some()) } @@ -455,8 +483,7 @@ impl<'a> Batch<'a> { /// Deletes a key/value pair from the db. pub fn delete(&mut self, key: &[u8]) -> Result<(), Error> { - let db = self.store.db.read(); - db.as_ref().unwrap().delete(&mut self.write, key)?; + self.store.db.delete(&mut self.write, key)?; Ok(()) } @@ -469,6 +496,7 @@ impl<'a> Batch<'a> { /// Creates a child of this batch. It will be merged with its parent on /// commit, abandoned otherwise. pub fn child(&mut self) -> Result, Error> { + self.store.maybe_resize()?; let write = self.store.env.nested_write_txn(&mut self.write)?; Ok(Batch { store: self.store, @@ -483,7 +511,7 @@ pub struct PrefixIterator<'a, F, T> where F: Fn(&[u8], &[u8]) -> Result, { - db: Arc>>>, + db: Arc>, read: Arc>, skip: usize, prefix: Vec, @@ -497,8 +525,7 @@ where type Item = T; fn next(&mut self) -> Option { - let db = self.db.as_ref(); - if let Ok(iter) = db.as_ref().unwrap().iter(&self.read) { + if let Ok(iter) = self.db.iter(&self.read) { let kv = iter .filter(|i| { if let Ok(i) = i { @@ -528,7 +555,7 @@ where { /// Initialize a new prefix iterator. pub fn new( - db: Arc>>>, + db: Arc>, read: RoTxn<'a, WithoutTls>, prefix: &[u8], deserialize: F, From 5adec7ad13c1f9bac4cfdaa2a2687a876d2bc97f Mon Sep 17 00:00:00 2001 From: ardocrat Date: Mon, 20 Apr 2026 21:13:50 +0300 Subject: [PATCH 07/37] build: remove unused dependency --- Cargo.lock | 1 - store/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3bb392e394..2b8bc7c4a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1201,7 +1201,6 @@ dependencies = [ "grin_core", "grin_util", "heed", - "lazy_static", "libc", "log", "memmap", diff --git a/store/Cargo.toml b/store/Cargo.toml index 5154b91b57..9654cc1fc5 100644 --- a/store/Cargo.toml +++ b/store/Cargo.toml @@ -20,7 +20,6 @@ serde = "1" serde_derive = "1" thiserror = "1" log = "0.4" -lazy_static = "1.5.0" grin_core = { path = "../core", version = "5.4.0" } grin_util = { path = "../util", version = "5.4.0" } From b5eeb37641eb9db2d1c5aeaded2eb9083f238d34 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Tue, 21 Apr 2026 18:46:04 +0300 Subject: [PATCH 08/37] fix: resize to have correct multiplier of the system page size --- store/src/lmdb.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index f4f21b1576..da2c3a0764 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -278,7 +278,7 @@ impl Store { if env_info.map_size < alloc_chunk_size { alloc_chunk_size } else { - let mut tot = env_info.map_size; + let mut tot = env_info.map_size - (env_info.map_size % alloc_chunk_size); while size_used as f32 / tot as f32 > RESIZE_MIN_TARGET_PERCENT { tot += alloc_chunk_size; } From b18f453fa801abb851d612c4685502c49567d122 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Wed, 22 Apr 2026 19:23:28 +0300 Subject: [PATCH 09/37] lmdb: speed up prefix iter by storing keys --- store/src/lmdb.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index da2c3a0764..8a3cd72462 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -513,8 +513,8 @@ where { db: Arc>, read: Arc>, + keys: Vec>, skip: usize, - prefix: Vec, deserialize: F, } @@ -525,22 +525,14 @@ where type Item = T; fn next(&mut self) -> Option { - if let Ok(iter) = self.db.iter(&self.read) { - let kv = iter - .filter(|i| { - if let Ok(i) = i { - return i.0.starts_with(&self.prefix); - } - false - }) - .skip(self.skip) - .next() - .transpose() - .unwrap_or(None); - self.skip += 1; - if let Some((k, v)) = kv { + if let Some(k) = self.keys.iter().skip(self.skip).next() { + let v = self.db.get(&self.read, k).unwrap_or(None); + if let Some(v) = v { return match (self.deserialize)(k, v) { - Ok(v) => Some(v), + Ok(v) => { + self.skip += 1; + Some(v) + } Err(_) => None, }; } @@ -560,11 +552,19 @@ where prefix: &[u8], deserialize: F, ) -> PrefixIterator<'a, F, T> { + let keys = if let Ok(iter) = db.prefix_iter(&read, &prefix) { + iter.move_between_keys() + .filter(|kv| kv.is_ok()) + .map(|kv| kv.unwrap().0.to_vec()) + .collect::>>() + } else { + vec![] + }; PrefixIterator { db, read: Arc::new(read), + keys, skip: 0, - prefix: prefix.to_vec(), deserialize, } } From eef000dfe8bb417c97ba878fefba2d1eb7a4293a Mon Sep 17 00:00:00 2001 From: ardocrat Date: Thu, 23 Apr 2026 00:27:22 +0300 Subject: [PATCH 10/37] lmdb: default env name --- store/src/lmdb.rs | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index 8a3cd72462..a0bfe55396 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -111,20 +111,11 @@ impl Store { db_name: Option<&str>, max_readers: Option, ) -> Result { - let name = env_name - .map(|n| { - if n != DEFAULT_ENV_NAME { - DEFAULT_ENV_NAME - } else { - n - } - }) - .unwrap_or_else(|| DEFAULT_ENV_NAME); let db_name = db_name.unwrap_or_else(|| DEFAULT_ENV_NAME); // Database path setup. let full_path = Path::new(root_path) - .join(name) + .join(DEFAULT_ENV_NAME) .to_str() .unwrap() .to_string(); @@ -189,7 +180,7 @@ impl Store { if s.migrate_to_default_env(&migrate_from).is_ok() { let _ = fs::remove_dir_all(&migrate_from); } else { - error!("Migrating {} failed", name); + error!("Migrating DB {} failed", env_name); } } } @@ -202,7 +193,7 @@ impl Store { if !from_path.exists() { return Ok(()); }; - debug!("Migrating {} to {:?}", self.name, self.env_path); + debug!("Migrating DB {} to {}", self.name, DEFAULT_ENV_NAME); let from_env = unsafe { let mut options = EnvOpenOptions::new().read_txn_without_tls(); let env_options = options.map_size(self.alloc_chunk_size).max_dbs(1); @@ -225,7 +216,7 @@ impl Store { } } write_to.commit()?; - debug!("Migrated {} records from {}", count, self.name); + debug!("Migrated {} records from DB {}", count, self.name); Ok(()) } From 70040d2160c081dae21d0495ea00206764227322 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Fri, 24 Apr 2026 13:27:28 +0300 Subject: [PATCH 11/37] lmdb: wait db resize before read, reduce timeout before resizing --- store/src/lmdb.rs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index a0bfe55396..710d890415 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -279,6 +279,10 @@ impl Store { env_info.map_size }; + if resize { + debug!("Resizing DB to {} from {}", new_size, env_info.map_size); + } + (resize, new_size) } @@ -306,6 +310,7 @@ impl Store { Some(d) => d, _ => DeserializationMode::default(), }; + self.wait_for_resize(); let read = self.env.read_txn()?; self.get_with(key, &read, |_, mut data| { ser::deserialize(&mut data, self.protocol_version(), d).map_err(From::from) @@ -314,6 +319,7 @@ impl Store { /// Whether the provided key exists. pub fn exists(&self, key: &[u8]) -> Result { + self.wait_for_resize(); let read = self.env.read_txn()?; let res = self.db.get(&read, key)?; Ok(res.is_some()) @@ -328,6 +334,7 @@ impl Store { where F: Fn(&[u8], &[u8]) -> Result, { + self.wait_for_resize(); let read = self.env.read_txn()?; Ok(PrefixIterator::new( self.db.clone(), @@ -337,8 +344,8 @@ impl Store { )) } - /// Resize database environment if needed. - fn maybe_resize(&self) -> Result<(), Error> { + /// Wait while DB is resizing. + fn wait_for_resize(&self) { loop { let resizing = { let res_map = ENV_RESIZING.get_or_init(|| Arc::new(RwLock::new(HashMap::new()))); @@ -348,9 +355,14 @@ impl Store { if !resizing { break; } - trace!("Wait resizing DB"); + debug!("Wait on {}, resizing DB", self.name); thread::sleep(Duration::from_millis(500)); } + } + + /// Resize database environment if needed. + fn maybe_resize(&self) -> Result<(), Error> { + self.wait_for_resize(); let (resize, new_size) = Self::needs_resize(&self.env, self.alloc_chunk_size); if resize { let res_map = ENV_RESIZING.get().unwrap(); @@ -358,16 +370,16 @@ impl Store { let mut w_res_map = res_map.write(); w_res_map.insert(self.env_path.clone(), true); } - trace!("Start resizing {} DB", self.name); + debug!("Start resizing {} DB", self.name); unsafe { - thread::sleep(Duration::from_millis(3000)); + thread::sleep(Duration::from_millis(2000)); self.env.resize(new_size)?; } { let mut w_res_map = res_map.write(); w_res_map.insert(self.env_path.clone(), false); } - trace!("End resizing {} DB", self.name); + debug!("End resizing {} DB", self.name); } Ok(()) } From deb5b49310de4abd1535d3300cb0852693ab6121 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Fri, 24 Apr 2026 18:13:30 +0300 Subject: [PATCH 12/37] lmdb: use static reader for iterator, count existing batches for stable resize --- chain/src/store.rs | 6 +- p2p/src/store.rs | 2 +- store/src/lmdb.rs | 181 +++++++++++++++++++++++++++------------------ 3 files changed, 114 insertions(+), 75 deletions(-) diff --git a/chain/src/store.rs b/chain/src/store.rs index 56fb706018..6a6f536c90 100644 --- a/chain/src/store.rs +++ b/chain/src/store.rs @@ -309,9 +309,7 @@ impl<'a> Batch<'a> { } /// Iterator over the output_pos index. - pub fn output_pos_iter( - &self, - ) -> Result, CommitPos)> + 'a, Error> { + pub fn output_pos_iter(&self) -> Result, CommitPos)>, Error> { let key = to_key(OUTPUT_POS_PREFIX, ""); let protocol_version = self.db.protocol_version(); self.db.iter(&key, move |k, mut v| { @@ -440,7 +438,7 @@ impl<'a> Batch<'a> { /// Iterator over raw data for full blocks in the db. /// Used during block migration (we need flexibility around deserialization). - pub fn blocks_raw_iter(&self) -> Result, Vec)> + 'a, Error> { + pub fn blocks_raw_iter(&self) -> Result, Vec)>, Error> { let key = to_key(BLOCK_PREFIX, ""); self.db.iter(&key, |k, v| Ok((k.to_vec(), v.to_vec()))) } diff --git a/p2p/src/store.rs b/p2p/src/store.rs index 85b30437a0..931631e6e5 100644 --- a/p2p/src/store.rs +++ b/p2p/src/store.rs @@ -184,7 +184,7 @@ pub struct PeersIterBatch<'a> { impl<'a> PeersIterBatch<'a> { /// Iterator over all known peers. - pub fn peers_iter(&self) -> Result + 'a, Error> { + pub fn peers_iter(&self) -> Result, Error> { let key = to_key(PEER_PREFIX, ""); let protocol_version = self.db.protocol_version(); self.db.iter(&key, move |_, mut v| { diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index 710d890415..4463fb984c 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -85,6 +85,8 @@ const DEFAULT_ENV_NAME: &'static str = "lmdb"; /// Mapping of database path to environment. static ENV_MAP: OnceLock>>>> = OnceLock::new(); +/// Mapping of database path to count of active batches to wait before resizing. +static ENV_BATCHES_COUNT: OnceLock>>> = OnceLock::new(); /// Mapping of database path to check if database is resizing. static ENV_RESIZING: OnceLock>>> = OnceLock::new(); @@ -146,7 +148,7 @@ impl Store { } env_options.open(&full_path)? }; - let (resize, new_size) = Self::needs_resize(&env, alloc_chunk_size); + let (resize, new_size) = needs_resize(&env, alloc_chunk_size); if resize { unsafe { env.resize(new_size)?; @@ -238,54 +240,6 @@ impl Store { self.version } - /// Determines whether the environment needs a resize based on a simple percentage threshold. - pub fn needs_resize(env: &Env, alloc_chunk_size: usize) -> (bool, usize) { - let env_info = env.info(); - let stat = env.stat(); - let size_used = stat.page_size as usize * env_info.last_page_number; - trace!("DB map size: {}", env_info.map_size); - trace!("Space used: {}", size_used); - trace!("Space remaining: {}", env_info.map_size - size_used); - let resize_percent = RESIZE_PERCENT; - trace!( - "Percent used: {:.*} Percent threshold: {:.*}", - 4, - size_used as f64 / env_info.map_size as f64, - 4, - resize_percent - ); - - let resize = if size_used as f32 / env_info.map_size as f32 > resize_percent - || env_info.map_size < alloc_chunk_size - { - trace!("Resize threshold met (percent-based)"); - true - } else { - trace!("Resize threshold not met (percent-based)"); - false - }; - - let new_size = if resize { - if env_info.map_size < alloc_chunk_size { - alloc_chunk_size - } else { - let mut tot = env_info.map_size - (env_info.map_size % alloc_chunk_size); - while size_used as f32 / tot as f32 > RESIZE_MIN_TARGET_PERCENT { - tot += alloc_chunk_size; - } - tot - } - } else { - env_info.map_size - }; - - if resize { - debug!("Resizing DB to {} from {}", new_size, env_info.map_size); - } - - (resize, new_size) - } - /// Gets a value from the db, provided its key. /// Deserializes the retrieved data using the provided function. fn get_with(&self, key: &[u8], read: &RoTxn, deserialize: F) -> Result, Error> @@ -326,16 +280,12 @@ impl Store { } /// Produces an iterator from the provided key prefix. - pub fn iter( - &'_ self, - prefix: &[u8], - deserialize: F, - ) -> Result, Error> + pub fn iter(&self, prefix: &[u8], deserialize: F) -> Result, Error> where F: Fn(&[u8], &[u8]) -> Result, { self.wait_for_resize(); - let read = self.env.read_txn()?; + let read = self.env.clone().static_read_txn()?; Ok(PrefixIterator::new( self.db.clone(), read, @@ -356,14 +306,14 @@ impl Store { break; } debug!("Wait on {}, resizing DB", self.name); - thread::sleep(Duration::from_millis(500)); + thread::sleep(Duration::from_millis(100)); } } /// Resize database environment if needed. fn maybe_resize(&self) -> Result<(), Error> { self.wait_for_resize(); - let (resize, new_size) = Self::needs_resize(&self.env, self.alloc_chunk_size); + let (resize, new_size) = needs_resize(&self.env, self.alloc_chunk_size); if resize { let res_map = ENV_RESIZING.get().unwrap(); { @@ -372,7 +322,14 @@ impl Store { } debug!("Start resizing {} DB", self.name); unsafe { - thread::sleep(Duration::from_millis(2000)); + let batches_count = + ENV_BATCHES_COUNT.get_or_init(|| Arc::new(RwLock::new(HashMap::new()))); + let batches = batches_count.write(); + let cur = batches.get(&self.env_path).unwrap_or(&0); + debug!("Wait {} batches to complete", cur); + while cur != &0 { + thread::sleep(Duration::from_millis(100)); + } self.env.resize(new_size)?; } { @@ -387,21 +344,41 @@ impl Store { /// Builds a new batch to be used with this store. pub fn batch(&self) -> Result, Error> { self.maybe_resize()?; + on_change_batches_count(&self.env_path, true); Ok(Batch::new(self)?) } } +/// Batches counter to decrement value on drop. +struct BatchesCounter<'a> { + env_path: &'a String, +} + +impl Drop for BatchesCounter<'_> { + fn drop(&mut self) { + on_change_batches_count(&self.env_path, false); + } +} + /// Batch to write multiple Writeables to db in an atomic manner. pub struct Batch<'a> { store: &'a Store, write: RwTxn<'a>, + #[allow(dead_code)] + counter: BatchesCounter<'a>, } impl<'a> Batch<'a> { /// Creates a new batch for provided db. pub fn new(store: &'a Store) -> Result, Error> { let write = store.env.write_txn()?; - Ok(Batch { store, write }) + Ok(Batch { + store, + write, + counter: BatchesCounter { + env_path: &store.env_path, + }, + }) } /// Writes a single key/value pair to the db. @@ -455,11 +432,7 @@ impl<'a> Batch<'a> { } /// Produces an iterator from the provided key prefix. - pub fn iter( - &self, - prefix: &[u8], - deserialize: F, - ) -> Result, Error> + pub fn iter(&self, prefix: &[u8], deserialize: F) -> Result, Error> where F: Fn(&[u8], &[u8]) -> Result, { @@ -500,28 +473,32 @@ impl<'a> Batch<'a> { /// commit, abandoned otherwise. pub fn child(&mut self) -> Result, Error> { self.store.maybe_resize()?; + on_change_batches_count(&self.store.env_path, true); let write = self.store.env.nested_write_txn(&mut self.write)?; Ok(Batch { store: self.store, write, + counter: BatchesCounter { + env_path: &self.store.env_path, + }, }) } } /// An iterator based on key prefix. /// Caller is responsible for deserialization of the data. -pub struct PrefixIterator<'a, F, T> +pub struct PrefixIterator where F: Fn(&[u8], &[u8]) -> Result, { db: Arc>, - read: Arc>, + read: Arc>, keys: Vec>, skip: usize, deserialize: F, } -impl<'a, F, T> Iterator for PrefixIterator<'a, F, T> +impl Iterator for PrefixIterator where F: Fn(&[u8], &[u8]) -> Result, { @@ -544,17 +521,17 @@ where } } -impl<'a, F, T> PrefixIterator<'a, F, T> +impl PrefixIterator where F: Fn(&[u8], &[u8]) -> Result, { /// Initialize a new prefix iterator. pub fn new( db: Arc>, - read: RoTxn<'a, WithoutTls>, + read: RoTxn<'static, WithoutTls>, prefix: &[u8], deserialize: F, - ) -> PrefixIterator<'a, F, T> { + ) -> PrefixIterator { let keys = if let Ok(iter) = db.prefix_iter(&read, &prefix) { iter.move_between_keys() .filter(|kv| kv.is_ok()) @@ -572,3 +549,67 @@ where } } } + +/// Determines whether the environment needs a resize based on a simple percentage threshold. +pub fn needs_resize(env: &Env, alloc_chunk_size: usize) -> (bool, usize) { + let env_info = env.info(); + let stat = env.stat(); + let size_used = stat.page_size as usize * env_info.last_page_number; + trace!("DB map size: {}", env_info.map_size); + trace!("Space used: {}", size_used); + trace!("Space remaining: {}", env_info.map_size - size_used); + let resize_percent = RESIZE_PERCENT; + trace!( + "Percent used: {:.*} Percent threshold: {:.*}", + 4, + size_used as f64 / env_info.map_size as f64, + 4, + resize_percent + ); + + let resize = if size_used as f32 / env_info.map_size as f32 > resize_percent + || env_info.map_size < alloc_chunk_size + { + trace!("Resize threshold met (percent-based)"); + true + } else { + trace!("Resize threshold not met (percent-based)"); + false + }; + + let new_size = if resize { + if env_info.map_size < alloc_chunk_size { + alloc_chunk_size + } else { + let mut tot = env_info.map_size - (env_info.map_size % alloc_chunk_size); + while size_used as f32 / tot as f32 > RESIZE_MIN_TARGET_PERCENT { + tot += alloc_chunk_size; + } + tot + } + } else { + env_info.map_size + }; + + if resize { + debug!("Resizing DB to {} from {}", new_size, env_info.map_size); + } + + (resize, new_size) +} + +/// Increment or decrement active batches count for current environment. +fn on_change_batches_count(env_path: &String, inc: bool) { + let batches_count = ENV_BATCHES_COUNT.get_or_init(|| Arc::new(RwLock::new(HashMap::new()))); + let mut w_batches = batches_count.write(); + let batches = w_batches.clone(); + let count = { + let cur = batches.get(env_path).unwrap_or(&0); + if inc { + cur + 1 + } else { + cur - 1 + } + }; + w_batches.insert(env_path.clone(), count); +} From e22f5acb11c52535af82546cefaae32eeb5790f0 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Fri, 24 Apr 2026 18:25:07 +0300 Subject: [PATCH 13/37] fix: check batches count on resize waiting --- store/src/lmdb.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index 4463fb984c..7b7f5f9a01 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -322,12 +322,15 @@ impl Store { } debug!("Start resizing {} DB", self.name); unsafe { - let batches_count = - ENV_BATCHES_COUNT.get_or_init(|| Arc::new(RwLock::new(HashMap::new()))); - let batches = batches_count.write(); - let cur = batches.get(&self.env_path).unwrap_or(&0); - debug!("Wait {} batches to complete", cur); - while cur != &0 { + loop { + let batches_count = + ENV_BATCHES_COUNT.get_or_init(|| Arc::new(RwLock::new(HashMap::new()))); + let batches = batches_count.read(); + let cur = batches.get(&self.env_path).unwrap_or(&0); + if cur == &0 { + break; + } + debug!("Wait {} batches to complete", cur); thread::sleep(Duration::from_millis(100)); } self.env.resize(new_size)?; From 9ca157d24c7ab0f4f6e6a6476c8f463a2149d19a Mon Sep 17 00:00:00 2001 From: ardocrat Date: Mon, 27 Apr 2026 12:37:01 +0300 Subject: [PATCH 14/37] lmdb: use separate databases instead of prefixes, use default db for values without prefixes, migrate old environment --- chain/src/linked_list.rs | 117 +++++++++-------- chain/src/store.rs | 143 ++++++++++++-------- chain/src/txhashset/txhashset.rs | 2 +- p2p/src/store.rs | 44 ++++--- store/src/lib.rs | 43 +----- store/src/lmdb.rs | 219 +++++++++++++++++++------------ store/tests/lmdb.rs | 31 ++--- 7 files changed, 332 insertions(+), 267 deletions(-) diff --git a/chain/src/linked_list.rs b/chain/src/linked_list.rs index 79d22564fe..07c29fcd2f 100644 --- a/chain/src/linked_list.rs +++ b/chain/src/linked_list.rs @@ -18,10 +18,11 @@ use crate::core::ser::{self, Readable, Reader, Writeable, Writer}; use crate::store::Batch; use crate::types::CommitPos; use crate::util::secp::pedersen::Commitment; +use byteorder::{BigEndian, WriteBytesExt}; use enum_primitive::FromPrimitive; use grin_store as store; use std::marker::PhantomData; -use store::{to_key, to_key_u64, Error}; +use store::Error; enum_from_primitive! { #[derive(Copy, Clone, Debug, PartialEq)] @@ -74,28 +75,24 @@ pub trait ListIndex { /// List entry type type Entry: ListIndexEntry; - /// Construct a key for the list. - fn list_key(&self, commit: Commitment) -> Vec; - /// Construct a key for an individual entry in the list. - fn entry_key(&self, commit: Commitment, pos: u64) -> Vec; + fn entry_key(&self, commit: Commitment, pos: u64) -> (Option, Vec); /// Returns either a "Single" with embedded "pos" or a "list" with "head" and "tail". - /// Key is "prefix|commit". - /// Note the key for an individual entry in the list is "prefix|commit|pos". - fn get_list(&self, batch: &Batch<'_>, commit: Commitment) -> Result, Error> { - batch.db.get_ser(&self.list_key(commit), None) - } + /// Key is "commit". + /// Note the key for an individual entry in the list is "commit|pos". + fn get_list(&self, batch: &Batch<'_>, commit: Commitment) -> Result, Error>; /// Returns one of "head", "tail" or "middle" entry variants. - /// Key is "prefix|commit|pos". + /// Key is "commit|pos". fn get_entry( &self, batch: &Batch<'_>, commit: Commitment, pos: u64, ) -> Result, Error> { - batch.db.get_ser(&self.entry_key(commit, pos), None) + let (db_key, key) = self.entry_key(commit, pos); + batch.db.get_ser(db_key, &key, None) } /// Peek the head of the list for the specified commitment. @@ -243,16 +240,24 @@ where type List = ListWrapper; type Entry = ListEntry; - fn list_key(&self, commit: Commitment) -> Vec { - to_key(self.list_prefix, &mut commit.as_ref().to_vec()) + fn entry_key(&self, commit: Commitment, pos: u64) -> (Option, Vec) { + let mut key = commit.as_ref().to_vec(); + key.write_u64::(pos).unwrap(); + (Some(self.entry_prefix), key) } - fn entry_key(&self, commit: Commitment, pos: u64) -> Vec { - to_key_u64(self.entry_prefix, &mut commit.as_ref().to_vec(), pos) + fn get_list(&self, batch: &Batch<'_>, commit: Commitment) -> Result, Error> { + let list_key = (Some(self.list_prefix), commit.as_ref()); + batch + .db + .get_ser::>(list_key.0, list_key.1, None) } fn peek_pos(&self, batch: &Batch<'_>, commit: Commitment) -> Result, Error> { - match self.get_list(batch, commit)? { + match batch + .db + .get_ser(Some(self.list_prefix), commit.as_ref(), None)? + { None => Ok(None), Some(ListWrapper::Single { pos }) => Ok(Some(pos)), Some(ListWrapper::Multi { head, .. }) => { @@ -266,10 +271,11 @@ where } fn push_pos(&self, batch: &mut Batch<'_>, commit: Commitment, new_pos: T) -> Result<(), Error> { + let list_key = (Some(self.list_prefix), commit.as_ref()); match self.get_list(batch, commit)? { None => { let list = ListWrapper::Single { pos: new_pos }; - batch.db.put_ser(&self.list_key(commit), &list)?; + batch.db.put_ser(list_key.0, list_key.1, &list)?; } Some(ListWrapper::Single { pos: current_pos }) => { if new_pos.pos() <= current_pos.pos() { @@ -288,13 +294,11 @@ where head: new_pos.pos(), tail: current_pos.pos(), }; - batch - .db - .put_ser(&self.entry_key(commit, new_pos.pos()), &head)?; - batch - .db - .put_ser(&self.entry_key(commit, current_pos.pos()), &tail)?; - batch.db.put_ser(&self.list_key(commit), &list)?; + let (new_pos_db_key, new_pos_key) = self.entry_key(commit, new_pos.pos()); + batch.db.put_ser(new_pos_db_key, &new_pos_key, &head)?; + let (cur_pos_db_key, cur_pos_key) = self.entry_key(commit, current_pos.pos()); + batch.db.put_ser(cur_pos_db_key, &cur_pos_key, &tail)?; + batch.db.put_ser(list_key.0, list_key.1, &list)?; } Some(ListWrapper::Multi { head, tail }) => { if new_pos.pos() <= head { @@ -319,13 +323,11 @@ where head: new_pos.pos(), tail, }; - batch - .db - .put_ser(&self.entry_key(commit, new_pos.pos()), &head)?; - batch - .db - .put_ser(&self.entry_key(commit, current_pos.pos()), &middle)?; - batch.db.put_ser(&self.list_key(commit), &list)?; + let (new_pos_db_key, new_pos_key) = self.entry_key(commit, new_pos.pos()); + batch.db.put_ser(new_pos_db_key, &new_pos_key, &head)?; + let (cur_pos_db_key, cur_pos_key) = self.entry_key(commit, current_pos.pos()); + batch.db.put_ser(cur_pos_db_key, &cur_pos_key, &middle)?; + batch.db.put_ser(list_key.0, list_key.1, &list)?; } else { return Err(Error::OtherErr("expected head to be head variant".into())); } @@ -338,10 +340,11 @@ where /// Returns the output_pos. /// Returns None if list was empty. fn pop_pos(&self, batch: &mut Batch<'_>, commit: Commitment) -> Result, Error> { + let list_key = (Some(self.list_prefix), commit.as_ref()); match self.get_list(batch, commit)? { None => Ok(None), Some(ListWrapper::Single { pos }) => { - batch.delete(&self.list_key(commit))?; + batch.delete(list_key.0, list_key.1)?; Ok(Some(pos)) } Some(ListWrapper::Multi { head, tail }) => { @@ -357,17 +360,20 @@ where head: pos.pos(), tail, }; - batch.delete(&self.entry_key(commit, current_pos.pos()))?; - batch - .db - .put_ser(&self.entry_key(commit, pos.pos()), &head)?; - batch.db.put_ser(&self.list_key(commit), &list)?; + let (cur_pos_db_key, cur_pos_key) = + self.entry_key(commit, current_pos.pos()); + batch.delete(cur_pos_db_key, &cur_pos_key)?; + let (pos_db_key, pos_key) = self.entry_key(commit, current_pos.pos()); + batch.db.put_ser(pos_db_key, &pos_key, &head)?; + batch.db.put_ser(list_key.0, list_key.1, &list)?; Ok(Some(current_pos)) } Some(ListEntry::Tail { pos, .. }) => { let list = ListWrapper::Single { pos }; - batch.delete(&self.entry_key(commit, current_pos.pos()))?; - batch.db.put_ser(&self.list_key(commit), &list)?; + let (cur_pos_db_key, cur_pos_key) = + self.entry_key(commit, current_pos.pos()); + batch.delete(cur_pos_db_key, &cur_pos_key)?; + batch.db.put_ser(list_key.0, list_key.1, &list)?; Ok(Some(current_pos)) } Some(_) => Err(Error::OtherErr("next was unexpected".into())), @@ -404,14 +410,14 @@ impl PruneableListIndex for MultiIndex { fn clear(&self, batch: &mut Batch<'_>) -> Result<(), Error> { let mut list_count = 0; let mut entry_count = 0; - let prefix = to_key(self.list_prefix, ""); - for key in batch.db.iter(&prefix, |k, _| Ok(k.to_vec()))? { - let _ = batch.delete(&key); + let list_db_key = Some(self.list_prefix); + for key in batch.db.iter(list_db_key, |k, _| Ok(k.to_vec()))? { + let _ = batch.delete(list_db_key, &key); list_count += 1; } - let prefix = to_key(self.entry_prefix, ""); - for key in batch.db.iter(&prefix, |k, _| Ok(k.to_vec()))? { - let _ = batch.delete(&key); + let entry_db_key = Some(self.entry_prefix); + for key in batch.db.iter(entry_db_key, |k, _| Ok(k.to_vec()))? { + let _ = batch.delete(entry_db_key, &key); entry_count += 1; } debug!( @@ -436,10 +442,11 @@ impl PruneableListIndex for MultiIndex { /// Pop off the back/tail of the linked list. /// Used when pruning old data. fn pop_pos_back(&self, batch: &mut Batch<'_>, commit: Commitment) -> Result, Error> { + let list_key = (Some(self.list_prefix), commit.as_ref()); match self.get_list(batch, commit)? { None => Ok(None), Some(ListWrapper::Single { pos }) => { - batch.delete(&self.list_key(commit))?; + batch.delete(list_key.0, list_key.1)?; Ok(Some(pos)) } Some(ListWrapper::Multi { head, tail }) => { @@ -455,17 +462,19 @@ impl PruneableListIndex for MultiIndex { head, tail: pos.pos(), }; - batch.delete(&self.entry_key(commit, current_pos.pos()))?; - batch - .db - .put_ser(&self.entry_key(commit, pos.pos()), &tail)?; - batch.db.put_ser(&self.list_key(commit), &list)?; + let (cur_pos_db_key, cur_pos_key) = + self.entry_key(commit, current_pos.pos()); + batch.delete(cur_pos_db_key, &cur_pos_key)?; + let (pos_db_key, pos_key) = self.entry_key(commit, current_pos.pos()); + batch.db.put_ser(pos_db_key, &pos_key, &tail)?; + batch.db.put_ser(list_key.0, list_key.1, &list)?; Ok(Some(current_pos)) } Some(ListEntry::Head { pos, .. }) => { let list = ListWrapper::Single { pos }; - batch.delete(&self.entry_key(commit, current_pos.pos()))?; - batch.db.put_ser(&self.list_key(commit), &list)?; + let (pos_db_key, pos_key) = self.entry_key(commit, current_pos.pos()); + batch.delete(pos_db_key, &pos_key)?; + batch.db.put_ser(list_key.0, list_key.1, &list)?; Ok(Some(current_pos)) } Some(_) => Err(Error::OtherErr("prev was unexpected".into())), diff --git a/chain/src/store.rs b/chain/src/store.rs index 6a6f536c90..3a3df5757d 100644 --- a/chain/src/store.rs +++ b/chain/src/store.rs @@ -26,7 +26,7 @@ use crate::util::secp::pedersen::Commitment; use croaring::Bitmap; use grin_core::ser; use grin_store as store; -use grin_store::{option_to_not_found, to_key, Error}; +use grin_store::{option_to_not_found, Error}; use std::convert::TryInto; use std::sync::Arc; @@ -38,7 +38,8 @@ const HEAD_PREFIX: u8 = b'H'; const TAIL_PREFIX: u8 = b'T'; const PIBD_HEAD_PREFIX: u8 = b'I'; const HEADER_HEAD_PREFIX: u8 = b'G'; -const OUTPUT_POS_PREFIX: u8 = b'p'; +/// Prefix for output pos index. +pub const OUTPUT_POS_PREFIX: u8 = b'p'; /// Prefix for NRD kernel pos index lists. pub const NRD_KERNEL_LIST_PREFIX: u8 = b'K'; @@ -48,6 +49,17 @@ pub const NRD_KERNEL_ENTRY_PREFIX: u8 = b'k'; const BLOCK_SUMS_PREFIX: u8 = b'M'; const BLOCK_SPENT_PREFIX: u8 = b'S'; +/// All database prefixes. +const DB_PREFIXES: [u8; 7] = [ + BLOCK_HEADER_PREFIX, + BLOCK_PREFIX, + OUTPUT_POS_PREFIX, + NRD_KERNEL_LIST_PREFIX, + NRD_KERNEL_ENTRY_PREFIX, + BLOCK_SUMS_PREFIX, + BLOCK_SPENT_PREFIX, +]; + /// All chain-related database operations pub struct ChainStore { db: store::Store, @@ -56,30 +68,40 @@ pub struct ChainStore { impl ChainStore { /// Create new chain store pub fn new(db_root: &str) -> Result { - let db = store::Store::new(db_root, None, Some(STORE_SUBPATH), None)?; + let db = store::Store::new( + db_root, + None, + Some(STORE_SUBPATH), + DB_PREFIXES.to_vec(), + None, + )?; Ok(ChainStore { db }) } /// The current chain head. pub fn head(&self) -> Result { - option_to_not_found(self.db.get_ser(&[HEAD_PREFIX], None), || "HEAD".to_owned()) + option_to_not_found(self.db.get_ser(None, &[HEAD_PREFIX], None), || { + "HEAD".to_owned() + }) } /// The current header head (may differ from chain head). pub fn header_head(&self) -> Result { - option_to_not_found(self.db.get_ser(&[HEADER_HEAD_PREFIX], None), || { + option_to_not_found(self.db.get_ser(None, &[HEADER_HEAD_PREFIX], None), || { "HEADER_HEAD".to_owned() }) } /// The current chain "tail" (earliest block in the store). pub fn tail(&self) -> Result { - option_to_not_found(self.db.get_ser(&[TAIL_PREFIX], None), || "TAIL".to_owned()) + option_to_not_found(self.db.get_ser(None, &[TAIL_PREFIX], None), || { + "TAIL".to_owned() + }) } /// The current PIBD head (will differ from the other heads. Return genesis block if PIBD head doesn't exist). pub fn pibd_head(&self) -> Result { - let res = option_to_not_found(self.db.get_ser(&[PIBD_HEAD_PREFIX], None), || { + let res = option_to_not_found(self.db.get_ser(None, &[PIBD_HEAD_PREFIX], None), || { "PIBD_HEAD".to_owned() }); @@ -96,21 +118,23 @@ impl ChainStore { /// Get full block. pub fn get_block(&self, h: &Hash) -> Result { - option_to_not_found(self.db.get_ser(&to_key(BLOCK_PREFIX, h), None), || { - format!("BLOCK: {}", h) - }) + option_to_not_found( + self.db.get_ser(Some(BLOCK_PREFIX), h.as_ref(), None), + || format!("BLOCK: {}", h), + ) } /// Does this full block exist? pub fn block_exists(&self, h: &Hash) -> Result { - self.db.exists(&to_key(BLOCK_PREFIX, h)) + self.db.exists(Some(BLOCK_PREFIX), h.as_ref()) } /// Get block_sums for the block hash. pub fn get_block_sums(&self, h: &Hash) -> Result { - option_to_not_found(self.db.get_ser(&to_key(BLOCK_SUMS_PREFIX, h), None), || { - format!("Block sums for block: {}", h) - }) + option_to_not_found( + self.db.get_ser(Some(BLOCK_SUMS_PREFIX), h.as_ref(), None), + || format!("Block sums for block: {}", h), + ) } /// Get previous header. @@ -129,7 +153,7 @@ impl ChainStore { /// Get block header. pub fn get_block_header(&self, h: &Hash) -> Result { option_to_not_found( - self.db.get_ser(&to_key(BLOCK_HEADER_PREFIX, h), None), + self.db.get_ser(Some(BLOCK_HEADER_PREFIX), h.as_ref(), None), || format!("BLOCK HEADER: {}", h), ) } @@ -139,8 +163,9 @@ impl ChainStore { pub fn get_block_header_skip_proof(&self, h: &Hash) -> Result { option_to_not_found( self.db.get_ser( - &to_key(BLOCK_HEADER_PREFIX, h), - Some(ser::DeserializationMode::SkipPow), + Some(BLOCK_HEADER_PREFIX), + h.as_ref(), + Some(DeserializationMode::SkipPow), ), || format!("BLOCK HEADER: {}", h), ) @@ -159,7 +184,8 @@ impl ChainStore { /// Get PMMR pos and block height for the given output commitment. pub fn get_output_pos_height(&self, commit: &Commitment) -> Result, Error> { - self.db.get_ser(&to_key(OUTPUT_POS_PREFIX, commit), None) + self.db + .get_ser(Some(OUTPUT_POS_PREFIX), commit.as_ref(), None) } /// Builds a new batch to be used with this store. @@ -180,17 +206,21 @@ pub struct Batch<'a> { impl<'a> Batch<'a> { /// The head. pub fn head(&self) -> Result { - option_to_not_found(self.db.get_ser(&[HEAD_PREFIX], None), || "HEAD".to_owned()) + option_to_not_found(self.db.get_ser(None, &[HEAD_PREFIX], None), || { + "HEAD".to_owned() + }) } /// The tail. pub fn tail(&self) -> Result { - option_to_not_found(self.db.get_ser(&[TAIL_PREFIX], None), || "TAIL".to_owned()) + option_to_not_found(self.db.get_ser(None, &[TAIL_PREFIX], None), || { + "TAIL".to_owned() + }) } /// The current header head (may differ from chain head). pub fn header_head(&self) -> Result { - option_to_not_found(self.db.get_ser(&[HEADER_HEAD_PREFIX], None), || { + option_to_not_found(self.db.get_ser(None, &[HEADER_HEAD_PREFIX], None), || { "HEADER_HEAD".to_owned() }) } @@ -202,34 +232,35 @@ impl<'a> Batch<'a> { /// Save body head to db. pub fn save_body_head(&mut self, t: &Tip) -> Result<(), Error> { - self.db.put_ser(&[HEAD_PREFIX], t) + self.db.put_ser(None, &[HEAD_PREFIX], t) } /// Save body "tail" to db. pub fn save_body_tail(&mut self, t: &Tip) -> Result<(), Error> { - self.db.put_ser(&[TAIL_PREFIX], t) + self.db.put_ser(None, &[TAIL_PREFIX], t) } /// Save header head to db. pub fn save_header_head(&mut self, t: &Tip) -> Result<(), Error> { - self.db.put_ser(&[HEADER_HEAD_PREFIX], t) + self.db.put_ser(None, &[HEADER_HEAD_PREFIX], t) } /// Save PIBD head to db. pub fn save_pibd_head(&mut self, t: &Tip) -> Result<(), Error> { - self.db.put_ser(&[PIBD_HEAD_PREFIX], t) + self.db.put_ser(None, &[PIBD_HEAD_PREFIX], t) } /// get block pub fn get_block(&self, h: &Hash) -> Result { - option_to_not_found(self.db.get_ser(&to_key(BLOCK_PREFIX, h), None), || { - format!("Block with hash: {}", h) - }) + option_to_not_found( + self.db.get_ser(Some(BLOCK_PREFIX), h.as_ref(), None), + || format!("Block with hash: {}", h), + ) } /// Does the block exist? pub fn block_exists(&self, h: &Hash) -> Result { - self.db.exists(&to_key(BLOCK_PREFIX, h)) + self.db.exists(Some(BLOCK_PREFIX), h.as_ref()) } /// Save the block to the db. @@ -242,7 +273,7 @@ impl<'a> Batch<'a> { b.inputs().version_str(), self.db.protocol_version(), ); - self.db.put_ser(&to_key(BLOCK_PREFIX, b.hash())[..], b)?; + self.db.put_ser(Some(BLOCK_PREFIX), b.hash().as_ref(), b)?; Ok(()) } @@ -250,19 +281,19 @@ impl<'a> Batch<'a> { /// to be easily reverted during rewind. pub fn save_spent_index(&mut self, h: &Hash, spent: &[CommitPos]) -> Result<(), Error> { self.db - .put_ser(&to_key(BLOCK_SPENT_PREFIX, h)[..], &spent.to_vec())?; + .put_ser(Some(BLOCK_SPENT_PREFIX), h.as_ref(), &spent.to_vec())?; Ok(()) } /// Low level function to delete directly by raw key. - pub fn delete(&mut self, key: &[u8]) -> Result<(), Error> { - self.db.delete(key) + pub fn delete(&mut self, db_key: Option, key: &[u8]) -> Result<(), Error> { + self.db.delete(db_key, key) } /// Delete a full block. Does not delete any record associated with a block /// header. pub fn delete_block(&mut self, bh: &Hash) -> Result<(), Error> { - self.db.delete(&to_key(BLOCK_PREFIX, bh)[..])?; + self.db.delete(Some(BLOCK_PREFIX), bh.as_ref())?; // Best effort at deleting associated data for this block. // Not an error if these fail. @@ -280,7 +311,7 @@ impl<'a> Batch<'a> { // Store the header itself indexed by hash. self.db - .put_ser(&to_key(BLOCK_HEADER_PREFIX, hash)[..], header)?; + .put_ser(Some(BLOCK_HEADER_PREFIX), hash.as_ref(), header)?; Ok(()) } @@ -292,27 +323,25 @@ impl<'a> Batch<'a> { pos: CommitPos, ) -> Result<(), Error> { self.db - .put_ser(&to_key(OUTPUT_POS_PREFIX, commit)[..], &pos) + .put_ser(Some(OUTPUT_POS_PREFIX), commit.as_ref(), &pos) } /// Delete the output_pos index entry for a spent output. pub fn delete_output_pos_height(&mut self, commit: &Commitment) -> Result<(), Error> { - self.db.delete(&to_key(OUTPUT_POS_PREFIX, commit)) + self.db.delete(Some(OUTPUT_POS_PREFIX), commit.as_ref()) } /// When using the output_pos iterator we have access to the index keys but not the /// original commitment that the key is constructed from. So we need a way of comparing /// a key with another commitment without reconstructing the commitment from the key bytes. pub fn is_match_output_pos_key(&self, key: &[u8], commit: &Commitment) -> bool { - let commit_key = to_key(OUTPUT_POS_PREFIX, commit); - commit_key == key + commit.as_ref() == key } /// Iterator over the output_pos index. pub fn output_pos_iter(&self) -> Result, CommitPos)>, Error> { - let key = to_key(OUTPUT_POS_PREFIX, ""); let protocol_version = self.db.protocol_version(); - self.db.iter(&key, move |k, mut v| { + self.db.iter(Some(OUTPUT_POS_PREFIX), move |k, mut v| { ser::deserialize(&mut v, protocol_version, DeserializationMode::default()) .map(|pos| (k.to_vec(), pos)) .map_err(From::from) @@ -332,7 +361,8 @@ impl<'a> Batch<'a> { /// Get output_pos and block height from index. pub fn get_output_pos_height(&self, commit: &Commitment) -> Result, Error> { - self.db.get_ser(&to_key(OUTPUT_POS_PREFIX, commit), None) + self.db + .get_ser(Some(OUTPUT_POS_PREFIX), commit.as_ref(), None) } /// Get the previous header. @@ -352,7 +382,7 @@ impl<'a> Batch<'a> { /// Get block header. pub fn get_block_header(&self, h: &Hash) -> Result { option_to_not_found( - self.db.get_ser(&to_key(BLOCK_HEADER_PREFIX, h), None), + self.db.get_ser(Some(BLOCK_HEADER_PREFIX), h.as_ref(), None), || format!("BLOCK HEADER: {}", h), ) } @@ -362,8 +392,9 @@ impl<'a> Batch<'a> { pub fn get_block_header_skip_proof(&self, h: &Hash) -> Result { option_to_not_found( self.db.get_ser( - &to_key(BLOCK_HEADER_PREFIX, h), - Some(ser::DeserializationMode::SkipPow), + Some(BLOCK_HEADER_PREFIX), + h.as_ref(), + Some(DeserializationMode::SkipPow), ), || format!("BLOCK HEADER: {}", h), ) @@ -371,24 +402,25 @@ impl<'a> Batch<'a> { /// Delete the block spent index. fn delete_spent_index(&mut self, bh: &Hash) -> Result<(), Error> { - self.db.delete(&to_key(BLOCK_SPENT_PREFIX, bh)) + self.db.delete(Some(BLOCK_SPENT_PREFIX), bh.as_ref()) } /// Save block_sums for the block. pub fn save_block_sums(&mut self, h: &Hash, sums: BlockSums) -> Result<(), Error> { - self.db.put_ser(&to_key(BLOCK_SUMS_PREFIX, h)[..], &sums) + self.db.put_ser(Some(BLOCK_SUMS_PREFIX), h.as_ref(), &sums) } /// Get block_sums for the block. pub fn get_block_sums(&self, h: &Hash) -> Result { - option_to_not_found(self.db.get_ser(&to_key(BLOCK_SUMS_PREFIX, h), None), || { - format!("Block sums for block: {}", h) - }) + option_to_not_found( + self.db.get_ser(Some(BLOCK_SUMS_PREFIX), h.as_ref(), None), + || format!("Block sums for block: {}", h), + ) } /// Delete the block_sums for the block. fn delete_block_sums(&mut self, bh: &Hash) -> Result<(), Error> { - self.db.delete(&to_key(BLOCK_SUMS_PREFIX, bh)) + self.db.delete(Some(BLOCK_SUMS_PREFIX), bh.as_ref()) } /// Get the block input bitmap based on our spent index. @@ -406,7 +438,7 @@ impl<'a> Batch<'a> { /// If we need to rewind a block then we use this to "unspend" the spent outputs. pub fn get_spent_index(&self, bh: &Hash) -> Result, Error> { option_to_not_found( - self.db.get_ser(&to_key(BLOCK_SPENT_PREFIX, bh), None), + self.db.get_ser(Some(BLOCK_SPENT_PREFIX), bh.as_ref(), None), || format!("spent index: {}", bh), ) } @@ -428,9 +460,8 @@ impl<'a> Batch<'a> { /// Iterator over all full blocks in the db. /// Uses default db serialization strategy via db protocol version. pub fn blocks_iter(&self) -> Result + 'a, Error> { - let key = to_key(BLOCK_PREFIX, ""); let protocol_version = self.db.protocol_version(); - self.db.iter(&key, move |_, mut v| { + self.db.iter(Some(BLOCK_PREFIX), move |_, mut v| { ser::deserialize(&mut v, protocol_version, DeserializationMode::default()) .map_err(From::from) }) @@ -439,8 +470,8 @@ impl<'a> Batch<'a> { /// Iterator over raw data for full blocks in the db. /// Used during block migration (we need flexibility around deserialization). pub fn blocks_raw_iter(&self) -> Result, Vec)>, Error> { - let key = to_key(BLOCK_PREFIX, ""); - self.db.iter(&key, |k, v| Ok((k.to_vec(), v.to_vec()))) + self.db + .iter(Some(BLOCK_PREFIX), |k, v| Ok((k.to_vec(), v.to_vec()))) } /// Protocol version of our underlying db. diff --git a/chain/src/txhashset/txhashset.rs b/chain/src/txhashset/txhashset.rs index cbdb899f38..c9c55a7045 100644 --- a/chain/src/txhashset/txhashset.rs +++ b/chain/src/txhashset/txhashset.rs @@ -657,7 +657,7 @@ impl TxHashSet { } } } - batch.delete(&key)?; + batch.delete(Some(store::OUTPUT_POS_PREFIX), &key)?; removed_count += 1; } debug!( diff --git a/p2p/src/store.rs b/p2p/src/store.rs index 931631e6e5..e0f32e9f71 100644 --- a/p2p/src/store.rs +++ b/p2p/src/store.rs @@ -20,7 +20,7 @@ use rand::prelude::*; use crate::core::ser::{self, DeserializationMode, Readable, Reader, Writeable, Writer}; use crate::types::{Capabilities, PeerAddr, ReasonForBan}; -use grin_store::{self, option_to_not_found, to_key, Error}; +use grin_store::{self, option_to_not_found, Error}; const DB_NAME: &str = "peer"; const STORE_SUBPATH: &str = "peers"; @@ -118,7 +118,13 @@ pub struct PeerStore { impl PeerStore { /// Instantiates a new peer store under the provided root path. pub fn new(db_root: &str) -> Result { - let db = grin_store::Store::new(db_root, Some(DB_NAME), Some(STORE_SUBPATH), None)?; + let db = grin_store::Store::new( + db_root, + Some(DB_NAME), + Some(STORE_SUBPATH), + vec![PEER_PREFIX], + None, + )?; Ok(PeerStore { db }) } @@ -126,7 +132,8 @@ impl PeerStore { debug!("save_peer: {:?} marked {:?}", p.addr, p.flags); let mut batch = self.db.batch()?; - batch.put_ser(&peer_key(p.addr)[..], p)?; + let key = p.addr.as_key(); + batch.put_ser(Some(PEER_PREFIX), key.as_bytes(), p)?; batch.commit() } @@ -134,19 +141,23 @@ impl PeerStore { let mut batch = self.db.batch()?; for pd in p { debug!("save_peers: {:?} marked {:?}", pd.addr, pd.flags); - batch.put_ser(&peer_key(pd.addr)[..], &pd)?; + let key = pd.addr.as_key(); + batch.put_ser(Some(PEER_PREFIX), key.as_bytes(), &pd)?; } batch.commit() } pub fn get_peer(&self, peer_addr: PeerAddr) -> Result { - option_to_not_found(self.db.get_ser(&peer_key(peer_addr)[..], None), || { - format!("Peer at address: {}", peer_addr) - }) + let key = peer_addr.as_key(); + option_to_not_found( + self.db.get_ser(Some(PEER_PREFIX), key.as_bytes(), None), + || format!("Peer at address: {}", peer_addr), + ) } pub fn exists_peer(&self, peer_addr: PeerAddr) -> Result { - self.db.exists(&peer_key(peer_addr)[..]) + let key = peer_addr.as_key(); + self.db.exists(Some(PEER_PREFIX), key.as_bytes()) } /// Convenience method to load a peer data, update its status and save it @@ -154,9 +165,9 @@ impl PeerStore { /// If new state is Defunct last connection attempt will be updated too. pub fn update_state(&self, peer_addr: PeerAddr, new_state: State) -> Result<(), Error> { let mut batch = self.db.batch()?; - + let key = peer_addr.as_key(); let mut peer = option_to_not_found( - batch.get_ser::(&peer_key(peer_addr)[..], None), + batch.get_ser::(Some(PEER_PREFIX), key.as_bytes(), None), || format!("Peer at address: {}", peer_addr), )?; peer.flags = new_state; @@ -166,7 +177,7 @@ impl PeerStore { peer.last_attempt = Utc::now().timestamp(); } - batch.put_ser(&peer_key(peer_addr)[..], &peer)?; + batch.put_ser(Some(PEER_PREFIX), key.as_bytes(), &peer)?; batch.commit() } @@ -185,9 +196,8 @@ pub struct PeersIterBatch<'a> { impl<'a> PeersIterBatch<'a> { /// Iterator over all known peers. pub fn peers_iter(&self) -> Result, Error> { - let key = to_key(PEER_PREFIX, ""); let protocol_version = self.db.protocol_version(); - self.db.iter(&key, move |_, mut v| { + self.db.iter(Some(PEER_PREFIX), move |_, mut v| { ser::deserialize(&mut v, protocol_version, DeserializationMode::default()) .map_err(From::from) }) @@ -230,7 +240,8 @@ impl<'a> PeersIterBatch<'a> { // Delete peers in single batch if !to_remove.is_empty() { for peer in to_remove { - self.db.delete(&peer_key(peer.addr)[..])?; + let key = peer.addr.as_key(); + self.db.delete(Some(PEER_PREFIX), key.as_bytes())?; } self.db.commit()?; } @@ -238,8 +249,3 @@ impl<'a> PeersIterBatch<'a> { Ok(()) } } - -// Ignore the port unless ip is loopback address. -fn peer_key(peer_addr: PeerAddr) -> Vec { - to_key(PEER_PREFIX, &peer_addr.as_key()) -} diff --git a/store/src/lib.rs b/store/src/lib.rs index 215b43f70f..5814bf6cf5 100644 --- a/store/src/lib.rs +++ b/store/src/lib.rs @@ -26,62 +26,23 @@ extern crate log; extern crate grin_core; extern crate grin_util as util; -//use grin_core as core; - pub mod leaf_set; pub mod lmdb; pub mod pmmr; pub mod prune_list; pub mod types; -const SEP: u8 = b':'; - -use byteorder::{BigEndian, WriteBytesExt}; - pub use crate::lmdb::*; -/// Build a db key from a prefix and a byte vector identifier. -pub fn to_key>(prefix: u8, k: K) -> Vec { - let k = k.as_ref(); - let mut res = Vec::with_capacity(k.len() + 2); - res.push(prefix); - res.push(SEP); - res.extend_from_slice(k); - res -} - -/// Build a db key from a prefix and a byte vector identifier and numeric identifier -pub fn to_key_u64>(prefix: u8, k: K, val: u64) -> Vec { - let k = k.as_ref(); - let mut res = Vec::with_capacity(k.len() + 10); - res.push(prefix); - res.push(SEP); - res.extend_from_slice(k); - res.write_u64::(val).unwrap(); - res -} -/// Build a db key from a prefix and a numeric identifier. -pub fn u64_to_key(prefix: u8, val: u64) -> Vec { - let mut res = Vec::with_capacity(10); - res.push(prefix); - res.push(SEP); - res.write_u64::(val).unwrap(); - res -} - use std::ffi::OsStr; use std::fs::{remove_file, rename, File}; use std::path::Path; /// Creates temporary file with name created by adding `temp_suffix` to `path`. /// Applies writer function to it and renames temporary file into original specified by `path`. -pub fn save_via_temp_file( - path: P, - temp_suffix: E, - mut writer: F, -) -> Result<(), std::io::Error> +pub fn save_via_temp_file(path: P, temp_suffix: E, mut writer: F) -> Result<(), io::Error> where - F: FnMut(&mut File) -> Result<(), std::io::Error>, + F: FnMut(&mut File) -> Result<(), io::Error>, P: AsRef, E: AsRef, { diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index 7b7f5f9a01..c21d14a3e1 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -30,6 +30,7 @@ use crate::util::RwLock; pub const ALLOC_CHUNK_SIZE_DEFAULT: usize = 134_217_728; //128 MB /// And for test mode, to avoid too much disk allocation on windows pub const ALLOC_CHUNK_SIZE_DEFAULT_TEST: usize = 1_048_576; //1 MB +/// Minimal percent of used space when resizing must be performed. const RESIZE_PERCENT: f32 = 0.9; /// Want to ensure that each resize gives us at least this % /// of total space free @@ -81,7 +82,12 @@ where const DEFAULT_DB_VERSION: ProtocolVersion = ProtocolVersion(3); +/// Default environment. const DEFAULT_ENV_NAME: &'static str = "lmdb"; +/// Default multi-database environment without prefixes. +const DEFAULT_MULTI_DB_ENV_NAME: &'static str = "multi_lmdb"; +/// Prefix key separator. +pub const PREFIX_KEY_SEPARATOR: u8 = b':'; /// Mapping of database path to environment. static ENV_MAP: OnceLock>>>> = OnceLock::new(); @@ -95,29 +101,28 @@ static ENV_RESIZING: OnceLock>>> = OnceLock::ne pub struct Store { env: Env, env_path: String, - db: Arc>, - name: String, + pre_dbs: Arc>>, + def_db: Database, version: ProtocolVersion, alloc_chunk_size: usize, } impl Store { /// Create a new LMDB env under the provided directory. - /// By default creates an environment named "lmdb". + /// By default creates an environment named "multi_lmdb". /// Be aware of transactional semantics in lmdb /// (transactions are per environment, not per database). - /// db with non-default `env_name` will be migrated into default environment. + /// Data from non-default `env_name` and prefixes will be + /// migrated into default multi db env file if needed. pub fn new( root_path: &str, env_name: Option<&str>, db_name: Option<&str>, + prefixes: Vec, max_readers: Option, ) -> Result { - let db_name = db_name.unwrap_or_else(|| DEFAULT_ENV_NAME); - - // Database path setup. let full_path = Path::new(root_path) - .join(DEFAULT_ENV_NAME) + .join(DEFAULT_MULTI_DB_ENV_NAME) .to_str() .unwrap() .to_string(); @@ -142,7 +147,7 @@ impl Store { if !has_env { let env = unsafe { let mut options = EnvOpenOptions::new().read_txn_without_tls(); - let mut env_options = options.map_size(alloc_chunk_size).max_dbs(8); + let mut env_options = options.map_size(alloc_chunk_size).max_dbs(24); if let Some(max_readers) = max_readers { env_options = env_options.max_readers(max_readers); } @@ -154,7 +159,7 @@ impl Store { env.resize(new_size)?; }; } - debug!("DB Mapsize for {} is {}", db_name, env.info().map_size); + debug!("DB Mapsize is {}", env.info().map_size); let mut w_env_map = env_map.write(); w_env_map.insert(full_path.clone(), env); } @@ -163,26 +168,34 @@ impl Store { let r_env_map = env_map.read(); let env = r_env_map.get(&full_path).unwrap(); let mut write = env.write_txn()?; - let db = env.create_database(&mut write, Some(db_name))?; + let def_name = db_name.unwrap_or(DEFAULT_ENV_NAME); + let def_db = env.create_database(&mut write, Some(def_name))?; + let mut dbs_map = HashMap::>::new(); + for p in prefixes { + let db = env.create_database(&mut write, Some(p.to_string().as_str()))?; + dbs_map.insert(p, db); + } write.commit()?; let s = Store { env: env.clone(), env_path: full_path.clone(), - db: Arc::new(db), - name: db_name.to_string(), + pre_dbs: Arc::new(dbs_map), + def_db, version: DEFAULT_DB_VERSION, alloc_chunk_size, }; // Migrate to default environment if needed. - if let Some(env_name) = env_name { - if env_name != DEFAULT_ENV_NAME { - let migrate_from = Path::new(root_path).join(env_name); - if s.migrate_to_default_env(&migrate_from).is_ok() { - let _ = fs::remove_dir_all(&migrate_from); - } else { - error!("Migrating DB {} failed", env_name); + let env_name = env_name.unwrap_or(DEFAULT_ENV_NAME); + if env_name != DEFAULT_MULTI_DB_ENV_NAME { + let migrate_from = Path::new(root_path).join(env_name); + if migrate_from.exists() { + match s.migrate_to_default_env(db_name, &migrate_from) { + Ok(_) => { + let _ = fs::remove_dir_all(&migrate_from); + } + Err(e) => error!("DB {} migration error: {:?}", env_name, e), } } } @@ -191,20 +204,27 @@ impl Store { } /// Migrate db from provided path to store environment. - fn migrate_to_default_env(&self, from_path: &Path) -> Result<(), Error> { - if !from_path.exists() { - return Ok(()); - }; - debug!("Migrating DB {} to {}", self.name, DEFAULT_ENV_NAME); + fn migrate_to_default_env( + &self, + from_name: Option<&str>, + from_path: &Path, + ) -> Result<(), Error> { + debug!("Migrating DB {:?}", from_path); let from_env = unsafe { let mut options = EnvOpenOptions::new().read_txn_without_tls(); - let env_options = options.map_size(self.alloc_chunk_size).max_dbs(1); + let env_options = options.map_size(self.alloc_chunk_size).max_dbs(24); env_options.open(from_path)? }; + let (resize, new_size) = needs_resize(&from_env, self.alloc_chunk_size); + if resize { + unsafe { + from_env.resize(new_size)?; + self.env.resize(new_size)?; + }; + } let db_from = { let mut write = from_env.write_txn()?; - let db_name = self.name.as_str(); - let db: Database = from_env.create_database(&mut write, Some(db_name))?; + let db: Database = from_env.create_database(&mut write, from_name)?; write.commit()?; db }; @@ -212,41 +232,51 @@ impl Store { let read_from = from_env.read_txn()?; let mut count = 0; for kv in db_from.iter(&read_from)? { - count += 1; if let Ok((k, v)) = kv { - self.db.put(&mut write_to, &k, &v)?; + if k.contains(&PREFIX_KEY_SEPARATOR) { + let db_name = k.split_at(1).0; + let db = self.pre_dbs.get(&db_name[0]).unwrap(); + let key = k.split_at(2).1; + db.put(&mut write_to, key, &v)?; + count += 1; + } else { + self.def_db.put(&mut write_to, k, &v)?; + count += 1; + } } } write_to.commit()?; - debug!("Migrated {} records from DB {}", count, self.name); + debug!("Migrated {} records from DB {:?}", count, from_path); Ok(()) } - /// Construct a new store using a specific protocol version. - /// Permits access to the db with legacy protocol versions for db migrations. - pub fn with_version(&self, version: ProtocolVersion) -> Store { - Store { - env: self.env.clone(), - env_path: self.env_path.clone(), - db: self.db.clone(), - name: self.name.clone(), - version, - alloc_chunk_size: self.alloc_chunk_size, - } - } - /// Protocol version for the store. pub fn protocol_version(&self) -> ProtocolVersion { self.version } + /// Get database from provided key or return default. + fn get_db(&self, db_key: Option) -> &Database { + match db_key { + Some(db) => self.pre_dbs.get(&db).unwrap(), + None => &self.def_db, + } + } + /// Gets a value from the db, provided its key. /// Deserializes the retrieved data using the provided function. - fn get_with(&self, key: &[u8], read: &RoTxn, deserialize: F) -> Result, Error> + fn get_with( + &self, + db_key: Option, + key: &[u8], + read: &RoTxn, + deserialize: F, + ) -> Result, Error> where F: Fn(&[u8], &[u8]) -> Result, { - let res: Option<&[u8]> = self.db.get(read, key)?; + let db = self.get_db(db_key); + let res: Option<&[u8]> = db.get(read, key)?; match res { None => Ok(None), Some(res) => deserialize(key, res).map(Some), @@ -257,6 +287,7 @@ impl Store { /// Note: Creates a new read transaction so will *not* see any uncommitted data. pub fn get_ser( &self, + db_key: Option, key: &[u8], deser_mode: Option, ) -> Result, Error> { @@ -266,30 +297,35 @@ impl Store { }; self.wait_for_resize(); let read = self.env.read_txn()?; - self.get_with(key, &read, |_, mut data| { + self.get_with(db_key, key, &read, |_, mut data| { ser::deserialize(&mut data, self.protocol_version(), d).map_err(From::from) }) } /// Whether the provided key exists. - pub fn exists(&self, key: &[u8]) -> Result { + pub fn exists(&self, db_key: Option, key: &[u8]) -> Result { self.wait_for_resize(); let read = self.env.read_txn()?; - let res = self.db.get(&read, key)?; + let db = self.get_db(db_key); + let res = db.get(&read, key)?; Ok(res.is_some()) } - /// Produces an iterator from the provided key prefix. - pub fn iter(&self, prefix: &[u8], deserialize: F) -> Result, Error> + /// Produces an iterator from the provided db name. + pub fn iter( + &self, + db_key: Option, + deserialize: F, + ) -> Result, Error> where F: Fn(&[u8], &[u8]) -> Result, { self.wait_for_resize(); let read = self.env.clone().static_read_txn()?; - Ok(PrefixIterator::new( - self.db.clone(), + let db = self.get_db(db_key); + Ok(DatabaseIterator::new( + Arc::new(db.clone()), read, - prefix, deserialize, )) } @@ -305,7 +341,7 @@ impl Store { if !resizing { break; } - debug!("Wait on {}, resizing DB", self.name); + trace!("Wait on resizing DB {}", self.env_path); thread::sleep(Duration::from_millis(100)); } } @@ -320,7 +356,7 @@ impl Store { let mut w_res_map = res_map.write(); w_res_map.insert(self.env_path.clone(), true); } - debug!("Start resizing {} DB", self.name); + debug!("Start resizing DB {}", self.env_path); unsafe { loop { let batches_count = @@ -330,7 +366,10 @@ impl Store { if cur == &0 { break; } - debug!("Wait {} batches to complete", cur); + debug!( + "Wait {} batches to complete to resize DB {}", + cur, self.env_path + ); thread::sleep(Duration::from_millis(100)); } self.env.resize(new_size)?; @@ -339,7 +378,7 @@ impl Store { let mut w_res_map = res_map.write(); w_res_map.insert(self.env_path.clone(), false); } - debug!("End resizing {} DB", self.name); + debug!("End resizing DB {}", self.env_path); } Ok(()) } @@ -385,15 +424,21 @@ impl<'a> Batch<'a> { } /// Writes a single key/value pair to the db. - pub fn put(&mut self, key: &[u8], value: &[u8]) -> Result<(), Error> { - self.store.db.put(&mut self.write, key, value)?; + pub fn put(&mut self, db_key: Option, key: &[u8], value: &[u8]) -> Result<(), Error> { + let db = self.store.get_db(db_key); + db.put(&mut self.write, key, value)?; Ok(()) } /// Writes a single key and its `Writeable` value to the db. /// Encapsulates serialization using the (default) version configured on the store instance. - pub fn put_ser(&mut self, key: &[u8], value: &W) -> Result<(), Error> { - self.put_ser_with_version(key, value, self.store.protocol_version()) + pub fn put_ser( + &mut self, + db_key: Option, + key: &[u8], + value: &W, + ) -> Result<(), Error> { + self.put_ser_with_version(db_key, key, value, self.store.protocol_version()) } /// Protocol version used by this batch. @@ -405,46 +450,58 @@ impl<'a> Batch<'a> { /// Encapsulates serialization using the specified protocol version. pub fn put_ser_with_version( &mut self, + db_key: Option, key: &[u8], value: &W, version: ProtocolVersion, ) -> Result<(), Error> { let ser_value = ser::ser_vec(value, version); match ser_value { - Ok(data) => self.put(key, &data), + Ok(data) => self.put(db_key, key, &data), Err(err) => Err(err.into()), } } /// Low-level access for retrieving data by key. /// Takes a function for flexible deserialization. - fn get_with(&self, key: &[u8], deserialize: F) -> Result, Error> + fn get_with( + &self, + db_key: Option, + key: &[u8], + deserialize: F, + ) -> Result, Error> where F: Fn(&[u8], &[u8]) -> Result, { let read = self.write.nested_read_txn()?; - self.store.get_with(key, &read, deserialize) + self.store.get_with(db_key, key, &read, deserialize) } /// Whether the provided key exists. /// This is in the context of the current write transaction. - pub fn exists(&self, key: &[u8]) -> Result { + pub fn exists(&self, db_key: Option, key: &[u8]) -> Result { let read = self.write.nested_read_txn()?; - let res = self.store.db.get(&read, key)?; + let db = self.store.get_db(db_key); + let res = db.get(&read, key)?; Ok(res.is_some()) } - /// Produces an iterator from the provided key prefix. - pub fn iter(&self, prefix: &[u8], deserialize: F) -> Result, Error> + /// Produces an iterator from the provided db key. + pub fn iter( + &self, + db_key: Option, + deserialize: F, + ) -> Result, Error> where F: Fn(&[u8], &[u8]) -> Result, { - self.store.iter(prefix, deserialize) + self.store.iter(db_key, deserialize) } /// Gets a `Readable` value from the db by provided key and provided deserialization strategy. pub fn get_ser( &self, + db_key: Option, key: &[u8], deser_mode: Option, ) -> Result, Error> { @@ -452,7 +509,7 @@ impl<'a> Batch<'a> { Some(d) => d, _ => DeserializationMode::default(), }; - self.get_with(key, |_, mut data| { + self.get_with(db_key, key, |_, mut data| { match ser::deserialize(&mut data, self.protocol_version(), d) { Ok(res) => Ok(res), Err(e) => Err(From::from(e)), @@ -461,8 +518,9 @@ impl<'a> Batch<'a> { } /// Deletes a key/value pair from the db. - pub fn delete(&mut self, key: &[u8]) -> Result<(), Error> { - self.store.db.delete(&mut self.write, key)?; + pub fn delete(&mut self, db_key: Option, key: &[u8]) -> Result<(), Error> { + let db = self.store.get_db(db_key); + db.delete(&mut self.write, key)?; Ok(()) } @@ -488,9 +546,9 @@ impl<'a> Batch<'a> { } } -/// An iterator based on key prefix. +/// An iterator based on db key. /// Caller is responsible for deserialization of the data. -pub struct PrefixIterator +pub struct DatabaseIterator where F: Fn(&[u8], &[u8]) -> Result, { @@ -501,7 +559,7 @@ where deserialize: F, } -impl Iterator for PrefixIterator +impl Iterator for DatabaseIterator where F: Fn(&[u8], &[u8]) -> Result, { @@ -524,7 +582,7 @@ where } } -impl PrefixIterator +impl DatabaseIterator where F: Fn(&[u8], &[u8]) -> Result, { @@ -532,10 +590,9 @@ where pub fn new( db: Arc>, read: RoTxn<'static, WithoutTls>, - prefix: &[u8], deserialize: F, - ) -> PrefixIterator { - let keys = if let Ok(iter) = db.prefix_iter(&read, &prefix) { + ) -> DatabaseIterator { + let keys = if let Ok(iter) = db.iter(&read) { iter.move_between_keys() .filter(|kv| kv.is_ok()) .map(|kv| kv.unwrap().0.to_vec()) @@ -543,7 +600,7 @@ where } else { vec![] }; - PrefixIterator { + DatabaseIterator { db, read: Arc::new(read), keys, diff --git a/store/tests/lmdb.rs b/store/tests/lmdb.rs index c9fe6ceb1a..f8c6838601 100644 --- a/store/tests/lmdb.rs +++ b/store/tests/lmdb.rs @@ -70,25 +70,26 @@ fn test_exists() -> Result<(), store::Error> { let test_dir = "target/test_exists"; setup(test_dir); - let store = store::Store::new(test_dir, Some("test1"), None, None)?; + let prefix = b'P'; + let store = store::Store::new(test_dir, Some("test1"), None, vec![prefix], None)?; let key = [0, 0, 0, 1]; let value = [1, 1, 1, 1]; // Start new batch and insert a new key/value entry. let mut batch = store.batch()?; - batch.put(&key, &value)?; + batch.put(Some(prefix), &key, &value)?; // Check we can see the new entry in uncommitted batch. - assert!(batch.exists(&key)?); + assert!(batch.exists(Some(prefix), &key)?); // Check we cannot see the new entry yet outside of the uncommitted batch. - assert!(!store.exists(&key)?); + assert!(!store.exists(Some(prefix), &key)?); batch.commit()?; // Check we can see the new entry after committing the batch. - assert!(store.exists(&key)?); + assert!(store.exists(Some(prefix), &key)?); clean_output_dir(test_dir); Ok(()) @@ -99,14 +100,15 @@ fn test_iter() -> Result<(), store::Error> { let test_dir = "target/test_iter"; setup(test_dir); - let store = store::Store::new(test_dir, Some("test1"), None, None)?; + let prefix = b'P'; + let store = store::Store::new(test_dir, Some("test1"), None, vec![prefix], None)?; let key = [0, 0, 0, 1]; let value = [1, 1, 1, 1]; // Start new batch and insert a new key/value entry. let mut batch = store.batch()?; - batch.put(&key, &value)?; + batch.put(Some(prefix), &key, &value)?; // TODO - This is not currently possible (and we need to be aware of this). // Currently our SerIterator is limited to using a ReadTransaction only. @@ -117,13 +119,13 @@ fn test_iter() -> Result<(), store::Error> { // assert_eq!(iter.next(), None); // Check we can not yet see the new entry via an iterator outside the uncommitted batch. - let mut iter = store.iter(&[0], |_, v| Ok(v.to_vec()))?; + let mut iter = store.iter(Some(prefix), |_, v| Ok(v.to_vec()))?; assert_eq!(iter.next(), None); batch.commit()?; // Check we can see the new entry via an iterator after committing the batch. - let mut iter = store.iter(&[0], |_, v| Ok(v.to_vec()))?; + let mut iter = store.iter(Some(prefix), |_, v| Ok(v.to_vec()))?; assert_eq!(iter.next(), Some(value.to_vec())); assert_eq!(iter.next(), None); @@ -135,18 +137,18 @@ fn test_iter() -> Result<(), store::Error> { fn lmdb_allocate() -> Result<(), store::Error> { let test_dir = "target/lmdb_allocate"; setup(test_dir); + let prefix = b'P'; // Allocate more than the initial chunk, ensuring // the DB resizes underneath { - let store = store::Store::new(test_dir, Some("test1"), None, None)?; + let store = store::Store::new(test_dir, Some("test1"), None, vec![prefix], None)?; for i in 0..WRITE_CHUNK_SIZE * 2 { println!("Allocating chunk: {}", i); let chunk = PhatChunkStruct::new(); let key_val = format!("phat_chunk_set_1_{}", i); let mut batch = store.batch()?; - let key = store::to_key(b'P', &key_val); - batch.put_ser(&key, &chunk)?; + batch.put_ser(Some(prefix), key_val.as_bytes(), &chunk)?; batch.commit()?; } } @@ -155,14 +157,13 @@ fn lmdb_allocate() -> Result<(), store::Error> { println!("***********************************"); // Open env again and keep adding { - let store = store::Store::new(test_dir, Some("test1"), None, None)?; + let store = store::Store::new(test_dir, Some("test1"), None, vec![prefix], None)?; for i in 0..WRITE_CHUNK_SIZE * 2 { println!("Allocating chunk: {}", i); let chunk = PhatChunkStruct::new(); let key_val = format!("phat_chunk_set_2_{}", i); let mut batch = store.batch()?; - let key = store::to_key(b'P', &key_val); - batch.put_ser(&key, &chunk)?; + batch.put_ser(Some(prefix), key_val.as_bytes(), &chunk)?; batch.commit()?; } } From 4cadce12305c16f4a7e35e3aaf54af4373c810c5 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Mon, 27 Apr 2026 17:41:11 +0300 Subject: [PATCH 15/37] fix: pop pos key --- chain/src/linked_list.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/chain/src/linked_list.rs b/chain/src/linked_list.rs index 07c29fcd2f..289ffd1061 100644 --- a/chain/src/linked_list.rs +++ b/chain/src/linked_list.rs @@ -254,10 +254,7 @@ where } fn peek_pos(&self, batch: &Batch<'_>, commit: Commitment) -> Result, Error> { - match batch - .db - .get_ser(Some(self.list_prefix), commit.as_ref(), None)? - { + match self.get_list(batch, commit)? { None => Ok(None), Some(ListWrapper::Single { pos }) => Ok(Some(pos)), Some(ListWrapper::Multi { head, .. }) => { @@ -363,7 +360,7 @@ where let (cur_pos_db_key, cur_pos_key) = self.entry_key(commit, current_pos.pos()); batch.delete(cur_pos_db_key, &cur_pos_key)?; - let (pos_db_key, pos_key) = self.entry_key(commit, current_pos.pos()); + let (pos_db_key, pos_key) = self.entry_key(commit, pos.pos()); batch.db.put_ser(pos_db_key, &pos_key, &head)?; batch.db.put_ser(list_key.0, list_key.1, &list)?; Ok(Some(current_pos)) @@ -465,7 +462,7 @@ impl PruneableListIndex for MultiIndex { let (cur_pos_db_key, cur_pos_key) = self.entry_key(commit, current_pos.pos()); batch.delete(cur_pos_db_key, &cur_pos_key)?; - let (pos_db_key, pos_key) = self.entry_key(commit, current_pos.pos()); + let (pos_db_key, pos_key) = self.entry_key(commit, pos.pos()); batch.db.put_ser(pos_db_key, &pos_key, &tail)?; batch.db.put_ser(list_key.0, list_key.1, &list)?; Ok(Some(current_pos)) From f41d188dc90b1c192af98f937316a355abe9d12f Mon Sep 17 00:00:00 2001 From: ardocrat Date: Wed, 29 Apr 2026 22:25:56 +0300 Subject: [PATCH 16/37] lmdb: count all open transactions to finish before resizing --- store/src/lmdb.rs | 369 ++++++++++++++++++++++++++++++---------------- 1 file changed, 239 insertions(+), 130 deletions(-) diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index c21d14a3e1..5709d7c806 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -89,12 +89,16 @@ const DEFAULT_MULTI_DB_ENV_NAME: &'static str = "multi_lmdb"; /// Prefix key separator. pub const PREFIX_KEY_SEPARATOR: u8 = b':'; -/// Mapping of database path to environment. -static ENV_MAP: OnceLock>>>> = OnceLock::new(); -/// Mapping of database path to count of active batches to wait before resizing. -static ENV_BATCHES_COUNT: OnceLock>>> = OnceLock::new(); -/// Mapping of database path to check if database is resizing. -static ENV_RESIZING: OnceLock>>> = OnceLock::new(); +/// Mapping of database path to environment state. +static ENV_MAP: OnceLock>> = OnceLock::new(); + +/// State of active database environment. +struct EnvState { + env: Env, + open_txs_count: u32, + resizing: bool, + resize_checking: bool, +} /// LMDB-backed store facilitating data access and serialization. All writes /// are done through a Batch abstraction providing atomicity. @@ -109,7 +113,7 @@ pub struct Store { impl Store { /// Create a new LMDB env under the provided directory. - /// By default creates an environment named "multi_lmdb". + /// Creates default environment named "multi_lmdb". /// Be aware of transactional semantics in lmdb /// (transactions are per environment, not per database). /// Data from non-default `env_name` and prefixes will be @@ -139,7 +143,7 @@ impl Store { }; // Environment setup. - let env_map = ENV_MAP.get_or_init(|| Arc::new(RwLock::new(HashMap::new()))); + let env_map = ENV_MAP.get_or_init(|| RwLock::new(HashMap::new())); let has_env = { let r_env_map = env_map.read(); r_env_map.contains_key(&full_path) @@ -161,12 +165,20 @@ impl Store { } debug!("DB Mapsize is {}", env.info().map_size); let mut w_env_map = env_map.write(); - w_env_map.insert(full_path.clone(), env); + w_env_map.insert( + full_path.clone(), + EnvState { + env, + open_txs_count: 0, + resizing: false, + resize_checking: false, + }, + ); } // Database setup. let r_env_map = env_map.read(); - let env = r_env_map.get(&full_path).unwrap(); + let env = r_env_map.get(&full_path).unwrap().env.clone(); let mut write = env.write_txn()?; let def_name = db_name.unwrap_or(DEFAULT_ENV_NAME); let def_db = env.create_database(&mut write, Some(def_name))?; @@ -203,7 +215,7 @@ impl Store { Ok(s) } - /// Migrate db from provided path to store environment. + /// Migrate database from provided path to default environment. fn migrate_to_default_env( &self, from_name: Option<&str>, @@ -250,6 +262,111 @@ impl Store { Ok(()) } + /// Wait while database is resizing. + fn wait_for_resize(&self) { + loop { + if !ENV_MAP + .get() + .unwrap() + .read() + .get(&self.env_path) + .unwrap() + .resizing + { + break; + } + trace!("Wait on resizing DB {}", self.env_path); + thread::sleep(Duration::from_millis(100)); + } + } + + /// Resize database environment if needed. + fn maybe_resize(&self) { + self.wait_for_resize(); + + // Check only one resize per time. + if ENV_MAP + .get() + .unwrap() + .read() + .get(&self.env_path) + .unwrap() + .resize_checking + { + return; + } else { + ENV_MAP + .get() + .unwrap() + .write() + .get_mut(&self.env_path) + .unwrap() + .resize_checking = true; + } + + let (resize, new_size) = needs_resize(&self.env, self.alloc_chunk_size); + if resize { + // Resize at another thread to not interrupt any tx waiting all txs to be closed. + let env_path = self.env_path.clone(); + let env = self.env.clone(); + thread::spawn(move || { + loop { + let txs_count = ENV_MAP + .get() + .unwrap() + .read() + .get(&env_path) + .unwrap() + .open_txs_count; + if txs_count == 0 { + debug!("Start resizing DB {}", env_path); + ENV_MAP + .get() + .unwrap() + .write() + .get_mut(&env_path) + .unwrap() + .resizing = true; + // Wait to make sure there are no more active txs left. + thread::sleep(Duration::from_millis(1000)); + break; + } + } + + unsafe { + match env.resize(new_size) { + Ok(_) => debug!("End resizing DB {}", env_path), + Err(e) => error!("Resize DB {} error: {:?}", env_path, e), + } + } + + ENV_MAP + .get() + .unwrap() + .write() + .get_mut(&env_path) + .unwrap() + .resizing = false; + ENV_MAP + .get() + .unwrap() + .write() + .get_mut(&env_path) + .unwrap() + .resize_checking = false; + }); + return; + } + + ENV_MAP + .get() + .unwrap() + .write() + .get_mut(&self.env_path) + .unwrap() + .resize_checking = false; + } + /// Protocol version for the store. pub fn protocol_version(&self) -> ProtocolVersion { self.version @@ -263,7 +380,7 @@ impl Store { } } - /// Gets a value from the db, provided its key. + /// Gets a value from the database, provided its key. /// Deserializes the retrieved data using the provided function. fn get_with( &self, @@ -283,7 +400,7 @@ impl Store { } } - /// Gets a `Readable` value from the db, provided its key. + /// Gets a `Readable` value from the database, provided its key. /// Note: Creates a new read transaction so will *not* see any uncommitted data. pub fn get_ser( &self, @@ -291,27 +408,48 @@ impl Store { key: &[u8], deser_mode: Option, ) -> Result, Error> { - let d = match deser_mode { - Some(d) => d, - _ => DeserializationMode::default(), - }; self.wait_for_resize(); - let read = self.env.read_txn()?; - self.get_with(db_key, key, &read, |_, mut data| { - ser::deserialize(&mut data, self.protocol_version(), d).map_err(From::from) - }) + + TxCounter::on_change_tx_count(&self.env_path, true); + let res = { + let d = match deser_mode { + Some(d) => d, + _ => DeserializationMode::default(), + }; + match self.env.read_txn() { + Ok(read) => self.get_with(db_key, key, &read, |_, mut data| { + ser::deserialize(&mut data, self.protocol_version(), d).map_err(From::from) + }), + Err(e) => Err(Error::from(e)), + } + }; + TxCounter::on_change_tx_count(&self.env_path, false); + res } - /// Whether the provided key exists. + /// Whether the key exists at the provided database key. pub fn exists(&self, db_key: Option, key: &[u8]) -> Result { self.wait_for_resize(); - let read = self.env.read_txn()?; - let db = self.get_db(db_key); - let res = db.get(&read, key)?; - Ok(res.is_some()) + + TxCounter::on_change_tx_count(&self.env_path, true); + let res = { + match self.env.read_txn() { + Ok(read) => { + let db = self.get_db(db_key); + let res = db.get(&read, key); + match res { + Ok(r) => Ok(r.is_some()), + Err(e) => Err(Error::from(e)), + } + } + Err(e) => Err(Error::from(e)), + } + }; + TxCounter::on_change_tx_count(&self.env_path, false); + res } - /// Produces an iterator from the provided db name. + /// Produces an iterator from the provided database key. pub fn iter( &self, db_key: Option, @@ -321,116 +459,93 @@ impl Store { F: Fn(&[u8], &[u8]) -> Result, { self.wait_for_resize(); - let read = self.env.clone().static_read_txn()?; - let db = self.get_db(db_key); - Ok(DatabaseIterator::new( - Arc::new(db.clone()), - read, - deserialize, - )) - } - /// Wait while DB is resizing. - fn wait_for_resize(&self) { - loop { - let resizing = { - let res_map = ENV_RESIZING.get_or_init(|| Arc::new(RwLock::new(HashMap::new()))); - let r_res_map = res_map.read(); - r_res_map.get(&self.env_path).map(|r| *r).unwrap_or(false) - }; - if !resizing { - break; + TxCounter::on_change_tx_count(&self.env_path, true); + match self.env.clone().static_read_txn() { + Ok(read) => { + let db = self.get_db(db_key); + Ok(DatabaseIterator::new( + self, + Arc::new(db.clone()), + read, + deserialize, + )) } - trace!("Wait on resizing DB {}", self.env_path); - thread::sleep(Duration::from_millis(100)); - } - } - - /// Resize database environment if needed. - fn maybe_resize(&self) -> Result<(), Error> { - self.wait_for_resize(); - let (resize, new_size) = needs_resize(&self.env, self.alloc_chunk_size); - if resize { - let res_map = ENV_RESIZING.get().unwrap(); - { - let mut w_res_map = res_map.write(); - w_res_map.insert(self.env_path.clone(), true); - } - debug!("Start resizing DB {}", self.env_path); - unsafe { - loop { - let batches_count = - ENV_BATCHES_COUNT.get_or_init(|| Arc::new(RwLock::new(HashMap::new()))); - let batches = batches_count.read(); - let cur = batches.get(&self.env_path).unwrap_or(&0); - if cur == &0 { - break; - } - debug!( - "Wait {} batches to complete to resize DB {}", - cur, self.env_path - ); - thread::sleep(Duration::from_millis(100)); - } - self.env.resize(new_size)?; + Err(e) => { + TxCounter::on_change_tx_count(&self.env_path, false); + Err(Error::from(e)) } - { - let mut w_res_map = res_map.write(); - w_res_map.insert(self.env_path.clone(), false); - } - debug!("End resizing DB {}", self.env_path); } - Ok(()) } /// Builds a new batch to be used with this store. pub fn batch(&self) -> Result, Error> { - self.maybe_resize()?; - on_change_batches_count(&self.env_path, true); - Ok(Batch::new(self)?) + self.maybe_resize(); + + TxCounter::on_change_tx_count(&self.env_path, true); + match Batch::new(self) { + Ok(batch) => Ok(batch), + Err(e) => { + TxCounter::on_change_tx_count(&self.env_path, false); + Err(e) + } + } } } -/// Batches counter to decrement value on drop. -struct BatchesCounter<'a> { - env_path: &'a String, +/// Environment transactions counter, allows to decrement value on drop. +struct TxCounter { + env_path: String, } -impl Drop for BatchesCounter<'_> { +impl Drop for TxCounter { fn drop(&mut self) { - on_change_batches_count(&self.env_path, false); + Self::on_change_tx_count(&self.env_path, false); } } -/// Batch to write multiple Writeables to db in an atomic manner. +impl TxCounter { + /// Increment or decrement active transactions count for current environment. + fn on_change_tx_count(env_path: &String, inc: bool) { + let mut w_env_map = ENV_MAP.get().unwrap().write(); + let env_state = w_env_map.get_mut(env_path).unwrap(); + if inc { + env_state.open_txs_count += 1; + } else { + env_state.open_txs_count -= 1; + } + } +} + +/// Batch to write multiple Writeables to the database in an atomic manner. pub struct Batch<'a> { store: &'a Store, write: RwTxn<'a>, #[allow(dead_code)] - counter: BatchesCounter<'a>, + tx_counter: TxCounter, } impl<'a> Batch<'a> { - /// Creates a new batch for provided db. + /// Creates a new batch for provided store. pub fn new(store: &'a Store) -> Result, Error> { let write = store.env.write_txn()?; Ok(Batch { store, write, - counter: BatchesCounter { - env_path: &store.env_path, + tx_counter: TxCounter { + env_path: store.env_path.clone(), }, }) } - /// Writes a single key/value pair to the db. + /// Writes a single key/value pair to the provided database key. pub fn put(&mut self, db_key: Option, key: &[u8], value: &[u8]) -> Result<(), Error> { let db = self.store.get_db(db_key); db.put(&mut self.write, key, value)?; Ok(()) } - /// Writes a single key and its `Writeable` value to the db. + /// Writes a single key and its `Writeable` value to the provided database key. /// Encapsulates serialization using the (default) version configured on the store instance. pub fn put_ser( &mut self, @@ -446,7 +561,7 @@ impl<'a> Batch<'a> { self.store.protocol_version() } - /// Writes a single key and its `Writeable` value to the db. + /// Writes a single key and its `Writeable` value to the provided database key. /// Encapsulates serialization using the specified protocol version. pub fn put_ser_with_version( &mut self, @@ -486,7 +601,7 @@ impl<'a> Batch<'a> { Ok(res.is_some()) } - /// Produces an iterator from the provided db key. + /// Produces an iterator from the provided database key. pub fn iter( &self, db_key: Option, @@ -498,7 +613,7 @@ impl<'a> Batch<'a> { self.store.iter(db_key, deserialize) } - /// Gets a `Readable` value from the db by provided key and provided deserialization strategy. + /// Gets a `Readable` value from the database by provided key and deserialization strategy. pub fn get_ser( &self, db_key: Option, @@ -517,14 +632,14 @@ impl<'a> Batch<'a> { }) } - /// Deletes a key/value pair from the db. + /// Deletes a key/value pair from the database. pub fn delete(&mut self, db_key: Option, key: &[u8]) -> Result<(), Error> { let db = self.store.get_db(db_key); db.delete(&mut self.write, key)?; Ok(()) } - /// Writes the batch to db. + /// Writes the batch to database. pub fn commit(self) -> Result<(), Error> { self.write.commit()?; Ok(()) @@ -533,20 +648,24 @@ impl<'a> Batch<'a> { /// Creates a child of this batch. It will be merged with its parent on /// commit, abandoned otherwise. pub fn child(&mut self) -> Result, Error> { - self.store.maybe_resize()?; - on_change_batches_count(&self.store.env_path, true); - let write = self.store.env.nested_write_txn(&mut self.write)?; - Ok(Batch { - store: self.store, - write, - counter: BatchesCounter { - env_path: &self.store.env_path, - }, - }) + TxCounter::on_change_tx_count(&self.store.env_path, true); + match self.store.env.nested_write_txn(&mut self.write) { + Ok(write) => Ok(Batch { + store: self.store, + write, + tx_counter: TxCounter { + env_path: self.store.env_path.clone(), + }, + }), + Err(e) => { + TxCounter::on_change_tx_count(&self.store.env_path, false); + Err(Error::from(e)) + } + } } } -/// An iterator based on db key. +/// An iterator based on database key. /// Caller is responsible for deserialization of the data. pub struct DatabaseIterator where @@ -557,6 +676,8 @@ where keys: Vec>, skip: usize, deserialize: F, + #[allow(dead_code)] + tx_counter: TxCounter, } impl Iterator for DatabaseIterator @@ -588,6 +709,7 @@ where { /// Initialize a new prefix iterator. pub fn new( + store: &Store, db: Arc>, read: RoTxn<'static, WithoutTls>, deserialize: F, @@ -606,6 +728,9 @@ where keys, skip: 0, deserialize, + tx_counter: TxCounter { + env_path: store.env_path.clone(), + }, } } } @@ -657,19 +782,3 @@ pub fn needs_resize(env: &Env, alloc_chunk_size: usize) -> (bool, us (resize, new_size) } - -/// Increment or decrement active batches count for current environment. -fn on_change_batches_count(env_path: &String, inc: bool) { - let batches_count = ENV_BATCHES_COUNT.get_or_init(|| Arc::new(RwLock::new(HashMap::new()))); - let mut w_batches = batches_count.write(); - let batches = w_batches.clone(); - let count = { - let cur = batches.get(env_path).unwrap_or(&0); - if inc { - cur + 1 - } else { - cur - 1 - } - }; - w_batches.insert(env_path.clone(), count); -} From d52cfe173b93d5df287e6eb98a92d72eadc22970 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Wed, 29 Apr 2026 23:36:37 +0300 Subject: [PATCH 17/37] lmdb: immediate resize if there are no open transactions --- store/src/lmdb.rs | 141 ++++++++++++++++++++++++++-------------------- 1 file changed, 81 insertions(+), 60 deletions(-) diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index 5709d7c806..d7222e2e7f 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -262,6 +262,39 @@ impl Store { Ok(()) } + /// Get number of active environment transactions. + fn open_txs_count(&self) -> u32 { + ENV_MAP + .get() + .unwrap() + .read() + .get(&self.env_path) + .unwrap() + .open_txs_count + } + + /// Check if requirement for environment resize is checking. + fn resize_checking(&self) -> bool { + ENV_MAP + .get() + .unwrap() + .read() + .get(&self.env_path) + .unwrap() + .resize_checking + } + + /// Set flag if requirement for environment resize is checking. + fn set_resize_checking(&self, resize_checking: bool) { + ENV_MAP + .get() + .unwrap() + .write() + .get_mut(&self.env_path) + .unwrap() + .resize_checking = resize_checking; + } + /// Wait while database is resizing. fn wait_for_resize(&self) { loop { @@ -284,87 +317,75 @@ impl Store { fn maybe_resize(&self) { self.wait_for_resize(); - // Check only one resize per time. - if ENV_MAP - .get() - .unwrap() - .read() - .get(&self.env_path) - .unwrap() - .resize_checking - { + // Check only one resize requirement per time to avoid multiple resizes. + if self.resize_checking() { return; } else { - ENV_MAP - .get() - .unwrap() - .write() - .get_mut(&self.env_path) - .unwrap() - .resize_checking = true; + self.set_resize_checking(true); } let (resize, new_size) = needs_resize(&self.env, self.alloc_chunk_size); if resize { - // Resize at another thread to not interrupt any tx waiting all txs to be closed. let env_path = self.env_path.clone(); let env = self.env.clone(); - thread::spawn(move || { - loop { - let txs_count = ENV_MAP - .get() - .unwrap() - .read() - .get(&env_path) - .unwrap() - .open_txs_count; - if txs_count == 0 { - debug!("Start resizing DB {}", env_path); - ENV_MAP + + // Resize immediately or at another thread to not interrupt current + // transaction waiting all open transactions to be closed. + if self.open_txs_count() != 0 { + thread::spawn(move || { + loop { + let txs_count = ENV_MAP .get() .unwrap() - .write() - .get_mut(&env_path) + .read() + .get(&env_path) .unwrap() - .resizing = true; - // Wait to make sure there are no more active txs left. - thread::sleep(Duration::from_millis(1000)); - break; + .open_txs_count; + if txs_count == 0 { + debug!("Start resizing DB {}", env_path); + ENV_MAP + .get() + .unwrap() + .write() + .get_mut(&env_path) + .unwrap() + .resizing = true; + // Wait to make sure there are no more active txs left. + thread::sleep(Duration::from_millis(1000)); + break; + } } - } + unsafe { + match env.resize(new_size) { + Ok(_) => debug!("End resizing DB {}", env_path), + Err(e) => error!("Resize DB {} error: {:?}", env_path, e), + } + } + + let mut w_env_map = ENV_MAP.get().unwrap().write(); + let env_state = w_env_map.get_mut(&env_path).unwrap(); + env_state.resizing = false; + env_state.resize_checking = false; + }); + return; + } else { + let mut w_env_map = ENV_MAP.get().unwrap().write(); + let env_state = w_env_map.get_mut(&env_path).unwrap(); + + debug!("Start immediate resizing DB {}", env_path); + env_state.resizing = true; unsafe { match env.resize(new_size) { Ok(_) => debug!("End resizing DB {}", env_path), Err(e) => error!("Resize DB {} error: {:?}", env_path, e), } } - - ENV_MAP - .get() - .unwrap() - .write() - .get_mut(&env_path) - .unwrap() - .resizing = false; - ENV_MAP - .get() - .unwrap() - .write() - .get_mut(&env_path) - .unwrap() - .resize_checking = false; - }); - return; + env_state.resizing = false; + } } - ENV_MAP - .get() - .unwrap() - .write() - .get_mut(&self.env_path) - .unwrap() - .resize_checking = false; + self.set_resize_checking(false); } /// Protocol version for the store. From 9d0925ab0879d148785422caf0f6f239edfa6017 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Fri, 1 May 2026 01:30:33 +0300 Subject: [PATCH 18/37] lmdb: remove env state when there are no more stores --- store/src/lmdb.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index d7222e2e7f..6d0b9405f2 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -98,6 +98,7 @@ struct EnvState { open_txs_count: u32, resizing: bool, resize_checking: bool, + stores_count: u32, } /// LMDB-backed store facilitating data access and serialization. All writes @@ -111,6 +112,29 @@ pub struct Store { alloc_chunk_size: usize, } +impl Drop for Store { + fn drop(&mut self) { + { + let mut w_map = ENV_MAP.get().unwrap().write(); + w_map.get_mut(&self.env_path).unwrap().stores_count -= 1; + } + let no_stores = { + ENV_MAP + .get() + .unwrap() + .read() + .get(&self.env_path) + .unwrap() + .stores_count + == 0 + }; + if no_stores { + let mut w_map = ENV_MAP.get().unwrap().write(); + w_map.remove(&self.env_path); + } + } +} + impl Store { /// Create a new LMDB env under the provided directory. /// Creates default environment named "multi_lmdb". @@ -172,8 +196,12 @@ impl Store { open_txs_count: 0, resizing: false, resize_checking: false, + stores_count: 1, }, ); + } else { + let mut w_env_map = env_map.write(); + w_env_map.get_mut(&full_path).unwrap().stores_count += 1; } // Database setup. From f921f8758cf2084f76bce9957d44bc1cf14e9fc1 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Fri, 15 May 2026 10:52:09 +0300 Subject: [PATCH 19/37] lmdb: use atomic for resize and resize checking flags --- store/src/lmdb.rs | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index 6d0b9405f2..ea07c1c4fb 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -18,6 +18,7 @@ use heed::types::Bytes; use heed::{Database, Env, EnvOpenOptions, RoTxn, RwTxn, WithoutTls}; use std::collections::HashMap; use std::path::Path; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, OnceLock}; use std::time::Duration; use std::{fs, thread}; @@ -96,8 +97,8 @@ static ENV_MAP: OnceLock>> = OnceLock::new(); struct EnvState { env: Env, open_txs_count: u32, - resizing: bool, - resize_checking: bool, + resizing: AtomicBool, + resize_checking: AtomicBool, stores_count: u32, } @@ -194,8 +195,8 @@ impl Store { EnvState { env, open_txs_count: 0, - resizing: false, - resize_checking: false, + resizing: AtomicBool::new(false), + resize_checking: AtomicBool::new(false), stores_count: 1, }, ); @@ -310,6 +311,7 @@ impl Store { .get(&self.env_path) .unwrap() .resize_checking + .load(Ordering::Relaxed) } /// Set flag if requirement for environment resize is checking. @@ -320,7 +322,8 @@ impl Store { .write() .get_mut(&self.env_path) .unwrap() - .resize_checking = resize_checking; + .resize_checking + .store(resize_checking, Ordering::Relaxed); } /// Wait while database is resizing. @@ -333,6 +336,7 @@ impl Store { .get(&self.env_path) .unwrap() .resizing + .load(Ordering::Relaxed) { break; } @@ -377,7 +381,8 @@ impl Store { .write() .get_mut(&env_path) .unwrap() - .resizing = true; + .resizing + .store(true, Ordering::Relaxed); // Wait to make sure there are no more active txs left. thread::sleep(Duration::from_millis(1000)); break; @@ -393,8 +398,8 @@ impl Store { let mut w_env_map = ENV_MAP.get().unwrap().write(); let env_state = w_env_map.get_mut(&env_path).unwrap(); - env_state.resizing = false; - env_state.resize_checking = false; + env_state.resizing.store(false, Ordering::Relaxed); + env_state.resize_checking.store(false, Ordering::Relaxed); }); return; } else { @@ -402,14 +407,14 @@ impl Store { let env_state = w_env_map.get_mut(&env_path).unwrap(); debug!("Start immediate resizing DB {}", env_path); - env_state.resizing = true; + env_state.resizing.store(true, Ordering::Relaxed); unsafe { match env.resize(new_size) { Ok(_) => debug!("End resizing DB {}", env_path), Err(e) => error!("Resize DB {} error: {:?}", env_path, e), } } - env_state.resizing = false; + env_state.resizing.store(false, Ordering::Relaxed); } } From 22bc94491832157d907702f32221fcba1781da2e Mon Sep 17 00:00:00 2001 From: ardocrat Date: Fri, 15 May 2026 10:53:20 +0300 Subject: [PATCH 20/37] lmdb: sleep 10ms when waiting all opened txs to be closed --- store/src/lmdb.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index ea07c1c4fb..d18dd6a062 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -387,6 +387,7 @@ impl Store { thread::sleep(Duration::from_millis(1000)); break; } + thread::sleep(Duration::from_millis(10)); } unsafe { From aa28adc6bd3f1523bff4586b2179295c72bf16cc Mon Sep 17 00:00:00 2001 From: ardocrat Date: Fri, 15 May 2026 18:20:13 +0300 Subject: [PATCH 21/37] lmdb: use atomic open txs and stores count --- store/src/lmdb.rs | 46 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index d18dd6a062..c40f802184 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -18,7 +18,7 @@ use heed::types::Bytes; use heed::{Database, Env, EnvOpenOptions, RoTxn, RwTxn, WithoutTls}; use std::collections::HashMap; use std::path::Path; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::{Arc, OnceLock}; use std::time::Duration; use std::{fs, thread}; @@ -96,10 +96,10 @@ static ENV_MAP: OnceLock>> = OnceLock::new(); /// State of active database environment. struct EnvState { env: Env, - open_txs_count: u32, + open_txs_count: AtomicU32, resizing: AtomicBool, resize_checking: AtomicBool, - stores_count: u32, + stores_count: AtomicU32, } /// LMDB-backed store facilitating data access and serialization. All writes @@ -117,7 +117,16 @@ impl Drop for Store { fn drop(&mut self) { { let mut w_map = ENV_MAP.get().unwrap().write(); - w_map.get_mut(&self.env_path).unwrap().stores_count -= 1; + let stores_count = w_map + .get(&self.env_path) + .unwrap() + .stores_count + .load(Ordering::Relaxed); + w_map + .get_mut(&self.env_path) + .unwrap() + .stores_count + .store(stores_count - 1, Ordering::Relaxed); } let no_stores = { ENV_MAP @@ -127,6 +136,7 @@ impl Drop for Store { .get(&self.env_path) .unwrap() .stores_count + .load(Ordering::Relaxed) == 0 }; if no_stores { @@ -194,15 +204,24 @@ impl Store { full_path.clone(), EnvState { env, - open_txs_count: 0, + open_txs_count: AtomicU32::new(0), resizing: AtomicBool::new(false), resize_checking: AtomicBool::new(false), - stores_count: 1, + stores_count: AtomicU32::new(1), }, ); } else { let mut w_env_map = env_map.write(); - w_env_map.get_mut(&full_path).unwrap().stores_count += 1; + let stores_count = w_env_map + .get(&full_path) + .unwrap() + .stores_count + .load(Ordering::Relaxed); + w_env_map + .get_mut(&full_path) + .unwrap() + .stores_count + .store(stores_count + 1, Ordering::Relaxed); } // Database setup. @@ -300,6 +319,7 @@ impl Store { .get(&self.env_path) .unwrap() .open_txs_count + .load(Ordering::Relaxed) } /// Check if requirement for environment resize is checking. @@ -372,7 +392,8 @@ impl Store { .read() .get(&env_path) .unwrap() - .open_txs_count; + .open_txs_count + .load(Ordering::Relaxed); if txs_count == 0 { debug!("Start resizing DB {}", env_path); ENV_MAP @@ -564,10 +585,15 @@ impl TxCounter { fn on_change_tx_count(env_path: &String, inc: bool) { let mut w_env_map = ENV_MAP.get().unwrap().write(); let env_state = w_env_map.get_mut(env_path).unwrap(); + let open_txs_count = env_state.open_txs_count.load(Ordering::Relaxed); if inc { - env_state.open_txs_count += 1; + env_state + .open_txs_count + .store(open_txs_count + 1, Ordering::Relaxed); } else { - env_state.open_txs_count -= 1; + env_state + .open_txs_count + .store(open_txs_count - 1, Ordering::Relaxed); } } } From 7d251d2267508b2eedc9d90d3eb732f859d469b8 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Fri, 15 May 2026 18:38:25 +0300 Subject: [PATCH 22/37] lmdb: use index to detect separator, ignore unknown db key to not have a panic --- store/src/lmdb.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index c40f802184..1c186a6a11 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -280,7 +280,7 @@ impl Store { unsafe { from_env.resize(new_size)?; self.env.resize(new_size)?; - }; + } } let db_from = { let mut write = from_env.write_txn()?; @@ -293,12 +293,15 @@ impl Store { let mut count = 0; for kv in db_from.iter(&read_from)? { if let Ok((k, v)) = kv { - if k.contains(&PREFIX_KEY_SEPARATOR) { + if k.len() > 1 && k[1] == PREFIX_KEY_SEPARATOR { let db_name = k.split_at(1).0; - let db = self.pre_dbs.get(&db_name[0]).unwrap(); - let key = k.split_at(2).1; - db.put(&mut write_to, key, &v)?; - count += 1; + if let Some(db) = self.pre_dbs.get(&db_name[0]) { + let key = k.split_at(2).1; + db.put(&mut write_to, key, &v)?; + count += 1; + } else { + error!("Migration: unknown db key: {}", db_name[0]); + } } else { self.def_db.put(&mut write_to, k, &v)?; count += 1; From d217bda12ed385a72ce65b3b73f4a680d15a9665 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sat, 16 May 2026 12:44:21 +0300 Subject: [PATCH 23/37] lmdb: store max 10k keys in the iterator --- store/src/lmdb.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index 1c186a6a11..6e3196a413 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -759,6 +759,8 @@ where read: Arc>, keys: Vec>, skip: usize, + total_keys: usize, + skip_keys: usize, deserialize: F, #[allow(dead_code)] tx_counter: TxCounter, @@ -777,11 +779,26 @@ where return match (self.deserialize)(k, v) { Ok(v) => { self.skip += 1; + self.skip_keys += 1; Some(v) } Err(_) => None, }; } + } else if self.total_keys > self.skip_keys { + let keys = if let Ok(iter) = self.db.iter(&self.read) { + iter.move_between_keys() + .skip(self.skip_keys) + .take(10000) + .filter(|kv| kv.is_ok()) + .map(|kv| kv.unwrap().0.to_vec()) + .collect::>>() + } else { + vec![] + }; + self.skip = 0; + self.keys = keys; + return self.next(); } None } @@ -798,8 +815,10 @@ where read: RoTxn<'static, WithoutTls>, deserialize: F, ) -> DatabaseIterator { + let total_keys = db.iter(&read).unwrap().count(); let keys = if let Ok(iter) = db.iter(&read) { iter.move_between_keys() + .take(10000) .filter(|kv| kv.is_ok()) .map(|kv| kv.unwrap().0.to_vec()) .collect::>>() @@ -810,6 +829,8 @@ where db, read: Arc::new(read), keys, + total_keys, + skip_keys: 0, skip: 0, deserialize, tx_counter: TxCounter { From 418d865c01f55fcdea3de9e91338757d226aa59a Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sat, 16 May 2026 12:51:25 +0300 Subject: [PATCH 24/37] lmdb: check iter result on getting total --- store/src/lmdb.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index 6e3196a413..f0b63aff53 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -815,15 +815,20 @@ where read: RoTxn<'static, WithoutTls>, deserialize: F, ) -> DatabaseIterator { - let total_keys = db.iter(&read).unwrap().count(); - let keys = if let Ok(iter) = db.iter(&read) { - iter.move_between_keys() - .take(10000) - .filter(|kv| kv.is_ok()) - .map(|kv| kv.unwrap().0.to_vec()) - .collect::>>() + let (keys, total_keys) = if let Ok(iter) = db.iter(&read) { + let total = iter.move_between_keys().count(); + let keys = if let Ok(iter) = db.iter(&read) { + iter.move_between_keys() + .take(10000) + .filter(|kv| kv.is_ok()) + .map(|kv| kv.unwrap().0.to_vec()) + .collect::>>() + } else { + vec![] + }; + (keys, total) } else { - vec![] + (vec![], 0) }; DatabaseIterator { db, From fc66b73e5b448baa2c0a70cb3d409391d7b7c8f7 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sun, 17 May 2026 12:36:31 +0300 Subject: [PATCH 25/37] lmdb: handle errors at iterator --- chain/src/chain.rs | 8 +++--- chain/src/linked_list.rs | 12 ++++++--- chain/src/store.rs | 10 ++++--- chain/src/txhashset/txhashset.rs | 26 +++++++++--------- p2p/src/peers.rs | 5 +++- p2p/src/store.rs | 16 ++++++++--- store/src/lmdb.rs | 46 +++++++++++++++----------------- 7 files changed, 72 insertions(+), 51 deletions(-) diff --git a/chain/src/chain.rs b/chain/src/chain.rs index aabadc3224..13325fadfd 100644 --- a/chain/src/chain.rs +++ b/chain/src/chain.rs @@ -1299,9 +1299,11 @@ impl Chain { // Remove old blocks (including short lived fork blocks) which height < tail.height for block in batch.blocks_iter()? { - if block.header.height < tail.height { - let _ = batch.delete_block(&block.hash()); - count += 1; + if let Ok(block) = block { + if block.header.height < tail.height { + let _ = batch.delete_block(&block.hash()); + count += 1; + } } } diff --git a/chain/src/linked_list.rs b/chain/src/linked_list.rs index 289ffd1061..11b0a1145e 100644 --- a/chain/src/linked_list.rs +++ b/chain/src/linked_list.rs @@ -409,13 +409,17 @@ impl PruneableListIndex for MultiIndex { let mut entry_count = 0; let list_db_key = Some(self.list_prefix); for key in batch.db.iter(list_db_key, |k, _| Ok(k.to_vec()))? { - let _ = batch.delete(list_db_key, &key); - list_count += 1; + if let Ok(key) = key { + let _ = batch.delete(list_db_key, &key); + list_count += 1; + } } let entry_db_key = Some(self.entry_prefix); for key in batch.db.iter(entry_db_key, |k, _| Ok(k.to_vec()))? { - let _ = batch.delete(entry_db_key, &key); - entry_count += 1; + if let Ok(key) = key { + let _ = batch.delete(entry_db_key, &key); + entry_count += 1; + } } debug!( "clear: lists deleted: {}, entries deleted: {}", diff --git a/chain/src/store.rs b/chain/src/store.rs index 3a3df5757d..e5ed2c600b 100644 --- a/chain/src/store.rs +++ b/chain/src/store.rs @@ -339,7 +339,9 @@ impl<'a> Batch<'a> { } /// Iterator over the output_pos index. - pub fn output_pos_iter(&self) -> Result, CommitPos)>, Error> { + pub fn output_pos_iter( + &self, + ) -> Result, CommitPos), Error>>, Error> { let protocol_version = self.db.protocol_version(); self.db.iter(Some(OUTPUT_POS_PREFIX), move |k, mut v| { ser::deserialize(&mut v, protocol_version, DeserializationMode::default()) @@ -459,7 +461,7 @@ impl<'a> Batch<'a> { /// Iterator over all full blocks in the db. /// Uses default db serialization strategy via db protocol version. - pub fn blocks_iter(&self) -> Result + 'a, Error> { + pub fn blocks_iter(&self) -> Result> + 'a, Error> { let protocol_version = self.db.protocol_version(); self.db.iter(Some(BLOCK_PREFIX), move |_, mut v| { ser::deserialize(&mut v, protocol_version, DeserializationMode::default()) @@ -469,7 +471,9 @@ impl<'a> Batch<'a> { /// Iterator over raw data for full blocks in the db. /// Used during block migration (we need flexibility around deserialization). - pub fn blocks_raw_iter(&self) -> Result, Vec)>, Error> { + pub fn blocks_raw_iter( + &self, + ) -> Result, Vec), Error>>, Error> { self.db .iter(Some(BLOCK_PREFIX), |k, v| Ok((k.to_vec(), v.to_vec()))) } diff --git a/chain/src/txhashset/txhashset.rs b/chain/src/txhashset/txhashset.rs index c9c55a7045..8155a54739 100644 --- a/chain/src/txhashset/txhashset.rs +++ b/chain/src/txhashset/txhashset.rs @@ -644,21 +644,23 @@ impl TxHashSet { // Iterate over the current output_pos index, removing any entries that // do not point to to the expected output. let mut removed_count = 0; - for (key, pos1) in batch.output_pos_iter()? { - let pos0 = pos1.pos - 1; - if let Some(out) = output_pmmr.get_data(pos0) { - if let Ok(pos0_via_mmr) = batch.get_output_pos(&out.commitment()) { - // If the pos matches and the index key matches the commitment - // then keep the entry, other we want to clean it up. - if pos0 == pos0_via_mmr - && batch.is_match_output_pos_key(&key, &out.commitment()) - { - continue; + for kp in batch.output_pos_iter()? { + if let Ok((key, pos1)) = kp { + let pos0 = pos1.pos - 1; + if let Some(out) = output_pmmr.get_data(pos0) { + if let Ok(pos0_via_mmr) = batch.get_output_pos(&out.commitment()) { + // If the pos matches and the index key matches the commitment + // then keep the entry, other we want to clean it up. + if pos0 == pos0_via_mmr + && batch.is_match_output_pos_key(&key, &out.commitment()) + { + continue; + } } } + batch.delete(Some(store::OUTPUT_POS_PREFIX), &key)?; + removed_count += 1; } - batch.delete(Some(store::OUTPUT_POS_PREFIX), &key)?; - removed_count += 1; } debug!( "init_output_pos_index: removed {} stale index entries", diff --git a/p2p/src/peers.rs b/p2p/src/peers.rs index 21340b1008..23746477c2 100644 --- a/p2p/src/peers.rs +++ b/p2p/src/peers.rs @@ -279,7 +279,10 @@ impl Peers { pub fn all_peer_data(&self) -> Vec { match self.store.iter_batch() { Ok(batch) => match batch.peers_iter() { - Ok(iter) => iter.collect(), + Ok(iter) => iter + .filter(|p| p.is_ok()) + .map(|p| p.ok().unwrap()) + .collect(), Err(e) => { error!("failed to get all peer data: {:?}", e); vec![] diff --git a/p2p/src/store.rs b/p2p/src/store.rs index e0f32e9f71..fe404e05c1 100644 --- a/p2p/src/store.rs +++ b/p2p/src/store.rs @@ -195,7 +195,7 @@ pub struct PeersIterBatch<'a> { impl<'a> PeersIterBatch<'a> { /// Iterator over all known peers. - pub fn peers_iter(&self) -> Result, Error> { + pub fn peers_iter(&self) -> Result>, Error> { let protocol_version = self.db.protocol_version(); self.db.iter(Some(PEER_PREFIX), move |_, mut v| { ser::deserialize(&mut v, protocol_version, DeserializationMode::default()) @@ -212,6 +212,8 @@ impl<'a> PeersIterBatch<'a> { ) -> Result, Error> { let peers = self .peers_iter()? + .filter(|p| p.is_ok()) + .map(|p| p.ok().unwrap()) .filter(|p| p.flags == state && p.capabilities.contains(cap)) .choose_multiple(&mut thread_rng(), count); Ok(peers) @@ -220,7 +222,11 @@ impl<'a> PeersIterBatch<'a> { /// List all known peers /// Used for /v1/peers/all api endpoint pub fn all_peers(&self) -> Result, Error> { - let peers: Vec = self.peers_iter()?.collect(); + let peers: Vec = self + .peers_iter()? + .filter(|p| p.is_ok()) + .map(|p| p.ok().unwrap()) + .collect(); Ok(peers) } @@ -232,8 +238,10 @@ impl<'a> PeersIterBatch<'a> { let mut to_remove = vec![]; for x in self.peers_iter()? { - if predicate(&x) { - to_remove.push(x) + if let Ok(x) = x { + if predicate(&x) { + to_remove.push(x) + } } } diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index f0b63aff53..0ed76c110e 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -770,35 +770,33 @@ impl Iterator for DatabaseIterator where F: Fn(&[u8], &[u8]) -> Result, { - type Item = T; + type Item = Result; fn next(&mut self) -> Option { if let Some(k) = self.keys.iter().skip(self.skip).next() { - let v = self.db.get(&self.read, k).unwrap_or(None); - if let Some(v) = v { - return match (self.deserialize)(k, v) { - Ok(v) => { - self.skip += 1; - self.skip_keys += 1; - Some(v) + match self.db.get(&self.read, k) { + Ok(v) => { + if let Some(v) = v { + return match (self.deserialize)(k, v) { + Ok(v) => { + self.skip += 1; + self.skip_keys += 1; + Some(Ok(v)) + } + Err(e) => { + error!("db iter: error deserializing: {}", e); + Some(Err(Error::from(e))) + } + }; } - Err(_) => None, - }; + } + Err(e) => { + return { + error!("db iter: error read value: {}", e); + Some(Err(Error::from(e))) + } + } } - } else if self.total_keys > self.skip_keys { - let keys = if let Ok(iter) = self.db.iter(&self.read) { - iter.move_between_keys() - .skip(self.skip_keys) - .take(10000) - .filter(|kv| kv.is_ok()) - .map(|kv| kv.unwrap().0.to_vec()) - .collect::>>() - } else { - vec![] - }; - self.skip = 0; - self.keys = keys; - return self.next(); } None } From 630bb2e5652da52784b9c90f4843986664b19ffb Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sun, 17 May 2026 12:51:42 +0300 Subject: [PATCH 26/37] lmdb: handle an error when db with provided key not found --- store/src/lmdb.rs | 53 +++++++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index 0ed76c110e..6db682b399 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -452,10 +452,16 @@ impl Store { } /// Get database from provided key or return default. - fn get_db(&self, db_key: Option) -> &Database { + fn get_db(&self, db_key: Option) -> Result<&Database, Error> { match db_key { - Some(db) => self.pre_dbs.get(&db).unwrap(), - None => &self.def_db, + Some(db) => { + if let Some(db) = self.pre_dbs.get(&db) { + Ok(db) + } else { + Err(Error::OtherErr("db for provided key not found".to_string())) + } + } + None => Ok(&self.def_db), } } @@ -471,7 +477,7 @@ impl Store { where F: Fn(&[u8], &[u8]) -> Result, { - let db = self.get_db(db_key); + let db = self.get_db(db_key)?; let res: Option<&[u8]> = db.get(read, key)?; match res { None => Ok(None), @@ -514,10 +520,15 @@ impl Store { let res = { match self.env.read_txn() { Ok(read) => { - let db = self.get_db(db_key); - let res = db.get(&read, key); - match res { - Ok(r) => Ok(r.is_some()), + let db_res = self.get_db(db_key); + match db_res { + Ok(db) => { + let res = db.get(&read, key); + match res { + Ok(r) => Ok(r.is_some()), + Err(e) => Err(Error::from(e)), + } + } Err(e) => Err(Error::from(e)), } } @@ -542,13 +553,19 @@ impl Store { TxCounter::on_change_tx_count(&self.env_path, true); match self.env.clone().static_read_txn() { Ok(read) => { - let db = self.get_db(db_key); - Ok(DatabaseIterator::new( - self, - Arc::new(db.clone()), - read, - deserialize, - )) + let db_res = self.get_db(db_key); + match db_res { + Ok(db) => Ok(DatabaseIterator::new( + self, + Arc::new(db.clone()), + read, + deserialize, + )), + Err(e) => { + TxCounter::on_change_tx_count(&self.env_path, false); + Err(Error::from(e)) + } + } } Err(e) => { TxCounter::on_change_tx_count(&self.env_path, false); @@ -624,7 +641,7 @@ impl<'a> Batch<'a> { /// Writes a single key/value pair to the provided database key. pub fn put(&mut self, db_key: Option, key: &[u8], value: &[u8]) -> Result<(), Error> { - let db = self.store.get_db(db_key); + let db = self.store.get_db(db_key)?; db.put(&mut self.write, key, value)?; Ok(()) } @@ -680,7 +697,7 @@ impl<'a> Batch<'a> { /// This is in the context of the current write transaction. pub fn exists(&self, db_key: Option, key: &[u8]) -> Result { let read = self.write.nested_read_txn()?; - let db = self.store.get_db(db_key); + let db = self.store.get_db(db_key)?; let res = db.get(&read, key)?; Ok(res.is_some()) } @@ -718,7 +735,7 @@ impl<'a> Batch<'a> { /// Deletes a key/value pair from the database. pub fn delete(&mut self, db_key: Option, key: &[u8]) -> Result<(), Error> { - let db = self.store.get_db(db_key); + let db = self.store.get_db(db_key)?; db.delete(&mut self.write, key)?; Ok(()) } From cbf2cafd69c2ed7cb821fdabac81ecb0583cc371 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sun, 17 May 2026 13:09:53 +0300 Subject: [PATCH 27/37] lmdb: fix iterate over 10k keys --- store/src/lmdb.rs | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index 6db682b399..f66b90e859 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -775,9 +775,9 @@ where db: Arc>, read: Arc>, keys: Vec>, - skip: usize, total_keys: usize, - skip_keys: usize, + skip_cur: usize, + skip_total: usize, deserialize: F, #[allow(dead_code)] tx_counter: TxCounter, @@ -790,14 +790,14 @@ where type Item = Result; fn next(&mut self) -> Option { - if let Some(k) = self.keys.iter().skip(self.skip).next() { + if let Some(k) = self.keys.iter().skip(self.skip_cur).next() { match self.db.get(&self.read, k) { Ok(v) => { if let Some(v) = v { return match (self.deserialize)(k, v) { Ok(v) => { - self.skip += 1; - self.skip_keys += 1; + self.skip_total += 1; + self.skip_cur += 1; Some(Ok(v)) } Err(e) => { @@ -814,6 +814,20 @@ where } } } + } else if self.total_keys > self.skip_total { + let keys = if let Ok(iter) = self.db.iter(&self.read) { + iter.move_between_keys() + .skip(self.skip_total) + .take(10000) + .filter(|kv| kv.is_ok()) + .map(|kv| kv.unwrap().0.to_vec()) + .collect::>>() + } else { + vec![] + }; + self.skip_cur = 0; + self.keys = keys; + return self.next(); } None } @@ -850,8 +864,8 @@ where read: Arc::new(read), keys, total_keys, - skip_keys: 0, - skip: 0, + skip_cur: 0, + skip_total: 0, deserialize, tx_counter: TxCounter { env_path: store.env_path.clone(), From b97fa28aa49468ec0cfc4a765c48a5604c1bc458 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sun, 17 May 2026 13:14:36 +0300 Subject: [PATCH 28/37] lmdb: document migration resize safety --- store/src/lmdb.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index f66b90e859..75cb6d886e 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -277,6 +277,7 @@ impl Store { }; let (resize, new_size) = needs_resize(&from_env, self.alloc_chunk_size); if resize { + // We are sure there are no active txs, cause migration is called on database creation. unsafe { from_env.resize(new_size)?; self.env.resize(new_size)?; From 17cd0b9ed4415dc5b9a78682c705e6607d2d8bf2 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sun, 17 May 2026 13:25:15 +0300 Subject: [PATCH 29/37] lmdb: fix iter test --- store/tests/lmdb.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/store/tests/lmdb.rs b/store/tests/lmdb.rs index f8c6838601..ac0b56f943 100644 --- a/store/tests/lmdb.rs +++ b/store/tests/lmdb.rs @@ -126,7 +126,7 @@ fn test_iter() -> Result<(), store::Error> { // Check we can see the new entry via an iterator after committing the batch. let mut iter = store.iter(Some(prefix), |_, v| Ok(v.to_vec()))?; - assert_eq!(iter.next(), Some(value.to_vec())); + assert_eq!(iter.next(), Some(Ok(value.to_vec()))); assert_eq!(iter.next(), None); clean_output_dir(test_dir); From 7bf460992d39f052647585f48d5b4307c9f9ad94 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sun, 17 May 2026 13:35:32 +0300 Subject: [PATCH 30/37] lmdb: clear new db after unsuccessful migration, handle read error on migration to interrupt process --- store/src/lmdb.rs | 46 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index 75cb6d886e..b0526ae28a 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -255,7 +255,15 @@ impl Store { Ok(_) => { let _ = fs::remove_dir_all(&migrate_from); } - Err(e) => error!("DB {} migration error: {:?}", env_name, e), + Err(e) => { + error!("DB {} migration error: {:?}", env_name, e); + match s.clear() { + Ok(_) => {} + Err(e) => { + error!("Can not clear new DB after unsuccessful migration: {:?}", e) + } + } + } } } } @@ -293,24 +301,23 @@ impl Store { let read_from = from_env.read_txn()?; let mut count = 0; for kv in db_from.iter(&read_from)? { - if let Ok((k, v)) = kv { - if k.len() > 1 && k[1] == PREFIX_KEY_SEPARATOR { - let db_name = k.split_at(1).0; - if let Some(db) = self.pre_dbs.get(&db_name[0]) { - let key = k.split_at(2).1; - db.put(&mut write_to, key, &v)?; - count += 1; - } else { - error!("Migration: unknown db key: {}", db_name[0]); - } - } else { - self.def_db.put(&mut write_to, k, &v)?; + let (k, v) = kv?; + if k.len() > 1 && k[1] == PREFIX_KEY_SEPARATOR { + let db_name = k.split_at(1).0; + if let Some(db) = self.pre_dbs.get(&db_name[0]) { + let key = k.split_at(2).1; + db.put(&mut write_to, key, &v)?; count += 1; + } else { + error!("Migration: unknown DB key: {}", db_name[0]); } + } else { + self.def_db.put(&mut write_to, k, &v)?; + count += 1; } } write_to.commit()?; - debug!("Migrated {} records from DB {:?}", count, from_path); + debug!("Migrated {} records from {:?}", count, from_path); Ok(()) } @@ -447,6 +454,17 @@ impl Store { self.set_resize_checking(false); } + /// Clear all data from database environment. + fn clear(&self) -> Result<(), Error> { + let mut w = self.env.write_txn()?; + self.def_db.clear(&mut w)?; + for db in self.pre_dbs.values() { + db.clear(&mut w)?; + } + w.commit()?; + Ok(()) + } + /// Protocol version for the store. pub fn protocol_version(&self) -> ProtocolVersion { self.version From 9f29af8e14fa39d36973bdeadb7f58a1da69e655 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sun, 17 May 2026 13:43:27 +0300 Subject: [PATCH 31/37] store: bring back old key methods to reproduce data migration --- store/src/lib.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/store/src/lib.rs b/store/src/lib.rs index 5814bf6cf5..b2408ca55e 100644 --- a/store/src/lib.rs +++ b/store/src/lib.rs @@ -32,6 +32,39 @@ pub mod pmmr; pub mod prune_list; pub mod types; +const SEP: u8 = b':'; + +use byteorder::{BigEndian, WriteBytesExt}; + +/// Build a db key from a prefix and a byte vector identifier. +pub fn to_key>(prefix: u8, k: K) -> Vec { + let k = k.as_ref(); + let mut res = Vec::with_capacity(k.len() + 2); + res.push(prefix); + res.push(SEP); + res.extend_from_slice(k); + res +} + +/// Build a db key from a prefix and a byte vector identifier and numeric identifier +pub fn to_key_u64>(prefix: u8, k: K, val: u64) -> Vec { + let k = k.as_ref(); + let mut res = Vec::with_capacity(k.len() + 10); + res.push(prefix); + res.push(SEP); + res.extend_from_slice(k); + res.write_u64::(val).unwrap(); + res +} +/// Build a db key from a prefix and a numeric identifier. +pub fn u64_to_key(prefix: u8, val: u64) -> Vec { + let mut res = Vec::with_capacity(10); + res.push(prefix); + res.push(SEP); + res.write_u64::(val).unwrap(); + res +} + pub use crate::lmdb::*; use std::ffi::OsStr; From 2dd726f17b07b348060131e63d1a1bd605bc759c Mon Sep 17 00:00:00 2001 From: ardocrat Date: Mon, 18 May 2026 12:48:17 +0300 Subject: [PATCH 32/37] lmdb: return an error on unsuccessful migration --- store/src/lmdb.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index b0526ae28a..2f0452e4e0 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -84,7 +84,7 @@ where const DEFAULT_DB_VERSION: ProtocolVersion = ProtocolVersion(3); /// Default environment. -const DEFAULT_ENV_NAME: &'static str = "lmdb"; +pub const DEFAULT_ENV_NAME: &'static str = "lmdb"; /// Default multi-database environment without prefixes. const DEFAULT_MULTI_DB_ENV_NAME: &'static str = "multi_lmdb"; /// Prefix key separator. @@ -263,6 +263,7 @@ impl Store { error!("Can not clear new DB after unsuccessful migration: {:?}", e) } } + return Err(e); } } } From 7a387d434b31b19e3e43efaabceddcda9699cf3b Mon Sep 17 00:00:00 2001 From: ardocrat Date: Mon, 18 May 2026 13:22:24 +0300 Subject: [PATCH 33/37] lmdb: migration test, clean data after allocate test --- store/tests/lmdb.rs | 117 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 2 deletions(-) diff --git a/store/tests/lmdb.rs b/store/tests/lmdb.rs index ac0b56f943..797614db8d 100644 --- a/store/tests/lmdb.rs +++ b/store/tests/lmdb.rs @@ -16,9 +16,17 @@ use grin_core as core; use grin_store as store; use grin_util as util; -use crate::core::global; -use crate::core::ser::{self, Readable, Reader, Writeable, Writer}; +use core::global; +use core::ser::{self, Readable, Reader, Writeable, Writer}; +use store::{ + needs_resize, to_key, to_key_u64, Store, ALLOC_CHUNK_SIZE_DEFAULT_TEST, DEFAULT_ENV_NAME, +}; + +use byteorder::WriteBytesExt; +use heed::types::Bytes; +use heed::{Database, Env, EnvOpenOptions, WithoutTls}; use std::fs; +use std::path::Path; const WRITE_CHUNK_SIZE: usize = 20; const TEST_ALLOC_SIZE: usize = store::lmdb::ALLOC_CHUNK_SIZE_DEFAULT / 8 / WRITE_CHUNK_SIZE; @@ -168,5 +176,110 @@ fn lmdb_allocate() -> Result<(), store::Error> { } } + clean_output_dir(test_dir); + Ok(()) +} + +fn create_old_db( + test_dir: &str, +) -> Result<(Database, Env), store::Error> { + let env_name = DEFAULT_ENV_NAME; + let alloc_chunk_size = ALLOC_CHUNK_SIZE_DEFAULT_TEST; + let full_path = Path::new(test_dir) + .join(env_name) + .to_str() + .unwrap() + .to_string(); + let _ = fs::create_dir_all(&full_path); + + let env = unsafe { + let mut options = EnvOpenOptions::new().read_txn_without_tls(); + let env_options = options.map_size(alloc_chunk_size).max_dbs(1); + env_options.open(&full_path)? + }; + let (resize, new_size) = needs_resize(&env, alloc_chunk_size); + if resize { + unsafe { + env.resize(new_size)?; + }; + } + + let mut write = env.write_txn()?; + let db: Database = env.create_database(&mut write, Some(env_name))?; + write.commit()?; + + Ok((db, env)) +} + +#[test] +fn test_migration() -> Result<(), store::Error> { + let test_dir = "target/test_migration"; + setup(test_dir); + + let test_prefix_1 = b'H'; + let test_key_1 = [0, 1, 2, 4]; + let test_data_1 = [1, 2, 3, 4]; + + let test_prefix_2 = b'G'; + let test_key_2 = [3, 4, 5, 6]; + let test_key_64_2 = 65480464; + let test_data_2 = [4, 5, 6, 7]; + + let test_key_3 = [6, 7, 8, 9]; + let test_data_3 = [7, 8, 9, 10]; + + // Create old db and fill the data. + { + let (old_db, old_env) = create_old_db(test_dir)?; + let mut w = old_env.write_txn()?; + + // Create old format key value. + let key_1 = to_key(test_prefix_1, test_key_1); + old_db.put(&mut w, key_1.as_slice(), test_data_1.as_slice())?; + + // Create old format 64 key value. + let key_2 = to_key_u64(test_prefix_2, test_key_2, test_key_64_2); + old_db.put(&mut w, key_2.as_slice(), test_data_2.as_slice())?; + + // Create key value without prefix. + old_db.put(&mut w, test_key_3.as_slice(), test_data_3.as_slice())?; + + w.commit()?; + } + + // Create new store to migrate data. + let store = Store::new( + test_dir, + None, + Some(DEFAULT_ENV_NAME), + vec![test_prefix_1, test_prefix_2], + None, + )?; + + // Check we can see key value. + { + assert!(store.exists(Some(test_prefix_1), &test_key_1)?); + let data = store.get_ser::>(Some(test_prefix_1), &test_key_1, None)?; + assert_eq!(data, Some(test_data_1.to_vec())); + } + + // Check we can see key 64 value. + { + let mut key = test_key_2.to_vec(); + key.write_u64::(test_key_64_2) + .unwrap(); + assert!(store.exists(Some(test_prefix_2), &key)?); + let data = store.get_ser::>(Some(test_prefix_2), &key, None)?; + assert_eq!(data, Some(test_data_2.to_vec())); + } + + // Check we can see key value without prefix. + { + assert!(store.exists(None, &test_key_3)?); + let data = store.get_ser::>(None, &test_key_3, None)?; + assert_eq!(data, Some(test_data_3.to_vec())); + } + + clean_output_dir(test_dir); Ok(()) } From dedaea205f455fd910e7e1f702a89f87283f8abf Mon Sep 17 00:00:00 2001 From: ardocrat Date: Tue, 19 May 2026 21:50:37 +0300 Subject: [PATCH 34/37] lmdb: info migration log --- store/src/lmdb.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index 2f0452e4e0..ee4c977f06 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -278,7 +278,7 @@ impl Store { from_name: Option<&str>, from_path: &Path, ) -> Result<(), Error> { - debug!("Migrating DB {:?}", from_path); + info!("Migrating DB {:?}, please wait...", from_path); let from_env = unsafe { let mut options = EnvOpenOptions::new().read_txn_without_tls(); let env_options = options.map_size(self.alloc_chunk_size).max_dbs(24); @@ -318,7 +318,7 @@ impl Store { } } write_to.commit()?; - debug!("Migrated {} records from {:?}", count, from_path); + info!("Migrated {} records from {:?}", count, from_path); Ok(()) } From e9e38bd88c39a71a81a68a6959df840c4469925a Mon Sep 17 00:00:00 2001 From: ardocrat Date: Mon, 25 May 2026 17:27:12 +0300 Subject: [PATCH 35/37] fix: move iterator before handling an error to allow skip bad value --- store/src/lmdb.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index ee4c977f06..f213e0ac5c 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -811,15 +811,13 @@ where fn next(&mut self) -> Option { if let Some(k) = self.keys.iter().skip(self.skip_cur).next() { + self.skip_total += 1; + self.skip_cur += 1; match self.db.get(&self.read, k) { Ok(v) => { if let Some(v) = v { return match (self.deserialize)(k, v) { - Ok(v) => { - self.skip_total += 1; - self.skip_cur += 1; - Some(Ok(v)) - } + Ok(v) => Some(Ok(v)), Err(e) => { error!("db iter: error deserializing: {}", e); Some(Err(Error::from(e))) From aca71f7aaec5ad13fef80bc9525b0f6f03ad2ff7 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Mon, 25 May 2026 17:58:31 +0300 Subject: [PATCH 36/37] lmdb: return an error if removal of old DB file failed after migration --- store/src/lmdb.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index f213e0ac5c..79f1193c2a 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -252,9 +252,15 @@ impl Store { let migrate_from = Path::new(root_path).join(env_name); if migrate_from.exists() { match s.migrate_to_default_env(db_name, &migrate_from) { - Ok(_) => { - let _ = fs::remove_dir_all(&migrate_from); - } + Ok(_) => match fs::remove_dir_all(&migrate_from) { + Ok(_) => {} + Err(e) => { + return Err(Error::FileErr(format!( + "Can not remove old DB file: {:?}", + e + ))); + } + }, Err(e) => { error!("DB {} migration error: {:?}", env_name, e); match s.clear() { From 6d10beaaa27b36d95702c577342476e6bc1bbf56 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Mon, 25 May 2026 21:48:05 +0300 Subject: [PATCH 37/37] lmdb: lifetime for iterator, use write transaction at batch iterator --- chain/src/chain.rs | 15 ++++++---- chain/src/linked_list.rs | 26 ++++++++++++---- chain/src/store.rs | 10 +++---- chain/src/txhashset/txhashset.rs | 10 +++++-- p2p/src/store.rs | 4 ++- store/src/lmdb.rs | 51 ++++++++++++++++++++++++-------- store/tests/lmdb.rs | 11 ++++--- 7 files changed, 88 insertions(+), 39 deletions(-) diff --git a/chain/src/chain.rs b/chain/src/chain.rs index 13325fadfd..11057398dd 100644 --- a/chain/src/chain.rs +++ b/chain/src/chain.rs @@ -1293,19 +1293,24 @@ impl Chain { return Ok(()); } - let mut count = 0; let tail_hash = header_pmmr.get_header_hash_by_height(head.height - horizon)?; let tail = batch.get_block_header(&tail_hash)?; - // Remove old blocks (including short lived fork blocks) which height < tail.height - for block in batch.blocks_iter()? { + // Remove old blocks (including short-lived fork blocks) which height < tail.height + let mut blocks_to_delete = vec![]; + let iter = batch.blocks_iter()?; + for block in iter { if let Ok(block) = block { if block.header.height < tail.height { - let _ = batch.delete_block(&block.hash()); - count += 1; + blocks_to_delete.push(block.hash()); } } } + let mut count = 0; + for bh in blocks_to_delete { + let _ = batch.delete_block(&bh); + count += 1; + } batch.save_body_tail(&Tip::from_header(&tail))?; diff --git a/chain/src/linked_list.rs b/chain/src/linked_list.rs index 11b0a1145e..40b9b36b38 100644 --- a/chain/src/linked_list.rs +++ b/chain/src/linked_list.rs @@ -405,22 +405,36 @@ impl RewindableListIndex for MultiIndex { impl PruneableListIndex for MultiIndex { fn clear(&self, batch: &mut Batch<'_>) -> Result<(), Error> { - let mut list_count = 0; - let mut entry_count = 0; + let mut lists_to_delete = vec![]; let list_db_key = Some(self.list_prefix); for key in batch.db.iter(list_db_key, |k, _| Ok(k.to_vec()))? { if let Ok(key) = key { - let _ = batch.delete(list_db_key, &key); - list_count += 1; + lists_to_delete.push(key); + } + } + let mut list_count = 0; + for l in lists_to_delete { + match batch.delete(list_db_key, &l) { + Ok(_) => list_count += 1, + Err(_) => {} } } + + let mut entries_to_delete = vec![]; let entry_db_key = Some(self.entry_prefix); for key in batch.db.iter(entry_db_key, |k, _| Ok(k.to_vec()))? { if let Ok(key) = key { - let _ = batch.delete(entry_db_key, &key); - entry_count += 1; + entries_to_delete.push(key); } } + let mut entry_count = 0; + for e in entries_to_delete { + match batch.delete(entry_db_key, &e) { + Ok(_) => entry_count += 1, + Err(_) => {} + } + } + debug!( "clear: lists deleted: {}, entries deleted: {}", list_count, entry_count diff --git a/chain/src/store.rs b/chain/src/store.rs index e5ed2c600b..d8df65f2c2 100644 --- a/chain/src/store.rs +++ b/chain/src/store.rs @@ -340,8 +340,8 @@ impl<'a> Batch<'a> { /// Iterator over the output_pos index. pub fn output_pos_iter( - &self, - ) -> Result, CommitPos), Error>>, Error> { + &'a self, + ) -> Result, CommitPos), Error>> + 'a, Error> { let protocol_version = self.db.protocol_version(); self.db.iter(Some(OUTPUT_POS_PREFIX), move |k, mut v| { ser::deserialize(&mut v, protocol_version, DeserializationMode::default()) @@ -461,7 +461,7 @@ impl<'a> Batch<'a> { /// Iterator over all full blocks in the db. /// Uses default db serialization strategy via db protocol version. - pub fn blocks_iter(&self) -> Result> + 'a, Error> { + pub fn blocks_iter(&'a self) -> Result> + 'a, Error> { let protocol_version = self.db.protocol_version(); self.db.iter(Some(BLOCK_PREFIX), move |_, mut v| { ser::deserialize(&mut v, protocol_version, DeserializationMode::default()) @@ -472,8 +472,8 @@ impl<'a> Batch<'a> { /// Iterator over raw data for full blocks in the db. /// Used during block migration (we need flexibility around deserialization). pub fn blocks_raw_iter( - &self, - ) -> Result, Vec), Error>>, Error> { + &'a self, + ) -> Result, Vec), Error>> + 'a, Error> { self.db .iter(Some(BLOCK_PREFIX), |k, v| Ok((k.to_vec(), v.to_vec()))) } diff --git a/chain/src/txhashset/txhashset.rs b/chain/src/txhashset/txhashset.rs index 8155a54739..5d88cd290d 100644 --- a/chain/src/txhashset/txhashset.rs +++ b/chain/src/txhashset/txhashset.rs @@ -643,7 +643,7 @@ impl TxHashSet { // Iterate over the current output_pos index, removing any entries that // do not point to to the expected output. - let mut removed_count = 0; + let mut pos_to_delete = vec![]; for kp in batch.output_pos_iter()? { if let Ok((key, pos1)) = kp { let pos0 = pos1.pos - 1; @@ -658,10 +658,14 @@ impl TxHashSet { } } } - batch.delete(Some(store::OUTPUT_POS_PREFIX), &key)?; - removed_count += 1; + pos_to_delete.push(key); } } + let mut removed_count = 0; + for p in pos_to_delete { + batch.delete(Some(store::OUTPUT_POS_PREFIX), &p)?; + removed_count += 1; + } debug!( "init_output_pos_index: removed {} stale index entries", removed_count diff --git a/p2p/src/store.rs b/p2p/src/store.rs index fe404e05c1..39018ad536 100644 --- a/p2p/src/store.rs +++ b/p2p/src/store.rs @@ -195,7 +195,9 @@ pub struct PeersIterBatch<'a> { impl<'a> PeersIterBatch<'a> { /// Iterator over all known peers. - pub fn peers_iter(&self) -> Result>, Error> { + pub fn peers_iter( + &'a self, + ) -> Result> + 'a, Error> { let protocol_version = self.db.protocol_version(); self.db.iter(Some(PEER_PREFIX), move |_, mut v| { ser::deserialize(&mut v, protocol_version, DeserializationMode::default()) diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index 79f1193c2a..9e8fd4cc79 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -316,7 +316,7 @@ impl Store { db.put(&mut write_to, key, &v)?; count += 1; } else { - error!("Migration: unknown DB key: {}", db_name[0]); + warn!("Migration: unknown DB key: {}", db_name[0]); } } else { self.def_db.put(&mut write_to, k, &v)?; @@ -566,11 +566,11 @@ impl Store { } /// Produces an iterator from the provided database key. - pub fn iter( + pub fn iter<'a, F, T>( &self, db_key: Option, deserialize: F, - ) -> Result, Error> + ) -> Result, Error> where F: Fn(&[u8], &[u8]) -> Result, { @@ -668,7 +668,8 @@ impl<'a> Batch<'a> { /// Writes a single key/value pair to the provided database key. pub fn put(&mut self, db_key: Option, key: &[u8], value: &[u8]) -> Result<(), Error> { let db = self.store.get_db(db_key)?; - db.put(&mut self.write, key, value)?; + let w = &mut self.write; + db.put(w, key, value)?; Ok(()) } @@ -730,14 +731,38 @@ impl<'a> Batch<'a> { /// Produces an iterator from the provided database key. pub fn iter( - &self, + &'a self, db_key: Option, deserialize: F, - ) -> Result, Error> + ) -> Result, Error> where F: Fn(&[u8], &[u8]) -> Result, { - self.store.iter(db_key, deserialize) + self.store.wait_for_resize(); + + TxCounter::on_change_tx_count(&self.store.env_path, true); + let read = self.write.nested_read_txn(); + match read { + Ok(read) => { + let db_res = self.store.get_db(db_key); + match db_res { + Ok(db) => Ok(DatabaseIterator::new( + self.store, + Arc::new(db.clone()), + read, + deserialize, + )), + Err(e) => { + TxCounter::on_change_tx_count(&self.store.env_path, false); + Err(Error::from(e)) + } + } + } + Err(e) => { + TxCounter::on_change_tx_count(&self.store.env_path, false); + Err(Error::from(e)) + } + } } /// Gets a `Readable` value from the database by provided key and deserialization strategy. @@ -794,12 +819,12 @@ impl<'a> Batch<'a> { /// An iterator based on database key. /// Caller is responsible for deserialization of the data. -pub struct DatabaseIterator +pub struct DatabaseIterator<'a, F, T> where F: Fn(&[u8], &[u8]) -> Result, { db: Arc>, - read: Arc>, + read: Arc>, keys: Vec>, total_keys: usize, skip_cur: usize, @@ -809,7 +834,7 @@ where tx_counter: TxCounter, } -impl Iterator for DatabaseIterator +impl Iterator for DatabaseIterator<'_, F, T> where F: Fn(&[u8], &[u8]) -> Result, { @@ -857,7 +882,7 @@ where } } -impl DatabaseIterator +impl<'a, F, T> DatabaseIterator<'a, F, T> where F: Fn(&[u8], &[u8]) -> Result, { @@ -865,9 +890,9 @@ where pub fn new( store: &Store, db: Arc>, - read: RoTxn<'static, WithoutTls>, + read: RoTxn<'a, WithoutTls>, deserialize: F, - ) -> DatabaseIterator { + ) -> DatabaseIterator<'a, F, T> { let (keys, total_keys) = if let Ok(iter) = db.iter(&read) { let total = iter.move_between_keys().count(); let keys = if let Ok(iter) = db.iter(&read) { diff --git a/store/tests/lmdb.rs b/store/tests/lmdb.rs index 797614db8d..7aebb52809 100644 --- a/store/tests/lmdb.rs +++ b/store/tests/lmdb.rs @@ -118,13 +118,12 @@ fn test_iter() -> Result<(), store::Error> { let mut batch = store.batch()?; batch.put(Some(prefix), &key, &value)?; - // TODO - This is not currently possible (and we need to be aware of this). - // Currently our SerIterator is limited to using a ReadTransaction only. - // // Check we can see the new entry via an iterator using the uncommitted batch. - // let mut iter: SerIterator> = batch.iter(&[0])?; - // assert_eq!(iter.next(), Some((key.to_vec(), value.to_vec()))); - // assert_eq!(iter.next(), None); + { + let mut iter = batch.iter(Some(prefix), |_, v| Ok(v.to_vec()))?; + assert_eq!(iter.next(), Some(Ok(value.to_vec()))); + assert_eq!(iter.next(), None); + } // Check we can not yet see the new entry via an iterator outside the uncommitted batch. let mut iter = store.iter(Some(prefix), |_, v| Ok(v.to_vec()))?;