fix(ecstore): preserve parity reserves for data-only GET (#6888)

fix(ecstore): hedge data-only GET with parity

Route the opt-in data-shards-only lockstep path through the bounded parity race and preserve deferred parity reserves across canceled hedges.

Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
houseme
2026-08-30 20:16:32 +08:00
committed by GitHub
parent 51532e19fb
commit 3d24526704
3 changed files with 103 additions and 65 deletions
@@ -1974,14 +1974,10 @@ pub(in crate::set_disk) fn fill_deferred_bitrot_readers(
return;
}
// Only CopySource uses disposable, stripe-aligned reopeners. Ordinary GET
// readers use the existing deferred handle and should not retain one
// heap-allocated closure (plus cloned path/disk state) for every parity
// slot.
let copy_source_demand_bound = matches!(
crate::set_disk::get_object_read_policy(),
crate::set_disk::GetObjectReadPolicy::CopySource
);
// Every demand-bound lockstep reader needs a disposable, stripe-aligned
// reopener. Otherwise a recovered slow data read can cancel and consume
// the only parity reserve needed by a later degraded stripe.
let demand_bound_lockstep = crate::erasure::coding::decode::get_lockstep_data_shards_only_enabled();
for idx in 0..disks.len() {
if setup.attempted[idx] {
@@ -1996,7 +1992,7 @@ pub(in crate::set_disk) fn fill_deferred_bitrot_readers(
let disk = disks[idx].clone();
let data_dir = files[idx].data_dir.unwrap_or_default();
let path = format!("{object}/{data_dir}/part.{part_number}");
let reopener = copy_source_demand_bound.then(|| {
let reopener = demand_bound_lockstep.then(|| {
deferred_reader_reopener(
inline_data.clone(),
disk.clone(),
@@ -2037,7 +2033,7 @@ pub(in crate::set_disk) fn fill_deferred_bitrot_readers(
// ready/error bookkeeping that quorum decisions rely on is left untouched.
// Gate off (default): keep the eagerly opened parity readers exactly as
// before — the lockstep path reads them on every stripe.
if !crate::erasure::coding::decode::get_lockstep_data_shards_only_enabled() {
if !demand_bound_lockstep {
return;
}
for idx in data_shards..disks.len() {
@@ -2049,7 +2045,7 @@ pub(in crate::set_disk) fn fill_deferred_bitrot_readers(
let disk = disks[idx].clone();
let data_dir = files[idx].data_dir.unwrap_or_default();
let path = format!("{object}/{data_dir}/part.{part_number}");
let reopener = copy_source_demand_bound.then(|| {
let reopener = demand_bound_lockstep.then(|| {
deferred_reader_reopener(
inline_data.clone(),
disk.clone(),
+21 -9
View File
@@ -5564,9 +5564,10 @@ mod tests {
/// backlog#923: with the data-shards-only lockstep gate on, every retained
/// parity reader must be an unopened deferred reader carrying a stripe
/// handle, so the decode path can realign it to a mid-object stripe. With
/// the gate off (default), eagerly opened parity readers are kept exactly
/// as before and carry no handles.
/// handle and disposable reopener, so the decode path can realign it to a
/// mid-object stripe without consuming the later-stripe reserve. With the
/// gate off (default), eagerly opened parity readers are kept exactly as
/// before and carry neither.
#[tokio::test]
#[serial_test::serial]
async fn bitrot_reader_setup_gates_parity_stripe_handle_conversion() {
@@ -5595,6 +5596,11 @@ mod tests {
enabled.is_some(),
"parity slot {idx} stripe handle must match the gate (enabled={enabled:?})"
);
assert_eq!(
setup.deferred_reopeners[idx].is_some(),
enabled.is_some(),
"parity slot {idx} reopener must match the gate (enabled={enabled:?})"
);
}
if enabled.is_some() {
@@ -5617,13 +5623,17 @@ mod tests {
}
#[tokio::test]
#[serial_test::serial]
async fn bitrot_reader_setup_data_blocks_first_keeps_deferred_fallback_readers() {
let mut setup = setup_inline_bitrot_readers_with_env(
vec![Some(b"aaaa"), Some(b"bbbb"), Some(b"cccc"), Some(b"dddd")],
2,
2,
BitrotReaderSetupMode::ReadQuorum,
true,
let mut setup = temp_env::async_with_vars(
[("RUSTFS_GET_LOCKSTEP_DATA_SHARDS_ONLY_ENABLE", Some("true"))],
setup_inline_bitrot_readers_with_env(
vec![Some(b"aaaa"), Some(b"bbbb"), Some(b"cccc"), Some(b"dddd")],
2,
2,
BitrotReaderSetupMode::ReadQuorum,
true,
),
)
.await;
@@ -5631,6 +5641,8 @@ mod tests {
assert_eq!(setup.available_shards(), 2);
assert_eq!(setup.scheduled_shards(), 2);
assert_eq!(setup.readers.iter().filter(|reader| reader.is_some()).count(), 4);
assert!(setup.deferred_reopeners[2].is_some());
assert!(setup.deferred_reopeners[3].is_some());
let fallback_index = setup
.attempted