mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-30 00:47:13 +00:00
fix(ecstore): bound copy-source shard read-ahead (#6663)
This commit is contained in:
@@ -1517,6 +1517,7 @@ pub(in crate::set_disk) async fn submit_read_repair_heal_with_submitter(
|
||||
}
|
||||
|
||||
pub(in crate::set_disk) type ObjectBitrotReader = BitrotReader<ShardReader>;
|
||||
pub(in crate::set_disk) type DeferredReaderReopener = crate::erasure::coding::decode::DeferredReaderReopener<ShardReader>;
|
||||
pub(in crate::set_disk) type BitrotReaderTask<'a> =
|
||||
Pin<Box<dyn Future<Output = (usize, std::result::Result<Option<ObjectBitrotReader>, DiskError>)> + Send + 'a>>;
|
||||
|
||||
@@ -1533,6 +1534,10 @@ pub(in crate::set_disk) struct BitrotReaderSetup {
|
||||
/// readers. The lockstep GET decode uses them to open a parity shard
|
||||
/// aligned to the stripe where a data shard failed (backlog#923).
|
||||
pub(in crate::set_disk) deferred_stripe_handles: Vec<Option<DeferredReaderStripeHandle>>,
|
||||
/// Factories for a fresh, stripe-aligned parity reader. CopySource hedges
|
||||
/// use these disposable readers so an abandoned hedge leaves the original
|
||||
/// deferred reserve untouched.
|
||||
pub(in crate::set_disk) deferred_reopeners: Vec<Option<DeferredReaderReopener>>,
|
||||
pub(in crate::set_disk) errors: Vec<Option<DiskError>>,
|
||||
pub(in crate::set_disk) scheduled: Vec<bool>,
|
||||
pub(in crate::set_disk) attempted: Vec<bool>,
|
||||
@@ -1595,6 +1600,16 @@ pub(in crate::set_disk) fn get_bitrot_reader_setup_strategy(
|
||||
mode: BitrotReaderSetupMode,
|
||||
prefer_data_blocks_first: bool,
|
||||
) -> BitrotReaderSetupStrategy {
|
||||
// CopyObject holds the source reader behind a backpressured destination.
|
||||
// Keep its setup demand-bound even when an operator has retained the
|
||||
// legacy all-shards environment setting for ordinary GETs.
|
||||
if matches!(
|
||||
crate::set_disk::get_object_read_policy(),
|
||||
crate::set_disk::GetObjectReadPolicy::CopySource
|
||||
) {
|
||||
return BitrotReaderSetupStrategy::DataBlocksFirst;
|
||||
}
|
||||
|
||||
match mode {
|
||||
BitrotReaderSetupMode::ReadQuorum
|
||||
if prefer_data_blocks_first
|
||||
@@ -1620,6 +1635,7 @@ impl BitrotReaderSetup {
|
||||
Self {
|
||||
readers: (0..shards).map(|_| None).collect(),
|
||||
deferred_stripe_handles: (0..shards).map(|_| None).collect(),
|
||||
deferred_reopeners: (0..shards).map(|_| None).collect(),
|
||||
errors: vec![Some(DiskError::DiskNotFound); shards],
|
||||
scheduled: vec![false; shards],
|
||||
attempted: vec![false; shards],
|
||||
@@ -1814,6 +1830,41 @@ pub(in crate::set_disk) fn next_unscheduled_reader_index(
|
||||
.find(|idx| !setup.scheduled[*idx])
|
||||
}
|
||||
|
||||
/// Build a cloneable opener for an unopened deferred shard. The returned
|
||||
/// reader is aligned to the requested stripe before its first poll, while the
|
||||
/// source reader created during setup remains untouched as a reserve.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn deferred_reader_reopener(
|
||||
inline_data: Option<Bytes>,
|
||||
disk: Option<DiskStore>,
|
||||
bucket: &str,
|
||||
path: &str,
|
||||
read_offset: usize,
|
||||
read_length: usize,
|
||||
shard_size: usize,
|
||||
checksum_algo: HashAlgorithm,
|
||||
skip_verify_bitrot: bool,
|
||||
use_mmap_read: bool,
|
||||
) -> DeferredReaderReopener {
|
||||
let bucket = bucket.to_owned();
|
||||
let path = path.to_owned();
|
||||
Arc::new(move |stripe_index| {
|
||||
let (reader, handle) = create_deferred_bitrot_reader_with_stripe_handle(
|
||||
inline_data.clone(),
|
||||
disk.clone(),
|
||||
&bucket,
|
||||
&path,
|
||||
read_offset,
|
||||
read_length,
|
||||
shard_size,
|
||||
checksum_algo.clone(),
|
||||
skip_verify_bitrot,
|
||||
use_mmap_read,
|
||||
);
|
||||
handle.advance_stripes(stripe_index).then_some(reader)
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(in crate::set_disk) fn fill_deferred_bitrot_readers(
|
||||
setup: &mut BitrotReaderSetup,
|
||||
@@ -1836,6 +1887,15 @@ 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
|
||||
);
|
||||
|
||||
for idx in 0..disks.len() {
|
||||
if setup.attempted[idx] {
|
||||
continue;
|
||||
@@ -1849,6 +1909,20 @@ 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(|| {
|
||||
deferred_reader_reopener(
|
||||
inline_data.clone(),
|
||||
disk.clone(),
|
||||
bucket,
|
||||
&path,
|
||||
read_offset,
|
||||
read_length,
|
||||
shard_size,
|
||||
checksum_algo.clone(),
|
||||
skip_verify_bitrot,
|
||||
use_mmap_read,
|
||||
)
|
||||
});
|
||||
let (reader, stripe_handle) = create_deferred_bitrot_reader_with_stripe_handle(
|
||||
inline_data,
|
||||
disk,
|
||||
@@ -1862,6 +1936,7 @@ pub(in crate::set_disk) fn fill_deferred_bitrot_readers(
|
||||
use_mmap_read,
|
||||
);
|
||||
setup.retain_deferred_reader(idx, reader, stripe_handle);
|
||||
setup.deferred_reopeners[idx] = reopener;
|
||||
}
|
||||
|
||||
// With the data-shards-only lockstep gate on (backlog#923), the GET decode
|
||||
@@ -1887,6 +1962,20 @@ 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(|| {
|
||||
deferred_reader_reopener(
|
||||
inline_data.clone(),
|
||||
disk.clone(),
|
||||
bucket,
|
||||
&path,
|
||||
read_offset,
|
||||
read_length,
|
||||
shard_size,
|
||||
checksum_algo.clone(),
|
||||
skip_verify_bitrot,
|
||||
use_mmap_read,
|
||||
)
|
||||
});
|
||||
let (reader, stripe_handle) = create_deferred_bitrot_reader_with_stripe_handle(
|
||||
inline_data,
|
||||
disk,
|
||||
@@ -1901,6 +1990,7 @@ pub(in crate::set_disk) fn fill_deferred_bitrot_readers(
|
||||
);
|
||||
setup.readers[idx] = Some(reader);
|
||||
setup.deferred_stripe_handles[idx] = Some(stripe_handle);
|
||||
setup.deferred_reopeners[idx] = reopener;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2210,6 +2300,10 @@ pub(in crate::set_disk) async fn create_bitrot_readers_until_quorum_with_prefere
|
||||
let strategy = get_bitrot_reader_setup_strategy(mode, prefer_data_blocks_first);
|
||||
|
||||
if use_mmap_read
|
||||
&& !matches!(
|
||||
crate::set_disk::get_object_read_policy(),
|
||||
crate::set_disk::GetObjectReadPolicy::CopySource
|
||||
)
|
||||
&& let Some(mut setup) = try_create_bitrot_readers_via_batch_pread(
|
||||
files,
|
||||
disks,
|
||||
|
||||
@@ -792,6 +792,65 @@ const ENV_RUSTFS_GET_METADATA_SLOWTAIL_FAULT_OBJECT_PREFIX: &str = "RUSTFS_GET_M
|
||||
const ENV_RUSTFS_GET_MULTIPART_READER_SETUP_PREFETCH: &str = "RUSTFS_GET_MULTIPART_READER_SETUP_PREFETCH";
|
||||
const DEFAULT_RUSTFS_GET_MULTIPART_READER_SETUP_PREFETCH: bool = true;
|
||||
|
||||
/// Identifies the caller's read contract for policies that are deliberately
|
||||
/// narrower than the storage API's ordinary GET contract.
|
||||
///
|
||||
/// Server-side copy consumes a source reader while a destination writer is
|
||||
/// applying backpressure. Its source read must not speculatively open the
|
||||
/// next multipart part: those extra shard streams can share an internode H2
|
||||
/// connection with the current part and starve the lockstep decoder. Keep
|
||||
/// this context internal so the public `ObjectOptions` and storage traits do
|
||||
/// not acquire a copy-only field.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub(crate) enum GetObjectReadPolicy {
|
||||
#[default]
|
||||
Default,
|
||||
CopySource,
|
||||
}
|
||||
|
||||
impl GetObjectReadPolicy {
|
||||
pub(crate) const fn allows_multipart_setup_prefetch(self) -> bool {
|
||||
matches!(self, Self::Default)
|
||||
}
|
||||
}
|
||||
|
||||
tokio::task_local! {
|
||||
static GET_OBJECT_READ_POLICY: GetObjectReadPolicy;
|
||||
static GET_OBJECT_READ_CANCELLATION: tokio_util::sync::CancellationToken;
|
||||
}
|
||||
|
||||
pub(crate) fn get_object_read_policy() -> GetObjectReadPolicy {
|
||||
GET_OBJECT_READ_POLICY.try_with(|policy| *policy).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) async fn with_get_object_read_policy<F>(policy: GetObjectReadPolicy, future: F) -> F::Output
|
||||
where
|
||||
F: std::future::Future,
|
||||
{
|
||||
let decode_policy = match policy {
|
||||
GetObjectReadPolicy::Default => crate::erasure::coding::decode::DecodeReadPolicy::Default,
|
||||
GetObjectReadPolicy::CopySource => crate::erasure::coding::decode::DecodeReadPolicy::DemandBound,
|
||||
};
|
||||
crate::erasure::coding::decode::with_decode_read_policy(decode_policy, GET_OBJECT_READ_POLICY.scope(policy, future)).await
|
||||
}
|
||||
|
||||
/// Return the request-owned cancellation token for a copy source, when one is
|
||||
/// installed. The token is read before the detached legacy producer is spawned;
|
||||
/// Tokio task-local values do not cross that spawn boundary on their own.
|
||||
pub(crate) fn get_object_read_cancellation() -> Option<tokio_util::sync::CancellationToken> {
|
||||
GET_OBJECT_READ_CANCELLATION.try_with(|token| token.clone()).ok()
|
||||
}
|
||||
|
||||
pub(crate) async fn with_get_object_read_cancellation<F>(
|
||||
cancellation: tokio_util::sync::CancellationToken,
|
||||
future: F,
|
||||
) -> F::Output
|
||||
where
|
||||
F: std::future::Future,
|
||||
{
|
||||
GET_OBJECT_READ_CANCELLATION.scope(cancellation, future).await
|
||||
}
|
||||
|
||||
static OBJECT_LOCK_DIAG_ENABLED: OnceLock<bool> = OnceLock::new();
|
||||
|
||||
mod core;
|
||||
@@ -2296,6 +2355,7 @@ enum GetCodecStreamingFallbackReason {
|
||||
InvalidMinSize,
|
||||
ReadQuorumNotSafe,
|
||||
MultipartPartLimit,
|
||||
CopySourceDemandBound,
|
||||
}
|
||||
|
||||
impl GetCodecStreamingFallbackReason {
|
||||
@@ -2317,6 +2377,7 @@ impl GetCodecStreamingFallbackReason {
|
||||
Self::InvalidMinSize => "invalid_min_size",
|
||||
Self::ReadQuorumNotSafe => "read_quorum_not_safe",
|
||||
Self::MultipartPartLimit => "multipart_part_limit",
|
||||
Self::CopySourceDemandBound => "copy_source_demand_bound",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2623,6 +2684,17 @@ fn get_codec_streaming_reader_gate(
|
||||
prefer_data_blocks_first_reader_setup: false,
|
||||
};
|
||||
}
|
||||
if matches!(get_object_read_policy(), GetObjectReadPolicy::CopySource) {
|
||||
// The codec reader has its own bounded fill worker. It may still
|
||||
// request an additional stripe for a plain single-part object even
|
||||
// when multipart setup prefetch is disabled, so copy sources use the
|
||||
// legacy demand-bound reader for every object class.
|
||||
return GetCodecStreamingGate {
|
||||
object_class,
|
||||
decision: GetCodecStreamingDecision::Fallback(GetCodecStreamingFallbackReason::CopySourceDemandBound),
|
||||
prefer_data_blocks_first_reader_setup: false,
|
||||
};
|
||||
}
|
||||
if !config.rollout.is_opted_in() {
|
||||
return GetCodecStreamingGate {
|
||||
object_class,
|
||||
@@ -5912,6 +5984,29 @@ mod tests {
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
#[tokio::test]
|
||||
async fn copy_source_read_policy_is_scoped_and_demand_bound() {
|
||||
assert_eq!(get_object_read_policy(), GetObjectReadPolicy::Default);
|
||||
assert!(GetObjectReadPolicy::Default.allows_multipart_setup_prefetch());
|
||||
assert!(!GetObjectReadPolicy::CopySource.allows_multipart_setup_prefetch());
|
||||
|
||||
with_get_object_read_policy(GetObjectReadPolicy::CopySource, async {
|
||||
assert_eq!(get_object_read_policy(), GetObjectReadPolicy::CopySource);
|
||||
assert!(!get_object_read_policy().allows_multipart_setup_prefetch());
|
||||
assert_eq!(
|
||||
crate::erasure::coding::decode::decode_read_policy(),
|
||||
crate::erasure::coding::decode::DecodeReadPolicy::DemandBound
|
||||
);
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(get_object_read_policy(), GetObjectReadPolicy::Default);
|
||||
assert_eq!(
|
||||
crate::erasure::coding::decode::decode_read_policy(),
|
||||
crate::erasure::coding::decode::DecodeReadPolicy::Default
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_part_error_maps_confirmed_missing_to_invalid_part() {
|
||||
for err in ["file not found", "Specified part could not be found", "part.7 not found"] {
|
||||
|
||||
@@ -61,6 +61,8 @@ use http::HeaderValue;
|
||||
use rustfs_utils::path::decode_dir_object;
|
||||
use std::future::Future;
|
||||
use std::sync::OnceLock;
|
||||
use std::task::{Context, Poll};
|
||||
use tokio::io::{AsyncRead, ReadBuf};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
const OLD_DATA_CLEANUP_RECEIPT_FILE: &str = ".rustfs-old-data-cleanup-receipt.json";
|
||||
@@ -1120,6 +1122,37 @@ where
|
||||
Ok((reader, offset, length))
|
||||
}
|
||||
|
||||
/// Cancels a detached legacy GET producer when its consumer is dropped.
|
||||
///
|
||||
/// The producer owns the shard readers and the object read lock, while the
|
||||
/// consumer owns only the duplex read half. Closing that half eventually
|
||||
/// unblocks a writer, but can leave a producer stuck in reader setup or remote
|
||||
/// recovery until a lower-level timeout fires. This small boundary wrapper
|
||||
/// provides an explicit cancellation signal without changing the public
|
||||
/// `GetObjectReader` shape.
|
||||
struct ProducerCancellationReader<R> {
|
||||
inner: R,
|
||||
cancellation: CancellationToken,
|
||||
}
|
||||
|
||||
impl<R> ProducerCancellationReader<R> {
|
||||
fn new(inner: R, cancellation: CancellationToken) -> Self {
|
||||
Self { inner, cancellation }
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: AsyncRead + Unpin> AsyncRead for ProducerCancellationReader<R> {
|
||||
fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll<std::io::Result<()>> {
|
||||
Pin::new(&mut self.inner).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Drop for ProducerCancellationReader<R> {
|
||||
fn drop(&mut self) {
|
||||
self.cancellation.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
fn data_read_metadata_early_stop_request_shape_allowed(range: &Option<HTTPRangeSpec>, opts: &ObjectOptions) -> bool {
|
||||
range.is_none()
|
||||
&& opts.part_number.is_none()
|
||||
@@ -1907,12 +1940,25 @@ impl crate::storage_api_contracts::object::ObjectIO for SetDisks {
|
||||
// lookup on the streaming miss path (ODC-16).
|
||||
reader.body_source = body_source;
|
||||
|
||||
// The producer is otherwise detached from the returned reader. Tie its
|
||||
// lifetime to the source stream so a cancelled copy (or an abandoned
|
||||
// GET) releases in-flight shard opens, response bodies, and the read
|
||||
// lock immediately instead of waiting for a disk timeout.
|
||||
let producer_cancellation = crate::set_disk::get_object_read_cancellation();
|
||||
if let Some(cancellation) = producer_cancellation.as_ref() {
|
||||
reader.stream = Box::new(ProducerCancellationReader::new(reader.stream, cancellation.clone()));
|
||||
}
|
||||
|
||||
// let disks = disks.clone();
|
||||
let bucket = bucket.to_owned();
|
||||
let object = object.to_owned();
|
||||
let set_index = self.set_index;
|
||||
let pool_index = self.pool_index;
|
||||
let skip_verify = opts.skip_verify_bitrot;
|
||||
// The producer runs in a separate Tokio task, so carry the caller's
|
||||
// read policy across the task boundary explicitly. Tokio task-local
|
||||
// values are not inherited by spawned tasks.
|
||||
let read_policy = crate::set_disk::get_object_read_policy();
|
||||
let erasure_cache = Arc::clone(&self.erasure_cache);
|
||||
let (fi, files, disks) = snapshot.into_owned();
|
||||
tokio::spawn(async move {
|
||||
@@ -1922,26 +1968,40 @@ impl crate::storage_api_contracts::object::ObjectIO for SetDisks {
|
||||
// `get_object_with_fileinfo` also waits on `writer`, so an outer timeout
|
||||
// would incorrectly treat downstream backpressure as disk-read latency.
|
||||
// Disk read timeouts must be enforced at the actual disk I/O operations.
|
||||
let producer_result = Self::get_object_with_fileinfo(
|
||||
&bucket,
|
||||
&object,
|
||||
erasure_cache,
|
||||
offset,
|
||||
length,
|
||||
&mut writer,
|
||||
fi,
|
||||
files,
|
||||
&disks,
|
||||
set_index,
|
||||
pool_index,
|
||||
skip_verify,
|
||||
false,
|
||||
GET_OBJECT_PATH_LEGACY_DUPLEX,
|
||||
object_class.as_str(),
|
||||
size_bucket,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = &producer_result {
|
||||
let producer_result = tokio::select! {
|
||||
biased;
|
||||
result = crate::set_disk::with_get_object_read_policy(
|
||||
read_policy,
|
||||
Self::get_object_with_fileinfo(
|
||||
&bucket,
|
||||
&object,
|
||||
erasure_cache,
|
||||
offset,
|
||||
length,
|
||||
&mut writer,
|
||||
fi,
|
||||
files,
|
||||
&disks,
|
||||
set_index,
|
||||
pool_index,
|
||||
skip_verify,
|
||||
false,
|
||||
GET_OBJECT_PATH_LEGACY_DUPLEX,
|
||||
object_class.as_str(),
|
||||
size_bucket,
|
||||
),
|
||||
) => result,
|
||||
_ = async {
|
||||
if let Some(cancellation) = producer_cancellation.as_ref() {
|
||||
cancellation.cancelled().await;
|
||||
} else {
|
||||
std::future::pending::<()>().await;
|
||||
}
|
||||
} => Err(Error::OperationCanceled),
|
||||
};
|
||||
if let Err(e) = &producer_result
|
||||
&& !matches!(e, Error::OperationCanceled)
|
||||
{
|
||||
let reason = classify_storage_error(e);
|
||||
if reason == GetObjectFailureReason::DownstreamClosed {
|
||||
debug!(
|
||||
@@ -3794,6 +3854,28 @@ mod legacy_duplex_producer_reader_tests {
|
||||
assert_eq!(out, b"complete");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn producer_cancellation_reader_cancels_pending_producer_on_drop() {
|
||||
let cancellation = CancellationToken::new();
|
||||
let producer_cancellation = cancellation.clone();
|
||||
let producer = tokio::spawn(async move {
|
||||
tokio::select! {
|
||||
_ = producer_cancellation.cancelled() => true,
|
||||
_ = std::future::pending::<()>() => false,
|
||||
}
|
||||
});
|
||||
|
||||
let reader = ProducerCancellationReader::new(tokio::io::empty(), cancellation);
|
||||
drop(reader);
|
||||
|
||||
assert!(
|
||||
tokio::time::timeout(std::time::Duration::from_secs(1), producer)
|
||||
.await
|
||||
.expect("dropping the consumer should cancel the producer promptly")
|
||||
.expect("producer task should not panic")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn legacy_duplex_reader_ignores_zero_capacity_read_buf() {
|
||||
let (mut writer, reader) = tokio::io::duplex(64);
|
||||
|
||||
@@ -792,7 +792,7 @@ impl SetDisks {
|
||||
let use_mmap_read = object_mmap_read_enabled();
|
||||
let files = Arc::new(files);
|
||||
let disks = Arc::new(disks);
|
||||
let prefetch_enabled = is_multipart_reader_setup_prefetch_enabled();
|
||||
let prefetch_enabled = multipart_reader_setup_prefetch_enabled(get_object_read_policy());
|
||||
let mut prefetched: Option<(usize, PrefetchedReaderSetup)> = None;
|
||||
|
||||
let mut total_read = 0;
|
||||
@@ -1065,8 +1065,9 @@ impl SetDisks {
|
||||
let unattempted_data_shards = !reader_setup.data_shards_attempted(erasure.data_shards);
|
||||
let readers = reader_setup.readers;
|
||||
let deferred_stripe_handles = reader_setup.deferred_stripe_handles;
|
||||
let deferred_reopeners = reader_setup.deferred_reopeners;
|
||||
let (written, err) = erasure
|
||||
.decode_with_stripe_handles(
|
||||
.decode_with_stripe_handles_and_reopeners(
|
||||
writer,
|
||||
readers,
|
||||
part_offset,
|
||||
@@ -1074,6 +1075,7 @@ impl SetDisks {
|
||||
part_size,
|
||||
read_costs,
|
||||
deferred_stripe_handles,
|
||||
deferred_reopeners,
|
||||
)
|
||||
.await;
|
||||
let decode_elapsed = decode_stage_start.elapsed();
|
||||
@@ -1476,6 +1478,7 @@ impl SetDisks {
|
||||
erasure.clone(),
|
||||
reader_setup.readers,
|
||||
reader_setup.deferred_stripe_handles,
|
||||
reader_setup.deferred_reopeners,
|
||||
read_costs,
|
||||
part_offset,
|
||||
part_length,
|
||||
@@ -1488,6 +1491,7 @@ impl SetDisks {
|
||||
|
||||
let readers = reader_setup.readers;
|
||||
let deferred_stripe_handles = reader_setup.deferred_stripe_handles;
|
||||
let deferred_reopeners = reader_setup.deferred_reopeners;
|
||||
let source = if let Some(read_costs) = read_costs {
|
||||
coding::decode::ParallelReader::new_with_metrics_path_read_costs_and_reconstruction_verification(
|
||||
readers,
|
||||
@@ -1506,7 +1510,8 @@ impl SetDisks {
|
||||
Some(metrics_path),
|
||||
)
|
||||
}
|
||||
.with_deferred_parity_handles(deferred_stripe_handles);
|
||||
.with_deferred_parity_handles(deferred_stripe_handles)
|
||||
.with_deferred_parity_reopeners(deferred_reopeners);
|
||||
let engine = build_get_codec_streaming_decode_engine(erasure.clone())?;
|
||||
let reader =
|
||||
coding::decode_reader::ErasureDecodeReader::new_with_metrics_path(source, engine, part_length, metrics_path)?;
|
||||
@@ -1535,6 +1540,10 @@ fn multipart_part_checksum_algo(fi: &FileInfo, part_number: usize) -> HashAlgori
|
||||
}
|
||||
}
|
||||
|
||||
fn multipart_reader_setup_prefetch_enabled(policy: GetObjectReadPolicy) -> bool {
|
||||
policy.allows_multipart_setup_prefetch() && is_multipart_reader_setup_prefetch_enabled()
|
||||
}
|
||||
|
||||
/// Run one part's bitrot reader setup and measure its wall-clock duration.
|
||||
///
|
||||
/// Shared by the synchronous path and the prefetch task in
|
||||
@@ -1762,10 +1771,12 @@ impl Drop for LazyMultipartCodecStreamingReader {
|
||||
/// background task drives the decode into the write half while the returned
|
||||
/// reader drains the read half. No extra file descriptors are opened — the
|
||||
/// readers are moved in from the setup that just ran.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn build_legacy_per_part_fallback_reader(
|
||||
erasure: coding::Erasure,
|
||||
readers: Vec<Option<ObjectBitrotReader>>,
|
||||
deferred_stripe_handles: Vec<Option<DeferredReaderStripeHandle>>,
|
||||
deferred_reopeners: Vec<Option<DeferredReaderReopener>>,
|
||||
read_costs: Option<Vec<ShardReadCost>>,
|
||||
part_offset: usize,
|
||||
part_length: usize,
|
||||
@@ -1775,7 +1786,7 @@ fn build_legacy_per_part_fallback_reader(
|
||||
let (read_half, mut write_half) = tokio::io::duplex(buffer);
|
||||
let decode = tokio::spawn(async move {
|
||||
let (_written, err) = erasure
|
||||
.decode_with_stripe_handles(
|
||||
.decode_with_stripe_handles_and_reopeners(
|
||||
&mut write_half,
|
||||
readers,
|
||||
part_offset,
|
||||
@@ -1783,6 +1794,7 @@ fn build_legacy_per_part_fallback_reader(
|
||||
part_size,
|
||||
read_costs,
|
||||
deferred_stripe_handles,
|
||||
deferred_reopeners,
|
||||
)
|
||||
.await;
|
||||
// Dropping `write_half` on return signals EOF to the reader half.
|
||||
@@ -3236,6 +3248,7 @@ mod metadata_cache_tests {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::erasure::coding::BitrotWriter;
|
||||
use serial_test::serial;
|
||||
use std::io::{Cursor, ErrorKind, IoSlice};
|
||||
use std::sync::{
|
||||
Arc,
|
||||
@@ -3246,6 +3259,15 @@ mod tests {
|
||||
const CODEC_STREAMING_TEST_BUCKET: &str = "bucket";
|
||||
const CODEC_STREAMING_TEST_OBJECT: &str = "object";
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn multipart_reader_setup_prefetch_is_disabled_only_for_copy_sources() {
|
||||
temp_env::with_var(ENV_RUSTFS_GET_MULTIPART_READER_SETUP_PREFETCH, Some("true"), || {
|
||||
assert!(multipart_reader_setup_prefetch_enabled(GetObjectReadPolicy::Default));
|
||||
assert!(!multipart_reader_setup_prefetch_enabled(GetObjectReadPolicy::CopySource));
|
||||
});
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn downstream_writer_marks_closed_duplex_reader_as_downstream_close() {
|
||||
let (reader, inner) = tokio::io::duplex(64);
|
||||
@@ -5481,6 +5503,7 @@ mod tests {
|
||||
erasure,
|
||||
setup.readers,
|
||||
setup.deferred_stripe_handles,
|
||||
Vec::new(),
|
||||
None,
|
||||
0,
|
||||
data.len(),
|
||||
@@ -5531,6 +5554,7 @@ mod tests {
|
||||
erasure,
|
||||
setup.readers,
|
||||
setup.deferred_stripe_handles,
|
||||
Vec::new(),
|
||||
None,
|
||||
0,
|
||||
part2_len,
|
||||
@@ -5571,6 +5595,7 @@ mod tests {
|
||||
erasure,
|
||||
setup.readers,
|
||||
setup.deferred_stripe_handles,
|
||||
Vec::new(),
|
||||
None,
|
||||
0,
|
||||
data.len(),
|
||||
@@ -6007,6 +6032,49 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn codec_streaming_reader_gate_keeps_copy_source_demand_bound_for_all_classes() {
|
||||
temp_env::async_with_vars(
|
||||
[
|
||||
(ENV_RUSTFS_GET_CODEC_STREAMING_ENABLE, Some("true")),
|
||||
(ENV_RUSTFS_GET_CODEC_STREAMING_ROLLOUT, Some("benchmark")),
|
||||
(ENV_RUSTFS_GET_CODEC_STREAMING_BODY_COMPAT_CONFIRMED, Some("true")),
|
||||
(ENV_RUSTFS_GET_CODEC_STREAMING_HEADER_COMPAT_CONFIRMED, Some("true")),
|
||||
(ENV_RUSTFS_GET_CODEC_STREAMING_MULTIPART_ENABLE, Some("true")),
|
||||
(ENV_RUSTFS_GET_CODEC_STREAMING_MIN_SIZE, Some("1")),
|
||||
],
|
||||
async {
|
||||
let fi = codec_streaming_test_fileinfo(1024, 2);
|
||||
let object_info = codec_streaming_test_object_info(&fi);
|
||||
let normal = codec_streaming_reader_gate_for_test(&None, &object_info, &fi, true);
|
||||
assert_eq!(normal.decision, GetCodecStreamingDecision::Use);
|
||||
|
||||
let plain_fi = codec_streaming_test_fileinfo(1024, 1);
|
||||
let plain_object_info = codec_streaming_test_object_info(&plain_fi);
|
||||
let normal_plain = codec_streaming_reader_gate_for_test(&None, &plain_object_info, &plain_fi, true);
|
||||
assert_eq!(normal_plain.object_class, GetCodecStreamingObjectClass::PlainSinglePart);
|
||||
assert_eq!(normal_plain.decision, GetCodecStreamingDecision::Use);
|
||||
|
||||
let copy = with_get_object_read_policy(GetObjectReadPolicy::CopySource, async {
|
||||
let multipart = codec_streaming_reader_gate_for_test(&None, &object_info, &fi, true);
|
||||
let plain = codec_streaming_reader_gate_for_test(&None, &plain_object_info, &plain_fi, true);
|
||||
(multipart, plain)
|
||||
})
|
||||
.await;
|
||||
assert_eq!(
|
||||
copy.0.decision,
|
||||
GetCodecStreamingDecision::Fallback(GetCodecStreamingFallbackReason::CopySourceDemandBound)
|
||||
);
|
||||
assert_eq!(
|
||||
copy.1.decision,
|
||||
GetCodecStreamingDecision::Fallback(GetCodecStreamingFallbackReason::CopySourceDemandBound)
|
||||
);
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codec_streaming_reader_gate_keeps_multipart_default_off() {
|
||||
temp_env::with_vars(
|
||||
@@ -6108,6 +6176,10 @@ mod tests {
|
||||
assert_eq!(GetCodecStreamingFallbackReason::InvalidMinSize.as_str(), "invalid_min_size");
|
||||
assert_eq!(GetCodecStreamingFallbackReason::ReadQuorumNotSafe.as_str(), "read_quorum_not_safe");
|
||||
assert_eq!(GetCodecStreamingFallbackReason::MultipartPartLimit.as_str(), "multipart_part_limit");
|
||||
assert_eq!(
|
||||
GetCodecStreamingFallbackReason::CopySourceDemandBound.as_str(),
|
||||
"copy_source_demand_bound"
|
||||
);
|
||||
assert_eq!(GetCodecStreamingObjectClass::PlainSinglePart.as_str(), "plain_single_part");
|
||||
assert_eq!(GetCodecStreamingObjectClass::Range.as_str(), "range");
|
||||
assert_eq!(GetCodecStreamingObjectClass::Encrypted.as_str(), "encrypted");
|
||||
|
||||
Reference in New Issue
Block a user