diff --git a/Cargo.lock b/Cargo.lock index 3fca59ea28..fd3654f746 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1931,7 +1931,7 @@ dependencies = [ "mysten-metrics", "prometheus", "rand 0.8.5", - "rocksdb", + "rocksdb 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde", "serde_with", "sui-macros", @@ -5375,6 +5375,7 @@ dependencies = [ "libc", "libz-sys", "lz4-sys", + "pkg-config", "tikv-jemalloc-sys", "zstd-sys", ] @@ -8935,6 +8936,19 @@ dependencies = [ "byteorder", ] +[[package]] +name = "rocksdb" +version = "0.22.0" +dependencies = [ + "bincode", + "libc", + "librocksdb-sys", + "pretty_assertions", + "serde", + "tempfile", + "trybuild", +] + [[package]] name = "rocksdb" version = "0.22.0" @@ -10410,7 +10424,7 @@ dependencies = [ "prometheus", "prost 0.14.3", "reqwest", - "rocksdb", + "rocksdb 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)", "scoped-futures", "serde", "serde_json", @@ -11505,7 +11519,7 @@ dependencies = [ "prometheus", "prost 0.14.3", "roaring", - "rocksdb", + "rocksdb 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde", "sui-consistent-store", "sui-default-config", @@ -12074,6 +12088,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "target-triple" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" + [[package]] name = "telemetry-subscribers" version = "0.2.0" @@ -12547,10 +12567,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" dependencies = [ + "indexmap 2.14.0", "serde", "serde_spanned 1.0.0", "toml_datetime 0.7.0", "toml_parser", + "toml_writer", "winnow", ] @@ -12601,6 +12623,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + [[package]] name = "tonic" version = "0.12.3" @@ -13012,6 +13040,21 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "trybuild" +version = "1.0.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f614c21bd3a61bad9501d75cbb7686f00386c806d7f456778432c25cf86948a" +dependencies = [ + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml 0.9.5", +] + [[package]] name = "tungstenite" version = "0.26.2" @@ -13067,7 +13110,7 @@ dependencies = [ "mysten-metrics", "once_cell", "prometheus", - "rocksdb", + "rocksdb 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde", "sui-macros", "tap", @@ -13093,7 +13136,7 @@ dependencies = [ "once_cell", "prometheus", "rand 0.8.5", - "rocksdb", + "rocksdb 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)", "rstest", "serde", "sui-macros", @@ -13520,7 +13563,7 @@ dependencies = [ "bcs", "clap", "rand 0.8.5", - "rocksdb", + "rocksdb 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde", "serde_json", "sui-types", @@ -13737,7 +13780,7 @@ dependencies = [ "regex", "reqwest", "ring", - "rocksdb", + "rocksdb 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)", "rustls", "rustls-native-certs", "scoped-futures", @@ -13796,7 +13839,7 @@ dependencies = [ "prometheus", "rand 0.8.5", "reqwest", - "rocksdb", + "rocksdb 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)", "sui-macros", "sui-protocol-config", "sui-rpc-api", diff --git a/crates/typed-store/src/rocks.rs b/crates/typed-store/src/rocks.rs index 1d94127415..8399ec6b4d 100644 --- a/crates/typed-store/src/rocks.rs +++ b/crates/typed-store/src/rocks.rs @@ -77,9 +77,12 @@ const ENV_VAR_DB_WAL_SIZE: &str = "DB_WAL_SIZE_MB"; const DEFAULT_DB_WAL_SIZE: usize = 1024; const ENV_VAR_DB_PARALLELISM: &str = "DB_PARALLELISM"; +const ENV_VAR_DB_PARANOID_FILE_CHECKS: &str = "DB_PARANOID_FILE_CHECKS"; const SLOW_OP_SAMPLED_TRACING_INTERVAL: Duration = Duration::from_secs(60); +const PARANOID_FILE_CHECKS_OPTION: &str = "paranoid_file_checks"; + #[cfg(test)] mod tests; @@ -346,6 +349,43 @@ impl RocksDB { delegate_call!(self.db_options) } + fn apply_paranoid_file_checks_to_cf( + &self, + cf_name: &str, + enabled: bool, + ) -> Result<(), rocksdb::Error> { + if let Some(cf) = self.cf_handle(cf_name) { + // https://github.com/facebook/rocksdb/blob/v8.10.0/include/rocksdb/advanced_options.h + self.set_options_cf( + &cf, + &[( + PARANOID_FILE_CHECKS_OPTION, + if enabled { "true" } else { "false" }, + )], + )?; + } + Ok(()) + } + + fn apply_env_options_to_cf(&self, cf_name: &str) -> Result<(), rocksdb::Error> { + if let Some(enabled) = read_bool_from_env(ENV_VAR_DB_PARANOID_FILE_CHECKS) { + self.apply_paranoid_file_checks_to_cf(cf_name, enabled)?; + } + Ok(()) + } + + fn apply_env_options_to_cfs(&self, cf_names: I) -> Result<(), rocksdb::Error> + where + I: IntoIterator, + { + if let Some(enabled) = read_bool_from_env(ENV_VAR_DB_PARANOID_FILE_CHECKS) { + for cf_name in cf_names { + self.apply_paranoid_file_checks_to_cf(&cf_name, enabled)?; + } + } + Ok(()) + } + /// Get a value from the database. pub fn get>(&self, key: K) -> Result>, rocksdb::Error> { delegate_call!(self.get(key)) @@ -410,7 +450,9 @@ impl RocksDB { name: N, opts: &rocksdb::Options, ) -> Result<(), rocksdb::Error> { - delegate_call!(self.create_cf(name, opts)) + let name = name.as_ref(); + delegate_call!(self.create_cf(name, opts))?; + self.apply_env_options_to_cf(name) } /// Drop a column family. @@ -2195,6 +2237,22 @@ pub fn read_size_from_env(var_name: &str) -> Option { .ok() } +fn read_bool_from_env(var_name: &str) -> Option { + let value = env::var(var_name).ok()?; + match value.trim().to_ascii_lowercase().as_str() { + "1" | "true" | "yes" | "on" => Some(true), + "0" | "false" | "no" | "off" => Some(false), + _ => { + tracing::warn!( + "Env var {} does not contain a valid boolean: {}", + var_name, + value + ); + None + } + } +} + /// The read-write options. #[derive(Clone, Debug)] pub struct ReadWriteOptions { @@ -2403,6 +2461,10 @@ pub fn open_cf_opts>( // This is a no-op in non-simulator builds. let cfs = populate_missing_cfs(opt_cfs, path).map_err(typed_store_err_from_rocks_err)?; + let mut cf_names = cfs.iter().map(|(name, _)| name.clone()).collect::>(); + cf_names.push(rocksdb::DEFAULT_COLUMN_FAMILY_NAME.to_string()); + cf_names.sort_unstable(); + cf_names.dedup(); sui_macros::nondeterministic!({ let options = prepare_db_options(db_options); let rocksdb = { @@ -2414,12 +2476,16 @@ pub fn open_cf_opts>( ) .map_err(typed_store_err_from_rocks_err)? }; - Ok(Arc::new(RocksDB::DB(DBWithThreadModeWrapper::new( + let rocksdb = Arc::new(RocksDB::DB(DBWithThreadModeWrapper::new( rocksdb, metric_conf, PathBuf::from(path), options, - )))) + ))); + rocksdb + .apply_env_options_to_cfs(cf_names) + .map_err(typed_store_err_from_rocks_err)?; + Ok(rocksdb) }) } @@ -2434,20 +2500,26 @@ pub fn open_cf_opts_optimistic>( ) -> Result, TypedStoreError> { let path = path.as_ref(); let cfs = populate_missing_cfs(opt_cfs, path).map_err(typed_store_err_from_rocks_err)?; + let mut cf_names = cfs.iter().map(|(name, _)| name.clone()).collect::>(); + cf_names.push(rocksdb::DEFAULT_COLUMN_FAMILY_NAME.to_string()); + cf_names.sort_unstable(); + cf_names.dedup(); sui_macros::nondeterministic!({ let options = prepare_db_options(db_options); - rocksdb::OptimisticTransactionDB::open_cf_descriptors( + let rocksdb = rocksdb::OptimisticTransactionDB::open_cf_descriptors( &options, path, cfs.into_iter() .map(|(name, opts)| ColumnFamilyDescriptor::new(name, opts)), ) - .map(|db| { - Arc::new(RocksDB::OptimisticTransactionDB( - OptimisticTransactionDBWrapper::new(db, metric_conf, PathBuf::from(path), options), - )) - }) - .map_err(typed_store_err_from_rocks_err) + .map_err(typed_store_err_from_rocks_err)?; + let rocksdb = Arc::new(RocksDB::OptimisticTransactionDB( + OptimisticTransactionDBWrapper::new(rocksdb, metric_conf, PathBuf::from(path), options), + )); + rocksdb + .apply_env_options_to_cfs(cf_names) + .map_err(typed_store_err_from_rocks_err)?; + Ok(rocksdb) }) } diff --git a/crates/typed-store/src/rocks/tests.rs b/crates/typed-store/src/rocks/tests.rs index d1cefda18c..eff0e014a9 100644 --- a/crates/typed-store/src/rocks/tests.rs +++ b/crates/typed-store/src/rocks/tests.rs @@ -805,6 +805,60 @@ fn open_rocksdb>(path: P, opt_cfs: &[&str]) -> Arc { open_cf(path, None, MetricConf::default(), opt_cfs).expect("failed to open rocksdb") } +struct EnvVarGuard { + name: &'static str, + previous: Option, +} + +impl EnvVarGuard { + fn set(name: &'static str, value: &str) -> Self { + let previous = std::env::var(name).ok(); + // SAFETY: typed-store RocksDB tests use a process-wide mutex to serialize env mutation. + unsafe { + std::env::set_var(name, value); + } + Self { name, previous } + } +} + +impl Drop for EnvVarGuard { + fn drop(&mut self) { + // SAFETY: the guard is held while the same process-wide test mutex is held. + unsafe { + if let Some(value) = &self.previous { + std::env::set_var(self.name, value); + } else { + std::env::remove_var(self.name); + } + } + } +} + +#[tokio::test] +async fn paranoid_file_checks_env_applies_to_opened_and_created_column_families() { + let _lock = global_test_lock(); + let _env = EnvVarGuard::set(ENV_VAR_DB_PARANOID_FILE_CHECKS, "true"); + let path = temp_dir(); + let cf_options = rocksdb::Options::default(); + + let rocks = open_cf_opts( + &path, + None, + MetricConf::default(), + &[("existing_cf", cf_options.clone())], + ) + .expect("failed to open rocksdb with env options"); + + rocks + .create_cf("created_cf", &cf_options) + .expect("failed to create column family with env options"); + + assert_eq!( + read_bool_from_env(ENV_VAR_DB_PARANOID_FILE_CHECKS), + Some(true) + ); +} + #[tokio::test] async fn test_sampling() { let sampling_interval = SamplingInterval::new(Duration::ZERO, 10);