diff --git a/crates/config/src/constants/object.rs b/crates/config/src/constants/object.rs index e19dbee9..43fbb15c 100644 --- a/crates/config/src/constants/object.rs +++ b/crates/config/src/constants/object.rs @@ -167,3 +167,16 @@ pub const DEFAULT_OBJECT_CACHE_TTI_SECS: u64 = 120; /// /// Default is set to 5 hits. pub const DEFAULT_OBJECT_HOT_MIN_HITS_TO_EXTEND: usize = 5; + +/// Skip bitrot hash verification on GetObject reads. +/// +/// When enabled, GetObject reads skip the per-shard hash +/// computation and comparison, reducing CPU usage on the read path. +/// The background scanner still performs full integrity verification. +/// Does not affect writes, heals, or scanner operations. +/// +/// Default is false (verify on every read, matching pre-existing behavior). +pub const ENV_OBJECT_GET_SKIP_BITROT_VERIFY: &str = "RUSTFS_OBJECT_GET_SKIP_BITROT_VERIFY"; + +/// Default: bitrot verification is enabled on GetObject reads (do not skip). +pub const DEFAULT_OBJECT_GET_SKIP_BITROT_VERIFY: bool = false; diff --git a/crates/ecstore/src/bitrot.rs b/crates/ecstore/src/bitrot.rs index c02467a7..d575c82d 100644 --- a/crates/ecstore/src/bitrot.rs +++ b/crates/ecstore/src/bitrot.rs @@ -39,6 +39,7 @@ pub async fn create_bitrot_reader( length: usize, shard_size: usize, checksum_algo: HashAlgorithm, + skip_verify: bool, ) -> disk::error::Result>>> { // Calculate the total length to read, including the checksum overhead let length = length.div_ceil(shard_size) * checksum_algo.size() + length; @@ -47,13 +48,18 @@ pub async fn create_bitrot_reader( // Use inline data let mut rd = Cursor::new(data.to_vec()); rd.set_position(offset as u64); - let reader = BitrotReader::new(Box::new(rd) as Box, shard_size, checksum_algo); + let reader = BitrotReader::new( + Box::new(rd) as Box, + shard_size, + checksum_algo, + skip_verify, + ); Ok(Some(reader)) } else if let Some(disk) = disk { // Read from disk match disk.read_file_stream(bucket, path, offset, length - offset).await { Ok(rd) => { - let reader = BitrotReader::new(rd, shard_size, checksum_algo); + let reader = BitrotReader::new(rd, shard_size, checksum_algo, skip_verify); Ok(Some(reader)) } Err(e) => Err(e), @@ -116,7 +122,7 @@ mod tests { let checksum_algo = HashAlgorithm::HighwayHash256; let result = - create_bitrot_reader(Some(test_data), None, "test-bucket", "test-path", 0, 0, shard_size, checksum_algo).await; + create_bitrot_reader(Some(test_data), None, "test-bucket", "test-path", 0, 0, shard_size, checksum_algo, false).await; assert!(result.is_ok()); assert!(result.unwrap().is_some()); @@ -127,7 +133,8 @@ mod tests { let shard_size = 16; let checksum_algo = HashAlgorithm::HighwayHash256; - let result = create_bitrot_reader(None, None, "test-bucket", "test-path", 0, 1024, shard_size, checksum_algo).await; + let result = + create_bitrot_reader(None, None, "test-bucket", "test-path", 0, 1024, shard_size, checksum_algo, false).await; assert!(result.is_ok()); assert!(result.unwrap().is_none()); diff --git a/crates/ecstore/src/erasure_coding/bitrot.rs b/crates/ecstore/src/erasure_coding/bitrot.rs index 13426765..3b11b1d4 100644 --- a/crates/ecstore/src/erasure_coding/bitrot.rs +++ b/crates/ecstore/src/erasure_coding/bitrot.rs @@ -28,10 +28,7 @@ pin_project! { shard_size: usize, buf: Vec, hash_buf: Vec, - // hash_read: usize, - // data_buf: Vec, - // data_read: usize, - // hash_checked: bool, + skip_verify: bool, id: Uuid, } } @@ -41,7 +38,7 @@ where R: AsyncRead + Unpin + Send + Sync, { /// Create a new BitrotReader. - pub fn new(inner: R, shard_size: usize, algo: HashAlgorithm) -> Self { + pub fn new(inner: R, shard_size: usize, algo: HashAlgorithm, skip_verify: bool) -> Self { let hash_size = algo.size(); Self { inner, @@ -49,10 +46,7 @@ where shard_size, buf: Vec::new(), hash_buf: vec![0u8; hash_size], - // hash_read: 0, - // data_buf: Vec::new(), - // data_read: 0, - // hash_checked: false, + skip_verify, id: Uuid::new_v4(), } } @@ -90,7 +84,7 @@ where data_len += n; } - if hash_size > 0 { + if hash_size > 0 && !self.skip_verify { let actual_hash = self.hash_algo.hash_encode(&out[..data_len]); if actual_hash.as_ref() != self.hash_buf.as_slice() { error!("bitrot reader hash mismatch, id={} data_len={}, out_len={}", self.id, data_len, out.len()); @@ -388,7 +382,7 @@ mod tests { // Read let reader = bitrot_writer.into_inner(); let reader = Cursor::new(reader.into_inner()); - let mut bitrot_reader = BitrotReader::new(reader, shard_size, HashAlgorithm::HighwayHash256); + let mut bitrot_reader = BitrotReader::new(reader, shard_size, HashAlgorithm::HighwayHash256, false); let mut out = Vec::new(); let mut n = 0; while n < data_size { @@ -420,7 +414,7 @@ mod tests { let pos = written.len() - 1; written[pos] ^= 0xFF; let reader = Cursor::new(written); - let mut bitrot_reader = BitrotReader::new(reader, shard_size, HashAlgorithm::HighwayHash256); + let mut bitrot_reader = BitrotReader::new(reader, shard_size, HashAlgorithm::HighwayHash256, false); let count = data_size.div_ceil(shard_size); @@ -464,7 +458,7 @@ mod tests { let reader = bitrot_writer.into_inner(); let reader = Cursor::new(reader.into_inner()); - let mut bitrot_reader = BitrotReader::new(reader, shard_size, HashAlgorithm::None); + let mut bitrot_reader = BitrotReader::new(reader, shard_size, HashAlgorithm::None, false); let mut out = Vec::new(); let mut n = 0; while n < data_size { diff --git a/crates/ecstore/src/erasure_coding/decode.rs b/crates/ecstore/src/erasure_coding/decode.rs index 9e0925d8..0e5d03ed 100644 --- a/crates/ecstore/src/erasure_coding/decode.rs +++ b/crates/ecstore/src/erasure_coding/decode.rs @@ -454,6 +454,6 @@ mod tests { } let reader_cursor = Cursor::new(buf); - BitrotReader::new(reader_cursor, shard_size, hash_algo.clone()) + BitrotReader::new(reader_cursor, shard_size, hash_algo.clone(), false) } } diff --git a/crates/ecstore/src/set_disk.rs b/crates/ecstore/src/set_disk.rs index 4b72bf1b..8d86ad56 100644 --- a/crates/ecstore/src/set_disk.rs +++ b/crates/ecstore/src/set_disk.rs @@ -494,6 +494,7 @@ impl ObjectIO for SetDisks { let object = object.to_owned(); let set_index = self.set_index; let pool_index = self.pool_index; + let skip_verify = opts.skip_verify_bitrot; // Move the read-lock guard into the task so it lives for the duration of the read // let _guard_to_hold = _read_lock_guard; // moved into closure below tokio::spawn(async move { @@ -510,6 +511,7 @@ impl ObjectIO for SetDisks { &disks, set_index, pool_index, + skip_verify, ) .await { @@ -1654,6 +1656,7 @@ impl ObjectOperations for SetDisks { let cloned_fi = fi.clone(); let set_index = self.set_index; let pool_index = self.pool_index; + let skip_verify = opts.skip_verify_bitrot; tokio::spawn(async move { if let Err(e) = Self::get_object_with_fileinfo( &cloned_bucket, @@ -1666,6 +1669,7 @@ impl ObjectOperations for SetDisks { &online_disks, set_index, pool_index, + skip_verify, ) .await { diff --git a/crates/ecstore/src/set_disk/heal.rs b/crates/ecstore/src/set_disk/heal.rs index faca6a17..cdc3cb07 100644 --- a/crates/ecstore/src/set_disk/heal.rs +++ b/crates/ecstore/src/set_disk/heal.rs @@ -375,6 +375,7 @@ impl SetDisks { till_offset, erasure.shard_size(), checksum_algo.clone(), + false, ) .await { diff --git a/crates/ecstore/src/set_disk/read.rs b/crates/ecstore/src/set_disk/read.rs index c2cfb74f..be059408 100644 --- a/crates/ecstore/src/set_disk/read.rs +++ b/crates/ecstore/src/set_disk/read.rs @@ -568,6 +568,7 @@ impl SetDisks { disks: &[Option], set_index: usize, pool_index: usize, + skip_verify_bitrot: bool, ) -> Result<()> where W: AsyncWrite + Send + Sync + Unpin + 'static, @@ -659,6 +660,7 @@ impl SetDisks { till_offset, erasure.shard_size(), HashAlgorithm::HighwayHash256, + skip_verify_bitrot, ) .await { diff --git a/crates/ecstore/src/store_api/types.rs b/crates/ecstore/src/store_api/types.rs index 4766f85b..5a41ac1c 100644 --- a/crates/ecstore/src/store_api/types.rs +++ b/crates/ecstore/src/store_api/types.rs @@ -70,6 +70,7 @@ pub struct ObjectOptions { pub eval_metadata: Option>, pub want_checksum: Option, + pub skip_verify_bitrot: bool, } impl ObjectOptions { diff --git a/rustfs/src/storage/options.rs b/rustfs/src/storage/options.rs index a52c7eed..784d887c 100644 --- a/rustfs/src/storage/options.rs +++ b/rustfs/src/storage/options.rs @@ -164,6 +164,13 @@ pub async fn get_opts( opts.version_suspended = version_suspended; opts.versioned = versioned; + // Optionally skip per-shard bitrot hash verification on reads to save CPU. + // Background scanner still performs full integrity checks asynchronously. + opts.skip_verify_bitrot = rustfs_utils::get_env_bool( + rustfs_config::ENV_OBJECT_GET_SKIP_BITROT_VERIFY, + rustfs_config::DEFAULT_OBJECT_GET_SKIP_BITROT_VERIFY, + ); + fill_conditional_writes_opts_from_header(headers, &mut opts)?; Ok(opts)