mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-08 14:23:13 +00:00
fix(get): remove GET chunk fast path (#2507)
This commit is contained in:
@@ -18,8 +18,6 @@ mod get_object_flow;
|
||||
mod get_object_zero_copy;
|
||||
mod put_object_extract;
|
||||
mod put_object_flow;
|
||||
#[cfg(test)]
|
||||
mod zero_copy_tests;
|
||||
use self::get_object_flow::GetObjectBootstrap;
|
||||
|
||||
use crate::app::context::{AppContext, default_notify_interface, get_global_app_context};
|
||||
|
||||
@@ -14,20 +14,14 @@
|
||||
|
||||
use super::DeadlockRequestGuard;
|
||||
use super::GetObjectRequestContext;
|
||||
use super::get_object_zero_copy::{
|
||||
GetObjectIoPlanning, GetObjectPreparedRead, prepare_get_object_read, prepare_get_object_read_execution,
|
||||
};
|
||||
use super::get_object_zero_copy::{GetObjectIoPlanning, GetObjectPreparedRead, prepare_get_object_read_execution};
|
||||
use crate::error::ApiError;
|
||||
use crate::storage::concurrency::{ConcurrencyManager, GetObjectGuard, get_buffer_size_opt_in};
|
||||
use crate::storage::get_validated_store;
|
||||
use crate::storage::options::filter_object_metadata;
|
||||
use crate::storage::timeout_wrapper::{RequestTimeoutWrapper, TimeoutConfig};
|
||||
use bytes::Bytes;
|
||||
use futures_util::StreamExt;
|
||||
use rustfs_ecstore::bucket::versioning_sys::BucketVersioningSys;
|
||||
use rustfs_ecstore::error::StorageError;
|
||||
use rustfs_ecstore::store_api::{HTTPRangeSpec, ObjectInfo};
|
||||
use rustfs_io_core::BoxChunkStream;
|
||||
use rustfs_object_io::get::{
|
||||
GetObjectBodyPlan as ObjectIoGetObjectBodyPlan, GetObjectBodyPlanningInputs as ObjectIoGetObjectBodyPlanningInputs,
|
||||
GetObjectBodySource, GetObjectDataPlaneMetricContract as ObjectIoGetObjectDataPlaneMetricContract, GetObjectFlowResult,
|
||||
@@ -35,8 +29,6 @@ use rustfs_object_io::get::{
|
||||
build_cors_wrapped_get_object_flow_result as object_io_build_cors_wrapped_get_object_flow_result,
|
||||
build_get_object_checksums as object_io_build_get_object_checksums,
|
||||
build_get_object_output_context as object_io_build_get_object_output_context,
|
||||
build_memory_blob as object_io_build_memory_blob, chunk_body_data_plane_labels as object_io_chunk_body_data_plane_labels,
|
||||
get_object_chunk_path_label as object_io_get_object_chunk_path_label,
|
||||
materialize_get_object_body as object_io_materialize_get_object_body, plan_get_object_body as object_io_plan_get_object_body,
|
||||
plan_get_object_strategy_layout as object_io_plan_get_object_strategy_layout,
|
||||
};
|
||||
@@ -54,95 +46,6 @@ pub(super) struct GetObjectBootstrap {
|
||||
pub(super) _deadlock_request_guard: DeadlockRequestGuard,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ChunkCommitMaterializationError {
|
||||
source: std::io::Error,
|
||||
streamed_bytes: usize,
|
||||
}
|
||||
|
||||
fn build_chunk_materialization_length_error(actual: usize, expected: usize) -> std::io::Error {
|
||||
let error_kind = if actual > expected {
|
||||
std::io::ErrorKind::InvalidData
|
||||
} else {
|
||||
std::io::ErrorKind::UnexpectedEof
|
||||
};
|
||||
|
||||
std::io::Error::new(
|
||||
error_kind,
|
||||
format!("chunk fast path produced {actual} bytes before response commit, expected {expected}"),
|
||||
)
|
||||
}
|
||||
|
||||
async fn materialize_chunk_stream_before_commit_with_threshold(
|
||||
mut chunk_stream: BoxChunkStream,
|
||||
response_content_length: i64,
|
||||
optimal_buffer_size: usize,
|
||||
in_memory_threshold_bytes: usize,
|
||||
) -> Result<Option<StreamingBlob>, ChunkCommitMaterializationError> {
|
||||
let expected_bytes = usize::try_from(response_content_length).map_err(|_| ChunkCommitMaterializationError {
|
||||
source: std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("negative response content length {response_content_length} for chunk fast path"),
|
||||
),
|
||||
streamed_bytes: 0,
|
||||
})?;
|
||||
|
||||
// Objects larger than the in-memory threshold fall back to the legacy reader path
|
||||
// rather than spooling to disk, to avoid exhausting local disk under concurrent large downloads.
|
||||
if expected_bytes > in_memory_threshold_bytes {
|
||||
return Err(ChunkCommitMaterializationError {
|
||||
source: std::io::Error::other(format!(
|
||||
"chunk fast path object size {expected_bytes} exceeds in-memory threshold \
|
||||
{in_memory_threshold_bytes}; falling back to legacy reader"
|
||||
)),
|
||||
streamed_bytes: 0,
|
||||
});
|
||||
}
|
||||
|
||||
let mut buf = Vec::with_capacity(expected_bytes);
|
||||
let mut streamed_bytes = 0usize;
|
||||
|
||||
while let Some(result) = chunk_stream.next().await {
|
||||
let chunk = result.map_err(|source| ChunkCommitMaterializationError { source, streamed_bytes })?;
|
||||
let bytes = chunk.as_bytes();
|
||||
streamed_bytes = streamed_bytes.saturating_add(bytes.len());
|
||||
if streamed_bytes > expected_bytes {
|
||||
return Err(ChunkCommitMaterializationError {
|
||||
source: build_chunk_materialization_length_error(streamed_bytes, expected_bytes),
|
||||
streamed_bytes,
|
||||
});
|
||||
}
|
||||
buf.extend_from_slice(bytes.as_ref());
|
||||
}
|
||||
|
||||
if streamed_bytes != expected_bytes {
|
||||
return Err(ChunkCommitMaterializationError {
|
||||
source: build_chunk_materialization_length_error(streamed_bytes, expected_bytes),
|
||||
streamed_bytes,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(object_io_build_memory_blob(
|
||||
Bytes::from(buf),
|
||||
response_content_length,
|
||||
optimal_buffer_size,
|
||||
))
|
||||
}
|
||||
|
||||
async fn materialize_chunk_stream_before_commit(
|
||||
chunk_stream: BoxChunkStream,
|
||||
response_content_length: i64,
|
||||
optimal_buffer_size: usize,
|
||||
) -> Result<Option<StreamingBlob>, ChunkCommitMaterializationError> {
|
||||
materialize_chunk_stream_before_commit_with_threshold(
|
||||
chunk_stream,
|
||||
response_content_length,
|
||||
optimal_buffer_size,
|
||||
rustfs_config::DEFAULT_OBJECT_SEEK_SUPPORT_THRESHOLD,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn build_get_object_body_adapter<R>(
|
||||
final_stream: R,
|
||||
bucket: &str,
|
||||
@@ -340,14 +243,62 @@ pub(super) async fn build_get_object_output_context(
|
||||
let bucket = &request_context.bucket;
|
||||
let key = &request_context.key;
|
||||
let part_number = request_context.part_number;
|
||||
let mut active_read_setup = read_setup;
|
||||
let GetObjectReadSetup {
|
||||
info,
|
||||
event_info,
|
||||
body_source,
|
||||
rs,
|
||||
content_type,
|
||||
last_modified,
|
||||
response_content_length,
|
||||
content_range,
|
||||
server_side_encryption,
|
||||
sse_customer_algorithm,
|
||||
sse_customer_key_md5,
|
||||
ssekms_key_id,
|
||||
encryption_applied,
|
||||
} = read_setup;
|
||||
|
||||
loop {
|
||||
let GetObjectReadSetup {
|
||||
let optimal_buffer_size = finalize_get_object_strategy_runtime(
|
||||
request_context,
|
||||
rs.as_ref(),
|
||||
manager,
|
||||
base_buffer_size,
|
||||
&info,
|
||||
response_content_length,
|
||||
io_planning,
|
||||
);
|
||||
|
||||
let GetObjectBodySource::Reader(final_stream) = body_source;
|
||||
|
||||
let body = build_get_object_body_adapter(
|
||||
final_stream,
|
||||
bucket,
|
||||
key,
|
||||
response_content_length,
|
||||
optimal_buffer_size,
|
||||
ObjectIoGetObjectBodyPlanningInputs {
|
||||
is_part_request: part_number.is_some(),
|
||||
is_range_request: rs.is_some(),
|
||||
encryption_applied,
|
||||
response_size: response_content_length,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let metric_contract = ObjectIoGetObjectDataPlaneMetricContract::disk(
|
||||
rustfs_io_metrics::IoPath::Legacy,
|
||||
rustfs_io_metrics::CopyMode::SingleCopy,
|
||||
);
|
||||
|
||||
let checksums = object_io_build_get_object_checksums(&info, &request_context.headers, part_number, rs.as_ref())
|
||||
.map_err(ApiError::from)?;
|
||||
let filtered_metadata = filter_object_metadata(&info.user_defined);
|
||||
|
||||
Ok((
|
||||
object_io_build_get_object_output_context(
|
||||
body,
|
||||
info,
|
||||
event_info,
|
||||
body_source,
|
||||
rs,
|
||||
content_type,
|
||||
last_modified,
|
||||
response_content_length,
|
||||
@@ -356,108 +307,14 @@ pub(super) async fn build_get_object_output_context(
|
||||
sse_customer_algorithm,
|
||||
sse_customer_key_md5,
|
||||
ssekms_key_id,
|
||||
encryption_applied,
|
||||
} = active_read_setup;
|
||||
|
||||
let optimal_buffer_size = finalize_get_object_strategy_runtime(
|
||||
request_context,
|
||||
rs.as_ref(),
|
||||
manager,
|
||||
base_buffer_size,
|
||||
&info,
|
||||
response_content_length,
|
||||
io_planning,
|
||||
);
|
||||
|
||||
let (body, metric_contract) = match body_source {
|
||||
GetObjectBodySource::Reader(final_stream) => {
|
||||
let body = build_get_object_body_adapter(
|
||||
final_stream,
|
||||
bucket,
|
||||
key,
|
||||
response_content_length,
|
||||
optimal_buffer_size,
|
||||
ObjectIoGetObjectBodyPlanningInputs {
|
||||
is_part_request: part_number.is_some(),
|
||||
is_range_request: rs.is_some(),
|
||||
encryption_applied,
|
||||
response_size: response_content_length,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let metric_contract = ObjectIoGetObjectDataPlaneMetricContract::disk(
|
||||
rustfs_io_metrics::IoPath::Legacy,
|
||||
rustfs_io_metrics::CopyMode::SingleCopy,
|
||||
);
|
||||
|
||||
(body, metric_contract)
|
||||
}
|
||||
GetObjectBodySource::Chunk {
|
||||
stream: chunk_stream,
|
||||
path,
|
||||
copy_mode,
|
||||
} => {
|
||||
let (io_path, copy_mode) = object_io_chunk_body_data_plane_labels(path, copy_mode);
|
||||
match materialize_chunk_stream_before_commit(chunk_stream, response_content_length, optimal_buffer_size).await {
|
||||
Ok(body) => (body, ObjectIoGetObjectDataPlaneMetricContract::disk(io_path, copy_mode)),
|
||||
Err(err) => {
|
||||
let path_label = object_io_get_object_chunk_path_label(path);
|
||||
rustfs_io_metrics::record_io_fallback(
|
||||
rustfs_io_metrics::IoStage::ReadSetup,
|
||||
rustfs_io_metrics::FallbackReason::ProbeFailed,
|
||||
);
|
||||
rustfs_io_metrics::record_get_object_fast_path_probe_failed(
|
||||
path_label,
|
||||
copy_mode,
|
||||
response_content_length,
|
||||
);
|
||||
warn!(
|
||||
bucket = %request_context.bucket,
|
||||
key = %request_context.key,
|
||||
version_id = ?request_context.opts.version_id,
|
||||
path = path_label,
|
||||
copy_mode = copy_mode.as_str(),
|
||||
promised_bytes = response_content_length,
|
||||
materialized_bytes = err.streamed_bytes,
|
||||
error = %err.source,
|
||||
"GetObject chunk fast path full-body materialization failed before response commit"
|
||||
);
|
||||
|
||||
let store = get_validated_store(&request_context.bucket).await?;
|
||||
active_read_setup =
|
||||
prepare_get_object_read(request_context, &store, manager, std::time::Instant::now()).await?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let checksums = object_io_build_get_object_checksums(&info, &request_context.headers, part_number, rs.as_ref())
|
||||
.map_err(ApiError::from)?;
|
||||
let filtered_metadata = filter_object_metadata(&info.user_defined);
|
||||
|
||||
return Ok((
|
||||
object_io_build_get_object_output_context(
|
||||
body,
|
||||
info,
|
||||
event_info,
|
||||
content_type,
|
||||
last_modified,
|
||||
response_content_length,
|
||||
content_range,
|
||||
server_side_encryption,
|
||||
sse_customer_algorithm,
|
||||
sse_customer_key_md5,
|
||||
ssekms_key_id,
|
||||
&checksums,
|
||||
filtered_metadata,
|
||||
versioned,
|
||||
optimal_buffer_size,
|
||||
Some(metric_contract.copy_mode),
|
||||
),
|
||||
metric_contract,
|
||||
));
|
||||
}
|
||||
&checksums,
|
||||
filtered_metadata,
|
||||
versioned,
|
||||
optimal_buffer_size,
|
||||
Some(metric_contract.copy_mode),
|
||||
),
|
||||
metric_contract,
|
||||
))
|
||||
}
|
||||
|
||||
pub(super) async fn run_get_object_flow(
|
||||
@@ -498,10 +355,8 @@ pub(super) async fn run_get_object_flow(
|
||||
mod tests {
|
||||
use super::get_object_strategy_range;
|
||||
use super::*;
|
||||
use futures_util::StreamExt;
|
||||
use http::HeaderMap;
|
||||
use rustfs_ecstore::store_api::ObjectOptions;
|
||||
use rustfs_io_core::IoChunk;
|
||||
|
||||
fn sample_range(start: i64, end: i64) -> HTTPRangeSpec {
|
||||
HTTPRangeSpec {
|
||||
@@ -545,84 +400,4 @@ mod tests {
|
||||
assert_eq!(strategy_range.start, 0);
|
||||
assert_eq!(strategy_range.end, 511);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn materialize_chunk_stream_before_commit_buffers_small_payload_in_memory() {
|
||||
let chunk_stream: BoxChunkStream = Box::pin(futures_util::stream::iter(vec![
|
||||
Ok(IoChunk::Shared(bytes::Bytes::from_static(b"hello"))),
|
||||
Ok(IoChunk::Shared(bytes::Bytes::from_static(b" world"))),
|
||||
]));
|
||||
|
||||
let mut body = materialize_chunk_stream_before_commit_with_threshold(chunk_stream, 11, 8 * 1024, 1024)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let mut collected = Vec::new();
|
||||
while let Some(chunk) = body.next().await {
|
||||
collected.extend_from_slice(&chunk.unwrap());
|
||||
}
|
||||
|
||||
assert_eq!(collected, b"hello world");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn materialize_chunk_stream_before_commit_falls_back_for_large_payload() {
|
||||
let chunk_stream: BoxChunkStream = Box::pin(futures_util::stream::iter(vec![
|
||||
Ok(IoChunk::Shared(bytes::Bytes::from_static(b"hello"))),
|
||||
Ok(IoChunk::Shared(bytes::Bytes::from_static(b" world"))),
|
||||
]));
|
||||
|
||||
// When payload exceeds the in-memory threshold an error is returned so the
|
||||
// caller can fall back to the legacy reader path rather than spooling to disk.
|
||||
let err = materialize_chunk_stream_before_commit_with_threshold(chunk_stream, 11, 8 * 1024, 4)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(err.streamed_bytes, 0);
|
||||
assert_eq!(err.source.kind(), std::io::ErrorKind::Other);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn materialize_chunk_stream_before_commit_rejects_short_body() {
|
||||
let chunk_stream: BoxChunkStream =
|
||||
Box::pin(futures_util::stream::iter(vec![Ok(IoChunk::Shared(bytes::Bytes::from_static(b"hello")))]));
|
||||
|
||||
let err = materialize_chunk_stream_before_commit_with_threshold(chunk_stream, 11, 8 * 1024, 1024)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(err.streamed_bytes, 5);
|
||||
assert_eq!(err.source.kind(), std::io::ErrorKind::UnexpectedEof);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn materialize_chunk_stream_before_commit_rejects_long_body() {
|
||||
let chunk_stream: BoxChunkStream = Box::pin(futures_util::stream::iter(vec![
|
||||
Ok(IoChunk::Shared(bytes::Bytes::from_static(b"hello "))),
|
||||
Ok(IoChunk::Shared(bytes::Bytes::from_static(b"world!"))),
|
||||
]));
|
||||
|
||||
let err = materialize_chunk_stream_before_commit_with_threshold(chunk_stream, 11, 8 * 1024, 1024)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(err.streamed_bytes, 12);
|
||||
assert_eq!(err.source.kind(), std::io::ErrorKind::InvalidData);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn materialize_chunk_stream_before_commit_preserves_midstream_io_errors() {
|
||||
let chunk_stream: BoxChunkStream = Box::pin(futures_util::stream::iter(vec![
|
||||
Ok(IoChunk::Shared(bytes::Bytes::from_static(b"hello"))),
|
||||
Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "writer closed")),
|
||||
]));
|
||||
|
||||
let err = materialize_chunk_stream_before_commit_with_threshold(chunk_stream, 11, 8 * 1024, 1024)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(err.streamed_bytes, 5);
|
||||
assert_eq!(err.source.kind(), std::io::ErrorKind::BrokenPipe);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,56 +20,18 @@ use crate::storage::{
|
||||
DecryptionRequest, check_preconditions, get_validated_store, sse_decryption, validate_sse_headers_for_read,
|
||||
validate_ssec_for_read,
|
||||
};
|
||||
use futures_util::{StreamExt, stream};
|
||||
use http::HeaderMap;
|
||||
use rustfs_concurrency::GetObjectQueueSnapshot;
|
||||
use rustfs_ecstore::store_api::{ObjectIO, ObjectOperations};
|
||||
use rustfs_io_core::{BoxChunkStream, IoChunk};
|
||||
use rustfs_ecstore::store_api::ObjectIO;
|
||||
use rustfs_object_io::get::{
|
||||
ChunkReadDecision, ChunkReadPlanError, GetObjectEncryptionState as ObjectIoGetObjectEncryptionState, GetObjectReadSetup,
|
||||
build_reader_read_setup as object_io_build_reader_read_setup,
|
||||
finalize_chunk_read_setup as object_io_finalize_chunk_read_setup,
|
||||
get_object_chunk_fast_path_guard as object_io_get_object_chunk_fast_path_guard,
|
||||
get_object_chunk_path_label as object_io_get_object_chunk_path_label, map_chunk_copy_mode as object_io_map_chunk_copy_mode,
|
||||
plan_chunk_read as object_io_plan_chunk_read, plan_legacy_read as object_io_plan_legacy_read,
|
||||
GetObjectEncryptionState as ObjectIoGetObjectEncryptionState, GetObjectReadSetup,
|
||||
build_reader_read_setup as object_io_build_reader_read_setup, plan_legacy_read as object_io_plan_legacy_read,
|
||||
};
|
||||
use rustfs_rio::{Reader, WarpReader};
|
||||
use s3s::{S3Error, S3ErrorCode, S3Result, s3_error};
|
||||
use s3s::{S3Result, s3_error};
|
||||
use std::time::Duration;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
fn get_object_chunk_fast_path_enabled() -> bool {
|
||||
rustfs_utils::get_env_bool(
|
||||
rustfs_config::ENV_OBJECT_GET_CHUNK_FAST_PATH_ENABLE,
|
||||
rustfs_config::DEFAULT_OBJECT_GET_CHUNK_FAST_PATH_ENABLE,
|
||||
)
|
||||
}
|
||||
|
||||
async fn probe_chunk_stream_before_commit(
|
||||
mut chunk_stream: BoxChunkStream,
|
||||
response_content_length: i64,
|
||||
) -> Result<BoxChunkStream, rustfs_io_metrics::FallbackReason> {
|
||||
if response_content_length <= 0 {
|
||||
return Ok(chunk_stream);
|
||||
}
|
||||
|
||||
let mut prefetched = Vec::new();
|
||||
|
||||
loop {
|
||||
match chunk_stream.next().await {
|
||||
Some(Ok(chunk)) => {
|
||||
let chunk_len = chunk.len();
|
||||
prefetched.push(chunk);
|
||||
if chunk_len > 0 {
|
||||
let prefix = stream::iter(prefetched.into_iter().map(Ok::<IoChunk, std::io::Error>));
|
||||
return Ok(Box::pin(prefix.chain(chunk_stream)));
|
||||
}
|
||||
}
|
||||
Some(Err(_)) | None => return Err(rustfs_io_metrics::FallbackReason::ProbeFailed),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct GetObjectIoPlanning<'a> {
|
||||
pub(super) _disk_permit: tokio::sync::SemaphorePermit<'a>,
|
||||
pub(super) permit_wait_duration: Duration,
|
||||
@@ -262,194 +224,7 @@ pub(super) async fn prepare_get_object_read_execution<'a>(
|
||||
let io_planning =
|
||||
acquire_get_object_io_planning(manager, wrapper, timeout_config, &request_context.bucket, &request_context.key).await?;
|
||||
let store = get_validated_store(&request_context.bucket).await?;
|
||||
|
||||
let read_start = std::time::Instant::now();
|
||||
let read_setup = if !get_object_chunk_fast_path_enabled() {
|
||||
rustfs_io_metrics::record_io_fallback(
|
||||
rustfs_io_metrics::IoStage::ReadSetup,
|
||||
rustfs_io_metrics::FallbackReason::FeatureDisabled,
|
||||
);
|
||||
prepare_get_object_read(request_context, &store, manager, read_start).await?
|
||||
} else {
|
||||
match object_io_get_object_chunk_fast_path_guard(
|
||||
request_context.sse_customer_key.is_some(),
|
||||
request_context.sse_customer_key_md5.is_some(),
|
||||
) {
|
||||
Ok(()) => match prepare_get_object_chunk_read(request_context, &store, manager, read_start).await? {
|
||||
Some(read_setup) => read_setup,
|
||||
None => prepare_get_object_read(request_context, &store, manager, read_start).await?,
|
||||
},
|
||||
Err(fallback) => {
|
||||
rustfs_io_metrics::record_io_fallback(fallback.stage, fallback.reason);
|
||||
prepare_get_object_read(request_context, &store, manager, read_start).await?
|
||||
}
|
||||
}
|
||||
};
|
||||
let read_setup = prepare_get_object_read(request_context, &store, manager, std::time::Instant::now()).await?;
|
||||
|
||||
Ok(GetObjectPreparedRead { io_planning, read_setup })
|
||||
}
|
||||
|
||||
pub(super) async fn prepare_get_object_chunk_read(
|
||||
request_context: &GetObjectRequestContext,
|
||||
store: &rustfs_ecstore::store::ECStore,
|
||||
manager: &ConcurrencyManager,
|
||||
read_start: std::time::Instant,
|
||||
) -> S3Result<Option<GetObjectReadSetup>> {
|
||||
let info = store
|
||||
.get_object_info(&request_context.bucket, &request_context.key, &request_context.opts)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
validate_sse_headers_for_read(&info.user_defined, &request_context.headers)?;
|
||||
validate_ssec_for_read(
|
||||
&info.user_defined,
|
||||
request_context.sse_customer_key.as_ref(),
|
||||
request_context.sse_customer_key_md5.as_ref(),
|
||||
)?;
|
||||
check_preconditions(&request_context.headers, &info)?;
|
||||
|
||||
let encrypted_object = info.user_defined.contains_key("x-rustfs-encryption-key")
|
||||
|| info
|
||||
.user_defined
|
||||
.contains_key("x-amz-server-side-encryption-customer-algorithm");
|
||||
if encrypted_object {
|
||||
rustfs_io_metrics::record_io_fallback(
|
||||
rustfs_io_metrics::IoStage::ReadSetup,
|
||||
rustfs_io_metrics::FallbackReason::EncryptionEnabled,
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let plan = match object_io_plan_chunk_read(
|
||||
&info,
|
||||
request_context.opts.version_id.is_none(),
|
||||
request_context.rs.clone(),
|
||||
request_context.part_number,
|
||||
) {
|
||||
Ok(ChunkReadDecision::Eligible(plan)) => plan,
|
||||
Ok(ChunkReadDecision::Fallback(fallback)) => {
|
||||
rustfs_io_metrics::record_io_fallback(fallback.stage, fallback.reason);
|
||||
return Ok(None);
|
||||
}
|
||||
Err(ChunkReadPlanError::NoSuchKey) => return Err(S3Error::new(S3ErrorCode::NoSuchKey)),
|
||||
Err(ChunkReadPlanError::MethodNotAllowed) => return Err(S3Error::new(S3ErrorCode::MethodNotAllowed)),
|
||||
Err(ChunkReadPlanError::Io(err)) => return Err(ApiError::from(err).into()),
|
||||
};
|
||||
let rs = plan.rs.clone();
|
||||
let response_content_length = plan.response_content_length;
|
||||
|
||||
let read_duration = read_start.elapsed();
|
||||
manager.record_disk_operation(info.size as u64, read_duration, true).await;
|
||||
let event_info = info.clone();
|
||||
|
||||
let chunk_result = match store
|
||||
.get_object_chunks(
|
||||
&request_context.bucket,
|
||||
&request_context.key,
|
||||
rs.clone(),
|
||||
HeaderMap::new(),
|
||||
&request_context.opts,
|
||||
)
|
||||
.await
|
||||
.map_err(ApiError::from)
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(_err) => {
|
||||
rustfs_io_metrics::record_io_fallback(
|
||||
rustfs_io_metrics::IoStage::HttpBridge,
|
||||
rustfs_io_metrics::FallbackReason::ChunkBridgeUnavailable,
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
let path_label = object_io_get_object_chunk_path_label(chunk_result.path);
|
||||
let copy_mode = object_io_map_chunk_copy_mode(chunk_result.copy_mode);
|
||||
let chunk_result = match probe_chunk_stream_before_commit(chunk_result.stream, response_content_length).await {
|
||||
Ok(stream) => rustfs_ecstore::store_api::GetObjectChunkResult {
|
||||
stream,
|
||||
path: chunk_result.path,
|
||||
copy_mode: chunk_result.copy_mode,
|
||||
},
|
||||
Err(reason) => {
|
||||
rustfs_io_metrics::record_io_fallback(rustfs_io_metrics::IoStage::ReadSetup, reason);
|
||||
rustfs_io_metrics::record_get_object_fast_path_probe_failed(path_label, copy_mode, response_content_length);
|
||||
warn!(
|
||||
bucket = %request_context.bucket,
|
||||
key = %request_context.key,
|
||||
version_id = ?request_context.opts.version_id,
|
||||
path = path_label,
|
||||
copy_mode = copy_mode.as_str(),
|
||||
promised_bytes = response_content_length,
|
||||
fallback_reason = reason.as_str(),
|
||||
"GetObject chunk fast path probe failed before response commit"
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
let setup_result = object_io_finalize_chunk_read_setup(info, event_info, chunk_result, plan);
|
||||
rustfs_io_metrics::record_get_object_fast_path_selected(path_label, copy_mode, response_content_length);
|
||||
rustfs_io_metrics::record_io_path_selected("get", setup_result.io_path);
|
||||
|
||||
Ok(Some(setup_result.read_setup))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{get_object_chunk_fast_path_enabled, probe_chunk_stream_before_commit};
|
||||
use bytes::Bytes;
|
||||
use futures_util::{StreamExt, stream};
|
||||
use rustfs_io_core::{BoxChunkStream, IoChunk};
|
||||
|
||||
#[test]
|
||||
fn get_object_chunk_fast_path_defaults_to_disabled() {
|
||||
temp_env::with_var_unset(rustfs_config::ENV_OBJECT_GET_CHUNK_FAST_PATH_ENABLE, || {
|
||||
assert!(!get_object_chunk_fast_path_enabled());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_object_chunk_fast_path_can_be_explicitly_enabled() {
|
||||
temp_env::with_var(rustfs_config::ENV_OBJECT_GET_CHUNK_FAST_PATH_ENABLE, Some("true"), || {
|
||||
assert!(get_object_chunk_fast_path_enabled());
|
||||
});
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn probe_chunk_stream_before_commit_preserves_prefetched_payload() {
|
||||
let stream: BoxChunkStream = Box::pin(stream::iter(vec![
|
||||
Ok(IoChunk::Shared(Bytes::from_static(b"hello "))),
|
||||
Ok(IoChunk::Shared(Bytes::from_static(b"world"))),
|
||||
]));
|
||||
|
||||
let mut probed = probe_chunk_stream_before_commit(stream, 11).await.unwrap();
|
||||
let mut collected = Vec::new();
|
||||
while let Some(chunk) = probed.next().await {
|
||||
collected.extend_from_slice(chunk.unwrap().as_bytes().as_ref());
|
||||
}
|
||||
|
||||
assert_eq!(collected, b"hello world");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn probe_chunk_stream_before_commit_rejects_midstream_failure_before_first_chunk() {
|
||||
let stream: BoxChunkStream = Box::pin(stream::iter(vec![Err(std::io::Error::other("probe failed"))]));
|
||||
|
||||
let err = match probe_chunk_stream_before_commit(stream, 1).await {
|
||||
Ok(_) => panic!("expected probe failure"),
|
||||
Err(err) => err,
|
||||
};
|
||||
assert_eq!(err, rustfs_io_metrics::FallbackReason::ProbeFailed);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn probe_chunk_stream_before_commit_rejects_unexpected_empty_stream() {
|
||||
let stream: BoxChunkStream = Box::pin(stream::empty());
|
||||
|
||||
let err = match probe_chunk_stream_before_commit(stream, 1).await {
|
||||
Ok(_) => panic!("expected probe failure"),
|
||||
Err(err) => err,
|
||||
};
|
||||
assert_eq!(err, rustfs_io_metrics::FallbackReason::ProbeFailed);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user