diff --git a/crates/ecstore/src/set_disk/core/io_primitives.rs b/crates/ecstore/src/set_disk/core/io_primitives.rs index 20e0924d3..bd9e1ee29 100644 --- a/crates/ecstore/src/set_disk/core/io_primitives.rs +++ b/crates/ecstore/src/set_disk/core/io_primitives.rs @@ -538,7 +538,7 @@ impl MetadataQuorumAccumulator { } pub(in crate::set_disk) fn default_write_quorum(&self) -> usize { - if self.default_parity_count == 0 { + if self.default_parity_count == 0 || self.default_parity_count >= self.total_disks { return self.total_disks; } let data_blocks = self.total_disks.saturating_sub(self.default_parity_count); @@ -550,7 +550,7 @@ impl MetadataQuorumAccumulator { } pub(in crate::set_disk) fn missing_response_quorum(&self) -> usize { - if self.default_parity_count == 0 { + if self.default_parity_count == 0 || self.default_parity_count >= self.total_disks { self.total_disks } else { self.total_disks / 2 @@ -5376,7 +5376,7 @@ mod tests { temp_env::async_with_vars( [ ("RUSTFS_GET_METADATA_EARLY_STOP_ENABLE", Some("true")), - ("RUSTFS_GET_METADATA_DATA_READ_EARLY_STOP_ENABLE", None), + ("RUSTFS_GET_METADATA_DATA_READ_EARLY_STOP_ENABLE", Some("false")), ("RUSTFS_GET_METADATA_EARLY_STOP_BOUNDED_FANOUT", Some("true")), ], async { @@ -5389,7 +5389,7 @@ mod tests { assert_eq!( calls.total(disk_call_counters::KIND_READ_VERSION), DISKS as u64, - "control path should keep the default data-read full fanout" + "control path should keep full fanout when data-read early stop is explicitly disabled" ); assert_eq!(diagnostics.total_responses(), DISKS); }, @@ -5434,6 +5434,42 @@ mod tests { drop(dirs); } + #[tokio::test] + async fn bounded_metadata_early_stop_defaults_keep_data_get_full_fanout() { + const DISKS: usize = 4; + let bucket = "bounded-data-get-default-bucket"; + let object = "bounded-data-get-default-object"; + let (dirs, disks) = call_counter_local_disks(bucket, DISKS).await; + install_metadata_fanout_fileinfo(&disks, bucket, object, None).await; + + temp_env::async_with_vars( + [ + ("RUSTFS_GET_METADATA_EARLY_STOP_ENABLE", None::<&str>), + ("RUSTFS_GET_METADATA_DATA_READ_EARLY_STOP_ENABLE", None::<&str>), + ("RUSTFS_GET_METADATA_EARLY_STOP_BOUNDED_FANOUT", None::<&str>), + ], + async { + let calls = disk_call_counters::observe(object); + let (parts_metadata, errs, diagnostics) = + SetDisks::read_all_fileinfo_observed(&disks, bucket, bucket, object, "", true, false, false, true, 2) + .await + .expect("default data-read metadata should resolve"); + + assert_eq!( + calls.total(disk_call_counters::KIND_READ_VERSION), + DISKS as u64, + "default GET data-read metadata must keep full fanout for read-failure tolerance" + ); + assert_eq!(diagnostics.total_responses(), DISKS); + assert_eq!(parts_metadata.iter().filter(|fi| fi.name == object).count(), DISKS); + assert!(errs.iter().all(Option::is_none)); + }, + ) + .await; + + drop(dirs); + } + #[tokio::test] async fn bounded_metadata_early_stop_falls_back_to_full_fanout_on_data_read_error() { const DISKS: usize = 4; @@ -6009,6 +6045,16 @@ mod tests { assert_eq!(accumulator.candidate_latest_quorum(&impossible_parity), None); } + #[test] + fn metadata_quorum_accumulator_treats_invalid_default_parity_as_full_fanout() { + let accumulator = MetadataQuorumAccumulator::new(2, 2, true); + + assert_eq!(accumulator.default_write_quorum(), 2); + assert_eq!(accumulator.missing_response_quorum(), 2); + assert!(accumulator.can_still_reach_early_stop_with_pending(2)); + assert!(!accumulator.can_still_reach_early_stop_with_pending(1)); + } + #[test] fn confirmed_missing_part_error_recognizes_legacy_and_s3_markers() { assert!(!is_confirmed_missing_part_error(None)); diff --git a/crates/ecstore/src/set_disk/mod.rs b/crates/ecstore/src/set_disk/mod.rs index 0d1cea6e8..d25780de3 100644 --- a/crates/ecstore/src/set_disk/mod.rs +++ b/crates/ecstore/src/set_disk/mod.rs @@ -672,10 +672,10 @@ const DEFAULT_RUSTFS_GET_SMALL_OBJECT_DIRECT_MEMORY_THRESHOLD: usize = 128 * 102 const ENV_RUSTFS_GET_METADATA_EARLY_STOP_ENABLE: &str = "RUSTFS_GET_METADATA_EARLY_STOP_ENABLE"; // Enabled by default (backlog#872): the early-stop path only engages for -// requests `should_allow_metadata_early_stop` classifies as safe (metadata-only -// reads by default, without version_id / healing / free-version needs) and -// still requires a full read-quorum agreement before stopping. Set the env var -// to `false` to fall back to full-wait metadata fanout. +// requests `should_allow_metadata_early_stop` classifies as safe (latest-version +// metadata-only reads by default, without version_id / healing / free-version +// needs) and still requires a full read-quorum agreement before stopping. Set +// the env var to `false` to fall back to full-wait metadata fanout. const DEFAULT_RUSTFS_GET_METADATA_EARLY_STOP_ENABLE: bool = true; const ENV_RUSTFS_GET_METADATA_EARLY_STOP_ROLLOUT_PCT: &str = "RUSTFS_GET_METADATA_EARLY_STOP_ROLLOUT_PCT"; @@ -836,10 +836,10 @@ mod prepared_get_object_metadata_tests { .prepare_get_object_metadata(bucket, object, &opts) .await .expect("prepared metadata should resolve"); + let prepared_calls = calls.total(disk_call_counters::KIND_READ_VERSION); assert_eq!( - calls.total(disk_call_counters::KIND_READ_VERSION), - 4, - "preparation should fan out to each online disk exactly once" + prepared_calls, 3, + "default prepared GET metadata should stop after the 2+2 read/write quorum" ); let mut reader = set_disks @@ -864,7 +864,7 @@ mod prepared_get_object_metadata_tests { ); assert_eq!( calls.total(disk_call_counters::KIND_READ_VERSION), - 4, + 3, "reader construction must consume prepared metadata instead of repeating the fanout" ); } diff --git a/crates/ecstore/src/set_disk/read.rs b/crates/ecstore/src/set_disk/read.rs index 89dfa9640..ae7461f75 100644 --- a/crates/ecstore/src/set_disk/read.rs +++ b/crates/ecstore/src/set_disk/read.rs @@ -3834,25 +3834,24 @@ mod tests { assert!(metadata_early_stop_permitted(true, true, false, "", false, false)); // observe=false (non-observed fanout) also disables early-stop. assert!(!metadata_early_stop_permitted(true, false, false, "", false, false)); - // Data reads require their own explicit rollout gate. assert!(!metadata_early_stop_permitted(true, true, true, "", false, false)); }, ); } #[test] - fn metadata_early_stop_requires_explicit_data_read_opt_in() { + fn metadata_early_stop_keeps_data_reads_opt_in_by_default() { temp_env::with_vars( [ (ENV_RUSTFS_GET_METADATA_EARLY_STOP_ENABLE, Some("true")), - (ENV_RUSTFS_GET_METADATA_VERSION_EARLY_STOP_ENABLE, Some("true")), + (ENV_RUSTFS_GET_METADATA_VERSION_EARLY_STOP_ENABLE, None), (ENV_RUSTFS_GET_METADATA_DATA_READ_EARLY_STOP_ENABLE, None), ], || { assert!(!should_allow_metadata_early_stop(true, "", false, false)); assert!(!should_allow_metadata_early_stop(true, "version-id", false, false)); assert!(should_allow_metadata_early_stop(false, "", false, false)); - assert!(should_allow_metadata_early_stop(false, "version-id", false, false)); + assert!(!should_allow_metadata_early_stop(false, "version-id", false, false)); }, ); temp_env::with_vars( @@ -3866,6 +3865,19 @@ mod tests { assert!(should_allow_metadata_early_stop(true, "version-id", false, false)); }, ); + temp_env::with_vars( + [ + (ENV_RUSTFS_GET_METADATA_EARLY_STOP_ENABLE, Some("true")), + (ENV_RUSTFS_GET_METADATA_VERSION_EARLY_STOP_ENABLE, Some("true")), + (ENV_RUSTFS_GET_METADATA_DATA_READ_EARLY_STOP_ENABLE, Some("false")), + ], + || { + assert!(!should_allow_metadata_early_stop(true, "", false, false)); + assert!(!should_allow_metadata_early_stop(true, "version-id", false, false)); + assert!(should_allow_metadata_early_stop(false, "", false, false)); + assert!(should_allow_metadata_early_stop(false, "version-id", false, false)); + }, + ); } #[test]