diff --git a/storage/blockchain/src/free.rs b/storage/blockchain/src/free.rs index 8288e65f7..5262aa2a2 100644 --- a/storage/blockchain/src/free.rs +++ b/storage/blockchain/src/free.rs @@ -41,6 +41,7 @@ pub fn open(config: Config) -> Result { RuntimeError::KeyExists | RuntimeError::KeyNotFound | RuntimeError::ResizeNeeded + | RuntimeError::ResizedByAnotherProcess | RuntimeError::TableNotFound => unreachable!(), } } diff --git a/storage/database/src/backend/heed/env.rs b/storage/database/src/backend/heed/env.rs index 4c85bc763..167a7a090 100644 --- a/storage/database/src/backend/heed/env.rs +++ b/storage/database/src/backend/heed/env.rs @@ -206,12 +206,11 @@ impl Env for ConcreteEnv { Ok(self.env.read().unwrap().force_sync()?) } - fn resize_map(&self, resize_algorithm: Option) -> NonZeroUsize { - let resize_algorithm = resize_algorithm.unwrap_or_else(|| self.config().resize_algorithm); - - let current_size_bytes = self.current_map_size(); - let new_size_bytes = resize_algorithm.resize(current_size_bytes); - + fn resize_map( + &self, + resize_algorithm: Option, + resized_by_another_process: bool, + ) -> NonZeroUsize { // SAFETY: // Resizing requires that we have // exclusive access to the database environment. @@ -219,16 +218,30 @@ impl Env for ConcreteEnv { // and we have a WriteGuard to it, so we're safe. // // - unsafe { - // INVARIANT: `resize()` returns a valid `usize` to resize to. - self.env - .write() - .unwrap() - .resize(new_size_bytes.get()) - .unwrap(); - } + let resize = |new_size_bytes| unsafe { + self.env.write().unwrap().resize(new_size_bytes).unwrap(); + }; + + if resized_by_another_process { + // > If the mapsize is increased by another process, + // > and data has grown beyond the range of the current mapsize, + // > mdb_txn_begin() will return MDB_MAP_RESIZED. + // > + // > This function may be called with a size of zero to adopt the new size. + // + // + resize(0); + NonZeroUsize::new(self.current_map_size()).unwrap() + } else { + let resize_algorithm = + resize_algorithm.unwrap_or_else(|| self.config().resize_algorithm); - new_size_bytes + let current_size_bytes = self.current_map_size(); + + let x = resize_algorithm.resize(current_size_bytes); + resize(x.get()); + x + } } #[inline] diff --git a/storage/database/src/backend/heed/error.rs b/storage/database/src/backend/heed/error.rs index 75f11cb16..79326dd27 100644 --- a/storage/database/src/backend/heed/error.rs +++ b/storage/database/src/backend/heed/error.rs @@ -76,6 +76,7 @@ impl From for crate::RuntimeError { E2::KeyExist => Self::KeyExists, E2::NotFound => Self::KeyNotFound, E2::MapFull => Self::ResizeNeeded, + E2::MapResized => Self::ResizedByAnotherProcess, // Corruption errors, these have special panic messages. // @@ -104,18 +105,6 @@ impl From for crate::RuntimeError { // of being errors we can't control, these are errors // that only happen if we write incorrect code. - // "Database contents grew beyond environment mapsize." - // We should be resizing the map when needed, this error - // occurring indicates we did _not_ do that, which is a bug - // and we should panic. - // - // FIXME: This can also mean _another_ process wrote to our - // LMDB file and increased the size. I don't think we need to accommodate for this. - // - // Although `monerod` reacts to that instead of `MDB_MAP_FULL` - // which is what `mdb_put()` returns so... idk? - // - | E2::MapResized // We should be setting `heed::EnvOpenOptions::max_readers()` // with our reader thread value in [`crate::config::Config`], // thus this error should never occur. diff --git a/storage/database/src/backend/tests.rs b/storage/database/src/backend/tests.rs index 93454bf71..c80ed80ac 100644 --- a/storage/database/src/backend/tests.rs +++ b/storage/database/src/backend/tests.rs @@ -128,7 +128,7 @@ fn resize() { // Resize by the OS page size. let page_size = *crate::resize::PAGE_SIZE; let old_size = env.current_map_size(); - env.resize_map(Some(ResizeAlgorithm::FixedBytes(page_size))); + env.resize_map(Some(ResizeAlgorithm::FixedBytes(page_size)), false); // Assert it resized exactly by the OS page size. let new_size = env.current_map_size(); @@ -143,7 +143,7 @@ fn non_manual_resize_1() { unreachable!(); } let (env, _tempdir) = tmp_concrete_env(); - env.resize_map(None); + env.resize_map(None, false); } #[test] diff --git a/storage/database/src/env.rs b/storage/database/src/env.rs index 4420b2ff2..202369d63 100644 --- a/storage/database/src/env.rs +++ b/storage/database/src/env.rs @@ -114,16 +114,22 @@ pub trait Env: Sized { /// /// By default, this function will use the `ResizeAlgorithm` in [`Env::config`]. /// - /// If `resize_algorithm` is `Some`, that will be used instead. - /// - /// This function returns the _new_ memory map size in bytes. + /// - If `resized_by_another_process` is `true`, `0` will + /// be passed to the internal resize function, see: + // + /// - If `resize_algorithm` is `Some`, that will be used instead + /// - This function returns the _new_ memory map size in bytes /// /// # Invariant /// This function _must_ be re-implemented if [`Env::MANUAL_RESIZE`] is `true`. /// /// Otherwise, this function will panic with `unreachable!()`. #[expect(unused_variables)] - fn resize_map(&self, resize_algorithm: Option) -> NonZeroUsize { + fn resize_map( + &self, + resize_algorithm: Option, + resized_by_another_process: bool, + ) -> NonZeroUsize { unreachable!() } diff --git a/storage/database/src/error.rs b/storage/database/src/error.rs index 35238cd9a..7ce597f1b 100644 --- a/storage/database/src/error.rs +++ b/storage/database/src/error.rs @@ -92,6 +92,16 @@ pub enum RuntimeError { #[error("database memory map must be resized")] ResizeNeeded, + /// Another process resized the database. + /// + /// This error will continue to surface if the `heed` + /// backend is used and another process resized the database. + /// + /// To fix this, [`Env::resize_map`](crate::Env::resize_map) + /// must be called with `resized_by_another_process: true`. + #[error("database memory map resized by another process")] + ResizedByAnotherProcess, + /// The given table did not exist in the database. #[error("database table did not exist")] TableNotFound, diff --git a/storage/service/src/service/write.rs b/storage/service/src/service/write.rs index e6f4d9284..a752e745d 100644 --- a/storage/service/src/service/write.rs +++ b/storage/service/src/service/write.rs @@ -134,6 +134,12 @@ fn database_writer( // this won't have to be an enum. let response = inner_handler(env, &request); + if ConcreteEnv::MANUAL_RESIZE + && matches!(response, Err(RuntimeError::ResizedByAnotherProcess)) + { + panic!("Database was resized by another process - no other process should write to `cuprated`'s database."); + } + // If the database needs to resize, do so. if ConcreteEnv::MANUAL_RESIZE && matches!(response, Err(RuntimeError::ResizeNeeded)) { // If this is the last iteration of the outer `for` loop and we @@ -151,7 +157,7 @@ fn database_writer( // add that much instead of the default 1GB. // let old = env.current_map_size(); - let new = env.resize_map(None).get(); + let new = env.resize_map(None, false).get(); const fn bytes_to_megabytes(bytes: usize) -> usize { bytes / 1_000_000 diff --git a/storage/txpool/src/free.rs b/storage/txpool/src/free.rs index affeb80c5..21d3dfe98 100644 --- a/storage/txpool/src/free.rs +++ b/storage/txpool/src/free.rs @@ -53,9 +53,10 @@ pub fn open(config: &Config) -> Result { RuntimeError::KeyNotFound => InitError::InvalidVersion, // These errors shouldn't be happening here. - RuntimeError::KeyExists | RuntimeError::ResizeNeeded | RuntimeError::TableNotFound => { - unreachable!() - } + RuntimeError::KeyExists + | RuntimeError::ResizeNeeded + | RuntimeError::ResizedByAnotherProcess + | RuntimeError::TableNotFound => unreachable!(), } }