Merge branch 'main' of github.com:rustfs/rustfs into houseme/get-small-file-optimization

* 'main' of github.com:rustfs/rustfs:
  fix(ecstore): replace unwrap() with proper error handling in api_get_object_attributes (#729 batch 11) (#3991)
  fix(ecstore): improve expect() messages in admin_server_info (#729 batch 9) (#3990)
  fix(server): improve expect() messages in layer.rs (#729 batch 8) (#3989)
  fix(ecstore): improve expect() messages in replication_resyncer (#729 batch 7) (#3988)
  fix(admin): improve expect() messages in remaining admin handlers (#729 batch 6) (#3987)
  fix(admin): improve expect() messages in policies handler (#729 batch 5) (#3986)
  fix(admin): improve expect() messages in user handler (#729 batch 4) (#3985)
  fix(admin): replace unwrap() with safe pattern in bucket_meta handler (#729 batch 3) (#3984)
  feat(get): harden codec streaming rollout (#3981)
  fix(admin): replace unwrap() with proper error handling in tier handler (#729 batch 2) (#3983)

# Conflicts:
#	crates/ecstore/src/set_disk/mod.rs
This commit is contained in:
houseme
2026-06-28 11:31:06 +08:00
22 changed files with 2946 additions and 444 deletions
+421 -40
View File
@@ -22,7 +22,8 @@ use crate::diagnostics::get::{
GET_METADATA_RESPONSE_ERROR, GET_METADATA_RESPONSE_IGNORED, GET_METADATA_RESPONSE_NOT_FOUND, GET_METADATA_RESPONSE_TIMEOUT,
GET_METADATA_RESPONSE_VALID, GET_METADATA_RESPONSE_VERSION_NOT_FOUND, GET_OBJECT_PATH_CODEC_STREAMING,
GET_OBJECT_PATH_LEGACY_DUPLEX, GET_STAGE_DECODE, GET_STAGE_RANGE, GET_STAGE_READER_SETUP, GetObjectFailureReason,
classify_disk_error, record_get_object_pipeline_failure, record_get_object_pipeline_failure_for_path,
classify_disk_error, get_stage_timer_if_enabled, record_get_object_pipeline_failure,
record_get_object_pipeline_failure_for_path, record_get_stage_duration_if_enabled,
};
use crate::erasure::coding::BitrotReader;
use crate::io_support::bitrot::create_deferred_bitrot_reader;
@@ -31,13 +32,14 @@ use futures::stream::{FuturesUnordered, StreamExt};
use metrics::counter;
use rustfs_config::{DEFAULT_OBJECT_ZERO_COPY_ENABLE, ENV_OBJECT_ZERO_COPY_ENABLE};
use std::{
collections::HashMap,
collections::{HashMap, VecDeque},
future::Future,
pin::Pin,
sync::OnceLock,
task::{Context, Poll},
time::{Duration, Instant},
};
use tokio::io::AsyncRead;
use tokio::io::{AsyncRead, ReadBuf};
use tokio::sync::RwLock;
use tokio::task::JoinSet;
@@ -53,6 +55,39 @@ pub(super) enum GetCodecStreamingReaderBuildOutcome {
Fallback(GetCodecStreamingFallbackReason),
}
struct MultipartCodecStreamingReader {
readers: VecDeque<Box<dyn AsyncRead + Unpin + Send + Sync>>,
}
impl MultipartCodecStreamingReader {
fn new(readers: Vec<Box<dyn AsyncRead + Unpin + Send + Sync>>) -> Self {
Self {
readers: VecDeque::from(readers),
}
}
}
impl AsyncRead for MultipartCodecStreamingReader {
fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll<std::io::Result<()>> {
if buf.remaining() == 0 {
return Poll::Ready(Ok(()));
}
loop {
let Some(reader) = self.readers.front_mut() else {
return Poll::Ready(Ok(()));
};
let filled_before = buf.filled().len();
match Pin::new(reader).poll_read(cx, buf) {
Poll::Ready(Ok(())) if buf.filled().len() == filled_before => {
self.readers.pop_front();
}
result => return result,
}
}
}
}
pub(super) fn codec_streaming_reader_setup_fallback_reason(missing_shards: usize) -> Option<GetCodecStreamingFallbackReason> {
(missing_shards > 0).then_some(GetCodecStreamingFallbackReason::ReadQuorumNotSafe)
}
@@ -2242,9 +2277,6 @@ impl SetDisks {
skip_verify_bitrot: bool,
) -> Result<GetCodecStreamingReaderBuildOutcome> {
let (disks, files) = Self::shuffle_disks_and_parts_metadata_by_index(disks, files, fi);
if fi.parts.len() != 1 {
return Err(Error::other("codec streaming reader only supports single-part plain objects"));
}
let erasure = crate::erasure::coding::Erasure::new_with_options(
fi.erasure.data_blocks,
@@ -2252,14 +2284,94 @@ impl SetDisks {
fi.erasure.block_size,
fi.uses_legacy_checksum,
);
let part = &fi.parts[0];
let part_number = part.number;
let part_size = part.size;
let part_length = usize::try_from(fi.size).map_err(|_| Error::other("codec streaming reader object size is invalid"))?;
if fi.parts.len() == 1 {
let part = &fi.parts[0];
let part_length =
usize::try_from(fi.size).map_err(|_| Error::other("codec streaming reader object size is invalid"))?;
return Self::build_codec_streaming_part_reader(
bucket,
object,
fi,
&files,
&disks,
&erasure,
part.number,
0,
part_length,
part.size,
skip_verify_bitrot,
)
.await;
}
if !is_codec_streaming_multipart_enabled() {
return Ok(GetCodecStreamingReaderBuildOutcome::Fallback(GetCodecStreamingFallbackReason::Multipart));
}
if fi.parts.len() > get_codec_streaming_multipart_max_parts() {
return Ok(GetCodecStreamingReaderBuildOutcome::Fallback(
GetCodecStreamingFallbackReason::MultipartPartLimit,
));
}
let object_length =
usize::try_from(fi.size).map_err(|_| Error::other("codec streaming reader object size is invalid"))?;
let mut total_part_size = 0usize;
for part in &fi.parts {
total_part_size = total_part_size
.checked_add(part.size)
.ok_or_else(|| Error::other("codec streaming multipart part sizes overflow"))?;
}
if total_part_size != object_length {
return Err(Error::other("codec streaming multipart part sizes do not match object size"));
}
let mut readers = Vec::with_capacity(fi.parts.len());
for part in &fi.parts {
match Self::build_codec_streaming_part_reader(
bucket,
object,
fi,
&files,
&disks,
&erasure,
part.number,
0,
part.size,
part.size,
skip_verify_bitrot,
)
.await?
{
GetCodecStreamingReaderBuildOutcome::Reader(reader) => readers.push(reader),
GetCodecStreamingReaderBuildOutcome::Fallback(reason) => {
return Ok(GetCodecStreamingReaderBuildOutcome::Fallback(reason));
}
}
}
Ok(GetCodecStreamingReaderBuildOutcome::Reader(Box::new(MultipartCodecStreamingReader::new(
readers,
))))
}
#[allow(clippy::too_many_arguments)]
async fn build_codec_streaming_part_reader(
bucket: &str,
object: &str,
fi: &FileInfo,
files: &[FileInfo],
disks: &[Option<DiskStore>],
erasure: &crate::erasure::coding::Erasure,
part_number: usize,
part_offset: usize,
part_length: usize,
part_size: usize,
skip_verify_bitrot: bool,
) -> Result<GetCodecStreamingReaderBuildOutcome> {
if part_length > part_size {
return Err(Error::other("codec streaming reader part length exceeds part size"));
}
let checksum_info = fi.erasure.get_checksum_info(part_number);
let checksum_algo = if fi.uses_legacy_checksum && checksum_info.algorithm == rustfs_utils::HashAlgorithm::HighwayHash256S
{
@@ -2268,21 +2380,24 @@ impl SetDisks {
checksum_info.algorithm
};
let use_mmap_read = rustfs_utils::get_env_bool(ENV_OBJECT_ZERO_COPY_ENABLE, DEFAULT_OBJECT_ZERO_COPY_ENABLE);
let till_offset = erasure.shard_file_offset(0, part_length, part_size);
let till_offset = erasure.shard_file_offset(part_offset, part_length, part_size);
let read_offset = (part_offset / erasure.block_size) * erasure.shard_size();
let read_length = till_offset.saturating_sub(read_offset);
let reader_setup_stage_start = Instant::now();
let stage_metrics_enabled = rustfs_io_metrics::get_stage_metrics_enabled();
let reader_setup_stage_start = get_stage_timer_if_enabled(stage_metrics_enabled);
let read_costs = disks
.iter()
.map(|disk| shard_read_cost_for_disk(disk.as_ref()))
.collect::<Vec<_>>();
let reader_setup = create_bitrot_readers_until_quorum(
&files,
&disks,
files,
disks,
bucket,
object,
part_number,
0,
till_offset,
read_offset,
read_length,
erasure.shard_size(),
checksum_algo,
skip_verify_bitrot,
@@ -2293,11 +2408,7 @@ impl SetDisks {
)
.await;
let metrics_path = get_codec_streaming_metrics_path();
rustfs_io_metrics::record_get_object_stage_duration(
metrics_path,
GET_STAGE_READER_SETUP,
reader_setup_stage_start.elapsed().as_secs_f64(),
);
record_get_stage_duration_if_enabled(metrics_path, GET_STAGE_READER_SETUP, reader_setup_stage_start);
let available_shards = reader_setup.available_shards();
if available_shards < erasure.data_shards {
@@ -2320,12 +2431,12 @@ impl SetDisks {
crate::erasure::coding::decode::ParallelReader::new_with_metrics_path_read_costs_and_reconstruction_verification(
readers,
erasure.clone(),
0,
part_offset,
part_size,
Some(metrics_path),
read_costs,
);
let engine = build_get_codec_streaming_decode_engine(erasure)?;
let engine = build_get_codec_streaming_decode_engine(erasure.clone())?;
let reader = crate::erasure::coding::decode_reader::ErasureDecodeReader::new_with_metrics_path(
source,
engine,
@@ -2696,6 +2807,15 @@ mod metadata_cache_tests {
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
use tokio::io::AsyncReadExt;
const CODEC_STREAMING_TEST_BUCKET: &str = "bucket";
const CODEC_STREAMING_TEST_OBJECT: &str = "object";
fn metadata_fanout_test_fileinfo(object: &str) -> FileInfo {
let mut fi = FileInfo::new(object, 2, 2);
@@ -3247,7 +3367,147 @@ mod tests {
}
fn codec_streaming_test_object_info(fi: &FileInfo) -> ObjectInfo {
ObjectInfo::from_file_info(fi, "bucket", "object", false)
ObjectInfo::from_file_info(fi, CODEC_STREAMING_TEST_BUCKET, CODEC_STREAMING_TEST_OBJECT, false)
}
fn codec_streaming_reader_gate_for_test(
range: &Option<HTTPRangeSpec>,
object_info: &ObjectInfo,
fi: &FileInfo,
lock_optimization_enabled: bool,
) -> GetCodecStreamingGate {
get_codec_streaming_reader_gate(
CODEC_STREAMING_TEST_BUCKET,
CODEC_STREAMING_TEST_OBJECT,
range,
object_info,
fi,
lock_optimization_enabled,
)
}
#[tokio::test]
async fn multipart_codec_streaming_reader_reads_parts_in_order() {
let readers: Vec<Box<dyn AsyncRead + Unpin + Send + Sync>> = vec![
Box::new(Cursor::new(b"hello ".to_vec())),
Box::new(Cursor::new(b"multipart".to_vec())),
];
let mut reader = MultipartCodecStreamingReader::new(readers);
let mut output = Vec::new();
reader
.read_to_end(&mut output)
.await
.expect("multipart codec reader should read all parts");
assert_eq!(output, b"hello multipart");
}
struct OneByteAsyncReader {
data: Vec<u8>,
position: usize,
}
impl OneByteAsyncReader {
fn new(data: &'static [u8]) -> Self {
Self {
data: data.to_vec(),
position: 0,
}
}
}
impl AsyncRead for OneByteAsyncReader {
fn poll_read(mut self: Pin<&mut Self>, _cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll<std::io::Result<()>> {
if self.position >= self.data.len() || buf.remaining() == 0 {
return Poll::Ready(Ok(()));
}
buf.put_slice(&self.data[self.position..self.position + 1]);
self.position += 1;
Poll::Ready(Ok(()))
}
}
#[tokio::test]
async fn multipart_codec_streaming_reader_crosses_part_boundaries_with_short_reads() {
let readers: Vec<Box<dyn AsyncRead + Unpin + Send + Sync>> = vec![
Box::new(OneByteAsyncReader::new(b"abc")),
Box::new(OneByteAsyncReader::new(b"def")),
];
let mut reader = MultipartCodecStreamingReader::new(readers);
let mut first = [0u8; 5];
let mut second = Vec::new();
reader
.read_exact(&mut first)
.await
.expect("multipart codec reader should cross part boundaries");
reader
.read_to_end(&mut second)
.await
.expect("multipart codec reader should drain the final part");
assert_eq!(&first, b"abcde");
assert_eq!(second, b"f");
}
struct DropCountingReader {
data: Vec<u8>,
position: usize,
drops: Arc<AtomicUsize>,
}
impl DropCountingReader {
fn new(data: &'static [u8], drops: Arc<AtomicUsize>) -> Self {
Self {
data: data.to_vec(),
position: 0,
drops,
}
}
}
impl AsyncRead for DropCountingReader {
fn poll_read(mut self: Pin<&mut Self>, _cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll<std::io::Result<()>> {
if self.position >= self.data.len() || buf.remaining() == 0 {
return Poll::Ready(Ok(()));
}
let available = self.data.len() - self.position;
let count = available.min(buf.remaining());
let end = self.position + count;
buf.put_slice(&self.data[self.position..end]);
self.position = end;
Poll::Ready(Ok(()))
}
}
impl Drop for DropCountingReader {
fn drop(&mut self) {
self.drops.fetch_add(1, Ordering::SeqCst);
}
}
#[tokio::test]
async fn multipart_codec_streaming_reader_drops_remaining_parts_on_abort() {
let drops = Arc::new(AtomicUsize::new(0));
{
let readers: Vec<Box<dyn AsyncRead + Unpin + Send + Sync>> = vec![
Box::new(DropCountingReader::new(b"abc", Arc::clone(&drops))),
Box::new(DropCountingReader::new(b"def", Arc::clone(&drops))),
];
let mut reader = MultipartCodecStreamingReader::new(readers);
let mut first = [0u8; 1];
reader
.read_exact(&mut first)
.await
.expect("multipart codec reader should support partial reads");
assert_eq!(&first, b"a");
}
assert_eq!(drops.load(Ordering::SeqCst), 2);
}
fn inline_reader_setup_fileinfo(data: Option<&'static [u8]>) -> FileInfo {
@@ -3395,12 +3655,12 @@ mod tests {
let fi = codec_streaming_test_fileinfo(1024, 1);
let object_info = codec_streaming_test_object_info(&fi);
assert_eq!(
get_codec_streaming_reader_gate(&None, &object_info, &fi, true).decision,
codec_streaming_reader_gate_for_test(&None, &object_info, &fi, true).decision,
GetCodecStreamingDecision::Use
);
assert_eq!(
get_codec_streaming_reader_gate(&None, &object_info, &fi, false).decision,
codec_streaming_reader_gate_for_test(&None, &object_info, &fi, false).decision,
GetCodecStreamingDecision::Fallback(GetCodecStreamingFallbackReason::LockOptimizationDisabled)
);
@@ -3410,14 +3670,14 @@ mod tests {
end: 1,
});
assert_eq!(
get_codec_streaming_reader_gate(&range, &object_info, &fi, true).decision,
codec_streaming_reader_gate_for_test(&range, &object_info, &fi, true).decision,
GetCodecStreamingDecision::Fallback(GetCodecStreamingFallbackReason::Range)
);
let multipart_fi = codec_streaming_test_fileinfo(1024, 2);
let multipart_object_info = codec_streaming_test_object_info(&multipart_fi);
assert_eq!(
get_codec_streaming_reader_gate(&None, &multipart_object_info, &multipart_fi, true).decision,
codec_streaming_reader_gate_for_test(&None, &multipart_object_info, &multipart_fi, true).decision,
GetCodecStreamingDecision::Fallback(GetCodecStreamingFallbackReason::Multipart)
);
@@ -3427,7 +3687,7 @@ mod tests {
.insert("x-amz-server-side-encryption".to_string(), "AES256".to_string());
let encrypted = codec_streaming_test_object_info(&encrypted_fi);
assert_eq!(
get_codec_streaming_reader_gate(&None, &encrypted, &encrypted_fi, true).decision,
codec_streaming_reader_gate_for_test(&None, &encrypted, &encrypted_fi, true).decision,
GetCodecStreamingDecision::Fallback(GetCodecStreamingFallbackReason::Encrypted)
);
@@ -3435,14 +3695,14 @@ mod tests {
insert_str(&mut compressed_fi.metadata, SUFFIX_COMPRESSION, "lz4".to_string());
let compressed = codec_streaming_test_object_info(&compressed_fi);
assert_eq!(
get_codec_streaming_reader_gate(&None, &compressed, &compressed_fi, true).decision,
codec_streaming_reader_gate_for_test(&None, &compressed, &compressed_fi, true).decision,
GetCodecStreamingDecision::Fallback(GetCodecStreamingFallbackReason::Compressed)
);
let small_fi = codec_streaming_test_fileinfo(0, 1);
let small_object_info = codec_streaming_test_object_info(&small_fi);
assert_eq!(
get_codec_streaming_reader_gate(&None, &small_object_info, &small_fi, true).decision,
codec_streaming_reader_gate_for_test(&None, &small_object_info, &small_fi, true).decision,
GetCodecStreamingDecision::Fallback(GetCodecStreamingFallbackReason::BelowMinSize)
);
@@ -3450,7 +3710,7 @@ mod tests {
remote_fi.transition_status = crate::bucket::lifecycle::lifecycle::TRANSITION_COMPLETE.to_string();
let remote = codec_streaming_test_object_info(&remote_fi);
assert_eq!(
get_codec_streaming_reader_gate(&None, &remote, &remote_fi, true).decision,
codec_streaming_reader_gate_for_test(&None, &remote, &remote_fi, true).decision,
GetCodecStreamingDecision::Fallback(GetCodecStreamingFallbackReason::Remote)
);
},
@@ -3488,13 +3748,86 @@ mod tests {
let object_info = codec_streaming_test_object_info(&fi);
assert_eq!(
get_codec_streaming_reader_gate(&None, &object_info, &fi, true).decision,
codec_streaming_reader_gate_for_test(&None, &object_info, &fi, true).decision,
GetCodecStreamingDecision::Fallback(GetCodecStreamingFallbackReason::Disabled)
);
},
);
}
#[test]
fn codec_streaming_reader_gate_allows_multipart_when_explicitly_enabled() {
temp_env::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")),
],
|| {
let fi = codec_streaming_test_fileinfo(1024, 2);
let object_info = codec_streaming_test_object_info(&fi);
let gate = codec_streaming_reader_gate_for_test(&None, &object_info, &fi, true);
assert_eq!(gate.object_class, GetCodecStreamingObjectClass::Multipart);
assert_eq!(gate.decision, GetCodecStreamingDecision::Use);
},
);
}
#[test]
fn codec_streaming_reader_gate_keeps_multipart_default_off() {
temp_env::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, None),
(ENV_RUSTFS_GET_CODEC_STREAMING_MIN_SIZE, Some("1")),
],
|| {
let fi = codec_streaming_test_fileinfo(1024, 2);
let object_info = codec_streaming_test_object_info(&fi);
let gate = codec_streaming_reader_gate_for_test(&None, &object_info, &fi, true);
assert_eq!(gate.object_class, GetCodecStreamingObjectClass::Multipart);
assert_eq!(
gate.decision,
GetCodecStreamingDecision::Fallback(GetCodecStreamingFallbackReason::Multipart)
);
},
);
}
#[test]
fn codec_streaming_reader_gate_limits_multipart_part_count() {
temp_env::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_MULTIPART_MAX_PARTS, Some("1")),
(ENV_RUSTFS_GET_CODEC_STREAMING_MIN_SIZE, Some("1")),
],
|| {
let fi = codec_streaming_test_fileinfo(1024, 2);
let object_info = codec_streaming_test_object_info(&fi);
let gate = codec_streaming_reader_gate_for_test(&None, &object_info, &fi, true);
assert_eq!(gate.object_class, GetCodecStreamingObjectClass::Multipart);
assert_eq!(
gate.decision,
GetCodecStreamingDecision::Fallback(GetCodecStreamingFallbackReason::MultipartPartLimit)
);
},
);
}
#[test]
fn codec_streaming_decode_engine_builder_selects_rustfs() {
temp_env::with_var(ENV_RUSTFS_GET_CODEC_STREAMING_ENGINE, Some(GET_CODEC_STREAMING_ENGINE_RUSTFS), || {
@@ -3526,6 +3859,10 @@ mod tests {
fn codec_streaming_fallback_metric_labels_are_stable() {
assert_eq!(GetCodecStreamingFallbackReason::Disabled.as_str(), "disabled");
assert_eq!(GetCodecStreamingFallbackReason::RolloutNotOptedIn.as_str(), "rollout_not_opted_in");
assert_eq!(
GetCodecStreamingFallbackReason::RolloutPctNotSelected.as_str(),
"rollout_pct_not_selected"
);
assert_eq!(
GetCodecStreamingFallbackReason::BodyCompatibilityUnconfirmed.as_str(),
"body_compatibility_unconfirmed"
@@ -3546,6 +3883,7 @@ mod tests {
assert_eq!(GetCodecStreamingFallbackReason::Multipart.as_str(), "multipart");
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!(GetCodecStreamingObjectClass::PlainSinglePart.as_str(), "plain_single_part");
assert_eq!(GetCodecStreamingObjectClass::Range.as_str(), "range");
assert_eq!(GetCodecStreamingObjectClass::Encrypted.as_str(), "encrypted");
@@ -3569,7 +3907,7 @@ mod tests {
let object_info = codec_streaming_test_object_info(&fi);
assert_eq!(
get_codec_streaming_reader_gate(&None, &object_info, &fi, true).decision,
codec_streaming_reader_gate_for_test(&None, &object_info, &fi, true).decision,
GetCodecStreamingDecision::Fallback(GetCodecStreamingFallbackReason::Disabled)
);
},
@@ -3591,7 +3929,7 @@ mod tests {
let object_info = codec_streaming_test_object_info(&fi);
assert_eq!(
get_codec_streaming_reader_gate(&None, &object_info, &fi, true).decision,
codec_streaming_reader_gate_for_test(&None, &object_info, &fi, true).decision,
GetCodecStreamingDecision::Fallback(GetCodecStreamingFallbackReason::RolloutNotOptedIn)
);
},
@@ -3610,7 +3948,7 @@ mod tests {
let object_info = codec_streaming_test_object_info(&fi);
assert_eq!(
get_codec_streaming_reader_gate(&None, &object_info, &fi, true).decision,
codec_streaming_reader_gate_for_test(&None, &object_info, &fi, true).decision,
GetCodecStreamingDecision::Fallback(GetCodecStreamingFallbackReason::BodyCompatibilityUnconfirmed)
);
},
@@ -3629,13 +3967,56 @@ mod tests {
let object_info = codec_streaming_test_object_info(&fi);
assert_eq!(
get_codec_streaming_reader_gate(&None, &object_info, &fi, true).decision,
codec_streaming_reader_gate_for_test(&None, &object_info, &fi, true).decision,
GetCodecStreamingDecision::Fallback(GetCodecStreamingFallbackReason::HeaderCompatibilityUnconfirmed)
);
},
);
}
#[test]
fn codec_streaming_reader_gate_honors_rollout_percentage() {
temp_env::with_vars(
[
(ENV_RUSTFS_GET_CODEC_STREAMING_ENABLE, Some("true")),
(ENV_RUSTFS_GET_CODEC_STREAMING_ROLLOUT, Some("benchmark")),
(ENV_RUSTFS_GET_CODEC_STREAMING_ROLLOUT_PCT, Some("0")),
(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_MIN_SIZE, Some("1")),
],
|| {
let fi = codec_streaming_test_fileinfo(1024, 1);
let object_info = codec_streaming_test_object_info(&fi);
assert_eq!(
codec_streaming_reader_gate_for_test(&None, &object_info, &fi, true).decision,
GetCodecStreamingDecision::Fallback(GetCodecStreamingFallbackReason::RolloutPctNotSelected)
);
},
);
temp_env::with_vars(
[
(ENV_RUSTFS_GET_CODEC_STREAMING_ENABLE, Some("true")),
(ENV_RUSTFS_GET_CODEC_STREAMING_ROLLOUT, Some("benchmark")),
(ENV_RUSTFS_GET_CODEC_STREAMING_ROLLOUT_PCT, Some("100")),
(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_MIN_SIZE, Some("1")),
],
|| {
let fi = codec_streaming_test_fileinfo(1024, 1);
let object_info = codec_streaming_test_object_info(&fi);
assert_eq!(
codec_streaming_reader_gate_for_test(&None, &object_info, &fi, true).decision,
GetCodecStreamingDecision::Use
);
},
);
}
#[test]
fn codec_streaming_reader_gate_records_object_classes() {
temp_env::with_vars(
@@ -3650,7 +4031,7 @@ mod tests {
let fi = codec_streaming_test_fileinfo(1024, 1);
let object_info = codec_streaming_test_object_info(&fi);
assert_eq!(
get_codec_streaming_reader_gate(&None, &object_info, &fi, true).object_class,
codec_streaming_reader_gate_for_test(&None, &object_info, &fi, true).object_class,
GetCodecStreamingObjectClass::PlainSinglePart
);
@@ -3660,7 +4041,7 @@ mod tests {
end: 1,
});
assert_eq!(
get_codec_streaming_reader_gate(&range, &object_info, &fi, true).object_class,
codec_streaming_reader_gate_for_test(&range, &object_info, &fi, true).object_class,
GetCodecStreamingObjectClass::Range
);
},