fix(ecstore): bound copy-source shard read-ahead (#6663)

This commit is contained in:
cxymds
2026-08-26 21:24:37 +08:00
committed by GitHub
parent a96dd7d289
commit 7c2361757e
13 changed files with 2262 additions and 145 deletions
@@ -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,
+95
View File
@@ -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"] {
+102 -20
View File
@@ -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);
+76 -4
View File
@@ -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");