mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-04 04:17:44 +00:00
feat(storage): add direct chunk GET fast path (#2351)
Signed-off-by: houseme <housemecn@gmail.com> Co-authored-by: heihutu <heihutu@gmail.com> Co-authored-by: cxymds <Cxymds@qq.com>
This commit is contained in:
@@ -86,6 +86,7 @@ rustfs-utils = { workspace = true, features = ["full"] }
|
||||
rustfs-zip = { workspace = true }
|
||||
rustfs-io-core = { workspace = true }
|
||||
rustfs-io-metrics = { workspace = true }
|
||||
rustfs-object-io = { workspace = true }
|
||||
rustfs-concurrency = { workspace = true }
|
||||
rustfs-scanner = { workspace = true }
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ use rustfs_ecstore::{
|
||||
global::GLOBAL_TierConfigMgr,
|
||||
store::ECStore,
|
||||
store_api::{
|
||||
BucketOperations, BucketOptions, MakeBucketOptions, MultipartOperations, ObjectIO, ObjectOperations, ObjectOptions,
|
||||
PutObjReader,
|
||||
BucketOperations, BucketOptions, ChunkNativePutData, MakeBucketOptions, MultipartOperations, ObjectIO, ObjectOperations,
|
||||
ObjectOptions,
|
||||
},
|
||||
tier::{
|
||||
tier_config::{TierConfig, TierType},
|
||||
@@ -148,7 +148,7 @@ async fn upload_test_object(
|
||||
object: &str,
|
||||
data: &[u8],
|
||||
) -> rustfs_ecstore::store_api::ObjectInfo {
|
||||
let mut reader = PutObjReader::from_vec(data.to_vec());
|
||||
let mut reader = ChunkNativePutData::from_vec(data.to_vec());
|
||||
(**ecstore)
|
||||
.put_object(bucket, object, &mut reader, &ObjectOptions::default())
|
||||
.await
|
||||
@@ -446,7 +446,7 @@ async fn complete_multipart_upload_transitions_immediately_via_usecase() {
|
||||
.await
|
||||
.expect("Failed to create multipart upload");
|
||||
|
||||
let mut reader = PutObjReader::from_vec(payload.to_vec());
|
||||
let mut reader = ChunkNativePutData::from_vec(payload.to_vec());
|
||||
let uploaded_part = ecstore
|
||||
.put_object_part(bucket.as_str(), object, &upload.upload_id, 1, &mut reader, &ObjectOptions::default())
|
||||
.await
|
||||
|
||||
@@ -44,10 +44,13 @@ use rustfs_ecstore::compress::is_compressible;
|
||||
use rustfs_ecstore::error::{StorageError, is_err_object_not_found, is_err_version_not_found};
|
||||
use rustfs_ecstore::new_object_layer_fn;
|
||||
use rustfs_ecstore::set_disk::{MAX_PARTS_COUNT, is_valid_storage_class};
|
||||
use rustfs_ecstore::store_api::{CompletePart, HTTPRangeSpec, MultipartUploadResult, ObjectIO, ObjectOptions, PutObjReader};
|
||||
use rustfs_ecstore::store_api::{
|
||||
ChunkNativePutData, CompletePart, HTTPRangeSpec, MultipartUploadResult, ObjectIO, ObjectOptions,
|
||||
};
|
||||
use rustfs_ecstore::store_api::{MultipartOperations, ObjectOperations};
|
||||
use rustfs_filemeta::{ReplicationStatusType, ReplicationType};
|
||||
use rustfs_rio::{CompressReader, HashReader};
|
||||
use rustfs_object_io::put::PutObjectChecksums;
|
||||
use rustfs_rio::{CompressReader, HashReader, Reader, WarpReader};
|
||||
use rustfs_s3_common::S3Operation;
|
||||
use rustfs_targets::EventName;
|
||||
use rustfs_utils::CompressionAlgorithm;
|
||||
@@ -719,6 +722,13 @@ impl DefaultMultipartUsecase {
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
let mut size = size.ok_or_else(|| s3_error!(UnexpectedContent))?;
|
||||
let mut requested_checksum_type = rustfs_rio::ChecksumType::from_header(&req.headers);
|
||||
if !requested_checksum_type.is_set()
|
||||
&& let Some(checksum_algo) = fi.user_defined.get(rustfs_rio::RUSTFS_MULTIPART_CHECKSUM)
|
||||
&& let Some(checksum_type) = fi.user_defined.get(rustfs_rio::RUSTFS_MULTIPART_CHECKSUM_TYPE)
|
||||
{
|
||||
requested_checksum_type = rustfs_rio::ChecksumType::from_string_with_obj_type(checksum_algo, checksum_type);
|
||||
}
|
||||
|
||||
// Apply adaptive buffer sizing based on part size for optimal streaming performance.
|
||||
// Uses workload profile configuration (enabled by default) to select appropriate buffer size.
|
||||
@@ -731,6 +741,8 @@ impl DefaultMultipartUsecase {
|
||||
|
||||
let is_compressible = rustfs_utils::http::contains_key_str(&fi.user_defined, rustfs_utils::http::SUFFIX_COMPRESSION);
|
||||
|
||||
let mut reader: Box<dyn Reader> = Box::new(WarpReader::new(body));
|
||||
|
||||
let actual_size = size;
|
||||
|
||||
let mut md5hex = if let Some(base64_md5) = input.content_md5 {
|
||||
@@ -744,31 +756,32 @@ impl DefaultMultipartUsecase {
|
||||
|
||||
let mut sha256hex = get_content_sha256_with_query(&req.headers, req.uri.query());
|
||||
|
||||
let mut reader = if is_compressible {
|
||||
let mut hrd = HashReader::from_stream(body, size, actual_size, md5hex.take(), sha256hex.take(), false)
|
||||
.map_err(ApiError::from)?;
|
||||
if is_compressible {
|
||||
let mut hrd =
|
||||
HashReader::new(reader, size, actual_size, md5hex.take(), sha256hex.take(), false).map_err(ApiError::from)?;
|
||||
|
||||
if let Err(err) = hrd.add_checksum_from_s3s(&req.headers, req.trailing_headers.clone(), false) {
|
||||
return Err(ApiError::from(err).into());
|
||||
}
|
||||
if requested_checksum_type.is_set() && hrd.checksum().is_none() {
|
||||
hrd.enable_auto_checksum(requested_checksum_type).map_err(ApiError::from)?;
|
||||
}
|
||||
|
||||
let compress_reader = CompressReader::new(hrd, CompressionAlgorithm::default());
|
||||
reader = Box::new(compress_reader);
|
||||
size = HashReader::SIZE_PRESERVE_LAYER;
|
||||
HashReader::from_reader(
|
||||
CompressReader::new(hrd, CompressionAlgorithm::default()),
|
||||
size,
|
||||
actual_size,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.map_err(ApiError::from)?
|
||||
} else {
|
||||
HashReader::from_stream(body, size, actual_size, md5hex, sha256hex, false).map_err(ApiError::from)?
|
||||
};
|
||||
md5hex = None;
|
||||
sha256hex = None;
|
||||
}
|
||||
|
||||
let mut reader = HashReader::new(reader, size, actual_size, md5hex, sha256hex, false).map_err(ApiError::from)?;
|
||||
|
||||
if let Err(err) = reader.add_checksum_from_s3s(&req.headers, req.trailing_headers.clone(), size < 0) {
|
||||
return Err(ApiError::from(err).into());
|
||||
}
|
||||
if requested_checksum_type.is_set() && reader.checksum().is_none() {
|
||||
reader.enable_auto_checksum(requested_checksum_type).map_err(ApiError::from)?;
|
||||
}
|
||||
|
||||
let has_ssec = sse_customer_algorithm.is_some();
|
||||
// When SSE-C headers are present, skip managed-encryption metadata to avoid
|
||||
@@ -818,9 +831,8 @@ impl DefaultMultipartUsecase {
|
||||
let requested_kms_key_id = material.kms_key_id.clone();
|
||||
|
||||
let encrypted_reader = material.wrap_reader(reader);
|
||||
reader =
|
||||
HashReader::from_reader(encrypted_reader, HashReader::SIZE_PRESERVE_LAYER, actual_size, None, None, false)
|
||||
.map_err(ApiError::from)?;
|
||||
reader = HashReader::new(encrypted_reader, HashReader::SIZE_PRESERVE_LAYER, actual_size, None, None, false)
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
fi.user_defined.extend(material.metadata);
|
||||
|
||||
@@ -829,18 +841,20 @@ impl DefaultMultipartUsecase {
|
||||
None => (None, None),
|
||||
};
|
||||
|
||||
let mut reader = PutObjReader::new(reader);
|
||||
let mut reader = ChunkNativePutData::new(reader);
|
||||
|
||||
let info = store
|
||||
.put_object_part(&bucket, &key, &upload_id, part_id, &mut reader, &opts)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
let mut checksum_crc32 = input.checksum_crc32;
|
||||
let mut checksum_crc32c = input.checksum_crc32c;
|
||||
let mut checksum_sha1 = input.checksum_sha1;
|
||||
let mut checksum_sha256 = input.checksum_sha256;
|
||||
let mut checksum_crc64nvme = input.checksum_crc64nvme;
|
||||
let mut checksums = PutObjectChecksums {
|
||||
crc32: input.checksum_crc32,
|
||||
crc32c: input.checksum_crc32c,
|
||||
sha1: input.checksum_sha1,
|
||||
sha256: input.checksum_sha256,
|
||||
crc64nvme: input.checksum_crc64nvme,
|
||||
};
|
||||
|
||||
if let Some(alg) = &input.checksum_algorithm
|
||||
&& let Some(Some(checksum_str)) = req.trailing_headers.as_ref().map(|trailer| {
|
||||
@@ -860,25 +874,26 @@ impl DefaultMultipartUsecase {
|
||||
})
|
||||
{
|
||||
match alg.as_str() {
|
||||
ChecksumAlgorithm::CRC32 => checksum_crc32 = checksum_str,
|
||||
ChecksumAlgorithm::CRC32C => checksum_crc32c = checksum_str,
|
||||
ChecksumAlgorithm::SHA1 => checksum_sha1 = checksum_str,
|
||||
ChecksumAlgorithm::SHA256 => checksum_sha256 = checksum_str,
|
||||
ChecksumAlgorithm::CRC64NVME => checksum_crc64nvme = checksum_str,
|
||||
ChecksumAlgorithm::CRC32 => checksums.crc32 = checksum_str,
|
||||
ChecksumAlgorithm::CRC32C => checksums.crc32c = checksum_str,
|
||||
ChecksumAlgorithm::SHA1 => checksums.sha1 = checksum_str,
|
||||
ChecksumAlgorithm::SHA256 => checksums.sha256 = checksum_str,
|
||||
ChecksumAlgorithm::CRC64NVME => checksums.crc64nvme = checksum_str,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
checksums.merge_from_map(&reader.content_crc());
|
||||
|
||||
let output = UploadPartOutput {
|
||||
server_side_encryption: requested_sse,
|
||||
ssekms_key_id: requested_kms_key_id,
|
||||
sse_customer_algorithm,
|
||||
sse_customer_key_md5,
|
||||
checksum_crc32,
|
||||
checksum_crc32c,
|
||||
checksum_sha1,
|
||||
checksum_sha256,
|
||||
checksum_crc64nvme,
|
||||
checksum_crc32: checksums.crc32,
|
||||
checksum_crc32c: checksums.crc32c,
|
||||
checksum_sha1: checksums.sha1,
|
||||
checksum_sha256: checksums.sha256,
|
||||
checksum_crc64nvme: checksums.crc64nvme,
|
||||
e_tag: info.etag.map(|etag| to_s3s_etag(&etag)),
|
||||
..Default::default()
|
||||
};
|
||||
@@ -1116,6 +1131,8 @@ impl DefaultMultipartUsecase {
|
||||
|
||||
let is_compressible = rustfs_utils::http::contains_key_str(&mp_info.user_defined, rustfs_utils::http::SUFFIX_COMPRESSION);
|
||||
|
||||
let mut reader: Box<dyn Reader> = Box::new(WarpReader::new(src_stream));
|
||||
|
||||
let src_decryption_request = DecryptionRequest {
|
||||
bucket: &src_bucket,
|
||||
key: &src_key,
|
||||
@@ -1127,74 +1144,23 @@ impl DefaultMultipartUsecase {
|
||||
etag: src_info.etag.as_deref(),
|
||||
};
|
||||
|
||||
if let Some(material) = sse_decryption(src_decryption_request).await? {
|
||||
reader = material.wrap_single_reader(reader);
|
||||
if let Some(original) = material.original_size {
|
||||
src_info.actual_size = original;
|
||||
}
|
||||
}
|
||||
|
||||
let actual_size = length;
|
||||
let mut size = length;
|
||||
|
||||
let mut reader = match sse_decryption(src_decryption_request).await? {
|
||||
Some(material) => {
|
||||
if let Some(original) = material.original_size {
|
||||
src_info.actual_size = original;
|
||||
}
|
||||
if is_compressible {
|
||||
let hrd = HashReader::new(reader, size, actual_size, None, None, false).map_err(ApiError::from)?;
|
||||
reader = Box::new(CompressReader::new(hrd, CompressionAlgorithm::default()));
|
||||
size = HashReader::SIZE_PRESERVE_LAYER;
|
||||
}
|
||||
|
||||
if material.is_multipart {
|
||||
let (decrypted_stream, plaintext_size) =
|
||||
material.wrap_reader(src_stream, size).await.map_err(ApiError::from)?;
|
||||
size = plaintext_size;
|
||||
|
||||
if is_compressible {
|
||||
let hrd = HashReader::from_reader(decrypted_stream, size, actual_size, None, None, false)
|
||||
.map_err(ApiError::from)?;
|
||||
size = HashReader::SIZE_PRESERVE_LAYER;
|
||||
HashReader::from_reader(
|
||||
CompressReader::new(hrd, CompressionAlgorithm::default()),
|
||||
size,
|
||||
actual_size,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.map_err(ApiError::from)?
|
||||
} else {
|
||||
HashReader::from_reader(decrypted_stream, size, actual_size, None, None, false).map_err(ApiError::from)?
|
||||
}
|
||||
} else if is_compressible {
|
||||
let hrd =
|
||||
HashReader::from_stream(material.wrap_single_reader(src_stream), size, actual_size, None, None, false)
|
||||
.map_err(ApiError::from)?;
|
||||
size = HashReader::SIZE_PRESERVE_LAYER;
|
||||
HashReader::from_reader(
|
||||
CompressReader::new(hrd, CompressionAlgorithm::default()),
|
||||
size,
|
||||
actual_size,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.map_err(ApiError::from)?
|
||||
} else {
|
||||
HashReader::from_stream(material.wrap_single_reader(src_stream), size, actual_size, None, None, false)
|
||||
.map_err(ApiError::from)?
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if is_compressible {
|
||||
let hrd =
|
||||
HashReader::from_stream(src_stream, size, actual_size, None, None, false).map_err(ApiError::from)?;
|
||||
size = HashReader::SIZE_PRESERVE_LAYER;
|
||||
HashReader::from_reader(
|
||||
CompressReader::new(hrd, CompressionAlgorithm::default()),
|
||||
size,
|
||||
actual_size,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.map_err(ApiError::from)?
|
||||
} else {
|
||||
HashReader::from_stream(src_stream, size, actual_size, None, None, false).map_err(ApiError::from)?
|
||||
}
|
||||
}
|
||||
};
|
||||
let mut reader = HashReader::new(reader, size, actual_size, None, None, false).map_err(ApiError::from)?;
|
||||
|
||||
let server_side_encryption = mp_info
|
||||
.user_defined
|
||||
@@ -1235,9 +1201,8 @@ impl DefaultMultipartUsecase {
|
||||
let requested_kms_key_id = material.kms_key_id.clone();
|
||||
|
||||
let encrypted_reader = material.wrap_reader(reader);
|
||||
reader =
|
||||
HashReader::from_reader(encrypted_reader, HashReader::SIZE_PRESERVE_LAYER, actual_size, None, None, false)
|
||||
.map_err(ApiError::from)?;
|
||||
reader = HashReader::new(encrypted_reader, HashReader::SIZE_PRESERVE_LAYER, actual_size, None, None, false)
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
mp_info.user_defined.extend(material.metadata);
|
||||
|
||||
@@ -1246,7 +1211,7 @@ impl DefaultMultipartUsecase {
|
||||
None => (None, None),
|
||||
};
|
||||
|
||||
let mut reader = PutObjReader::new(reader);
|
||||
let mut reader = ChunkNativePutData::new(reader);
|
||||
|
||||
let dst_opts = ObjectOptions {
|
||||
user_defined: mp_info.user_defined.clone(),
|
||||
|
||||
+94
-2529
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,616 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::get_object_flow::GetObjectBootstrap;
|
||||
use super::*;
|
||||
use crate::app::context::NotifyInterface;
|
||||
use crate::storage::concurrency::{self, get_buffer_size_opt_in};
|
||||
use hashbrown::HashMap;
|
||||
use rustfs_object_io::get::{
|
||||
CachedGetObjectSource as ObjectIoCachedGetObjectSource, GetObjectBodyPlan as ObjectIoGetObjectBodyPlan,
|
||||
GetObjectCacheWriteback, GetObjectDataPlaneMetricContract as ObjectIoGetObjectDataPlaneMetricContract, GetObjectFlowResult,
|
||||
GetObjectResponseMode, MaterializeGetObjectBodyError as ObjectIoMaterializeGetObjectBodyError,
|
||||
build_cached_get_object_flow_result_from_source as object_io_build_cached_get_object_flow_result_from_source,
|
||||
finalize_get_object_cache_writeback as object_io_finalize_get_object_cache_writeback,
|
||||
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,
|
||||
};
|
||||
|
||||
pub(super) async fn prepare_get_object_request_context(req: &S3Request<GetObjectInput>) -> S3Result<GetObjectRequestContext> {
|
||||
let GetObjectInput {
|
||||
bucket,
|
||||
key,
|
||||
version_id,
|
||||
part_number,
|
||||
range,
|
||||
..
|
||||
} = req.input.clone();
|
||||
|
||||
validate_object_key(&key, "GET")?;
|
||||
|
||||
let part_number = part_number.map(|v| v as usize);
|
||||
|
||||
if let Some(part_num) = part_number
|
||||
&& part_num == 0
|
||||
{
|
||||
return Err(s3_error!(InvalidArgument, "Invalid part number: part number must be greater than 0"));
|
||||
}
|
||||
|
||||
let rs = range.map(|v| match v {
|
||||
Range::Int { first, last } => HTTPRangeSpec {
|
||||
is_suffix_length: false,
|
||||
start: first as i64,
|
||||
end: if let Some(last) = last { last as i64 } else { -1 },
|
||||
},
|
||||
Range::Suffix { length } => HTTPRangeSpec {
|
||||
is_suffix_length: true,
|
||||
start: length as i64,
|
||||
end: -1,
|
||||
},
|
||||
});
|
||||
|
||||
if rs.is_some() && part_number.is_some() {
|
||||
return Err(s3_error!(InvalidArgument, "range and part_number invalid"));
|
||||
}
|
||||
|
||||
let opts: ObjectOptions = get_opts(&bucket, &key, version_id.clone(), part_number, &req.headers)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
Ok(GetObjectRequestContext {
|
||||
cache_key: ConcurrencyManager::make_cache_key(&bucket, &key, version_id.as_deref()),
|
||||
version_id_for_event: version_id.unwrap_or_default(),
|
||||
bucket,
|
||||
key,
|
||||
part_number,
|
||||
rs,
|
||||
opts,
|
||||
headers: req.headers.clone(),
|
||||
method: req.method.clone(),
|
||||
sse_customer_key: req.input.sse_customer_key.clone(),
|
||||
sse_customer_key_md5: req.input.sse_customer_key_md5.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
impl ObjectIoCachedGetObjectSource for CachedGetObject {
|
||||
fn body(&self) -> &std::sync::Arc<bytes::Bytes> {
|
||||
&self.body
|
||||
}
|
||||
|
||||
fn content_length(&self) -> i64 {
|
||||
self.content_length
|
||||
}
|
||||
|
||||
fn content_type(&self) -> Option<&str> {
|
||||
self.content_type.as_deref()
|
||||
}
|
||||
|
||||
fn e_tag(&self) -> Option<&str> {
|
||||
self.e_tag.as_deref()
|
||||
}
|
||||
|
||||
fn last_modified(&self) -> Option<&str> {
|
||||
self.last_modified.as_deref()
|
||||
}
|
||||
|
||||
fn cache_control(&self) -> Option<&str> {
|
||||
self.cache_control.as_deref()
|
||||
}
|
||||
|
||||
fn content_disposition(&self) -> Option<&str> {
|
||||
self.content_disposition.as_deref()
|
||||
}
|
||||
|
||||
fn content_encoding(&self) -> Option<&str> {
|
||||
self.content_encoding.as_deref()
|
||||
}
|
||||
|
||||
fn content_language(&self) -> Option<&str> {
|
||||
self.content_language.as_deref()
|
||||
}
|
||||
|
||||
fn storage_class(&self) -> Option<&str> {
|
||||
self.storage_class.as_deref()
|
||||
}
|
||||
|
||||
fn version_id(&self) -> Option<&str> {
|
||||
self.version_id.as_deref()
|
||||
}
|
||||
|
||||
fn delete_marker(&self) -> bool {
|
||||
self.delete_marker
|
||||
}
|
||||
|
||||
fn tag_count(&self) -> Option<i32> {
|
||||
self.tag_count
|
||||
}
|
||||
|
||||
fn user_metadata(&self) -> &std::collections::HashMap<String, String> {
|
||||
&self.user_metadata
|
||||
}
|
||||
|
||||
fn checksum_crc32(&self) -> Option<&str> {
|
||||
self.checksum_crc32.as_deref()
|
||||
}
|
||||
|
||||
fn checksum_crc32c(&self) -> Option<&str> {
|
||||
self.checksum_crc32c.as_deref()
|
||||
}
|
||||
|
||||
fn checksum_sha1(&self) -> Option<&str> {
|
||||
self.checksum_sha1.as_deref()
|
||||
}
|
||||
|
||||
fn checksum_sha256(&self) -> Option<&str> {
|
||||
self.checksum_sha256.as_deref()
|
||||
}
|
||||
|
||||
fn checksum_crc64nvme(&self) -> Option<&str> {
|
||||
self.checksum_crc64nvme.as_deref()
|
||||
}
|
||||
|
||||
fn checksum_type(&self) -> Option<&ChecksumType> {
|
||||
self.checksum_type.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn init_get_object_bootstrap(bucket: &str, key: &str, request_id: &str) -> S3Result<GetObjectBootstrap> {
|
||||
let timeout_config = TimeoutConfig::from_env();
|
||||
let wrapper = RequestTimeoutWrapper::with_request_id(timeout_config.clone(), request_id.to_string());
|
||||
let request_start = std::time::Instant::now();
|
||||
let request_guard = ConcurrencyManager::track_request();
|
||||
let concurrent_requests = GetObjectGuard::concurrent_requests();
|
||||
|
||||
let deadlock_detector = deadlock_detector::get_deadlock_detector();
|
||||
deadlock_detector.register_request(request_id, format!("GetObject {bucket}/{key}"));
|
||||
let deadlock_request_guard = DeadlockRequestGuard::new(deadlock_detector, request_id.to_string());
|
||||
|
||||
if wrapper.is_timeout() {
|
||||
warn!(
|
||||
bucket = %bucket,
|
||||
key = %key,
|
||||
timeout_secs = timeout_config.get_object_timeout.as_secs(),
|
||||
elapsed_ms = wrapper.elapsed().as_millis(),
|
||||
"GetObject request timed out before processing"
|
||||
);
|
||||
return Err(s3_error!(InternalError, "Request timeout before processing"));
|
||||
}
|
||||
|
||||
rustfs_io_metrics::record_get_object_request_start(concurrent_requests);
|
||||
|
||||
debug!(
|
||||
"GetObject request started with {} concurrent requests, timeout={:?}",
|
||||
concurrent_requests, timeout_config.get_object_timeout
|
||||
);
|
||||
|
||||
Ok(GetObjectBootstrap {
|
||||
timeout_config,
|
||||
wrapper,
|
||||
request_start,
|
||||
request_guard,
|
||||
_deadlock_request_guard: deadlock_request_guard,
|
||||
concurrent_requests,
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) async fn maybe_get_cached_get_object_flow_result(
|
||||
manager: &ConcurrencyManager,
|
||||
bucket: &str,
|
||||
key: &str,
|
||||
cache_key: &str,
|
||||
version_id_for_event: String,
|
||||
part_number: Option<usize>,
|
||||
rs: Option<&HTTPRangeSpec>,
|
||||
request_start: std::time::Instant,
|
||||
) -> Option<GetObjectFlowResult> {
|
||||
if !manager.is_cache_enabled() || part_number.is_some() || rs.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let cached = manager.get_cached_object(cache_key).await?;
|
||||
let cache_serve_duration = request_start.elapsed();
|
||||
let metric_contract = ObjectIoGetObjectDataPlaneMetricContract::cache_served();
|
||||
|
||||
debug!("Serving object from response cache: {} (latency: {:?})", cache_key, cache_serve_duration);
|
||||
|
||||
if metric_contract.record_cache_served_metric {
|
||||
rustfs_io_metrics::record_get_object_cache_served(cache_serve_duration.as_secs_f64(), cached.body.len());
|
||||
}
|
||||
rustfs_io_metrics::record_io_path_selected("get", metric_contract.io_path);
|
||||
rustfs_io_metrics::record_io_copy_mode("get", metric_contract.copy_mode, cached.body.len());
|
||||
|
||||
manager.record_transfer(cached.content_length as u64, Duration::from_micros(1));
|
||||
|
||||
rustfs_io_metrics::record_get_object(request_start.elapsed().as_millis() as f64, cached.content_length, true);
|
||||
|
||||
Some(object_io_build_cached_get_object_flow_result_from_source(
|
||||
bucket,
|
||||
key,
|
||||
cached.as_ref(),
|
||||
version_id_for_event,
|
||||
))
|
||||
}
|
||||
|
||||
pub(super) struct GetObjectBodyAdapterOutput {
|
||||
pub(super) body: Option<StreamingBlob>,
|
||||
pub(super) body_plan: ObjectIoGetObjectBodyPlan,
|
||||
pub(super) cache_writeback: Option<GetObjectCacheWriteback>,
|
||||
}
|
||||
|
||||
pub(super) fn spawn_get_object_cache_writeback(
|
||||
cache_key: &str,
|
||||
writeback: GetObjectCacheWriteback,
|
||||
metric_contract: ObjectIoGetObjectDataPlaneMetricContract,
|
||||
) {
|
||||
debug_assert_eq!(
|
||||
metric_contract.request_source,
|
||||
rustfs_object_io::get::GetObjectDataPlaneRequestSource::Disk
|
||||
);
|
||||
debug_assert!(!metric_contract.record_cache_served_metric);
|
||||
debug_assert!(metric_contract.record_cache_writeback_metric);
|
||||
|
||||
let cached_response = CachedGetObject::from_get_object_cache_writeback(writeback);
|
||||
|
||||
let cache_key_clone = cache_key.to_string();
|
||||
crate::storage::request_context::spawn_traced(async move {
|
||||
let manager = get_concurrency_manager();
|
||||
manager.put_cached_object(cache_key_clone.clone(), cached_response).await;
|
||||
debug!("Object cached successfully with metadata: {}", cache_key_clone);
|
||||
});
|
||||
|
||||
if metric_contract.record_cache_writeback_metric {
|
||||
rustfs_io_metrics::record_object_cache_writeback();
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn build_get_object_body_adapter<R>(
|
||||
final_stream: R,
|
||||
info: &ObjectInfo,
|
||||
cache_key: &str,
|
||||
response_content_length: i64,
|
||||
optimal_buffer_size: usize,
|
||||
cache_eligibility: rustfs_concurrency::GetObjectCacheEligibility,
|
||||
) -> S3Result<GetObjectBodyAdapterOutput>
|
||||
where
|
||||
R: AsyncRead + Send + Sync + Unpin + 'static,
|
||||
{
|
||||
let body_plan = object_io_plan_get_object_body(cache_eligibility, rustfs_config::DEFAULT_OBJECT_SEEK_SUPPORT_THRESHOLD);
|
||||
|
||||
match body_plan {
|
||||
ObjectIoGetObjectBodyPlan::CacheWriteback => {
|
||||
debug!(
|
||||
"Reading object into memory for caching: key={} size={}",
|
||||
cache_key, response_content_length
|
||||
);
|
||||
}
|
||||
ObjectIoGetObjectBodyPlan::BufferSeekable => {
|
||||
debug!(
|
||||
"Reading small object into memory for seek support: key={} size={}",
|
||||
cache_key, response_content_length
|
||||
);
|
||||
}
|
||||
ObjectIoGetObjectBodyPlan::Stream if cache_eligibility.encryption_applied => {
|
||||
info!(
|
||||
"Encrypted object: Using unlimited stream for decryption with buffer size {}",
|
||||
optimal_buffer_size
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let materialized =
|
||||
object_io_materialize_get_object_body(final_stream, info, body_plan, response_content_length, optimal_buffer_size)
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
ObjectIoMaterializeGetObjectBodyError::CacheRead(err) => {
|
||||
error!("Failed to read object into memory for caching: {}", err);
|
||||
ApiError::from(StorageError::other(format!("Failed to read object for caching: {err}")))
|
||||
}
|
||||
ObjectIoMaterializeGetObjectBodyError::EncryptedRead(err) => {
|
||||
error!("Failed to read decrypted object into memory: {}", err);
|
||||
ApiError::from(StorageError::other(format!("Failed to read decrypted object: {err}")))
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(GetObjectBodyAdapterOutput {
|
||||
body: materialized.body,
|
||||
body_plan: materialized.plan,
|
||||
cache_writeback: materialized.cache_writeback.map(|writeback| {
|
||||
object_io_finalize_get_object_cache_writeback(
|
||||
info,
|
||||
writeback,
|
||||
filter_object_metadata(&info.user_defined).unwrap_or_default(),
|
||||
)
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn finalize_get_object_completion(
|
||||
cache_key: &str,
|
||||
wrapper: &RequestTimeoutWrapper,
|
||||
timeout_config: &TimeoutConfig,
|
||||
total_duration: Duration,
|
||||
response_content_length: i64,
|
||||
optimal_buffer_size: usize,
|
||||
metric_contract: ObjectIoGetObjectDataPlaneMetricContract,
|
||||
) {
|
||||
rustfs_io_metrics::record_get_object_completion(total_duration.as_secs_f64(), response_content_length, optimal_buffer_size);
|
||||
|
||||
rustfs_io_metrics::record_get_object(total_duration.as_millis() as f64, response_content_length, false);
|
||||
rustfs_io_metrics::record_io_copy_mode("get", metric_contract.copy_mode, response_content_length.max(0) as usize);
|
||||
|
||||
if wrapper.is_timeout() {
|
||||
warn!(
|
||||
"GetObject request exceeded timeout: key={} duration={:?} timeout={:?}",
|
||||
cache_key,
|
||||
wrapper.elapsed(),
|
||||
timeout_config.get_object_timeout
|
||||
);
|
||||
rustfs_io_metrics::record_get_object_timeout(None, Some(wrapper.elapsed().as_secs_f64()));
|
||||
}
|
||||
|
||||
debug!(
|
||||
"GetObject completed: key={} size={} duration={:?} buffer={}",
|
||||
cache_key, response_content_length, total_duration, optimal_buffer_size
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn finalize_get_object_strategy_runtime(
|
||||
base_buffer_size: usize,
|
||||
manager: &ConcurrencyManager,
|
||||
bucket: &str,
|
||||
key: &str,
|
||||
info: &ObjectInfo,
|
||||
rs: Option<&HTTPRangeSpec>,
|
||||
response_content_length: i64,
|
||||
permit_wait_duration: Duration,
|
||||
queue_utilization: f64,
|
||||
queue_status: &concurrency::IoQueueStatus,
|
||||
concurrent_requests: usize,
|
||||
) -> (concurrency::IoStrategy, usize) {
|
||||
let strategy_layout = object_io_plan_get_object_strategy_layout(
|
||||
rs,
|
||||
response_content_length,
|
||||
0,
|
||||
get_buffer_size_opt_in(response_content_length),
|
||||
);
|
||||
|
||||
if let Some(range_spec) = rs
|
||||
&& range_spec.start >= 0
|
||||
{
|
||||
manager.record_access(range_spec.start as u64, response_content_length as u64);
|
||||
}
|
||||
|
||||
if response_content_length > 0 {
|
||||
manager.record_transfer(response_content_length as u64, permit_wait_duration);
|
||||
}
|
||||
|
||||
let io_strategy = manager.calculate_io_strategy_with_context(
|
||||
info.size,
|
||||
base_buffer_size,
|
||||
permit_wait_duration,
|
||||
strategy_layout.is_sequential_hint,
|
||||
);
|
||||
|
||||
debug!(
|
||||
wait_ms = permit_wait_duration.as_millis() as u64,
|
||||
load_level = ?io_strategy.load_level,
|
||||
buffer_size = io_strategy.buffer_size,
|
||||
buffer_multiplier = io_strategy.buffer_multiplier,
|
||||
readahead = io_strategy.enable_readahead,
|
||||
cache_wb = io_strategy.cache_writeback_enabled,
|
||||
storage_media = ?io_strategy.storage_media,
|
||||
access_pattern = ?io_strategy.access_pattern,
|
||||
bandwidth_tier = ?io_strategy.bandwidth_tier,
|
||||
concurrent_requests = io_strategy.concurrent_requests,
|
||||
file_size = info.size,
|
||||
is_sequential = strategy_layout.is_sequential_hint,
|
||||
"Enhanced multi-factor I/O strategy calculated"
|
||||
);
|
||||
|
||||
let io_priority = manager.get_io_priority(response_content_length);
|
||||
|
||||
if manager.is_priority_scheduling_enabled() {
|
||||
debug!(
|
||||
bucket = %bucket,
|
||||
key = %key,
|
||||
priority = %io_priority,
|
||||
request_size = response_content_length,
|
||||
"I/O priority assigned (based on actual request size)"
|
||||
);
|
||||
|
||||
rustfs_io_metrics::record_io_priority_assignment(io_priority.as_str());
|
||||
}
|
||||
|
||||
rustfs_io_metrics::record_get_object_io_state(
|
||||
permit_wait_duration.as_secs_f64(),
|
||||
queue_utilization,
|
||||
queue_status.permits_in_use,
|
||||
queue_status.total_permits.saturating_sub(queue_status.permits_in_use),
|
||||
io_strategy.load_level.as_str(),
|
||||
io_strategy.buffer_multiplier,
|
||||
);
|
||||
rustfs_io_metrics::record_io_priority_assignment(io_priority.as_str());
|
||||
|
||||
let strategy_layout = object_io_plan_get_object_strategy_layout(
|
||||
rs,
|
||||
response_content_length,
|
||||
io_strategy.buffer_size,
|
||||
get_buffer_size_opt_in(response_content_length),
|
||||
);
|
||||
|
||||
debug!(
|
||||
actual_request_size = response_content_length,
|
||||
priority = %io_priority.as_str(),
|
||||
"I/O priority finalized with actual request size"
|
||||
);
|
||||
|
||||
debug!(
|
||||
"GetObject buffer sizing: file_size={}, base={}, optimal={}, concurrent_requests={}, io_strategy={:?}",
|
||||
response_content_length,
|
||||
get_buffer_size_opt_in(response_content_length),
|
||||
strategy_layout.optimal_buffer_size,
|
||||
concurrent_requests,
|
||||
io_strategy.load_level
|
||||
);
|
||||
|
||||
(io_strategy, strategy_layout.optimal_buffer_size)
|
||||
}
|
||||
|
||||
pub(super) fn prepare_put_object_request_context(req: &S3Request<PutObjectInput>) -> PutObjectRequestContext {
|
||||
PutObjectRequestContext {
|
||||
headers: req.headers.clone(),
|
||||
trailing_headers: req.trailing_headers.clone(),
|
||||
uri_query: req.uri.query().map(str::to_string),
|
||||
is_post_object: req.extensions.get::<PostObjectRequestMarker>().is_some(),
|
||||
method: req.method.clone(),
|
||||
uri: req.uri.clone(),
|
||||
extensions: req.extensions.clone(),
|
||||
credentials: req.credentials.clone(),
|
||||
region: req.region.clone(),
|
||||
service: req.service.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn put_object_execution_context(req: &S3Request<PutObjectInput>) -> (EventName, QuotaOperation, &'static str) {
|
||||
if req.extensions.get::<PostObjectRequestMarker>().is_some() {
|
||||
(EventName::ObjectCreatedPost, QuotaOperation::PostObject, "POST")
|
||||
} else {
|
||||
(EventName::ObjectCreatedPut, QuotaOperation::PutObject, "PUT")
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn new_operation_helper<T: Send + Sync>(
|
||||
req: &S3Request<T>,
|
||||
event_name: EventName,
|
||||
operation: S3Operation,
|
||||
suppress_event: bool,
|
||||
) -> OperationHelper {
|
||||
let helper = OperationHelper::new(req, event_name, operation);
|
||||
if suppress_event { helper.suppress_event() } else { helper }
|
||||
}
|
||||
|
||||
pub(super) fn bind_helper_object(
|
||||
helper: OperationHelper,
|
||||
object_info: ObjectInfo,
|
||||
version_id: Option<String>,
|
||||
) -> OperationHelper {
|
||||
let helper = helper.object(object_info);
|
||||
if let Some(version_id) = version_id {
|
||||
helper.version_id(version_id)
|
||||
} else {
|
||||
helper
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn complete_get_flow_result(
|
||||
helper: OperationHelper,
|
||||
request_context: &GetObjectRequestContext,
|
||||
flow_result: GetObjectFlowResult,
|
||||
) -> S3Result<S3Response<GetObjectOutput>> {
|
||||
match flow_result.response_mode {
|
||||
GetObjectResponseMode::Plain => {
|
||||
let helper = bind_helper_object(helper, flow_result.event_info, Some(flow_result.version_id_for_event));
|
||||
let result = Ok(S3Response::new(flow_result.output));
|
||||
let _ = helper.complete(&result);
|
||||
result
|
||||
}
|
||||
GetObjectResponseMode::CorsWrapped => {
|
||||
let helper = helper
|
||||
.object(flow_result.event_info)
|
||||
.version_id(flow_result.version_id_for_event);
|
||||
let response = wrap_response_with_cors(
|
||||
&request_context.bucket,
|
||||
&request_context.method,
|
||||
&request_context.headers,
|
||||
flow_result.output,
|
||||
)
|
||||
.await;
|
||||
let result = Ok(response);
|
||||
let _ = helper.complete(&result);
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn complete_put_response(helper: OperationHelper, output: PutObjectOutput) -> S3Result<S3Response<PutObjectOutput>> {
|
||||
let result = Ok(S3Response::new(output));
|
||||
let _ = helper.complete(&result);
|
||||
result
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn spawn_put_extract_notification(
|
||||
notify: Arc<dyn NotifyInterface>,
|
||||
request_context: Option<crate::storage::request_context::RequestContext>,
|
||||
bucket: String,
|
||||
req_params: HashMap<String, String>,
|
||||
version_id: String,
|
||||
host: String,
|
||||
port: u16,
|
||||
user_agent: String,
|
||||
obj_info: ObjectInfo,
|
||||
output: PutObjectOutput,
|
||||
) {
|
||||
let event_args = rustfs_notify::EventArgs {
|
||||
event_name: EventName::ObjectCreatedPut,
|
||||
bucket_name: bucket,
|
||||
object: obj_info,
|
||||
req_params,
|
||||
resp_elements: extract_resp_elements(&S3Response::new(output)),
|
||||
version_id,
|
||||
host,
|
||||
port,
|
||||
user_agent,
|
||||
};
|
||||
|
||||
crate::storage::helper::spawn_background_with_context(request_context, async move {
|
||||
notify.notify(event_args).await;
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) async fn get_validated_store_adapter(bucket: &str) -> S3Result<Arc<rustfs_ecstore::store::ECStore>> {
|
||||
get_validated_store(bucket).await
|
||||
}
|
||||
|
||||
pub(super) async fn bucket_prefix_versioning_enabled(bucket: &str, key: &str) -> bool {
|
||||
BucketVersioningSys::prefix_enabled(bucket, key).await
|
||||
}
|
||||
|
||||
pub(super) async fn authorize_extract_put_target(
|
||||
request_context: &PutObjectRequestContext,
|
||||
bucket: &str,
|
||||
object: &str,
|
||||
) -> S3Result<()> {
|
||||
let mut auth_req = S3Request {
|
||||
input: PutObjectInput::default(),
|
||||
method: request_context.method.clone(),
|
||||
uri: request_context.uri.clone(),
|
||||
headers: request_context.headers.clone(),
|
||||
extensions: request_context.extensions.clone(),
|
||||
credentials: request_context.credentials.clone(),
|
||||
region: request_context.region.clone(),
|
||||
service: request_context.service.clone(),
|
||||
trailing_headers: request_context.trailing_headers.clone(),
|
||||
};
|
||||
{
|
||||
let req_info = req_info_mut(&mut auth_req)?;
|
||||
req_info.bucket = Some(bucket.to_string());
|
||||
req_info.object = Some(object.to_string());
|
||||
req_info.version_id = None;
|
||||
}
|
||||
authorize_request(&mut auth_req, Action::S3Action(S3Action::PutObjectAction)).await
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::DeadlockRequestGuard;
|
||||
use super::app_adapters::{
|
||||
bucket_prefix_versioning_enabled, build_get_object_body_adapter, finalize_get_object_completion,
|
||||
finalize_get_object_strategy_runtime, maybe_get_cached_get_object_flow_result, spawn_get_object_cache_writeback,
|
||||
};
|
||||
use super::get_object_zero_copy::{GetObjectPreparedRead, prepare_get_object_read_execution};
|
||||
use super::types::GetObjectRequestContext;
|
||||
use crate::error::ApiError;
|
||||
use crate::storage::concurrency::{self, ConcurrencyManager, GetObjectGuard};
|
||||
use crate::storage::options::filter_object_metadata;
|
||||
use crate::storage::timeout_wrapper::{RequestTimeoutWrapper, TimeoutConfig};
|
||||
use rustfs_ecstore::store_api::{HTTPRangeSpec, ObjectInfo};
|
||||
use rustfs_object_io::get::{
|
||||
GetObjectBodyPlan as ObjectIoGetObjectBodyPlan, GetObjectBodySource,
|
||||
GetObjectDataPlaneMetricContract as ObjectIoGetObjectDataPlaneMetricContract, GetObjectFlowResult, GetObjectOutputContext,
|
||||
GetObjectReadSetup, build_chunk_blob as object_io_build_chunk_blob,
|
||||
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,
|
||||
chunk_body_data_plane_labels as object_io_chunk_body_data_plane_labels,
|
||||
};
|
||||
use s3s::S3Result;
|
||||
use s3s::dto::{ContentType, SSECustomerAlgorithm, SSECustomerKeyMD5, SSEKMSKeyId, ServerSideEncryption, Timestamp};
|
||||
use std::time::Duration;
|
||||
|
||||
pub(super) struct GetObjectBootstrap {
|
||||
pub(super) timeout_config: TimeoutConfig,
|
||||
pub(super) wrapper: RequestTimeoutWrapper,
|
||||
pub(super) request_start: std::time::Instant,
|
||||
pub(super) request_guard: GetObjectGuard,
|
||||
pub(super) _deadlock_request_guard: DeadlockRequestGuard,
|
||||
pub(super) concurrent_requests: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) struct GetObjectFlowRuntime<'a> {
|
||||
pub(super) manager: &'a ConcurrencyManager,
|
||||
pub(super) bootstrap: &'a GetObjectBootstrap,
|
||||
pub(super) base_buffer_size: usize,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) async fn build_get_object_output_context(
|
||||
request_context: &GetObjectRequestContext,
|
||||
cache_key: &str,
|
||||
manager: &ConcurrencyManager,
|
||||
bucket: &str,
|
||||
key: &str,
|
||||
info: ObjectInfo,
|
||||
event_info: ObjectInfo,
|
||||
body_source: GetObjectBodySource,
|
||||
rs: Option<HTTPRangeSpec>,
|
||||
content_type: Option<ContentType>,
|
||||
last_modified: Option<Timestamp>,
|
||||
response_content_length: i64,
|
||||
content_range: Option<String>,
|
||||
server_side_encryption: Option<ServerSideEncryption>,
|
||||
sse_customer_algorithm: Option<SSECustomerAlgorithm>,
|
||||
sse_customer_key_md5: Option<SSECustomerKeyMD5>,
|
||||
ssekms_key_id: Option<SSEKMSKeyId>,
|
||||
encryption_applied: bool,
|
||||
permit_wait_duration: Duration,
|
||||
queue_utilization: f64,
|
||||
queue_status: &concurrency::IoQueueStatus,
|
||||
concurrent_requests: usize,
|
||||
base_buffer_size: usize,
|
||||
part_number: Option<usize>,
|
||||
versioned: bool,
|
||||
) -> S3Result<(GetObjectOutputContext, ObjectIoGetObjectDataPlaneMetricContract)> {
|
||||
let (io_strategy, optimal_buffer_size) = finalize_get_object_strategy_runtime(
|
||||
base_buffer_size,
|
||||
manager,
|
||||
bucket,
|
||||
key,
|
||||
&info,
|
||||
rs.as_ref(),
|
||||
response_content_length,
|
||||
permit_wait_duration,
|
||||
queue_utilization,
|
||||
queue_status,
|
||||
concurrent_requests,
|
||||
);
|
||||
|
||||
let (body, metric_contract) = match body_source {
|
||||
GetObjectBodySource::Reader(final_stream) => {
|
||||
let cache_eligibility = manager.get_object_cache_eligibility(
|
||||
io_strategy.cache_writeback_enabled,
|
||||
part_number.is_some(),
|
||||
rs.is_some(),
|
||||
encryption_applied,
|
||||
response_content_length,
|
||||
);
|
||||
let adapter_output = build_get_object_body_adapter(
|
||||
final_stream,
|
||||
&info,
|
||||
cache_key,
|
||||
response_content_length,
|
||||
optimal_buffer_size,
|
||||
cache_eligibility,
|
||||
)
|
||||
.await?;
|
||||
let metric_contract = ObjectIoGetObjectDataPlaneMetricContract::disk(
|
||||
rustfs_io_metrics::IoPath::Legacy,
|
||||
rustfs_io_metrics::CopyMode::SingleCopy,
|
||||
adapter_output.body_plan,
|
||||
);
|
||||
if let Some(writeback) = adapter_output.cache_writeback {
|
||||
spawn_get_object_cache_writeback(cache_key, writeback, metric_contract);
|
||||
}
|
||||
|
||||
(adapter_output.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);
|
||||
(
|
||||
object_io_build_chunk_blob(chunk_stream),
|
||||
ObjectIoGetObjectDataPlaneMetricContract::disk(io_path, copy_mode, ObjectIoGetObjectBodyPlan::Stream),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
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,
|
||||
))
|
||||
}
|
||||
|
||||
pub(super) async fn run_get_object_flow(
|
||||
request_context: GetObjectRequestContext,
|
||||
runtime: GetObjectFlowRuntime<'_>,
|
||||
) -> S3Result<GetObjectFlowResult> {
|
||||
let GetObjectFlowRuntime {
|
||||
manager,
|
||||
bootstrap,
|
||||
base_buffer_size,
|
||||
} = runtime;
|
||||
let timeout_config = &bootstrap.timeout_config;
|
||||
let wrapper = &bootstrap.wrapper;
|
||||
let request_start = bootstrap.request_start;
|
||||
let concurrent_requests = bootstrap.concurrent_requests;
|
||||
let bucket = request_context.bucket.clone();
|
||||
let key = request_context.key.clone();
|
||||
let cache_key = request_context.cache_key.clone();
|
||||
let version_id_for_event = request_context.version_id_for_event.clone();
|
||||
let part_number = request_context.part_number;
|
||||
let rs = request_context.rs.clone();
|
||||
let opts = request_context.opts.clone();
|
||||
|
||||
if let Some(cached_result) = maybe_get_cached_get_object_flow_result(
|
||||
manager,
|
||||
&bucket,
|
||||
&key,
|
||||
&cache_key,
|
||||
version_id_for_event.clone(),
|
||||
part_number,
|
||||
rs.as_ref(),
|
||||
request_start,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Ok(cached_result);
|
||||
}
|
||||
|
||||
let prepared_read = prepare_get_object_read_execution(
|
||||
&request_context,
|
||||
manager,
|
||||
wrapper,
|
||||
timeout_config,
|
||||
&bucket,
|
||||
&key,
|
||||
rs,
|
||||
&opts,
|
||||
part_number,
|
||||
)
|
||||
.await?;
|
||||
let GetObjectPreparedRead { io_planning, read_setup } = prepared_read;
|
||||
let permit_wait_duration = io_planning.permit_wait_duration;
|
||||
let queue_status = io_planning.queue_status;
|
||||
let queue_utilization = io_planning.queue_utilization;
|
||||
|
||||
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;
|
||||
|
||||
let versioned = bucket_prefix_versioning_enabled(&bucket, &key).await;
|
||||
let (output_context, metric_contract) = build_get_object_output_context(
|
||||
&request_context,
|
||||
&cache_key,
|
||||
manager,
|
||||
&bucket,
|
||||
&key,
|
||||
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,
|
||||
permit_wait_duration,
|
||||
queue_utilization,
|
||||
&queue_status,
|
||||
concurrent_requests,
|
||||
base_buffer_size,
|
||||
part_number,
|
||||
versioned,
|
||||
)
|
||||
.await?;
|
||||
let response_content_length = output_context.response_content_length;
|
||||
let optimal_buffer_size = output_context.optimal_buffer_size;
|
||||
|
||||
let total_duration = request_start.elapsed();
|
||||
finalize_get_object_completion(
|
||||
&cache_key,
|
||||
wrapper,
|
||||
timeout_config,
|
||||
total_duration,
|
||||
response_content_length,
|
||||
optimal_buffer_size,
|
||||
metric_contract,
|
||||
);
|
||||
|
||||
Ok(object_io_build_cors_wrapped_get_object_flow_result(output_context, version_id_for_event))
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::app_adapters::get_validated_store_adapter;
|
||||
use super::types::GetObjectRequestContext;
|
||||
use crate::error::ApiError;
|
||||
use crate::storage::concurrency::{self, ConcurrencyManager};
|
||||
use crate::storage::timeout_wrapper::{RequestTimeoutWrapper, TimeoutConfig};
|
||||
use crate::storage::{
|
||||
DecryptionRequest, check_preconditions, sse_decryption, validate_sse_headers_for_read, validate_ssec_for_read,
|
||||
};
|
||||
use http::HeaderMap;
|
||||
use rustfs_concurrency::GetObjectQueueSnapshot;
|
||||
use rustfs_ecstore::store_api::{HTTPRangeSpec, ObjectIO, ObjectOperations, ObjectOptions};
|
||||
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, plan_chunk_read as object_io_plan_chunk_read,
|
||||
plan_legacy_read as object_io_plan_legacy_read,
|
||||
};
|
||||
use rustfs_rio::{Reader, WarpReader};
|
||||
use s3s::{S3Error, S3ErrorCode, S3Result, s3_error};
|
||||
use std::time::Duration;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
pub(super) struct GetObjectIoPlanning<'a> {
|
||||
pub(super) _disk_permit: tokio::sync::SemaphorePermit<'a>,
|
||||
pub(super) permit_wait_duration: Duration,
|
||||
pub(super) queue_status: concurrency::IoQueueStatus,
|
||||
pub(super) queue_utilization: f64,
|
||||
}
|
||||
|
||||
pub(super) struct GetObjectPreparedRead<'a> {
|
||||
pub(super) io_planning: GetObjectIoPlanning<'a>,
|
||||
pub(super) read_setup: GetObjectReadSetup,
|
||||
}
|
||||
|
||||
pub(super) async fn acquire_get_object_io_planning<'a>(
|
||||
manager: &'a ConcurrencyManager,
|
||||
wrapper: &RequestTimeoutWrapper,
|
||||
timeout_config: &TimeoutConfig,
|
||||
bucket: &str,
|
||||
key: &str,
|
||||
) -> S3Result<GetObjectIoPlanning<'a>> {
|
||||
let permit_wait_start = std::time::Instant::now();
|
||||
let disk_permit = manager
|
||||
.acquire_disk_read_permit()
|
||||
.await
|
||||
.map_err(|_| s3_error!(InternalError, "disk read semaphore closed"))?;
|
||||
let permit_wait_duration = permit_wait_start.elapsed();
|
||||
|
||||
if wrapper.is_timeout() {
|
||||
warn!(
|
||||
bucket = %bucket,
|
||||
key = %key,
|
||||
wait_ms = permit_wait_duration.as_millis(),
|
||||
timeout_secs = timeout_config.get_object_timeout.as_secs(),
|
||||
elapsed_ms = wrapper.elapsed().as_millis(),
|
||||
"GetObject request timed out while waiting for disk permit"
|
||||
);
|
||||
|
||||
rustfs_io_metrics::record_get_object_timeout(Some("disk_permit"), Some(wrapper.elapsed().as_secs_f64()));
|
||||
return Err(s3_error!(InternalError, "Request timeout while waiting for disk permit"));
|
||||
}
|
||||
|
||||
let queue_status = manager.io_queue_status();
|
||||
let queue_snapshot = GetObjectQueueSnapshot::from_available_permits(
|
||||
queue_status.total_permits,
|
||||
queue_status.total_permits.saturating_sub(queue_status.permits_in_use),
|
||||
);
|
||||
let queue_utilization = queue_snapshot.utilization_percent();
|
||||
|
||||
if queue_snapshot.is_congested(80.0) {
|
||||
warn!(
|
||||
bucket = %bucket,
|
||||
key = %key,
|
||||
queue_utilization = format!("{:.1}%", queue_utilization),
|
||||
permits_in_use = queue_status.permits_in_use,
|
||||
total_permits = queue_status.total_permits,
|
||||
"I/O queue congestion detected"
|
||||
);
|
||||
|
||||
rustfs_io_metrics::record_io_queue_congestion();
|
||||
}
|
||||
|
||||
if wrapper.is_timeout() {
|
||||
warn!(
|
||||
bucket = %bucket,
|
||||
key = %key,
|
||||
timeout_secs = timeout_config.get_object_timeout.as_secs(),
|
||||
elapsed_ms = wrapper.elapsed().as_millis(),
|
||||
"GetObject request timed out before reading object"
|
||||
);
|
||||
rustfs_io_metrics::record_get_object_timeout(Some("before_read"), Some(wrapper.elapsed().as_secs_f64()));
|
||||
return Err(s3_error!(InternalError, "Request timeout before reading object"));
|
||||
}
|
||||
|
||||
Ok(GetObjectIoPlanning {
|
||||
_disk_permit: disk_permit,
|
||||
permit_wait_duration,
|
||||
queue_status,
|
||||
queue_utilization,
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) async fn prepare_get_object_read(
|
||||
request_context: &GetObjectRequestContext,
|
||||
store: &rustfs_ecstore::store::ECStore,
|
||||
manager: &ConcurrencyManager,
|
||||
bucket: &str,
|
||||
key: &str,
|
||||
rs: Option<HTTPRangeSpec>,
|
||||
h: HeaderMap,
|
||||
opts: &ObjectOptions,
|
||||
part_number: Option<usize>,
|
||||
read_start: std::time::Instant,
|
||||
) -> S3Result<GetObjectReadSetup> {
|
||||
let reader = store
|
||||
.get_object_reader(bucket, key, rs.clone(), h, opts)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
let info = reader.object_info;
|
||||
|
||||
let read_duration = read_start.elapsed();
|
||||
rustfs_io_metrics::record_io_path_selected("get", rustfs_io_metrics::IoPath::Legacy);
|
||||
|
||||
manager.record_disk_operation(info.size as u64, read_duration, true).await;
|
||||
|
||||
check_preconditions(&request_context.headers, &info)?;
|
||||
|
||||
debug!(object_size = info.size, part_count = info.parts.len(), "GET object metadata snapshot");
|
||||
for part in &info.parts {
|
||||
debug!(
|
||||
part_number = part.number,
|
||||
part_size = part.size,
|
||||
part_actual_size = part.actual_size,
|
||||
"GET object part details"
|
||||
);
|
||||
}
|
||||
|
||||
let event_info = info.clone();
|
||||
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(),
|
||||
)?;
|
||||
let read_plan = object_io_plan_legacy_read(&info, rs, part_number).map_err(ApiError::from)?;
|
||||
|
||||
debug!(
|
||||
"GET object metadata check: parts={}, provided_sse_key={:?}",
|
||||
info.parts.len(),
|
||||
request_context.sse_customer_key.is_some()
|
||||
);
|
||||
|
||||
let decryption_request = DecryptionRequest {
|
||||
bucket,
|
||||
key,
|
||||
metadata: &info.user_defined,
|
||||
sse_customer_key: request_context.sse_customer_key.as_ref(),
|
||||
sse_customer_key_md5: request_context.sse_customer_key_md5.as_ref(),
|
||||
part_number: None,
|
||||
parts: &info.parts,
|
||||
etag: info.etag.as_deref(),
|
||||
};
|
||||
|
||||
let encrypted_stream = reader.stream;
|
||||
|
||||
let (encryption_state, final_stream) = match sse_decryption(decryption_request).await? {
|
||||
Some(material) => {
|
||||
let server_side_encryption = Some(material.server_side_encryption.clone());
|
||||
let sse_customer_algorithm = Some(material.algorithm.clone());
|
||||
let sse_customer_key_md5 = material.customer_key_md5.clone();
|
||||
let ssekms_key_id = material.kms_key_id.clone();
|
||||
let (decrypted_stream, plaintext_size) = material
|
||||
.wrap_reader(encrypted_stream, read_plan.response_content_length)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
(
|
||||
ObjectIoGetObjectEncryptionState {
|
||||
server_side_encryption,
|
||||
sse_customer_algorithm,
|
||||
sse_customer_key_md5,
|
||||
ssekms_key_id,
|
||||
encryption_applied: true,
|
||||
response_content_length_override: Some(plaintext_size),
|
||||
},
|
||||
decrypted_stream,
|
||||
)
|
||||
}
|
||||
None => (
|
||||
ObjectIoGetObjectEncryptionState::default(),
|
||||
Box::new(WarpReader::new(encrypted_stream)) as Box<dyn Reader>,
|
||||
),
|
||||
};
|
||||
|
||||
Ok(object_io_build_reader_read_setup(
|
||||
info,
|
||||
event_info,
|
||||
final_stream,
|
||||
read_plan,
|
||||
encryption_state,
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) async fn prepare_get_object_read_execution<'a>(
|
||||
request_context: &GetObjectRequestContext,
|
||||
manager: &'a ConcurrencyManager,
|
||||
wrapper: &RequestTimeoutWrapper,
|
||||
timeout_config: &TimeoutConfig,
|
||||
bucket: &str,
|
||||
key: &str,
|
||||
rs: Option<HTTPRangeSpec>,
|
||||
opts: &ObjectOptions,
|
||||
part_number: Option<usize>,
|
||||
) -> S3Result<GetObjectPreparedRead<'a>> {
|
||||
let h = HeaderMap::new();
|
||||
let io_planning = acquire_get_object_io_planning(manager, wrapper, timeout_config, bucket, key).await?;
|
||||
let store = get_validated_store_adapter(bucket).await?;
|
||||
|
||||
let read_start = std::time::Instant::now();
|
||||
let read_setup = 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,
|
||||
bucket,
|
||||
key,
|
||||
rs.clone(),
|
||||
part_number,
|
||||
opts,
|
||||
read_start,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some(read_setup) => read_setup,
|
||||
None => {
|
||||
prepare_get_object_read(request_context, &store, manager, bucket, key, rs, h, opts, part_number, read_start)
|
||||
.await?
|
||||
}
|
||||
},
|
||||
Err(fallback) => {
|
||||
rustfs_io_metrics::record_io_fallback(fallback.stage, fallback.reason);
|
||||
prepare_get_object_read(request_context, &store, manager, bucket, key, rs, h, opts, part_number, read_start).await?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(GetObjectPreparedRead { io_planning, read_setup })
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) async fn prepare_get_object_chunk_read(
|
||||
request_context: &GetObjectRequestContext,
|
||||
store: &rustfs_ecstore::store::ECStore,
|
||||
manager: &ConcurrencyManager,
|
||||
bucket: &str,
|
||||
key: &str,
|
||||
mut rs: Option<HTTPRangeSpec>,
|
||||
part_number: Option<usize>,
|
||||
opts: &ObjectOptions,
|
||||
read_start: std::time::Instant,
|
||||
) -> S3Result<Option<GetObjectReadSetup>> {
|
||||
let info = store.get_object_info(bucket, key, 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, opts.version_id.is_none(), rs.clone(), 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()),
|
||||
};
|
||||
rs = plan.rs.clone();
|
||||
|
||||
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(bucket, key, rs.clone(), HeaderMap::new(), 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 setup_result = object_io_finalize_chunk_read_setup(info, event_info, chunk_result, plan);
|
||||
rustfs_io_metrics::record_io_path_selected("get", setup_result.io_path);
|
||||
|
||||
Ok(Some(setup_result.read_setup))
|
||||
}
|
||||
@@ -0,0 +1,499 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::*;
|
||||
use crate::app::context::NotifyInterface;
|
||||
use rustfs_object_io::put::{
|
||||
apply_extract_entry_pax_extensions, apply_trailing_checksums, is_sse_kms_requested, map_extract_archive_error,
|
||||
normalize_extract_entry_key, resolve_put_object_extract_options,
|
||||
};
|
||||
|
||||
impl DefaultObjectUsecase {
|
||||
pub(super) async fn run_put_object_extract_flow(
|
||||
input: PutObjectInput,
|
||||
request_context: PutObjectRequestContext,
|
||||
notify: Arc<dyn NotifyInterface>,
|
||||
resolved_size: i64,
|
||||
) -> S3Result<PutObjectOutput> {
|
||||
if is_sse_kms_requested(&input, &request_context.headers) {
|
||||
return Err(s3_error!(NotImplemented, "SSE-KMS is not supported for extract uploads"));
|
||||
}
|
||||
|
||||
let PutObjectInput {
|
||||
body,
|
||||
bucket,
|
||||
key,
|
||||
version_id,
|
||||
cache_control,
|
||||
content_disposition,
|
||||
content_encoding,
|
||||
content_length: _content_length,
|
||||
content_language,
|
||||
content_type,
|
||||
content_md5,
|
||||
expires,
|
||||
object_lock_legal_hold_status,
|
||||
object_lock_mode,
|
||||
object_lock_retain_until_date,
|
||||
server_side_encryption,
|
||||
sse_customer_algorithm,
|
||||
sse_customer_key,
|
||||
sse_customer_key_md5,
|
||||
ssekms_key_id,
|
||||
storage_class,
|
||||
tagging,
|
||||
website_redirect_location,
|
||||
..
|
||||
} = input;
|
||||
|
||||
let event_version_id = version_id;
|
||||
let (h_algo, h_key, h_md5) = extract_ssec_params_from_headers(&request_context.headers)?;
|
||||
let sse_customer_algorithm = sse_customer_algorithm.or(h_algo);
|
||||
let sse_customer_key = sse_customer_key.or(h_key);
|
||||
let sse_customer_key_md5 = sse_customer_key_md5.or(h_md5);
|
||||
|
||||
let original_sse = server_side_encryption.or(extract_server_side_encryption_from_headers(&request_context.headers)?);
|
||||
let bucket_sse_config = metadata_sys::get_sse_config(&bucket).await.ok();
|
||||
let mut effective_sse = original_sse.or_else(|| {
|
||||
bucket_sse_config.as_ref().and_then(|(config, _timestamp)| {
|
||||
config.rules.first().and_then(|rule| {
|
||||
rule.apply_server_side_encryption_by_default
|
||||
.as_ref()
|
||||
.map(|sse| match sse.sse_algorithm.as_str() {
|
||||
"AES256" => ServerSideEncryption::from_static(ServerSideEncryption::AES256),
|
||||
"aws:kms" => ServerSideEncryption::from_static(ServerSideEncryption::AWS_KMS),
|
||||
_ => ServerSideEncryption::from_static(ServerSideEncryption::AES256),
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
let mut effective_kms_key_id = ssekms_key_id.or_else(|| {
|
||||
bucket_sse_config.as_ref().and_then(|(config, _timestamp)| {
|
||||
config.rules.first().and_then(|rule| {
|
||||
rule.apply_server_side_encryption_by_default
|
||||
.as_ref()
|
||||
.and_then(|sse| sse.kms_master_key_id.clone())
|
||||
})
|
||||
})
|
||||
});
|
||||
if effective_sse
|
||||
.as_ref()
|
||||
.is_some_and(|sse| sse.as_str().eq_ignore_ascii_case(ServerSideEncryption::AWS_KMS))
|
||||
{
|
||||
return Err(s3_error!(NotImplemented, "SSE-KMS is not supported for extract uploads"));
|
||||
}
|
||||
validate_sse_headers_for_write(
|
||||
effective_sse.as_ref(),
|
||||
effective_kms_key_id.as_ref(),
|
||||
sse_customer_algorithm.as_ref(),
|
||||
sse_customer_key.as_ref(),
|
||||
sse_customer_key_md5.as_ref(),
|
||||
true,
|
||||
)?;
|
||||
let Some(body) = body else { return Err(s3_error!(IncompleteBody)) };
|
||||
|
||||
let size = resolved_size;
|
||||
validate_object_key(&key, "PUT")?;
|
||||
|
||||
let buffer_size = get_buffer_size_opt_in(size);
|
||||
let body = tokio::io::BufReader::with_capacity(
|
||||
buffer_size,
|
||||
StreamReader::new(body.map(|f| f.map_err(|e| std::io::Error::other(e.to_string())))),
|
||||
);
|
||||
|
||||
let Some(ext) = Path::new(&key).extension().and_then(|s| s.to_str()) else {
|
||||
return Err(s3_error!(InvalidArgument, "key extension not found"));
|
||||
};
|
||||
|
||||
let ext = ext.to_owned();
|
||||
|
||||
let md5hex = if let Some(base64_md5) = content_md5 {
|
||||
let md5 = base64_simd::STANDARD
|
||||
.decode_to_vec(base64_md5.as_bytes())
|
||||
.map_err(|e| ApiError::from(StorageError::other(format!("Invalid content MD5: {e}"))))?;
|
||||
Some(hex_simd::encode_to_string(&md5, hex_simd::AsciiCase::Lower))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let sha256hex = get_content_sha256_with_query(&request_context.headers, request_context.uri_query.as_deref());
|
||||
let actual_size = size;
|
||||
|
||||
let mut archive_reader =
|
||||
HashReader::from_stream(body, size, actual_size, md5hex, sha256hex, false).map_err(ApiError::from)?;
|
||||
|
||||
if let Err(err) =
|
||||
archive_reader.add_checksum_from_s3s(&request_context.headers, request_context.trailing_headers.clone(), false)
|
||||
{
|
||||
return Err(ApiError::from(err).into());
|
||||
}
|
||||
|
||||
let archive_etag = Arc::new(Mutex::new(None));
|
||||
let decoder = CompressionFormat::from_extension(&ext)
|
||||
.get_decoder(ExtractArchiveEtagReader::new(archive_reader, archive_etag.clone()))
|
||||
.map_err(|e| {
|
||||
error!("get_decoder err {:?}", e);
|
||||
s3_error!(InvalidArgument, "get_decoder err")
|
||||
})?;
|
||||
|
||||
let mut ar = Archive::new(decoder);
|
||||
let mut entries = ar.entries().map_err(|e| {
|
||||
error!("get entries err {:?}", e);
|
||||
s3_error!(InvalidArgument, "get entries err")
|
||||
})?;
|
||||
|
||||
let Some(store) = new_object_layer_fn() else {
|
||||
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
|
||||
};
|
||||
|
||||
let extract_options = resolve_put_object_extract_options(&request_context.headers);
|
||||
let version_id = match event_version_id {
|
||||
Some(v) => v.to_string(),
|
||||
None => String::new(),
|
||||
};
|
||||
|
||||
let req_params = extract_params_header(&request_context.headers);
|
||||
let host = get_request_host(&request_context.headers);
|
||||
let port = get_request_port(&request_context.headers);
|
||||
let user_agent = get_request_user_agent(&request_context.headers);
|
||||
let tracing_context = request_context
|
||||
.extensions
|
||||
.get::<crate::storage::request_context::RequestContext>()
|
||||
.cloned();
|
||||
|
||||
while let Some(entry) = entries.next().await {
|
||||
let mut f = match entry {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
if extract_options.ignore_errors {
|
||||
warn!("Skipping archive entry because read failed and ignore-errors is enabled: {e}");
|
||||
continue;
|
||||
}
|
||||
error!("Failed to read archive entry: {}", e);
|
||||
return Err(s3_error!(InvalidArgument, "Failed to read archive entry: {:?}", e));
|
||||
}
|
||||
};
|
||||
|
||||
let fpath = match f.path() {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
if extract_options.ignore_errors {
|
||||
warn!("Skipping archive entry because path decode failed and ignore-errors is enabled: {e}");
|
||||
continue;
|
||||
}
|
||||
return Err(s3_error!(InvalidArgument, "Failed to decode archive entry path"));
|
||||
}
|
||||
};
|
||||
|
||||
let is_dir = f.header().entry_type().is_dir();
|
||||
let fpath = normalize_extract_entry_key(&fpath.to_string_lossy(), extract_options.prefix.as_deref(), is_dir);
|
||||
|
||||
authorize_extract_put_target(&request_context, &bucket, &fpath).await?;
|
||||
|
||||
let mut size = f.header().size().unwrap_or_default() as i64;
|
||||
let archive_entry_mod_time = f
|
||||
.header()
|
||||
.mtime()
|
||||
.ok()
|
||||
.and_then(|modified_at_secs| OffsetDateTime::from_unix_timestamp(modified_at_secs as i64).ok());
|
||||
let mut metadata = HashMap::new();
|
||||
apply_put_request_metadata(
|
||||
&mut metadata,
|
||||
&request_context.headers,
|
||||
&fpath,
|
||||
cache_control.clone(),
|
||||
content_disposition.clone(),
|
||||
content_encoding.clone(),
|
||||
content_language.clone(),
|
||||
content_type.clone(),
|
||||
expires.clone(),
|
||||
website_redirect_location.clone(),
|
||||
tagging.clone(),
|
||||
storage_class.clone(),
|
||||
)?;
|
||||
let mut opts = put_opts(&bucket, &fpath, None, &request_context.headers, metadata.clone())
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
apply_extract_entry_pax_extensions(&mut f, &mut metadata, &mut opts).await?;
|
||||
if archive_entry_mod_time.is_some() {
|
||||
opts.mod_time = archive_entry_mod_time;
|
||||
}
|
||||
|
||||
debug!("Extracting file: {}, size: {} bytes", fpath, size);
|
||||
|
||||
if is_dir {
|
||||
if extract_options.ignore_dirs {
|
||||
debug!("Skipping directory entry during archive extract: {}", fpath);
|
||||
continue;
|
||||
}
|
||||
size = 0;
|
||||
}
|
||||
|
||||
let actual_size = size;
|
||||
let should_compress = !is_dir && is_compressible(&HeaderMap::new(), &fpath) && size > MIN_COMPRESSIBLE_SIZE as i64;
|
||||
|
||||
let mut hrd = if is_dir {
|
||||
HashReader::from_stream(std::io::Cursor::new(Vec::new()), size, actual_size, None, None, false)
|
||||
.map_err(ApiError::from)?
|
||||
} else if should_compress {
|
||||
insert_str(&mut metadata, SUFFIX_COMPRESSION, CompressionAlgorithm::default().to_string());
|
||||
insert_str(&mut metadata, SUFFIX_ACTUAL_SIZE, size.to_string());
|
||||
|
||||
let hrd = HashReader::from_stream(f, size, actual_size, None, None, false).map_err(ApiError::from)?;
|
||||
size = HashReader::SIZE_PRESERVE_LAYER;
|
||||
HashReader::from_reader(
|
||||
CompressReader::new(hrd, CompressionAlgorithm::default()),
|
||||
size,
|
||||
actual_size,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.map_err(ApiError::from)?
|
||||
} else {
|
||||
HashReader::from_stream(f, size, actual_size, None, None, false).map_err(ApiError::from)?
|
||||
};
|
||||
apply_put_request_object_lock_opts(
|
||||
&bucket,
|
||||
object_lock_legal_hold_status.clone(),
|
||||
object_lock_mode.clone(),
|
||||
object_lock_retain_until_date.clone(),
|
||||
&mut opts,
|
||||
)
|
||||
.await?;
|
||||
if let Some(material) = sse_encryption(EncryptionRequest {
|
||||
bucket: &bucket,
|
||||
key: &fpath,
|
||||
server_side_encryption: effective_sse.clone(),
|
||||
ssekms_key_id: effective_kms_key_id.clone(),
|
||||
sse_customer_algorithm: sse_customer_algorithm.clone(),
|
||||
sse_customer_key: sse_customer_key.clone(),
|
||||
sse_customer_key_md5: sse_customer_key_md5.clone(),
|
||||
content_size: actual_size,
|
||||
part_number: None,
|
||||
part_key: None,
|
||||
part_nonce: None,
|
||||
})
|
||||
.await?
|
||||
{
|
||||
effective_sse = Some(material.server_side_encryption.clone());
|
||||
effective_kms_key_id = material.kms_key_id.clone();
|
||||
|
||||
let encrypted_reader = material.wrap_reader(hrd);
|
||||
hrd = HashReader::from_reader(encrypted_reader, HashReader::SIZE_PRESERVE_LAYER, actual_size, None, None, false)
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
let encryption_metadata = material.metadata;
|
||||
metadata.extend(encryption_metadata.clone());
|
||||
opts.user_defined.extend(encryption_metadata);
|
||||
}
|
||||
opts.user_defined.extend(metadata);
|
||||
let mut reader = rustfs_ecstore::store_api::ChunkNativePutData::new(hrd);
|
||||
|
||||
let obj_info = match store.put_object(&bucket, &fpath, &mut reader, &opts).await {
|
||||
Ok(info) => info,
|
||||
Err(e) => {
|
||||
if extract_options.ignore_errors {
|
||||
warn!("Skipping archive entry because object write failed and ignore-errors is enabled: {e}");
|
||||
continue;
|
||||
}
|
||||
return Err(ApiError::from(e).into());
|
||||
}
|
||||
};
|
||||
|
||||
let manager = get_concurrency_manager();
|
||||
let fpath_clone = fpath.clone();
|
||||
let bucket_clone = bucket.clone();
|
||||
crate::storage::request_context::spawn_traced(async move {
|
||||
manager.invalidate_cache_versioned(&bucket_clone, &fpath_clone, None).await;
|
||||
});
|
||||
|
||||
let e_tag = obj_info.etag.clone().map(|etag| to_s3s_etag(&etag));
|
||||
|
||||
let output = PutObjectOutput {
|
||||
e_tag,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
spawn_put_extract_notification(
|
||||
notify.clone(),
|
||||
tracing_context.clone(),
|
||||
bucket.clone(),
|
||||
req_params.clone(),
|
||||
version_id.clone(),
|
||||
host.clone(),
|
||||
port,
|
||||
user_agent.clone(),
|
||||
obj_info.clone(),
|
||||
output,
|
||||
);
|
||||
}
|
||||
|
||||
let mut checksums = PutObjectChecksums {
|
||||
crc32: input.checksum_crc32,
|
||||
crc32c: input.checksum_crc32c,
|
||||
sha1: input.checksum_sha1,
|
||||
sha256: input.checksum_sha256,
|
||||
crc64nvme: input.checksum_crc64nvme,
|
||||
};
|
||||
apply_trailing_checksums(
|
||||
input.checksum_algorithm.as_ref().map(|a| a.as_str()),
|
||||
&request_context.trailing_headers,
|
||||
&mut checksums,
|
||||
);
|
||||
|
||||
drop(entries);
|
||||
let mut decoder = match ar.into_inner() {
|
||||
Ok(decoder) => decoder,
|
||||
Err(_) => return Err(s3_error!(InvalidArgument, "Failed to finalize archive reader")),
|
||||
};
|
||||
tokio::io::copy(&mut decoder, &mut tokio::io::sink())
|
||||
.await
|
||||
.map_err(map_extract_archive_error)?;
|
||||
let archive_etag = archive_etag
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|etag| etag.clone())
|
||||
.map(|etag| to_s3s_etag(&etag));
|
||||
|
||||
let output = PutObjectOutput {
|
||||
e_tag: archive_etag,
|
||||
checksum_crc32: checksums.crc32,
|
||||
checksum_crc32c: checksums.crc32c,
|
||||
checksum_sha1: checksums.sha1,
|
||||
checksum_sha256: checksums.sha256,
|
||||
checksum_crc64nvme: checksums.crc64nvme,
|
||||
..Default::default()
|
||||
};
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use http::{Extensions, HeaderMap, HeaderValue, Method, Uri};
|
||||
use rustfs_utils::http::headers::{AMZ_SERVER_SIDE_ENCRYPTION, AMZ_SERVER_SIDE_ENCRYPTION_KMS_ID, AMZ_SNOWBALL_EXTRACT};
|
||||
|
||||
fn build_request<T>(input: T, method: Method) -> S3Request<T> {
|
||||
S3Request {
|
||||
input,
|
||||
method,
|
||||
uri: Uri::from_static("/"),
|
||||
headers: HeaderMap::new(),
|
||||
extensions: Extensions::new(),
|
||||
credentials: None,
|
||||
region: None,
|
||||
service: None,
|
||||
trailing_headers: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn execute_put_object_rejects_post_object_sse_kms_from_input() {
|
||||
let input = PutObjectInput::builder()
|
||||
.bucket("test-bucket".to_string())
|
||||
.key("test-key".to_string())
|
||||
.server_side_encryption(Some(ServerSideEncryption::from_static(ServerSideEncryption::AWS_KMS)))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let mut req = build_request(input, Method::POST);
|
||||
req.extensions.insert(PostObjectRequestMarker);
|
||||
|
||||
let usecase = DefaultObjectUsecase::without_context();
|
||||
let fs = FS::new();
|
||||
|
||||
let err = usecase.execute_put_object(&fs, req).await.unwrap_err();
|
||||
assert_eq!(err.code(), &S3ErrorCode::NotImplemented);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn execute_put_object_rejects_extract_sse_kms() {
|
||||
let input = PutObjectInput::builder()
|
||||
.bucket("test-bucket".to_string())
|
||||
.key("archive.tar".to_string())
|
||||
.server_side_encryption(Some(ServerSideEncryption::from_static(ServerSideEncryption::AWS_KMS)))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let mut req = build_request(input, Method::PUT);
|
||||
req.headers.insert(AMZ_SNOWBALL_EXTRACT, HeaderValue::from_static("true"));
|
||||
|
||||
let usecase = DefaultObjectUsecase::without_context();
|
||||
let fs = FS::new();
|
||||
|
||||
let err = usecase.execute_put_object(&fs, req).await.unwrap_err();
|
||||
assert_eq!(err.code(), &S3ErrorCode::NotImplemented);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn execute_put_object_extract_rejects_invalid_storage_class() {
|
||||
let input = PutObjectInput::builder()
|
||||
.bucket("test-bucket".to_string())
|
||||
.key("archive.tar".to_string())
|
||||
.storage_class(Some(StorageClass::from_static("INVALID")))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let mut req = build_request(input, Method::PUT);
|
||||
req.headers.insert(AMZ_SNOWBALL_EXTRACT, HeaderValue::from_static("true"));
|
||||
|
||||
let usecase = DefaultObjectUsecase::without_context();
|
||||
let fs = FS::new();
|
||||
|
||||
let err = usecase.execute_put_object(&fs, req).await.unwrap_err();
|
||||
assert_eq!(err.code(), &S3ErrorCode::InvalidStorageClass);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn execute_put_object_rejects_post_object_sse_kms_from_headers() {
|
||||
let input = PutObjectInput::builder()
|
||||
.bucket("test-bucket".to_string())
|
||||
.key("test-key".to_string())
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let mut req = build_request(input, Method::POST);
|
||||
req.extensions.insert(PostObjectRequestMarker);
|
||||
req.headers
|
||||
.insert(AMZ_SERVER_SIDE_ENCRYPTION, HeaderValue::from_static("aws:kms"));
|
||||
|
||||
let usecase = DefaultObjectUsecase::without_context();
|
||||
let fs = FS::new();
|
||||
|
||||
let err = usecase.execute_put_object(&fs, req).await.unwrap_err();
|
||||
assert_eq!(err.code(), &S3ErrorCode::NotImplemented);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn execute_put_object_rejects_post_object_sse_kms_key_id_header() {
|
||||
let input = PutObjectInput::builder()
|
||||
.bucket("test-bucket".to_string())
|
||||
.key("test-key".to_string())
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let mut req = build_request(input, Method::POST);
|
||||
req.extensions.insert(PostObjectRequestMarker);
|
||||
req.headers
|
||||
.insert(AMZ_SERVER_SIDE_ENCRYPTION_KMS_ID, HeaderValue::from_static("test-kms-key-id"));
|
||||
|
||||
let usecase = DefaultObjectUsecase::without_context();
|
||||
let fs = FS::new();
|
||||
|
||||
let err = usecase.execute_put_object(&fs, req).await.unwrap_err();
|
||||
assert_eq!(err.code(), &S3ErrorCode::NotImplemented);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,868 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::*;
|
||||
use bytes::Buf;
|
||||
use futures::{Stream, StreamExt};
|
||||
use rustfs_ecstore::config::GLOBAL_STORAGE_CLASS;
|
||||
use rustfs_io_core::{BytesPool, PooledBuffer};
|
||||
use rustfs_object_io::put::{
|
||||
PutObjectChecksums, PutObjectIngressKind, PutObjectLegacyHashStagePlan, PutObjectLegacyHashValues, PutObjectTransformStage,
|
||||
apply_trailing_checksums, build_put_object_ingress_source, build_put_object_legacy_hash_stage,
|
||||
build_put_object_plain_hash_stage, plan_put_object_body_with_transforms, resolve_put_transformed_fallback_reason,
|
||||
};
|
||||
use rustfs_rio::{BlockReadable, BoxReadBlockFuture, EtagResolvable, HashReaderDetector, TryGetIndex};
|
||||
use rustfs_utils::http::headers::AMZ_TRAILER;
|
||||
|
||||
const DEFAULT_SMALL_PUT_EAGER_MAX_BYTES: i64 = 1024 * 1024;
|
||||
const ENV_RUSTFS_PUT_SMALL_EAGER_MAX_BYTES: &str = "RUSTFS_PUT_SMALL_EAGER_MAX_BYTES";
|
||||
const ENV_RUSTFS_PUT_FORCE_DISABLE_SMALL_EAGER: &str = "RUSTFS_PUT_FORCE_DISABLE_SMALL_EAGER";
|
||||
const SLOW_PUT_PHASE_DEBUG_THRESHOLD_MS: u64 = 100;
|
||||
const SLOW_PUT_PHASE_WARN_THRESHOLD_MS: u64 = 1_000;
|
||||
const SLOW_PUT_PHASE_ERROR_THRESHOLD_MS: u64 = 5_000;
|
||||
|
||||
fn resolved_checksum_bytes(checksums: &PutObjectChecksums) -> Option<bytes::Bytes> {
|
||||
[
|
||||
(rustfs_rio::ChecksumType::CRC32, checksums.crc32.as_deref()),
|
||||
(rustfs_rio::ChecksumType::CRC32C, checksums.crc32c.as_deref()),
|
||||
(rustfs_rio::ChecksumType::SHA1, checksums.sha1.as_deref()),
|
||||
(rustfs_rio::ChecksumType::SHA256, checksums.sha256.as_deref()),
|
||||
(rustfs_rio::ChecksumType::CRC64_NVME, checksums.crc64nvme.as_deref()),
|
||||
]
|
||||
.into_iter()
|
||||
.find_map(|(checksum_type, value)| {
|
||||
value.and_then(|value| rustfs_rio::Checksum::new_with_type(checksum_type, value).map(|checksum| checksum.to_bytes(&[])))
|
||||
})
|
||||
}
|
||||
|
||||
fn clamp_small_put_eager_max_bytes(inline_object_limit_bytes: Option<usize>) -> i64 {
|
||||
inline_object_limit_bytes
|
||||
.unwrap_or(DEFAULT_SMALL_PUT_EAGER_MAX_BYTES as usize)
|
||||
.min(DEFAULT_SMALL_PUT_EAGER_MAX_BYTES as usize) as i64
|
||||
}
|
||||
|
||||
fn env_flag_enabled(name: &str) -> bool {
|
||||
rustfs_utils::get_env_bool(name, false)
|
||||
}
|
||||
|
||||
fn env_non_negative_i64(name: &str) -> Option<i64> {
|
||||
rustfs_utils::get_env_opt_i64(name).filter(|value| *value >= 0)
|
||||
}
|
||||
|
||||
fn topology_aware_small_put_eager_max_bytes(store: &rustfs_ecstore::store::ECStore, versioned: bool) -> i64 {
|
||||
let Some(first_pool) = store.pools.first() else {
|
||||
return DEFAULT_SMALL_PUT_EAGER_MAX_BYTES;
|
||||
};
|
||||
|
||||
let data_shards = first_pool
|
||||
.set_drive_count
|
||||
.saturating_sub(first_pool.default_parity_count)
|
||||
.max(1);
|
||||
|
||||
let inline_object_limit = GLOBAL_STORAGE_CLASS
|
||||
.get()
|
||||
.map(|config| config.inline_object_limit_bytes(data_shards, versioned));
|
||||
|
||||
clamp_small_put_eager_max_bytes(inline_object_limit)
|
||||
}
|
||||
|
||||
fn resolved_small_put_eager_max_bytes(default_max_bytes: i64) -> i64 {
|
||||
if env_flag_enabled(ENV_RUSTFS_PUT_FORCE_DISABLE_SMALL_EAGER) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
env_non_negative_i64(ENV_RUSTFS_PUT_SMALL_EAGER_MAX_BYTES)
|
||||
.map(|value| value.min(DEFAULT_SMALL_PUT_EAGER_MAX_BYTES).min(default_max_bytes))
|
||||
.unwrap_or(default_max_bytes)
|
||||
}
|
||||
|
||||
fn should_use_small_put_eager_path(size: i64, eager_max_bytes: i64, compression_enabled: bool, encryption_enabled: bool) -> bool {
|
||||
size > 0 && size <= eager_max_bytes && !compression_enabled && !encryption_enabled
|
||||
}
|
||||
|
||||
fn request_uses_trailing_checksum(headers: &HeaderMap, trailing_headers: &Option<s3s::TrailingHeaders>) -> bool {
|
||||
trailing_headers.is_some()
|
||||
|| headers.contains_key(AMZ_TRAILER)
|
||||
|| matches!(
|
||||
rustfs_rio::get_content_checksum(headers),
|
||||
Ok(Some(checksum)) if checksum.checksum_type.trailing()
|
||||
)
|
||||
}
|
||||
|
||||
fn put_path_label(small_eager: bool, reduced_copy: bool, compressed: bool) -> &'static str {
|
||||
if small_eager {
|
||||
"small_eager"
|
||||
} else if compressed {
|
||||
"compressed"
|
||||
} else if reduced_copy {
|
||||
"reduced_copy"
|
||||
} else {
|
||||
"legacy_plain"
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn log_put_flow_phase(
|
||||
bucket: &str,
|
||||
key: &str,
|
||||
phase: &str,
|
||||
elapsed: std::time::Duration,
|
||||
object_size: i64,
|
||||
small_eager: bool,
|
||||
reduced_copy: bool,
|
||||
compressed: bool,
|
||||
encrypted: bool,
|
||||
) {
|
||||
let duration_ms = elapsed.as_millis() as u64;
|
||||
if duration_ms < SLOW_PUT_PHASE_DEBUG_THRESHOLD_MS {
|
||||
return;
|
||||
}
|
||||
|
||||
let put_path = put_path_label(small_eager, reduced_copy, compressed);
|
||||
if duration_ms >= SLOW_PUT_PHASE_ERROR_THRESHOLD_MS {
|
||||
error!(
|
||||
phase,
|
||||
duration_ms, object_size, put_path, compressed, encrypted, bucket, key, "Small PUT phase is critically slow"
|
||||
);
|
||||
} else if duration_ms >= SLOW_PUT_PHASE_WARN_THRESHOLD_MS {
|
||||
warn!(
|
||||
phase,
|
||||
duration_ms, object_size, put_path, compressed, encrypted, bucket, key, "Small PUT phase is slow"
|
||||
);
|
||||
} else {
|
||||
debug!(
|
||||
phase,
|
||||
duration_ms, object_size, put_path, compressed, encrypted, bucket, key, "Small PUT phase exceeded debug threshold"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
struct PooledBufferReader {
|
||||
buffer: PooledBuffer,
|
||||
position: usize,
|
||||
}
|
||||
|
||||
impl PooledBufferReader {
|
||||
fn new(buffer: PooledBuffer) -> Self {
|
||||
Self { buffer, position: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl tokio::io::AsyncRead for PooledBufferReader {
|
||||
fn poll_read(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
buf: &mut tokio::io::ReadBuf<'_>,
|
||||
) -> std::task::Poll<std::io::Result<()>> {
|
||||
let remaining = &self.buffer[self.position..];
|
||||
if remaining.is_empty() {
|
||||
return std::task::Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
let to_copy = remaining.len().min(buf.remaining());
|
||||
buf.put_slice(&remaining[..to_copy]);
|
||||
self.position += to_copy;
|
||||
std::task::Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockReadable for PooledBufferReader {
|
||||
fn read_block<'a>(&'a mut self, buf: &'a mut [u8]) -> BoxReadBlockFuture<'a> {
|
||||
Box::pin(async move {
|
||||
let remaining = &self.buffer[self.position..];
|
||||
if remaining.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let to_copy = remaining.len().min(buf.len());
|
||||
buf[..to_copy].copy_from_slice(&remaining[..to_copy]);
|
||||
self.position += to_copy;
|
||||
Ok(to_copy)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl EtagResolvable for PooledBufferReader {}
|
||||
|
||||
impl HashReaderDetector for PooledBufferReader {}
|
||||
|
||||
impl TryGetIndex for PooledBufferReader {}
|
||||
|
||||
async fn read_small_put_body_eager<S, B, E>(body: S, size: i64, pool: std::sync::Arc<BytesPool>) -> S3Result<PooledBuffer>
|
||||
where
|
||||
S: Stream<Item = Result<B, E>>,
|
||||
B: Buf,
|
||||
E: std::fmt::Display,
|
||||
{
|
||||
let expected_len = usize::try_from(size).map_err(|_| s3_error!(InvalidRequest, "Object size overflow"))?;
|
||||
let mut data = pool.acquire_buffer(expected_len).await;
|
||||
let mut body = Box::pin(body);
|
||||
|
||||
while let Some(result) = body.next().await {
|
||||
let mut chunk = result.map_err(|err| S3Error::with_message(S3ErrorCode::IncompleteBody, err.to_string()))?;
|
||||
let chunk_len = chunk.remaining();
|
||||
if chunk_len == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let new_len = data
|
||||
.len()
|
||||
.checked_add(chunk_len)
|
||||
.ok_or_else(|| s3_error!(InvalidRequest, "Object size overflow"))?;
|
||||
if new_len > expected_len {
|
||||
return Err(s3_error!(IncompleteBody));
|
||||
}
|
||||
|
||||
let start = data.len();
|
||||
data.resize(new_len, 0);
|
||||
chunk.copy_to_slice(&mut data[start..new_len]);
|
||||
|
||||
if data.len() == expected_len {
|
||||
return Ok(data);
|
||||
}
|
||||
}
|
||||
|
||||
if data.len() != expected_len {
|
||||
return Err(s3_error!(IncompleteBody));
|
||||
}
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
async fn build_small_put_eager_hash_stage<S, B, E>(
|
||||
body: S,
|
||||
size: i64,
|
||||
pool: std::sync::Arc<BytesPool>,
|
||||
hash_values: PutObjectLegacyHashValues,
|
||||
headers: &HeaderMap,
|
||||
trailing_headers: Option<s3s::TrailingHeaders>,
|
||||
) -> S3Result<rustfs_object_io::put::PutObjectHashStage>
|
||||
where
|
||||
S: Stream<Item = Result<B, E>>,
|
||||
B: Buf,
|
||||
E: std::fmt::Display,
|
||||
{
|
||||
let data = read_small_put_body_eager(body, size, pool).await?;
|
||||
build_put_object_legacy_hash_stage(
|
||||
Box::new(PooledBufferReader::new(data)),
|
||||
hash_values,
|
||||
PutObjectLegacyHashStagePlan {
|
||||
size,
|
||||
actual_size: size,
|
||||
apply_s3_checksum: true,
|
||||
ignore_s3_checksum_value: false,
|
||||
},
|
||||
headers,
|
||||
trailing_headers,
|
||||
)
|
||||
.map_err(ApiError::from)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
impl DefaultObjectUsecase {
|
||||
pub(super) async fn run_put_object_flow(
|
||||
input: PutObjectInput,
|
||||
request_context: PutObjectRequestContext,
|
||||
request_method_name: &'static str,
|
||||
resolved_size: i64,
|
||||
) -> S3Result<PutObjectFlowResult> {
|
||||
let start_time = std::time::Instant::now();
|
||||
|
||||
let PutObjectInput {
|
||||
body,
|
||||
bucket,
|
||||
cache_control,
|
||||
key,
|
||||
content_length: _content_length,
|
||||
content_disposition,
|
||||
content_encoding,
|
||||
content_language,
|
||||
content_type,
|
||||
expires,
|
||||
tagging,
|
||||
metadata,
|
||||
version_id,
|
||||
server_side_encryption,
|
||||
sse_customer_algorithm,
|
||||
sse_customer_key,
|
||||
sse_customer_key_md5,
|
||||
ssekms_key_id,
|
||||
content_md5,
|
||||
object_lock_legal_hold_status,
|
||||
object_lock_mode,
|
||||
object_lock_retain_until_date,
|
||||
storage_class,
|
||||
website_redirect_location,
|
||||
..
|
||||
} = input;
|
||||
|
||||
let (h_algo, h_key, h_md5) = extract_ssec_params_from_headers(&request_context.headers)?;
|
||||
let sse_customer_algorithm = sse_customer_algorithm.or(h_algo);
|
||||
let sse_customer_key = sse_customer_key.or(h_key);
|
||||
let sse_customer_key_md5 = sse_customer_key_md5.or(h_md5);
|
||||
|
||||
let server_side_encryption =
|
||||
server_side_encryption.or(extract_server_side_encryption_from_headers(&request_context.headers)?);
|
||||
|
||||
validate_object_key(&key, request_method_name)?;
|
||||
|
||||
let Some(body) = body else { return Err(s3_error!(IncompleteBody)) };
|
||||
|
||||
let mut size = resolved_size;
|
||||
let mut transform_stage = PutObjectTransformStage::default();
|
||||
let mut plain_reduced_copy_stage = false;
|
||||
let mut small_object_eager_stage = false;
|
||||
let bytes_pool = get_concurrency_manager().bytes_pool();
|
||||
|
||||
let store = get_validated_store_adapter(&bucket).await?;
|
||||
|
||||
let bucket_sse_config = metadata_sys::get_sse_config(&bucket).await.ok();
|
||||
|
||||
let mut effective_sse = server_side_encryption.or_else(|| {
|
||||
bucket_sse_config.as_ref().and_then(|(config, _timestamp)| {
|
||||
config.rules.first().and_then(|rule| {
|
||||
rule.apply_server_side_encryption_by_default
|
||||
.as_ref()
|
||||
.map(|sse| match sse.sse_algorithm.as_str() {
|
||||
"AES256" => ServerSideEncryption::from_static(ServerSideEncryption::AES256),
|
||||
"aws:kms" => ServerSideEncryption::from_static(ServerSideEncryption::AWS_KMS),
|
||||
_ => ServerSideEncryption::from_static(ServerSideEncryption::AES256),
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
let mut effective_kms_key_id = ssekms_key_id.or_else(|| {
|
||||
bucket_sse_config.as_ref().and_then(|(config, _timestamp)| {
|
||||
config.rules.first().and_then(|rule| {
|
||||
rule.apply_server_side_encryption_by_default
|
||||
.as_ref()
|
||||
.and_then(|sse| sse.kms_master_key_id.clone())
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
validate_sse_headers_for_write(
|
||||
effective_sse.as_ref(),
|
||||
effective_kms_key_id.as_ref(),
|
||||
sse_customer_algorithm.as_ref(),
|
||||
sse_customer_key.as_ref(),
|
||||
sse_customer_key_md5.as_ref(),
|
||||
true,
|
||||
)?;
|
||||
|
||||
let encryption_enabled_for_put = effective_sse.is_some()
|
||||
|| effective_kms_key_id.is_some()
|
||||
|| sse_customer_algorithm.is_some()
|
||||
|| sse_customer_key.is_some()
|
||||
|| sse_customer_key_md5.is_some();
|
||||
|
||||
let body_plan = plan_put_object_body_with_transforms(
|
||||
size,
|
||||
&request_context.headers,
|
||||
&key,
|
||||
get_buffer_size_opt_in(size),
|
||||
encryption_enabled_for_put,
|
||||
);
|
||||
if body_plan.ingress.kind == PutObjectIngressKind::ReducedCopyCandidate {
|
||||
rustfs_io_metrics::record_put_object_attempted_fast_path(size);
|
||||
debug!(
|
||||
encryption_enabled = encryption_enabled_for_put,
|
||||
compressed = body_plan.should_compress(),
|
||||
"Zero-copy write enabled for {} byte object (bucket={}, key={})",
|
||||
size,
|
||||
bucket,
|
||||
key
|
||||
);
|
||||
} else if let Some(reason) = resolve_put_transformed_fallback_reason(
|
||||
body_plan.ingress.kind,
|
||||
body_plan.should_compress(),
|
||||
encryption_enabled_for_put,
|
||||
) {
|
||||
rustfs_io_metrics::record_io_fallback(rustfs_io_metrics::IoStage::PutTransform, reason);
|
||||
rustfs_io_metrics::record_put_fallback(size, reason);
|
||||
}
|
||||
|
||||
let mut metadata = metadata.unwrap_or_default();
|
||||
apply_put_request_metadata(
|
||||
&mut metadata,
|
||||
&request_context.headers,
|
||||
&key,
|
||||
cache_control,
|
||||
content_disposition,
|
||||
content_encoding,
|
||||
content_language,
|
||||
content_type,
|
||||
expires,
|
||||
website_redirect_location,
|
||||
tagging,
|
||||
storage_class.clone(),
|
||||
)?;
|
||||
|
||||
let mut opts: ObjectOptions = put_opts(&bucket, &key, version_id.clone(), &request_context.headers, metadata.clone())
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
apply_put_request_object_lock_opts(
|
||||
&bucket,
|
||||
object_lock_legal_hold_status,
|
||||
object_lock_mode,
|
||||
object_lock_retain_until_date,
|
||||
&mut opts,
|
||||
)
|
||||
.await?;
|
||||
let eager_max_bytes =
|
||||
resolved_small_put_eager_max_bytes(topology_aware_small_put_eager_max_bytes(&store, opts.versioned));
|
||||
let can_use_small_put_eager =
|
||||
!request_uses_trailing_checksum(&request_context.headers, &request_context.trailing_headers);
|
||||
|
||||
let current_opts: ObjectOptions = get_opts(&bucket, &key, version_id.clone(), None, &request_context.headers)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
match store.get_object_info(&bucket, &key, ¤t_opts).await {
|
||||
Ok(existing_obj_info) => validate_existing_object_lock_for_write(&existing_obj_info)?,
|
||||
Err(err) => {
|
||||
if !is_err_object_not_found(&err) && !is_err_version_not_found(&err) {
|
||||
return Err(ApiError::from(err).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let actual_size = size;
|
||||
let mut hash_values = PutObjectLegacyHashValues {
|
||||
md5hex: if let Some(base64_md5) = content_md5 {
|
||||
let md5 = base64_simd::STANDARD
|
||||
.decode_to_vec(base64_md5.as_bytes())
|
||||
.map_err(|e| ApiError::from(StorageError::other(format!("Invalid content MD5: {e}"))))?;
|
||||
Some(hex_simd::encode_to_string(&md5, hex_simd::AsciiCase::Lower))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
sha256hex: get_content_sha256_with_query(&request_context.headers, request_context.uri_query.as_deref()),
|
||||
};
|
||||
|
||||
let reader_stage_start = std::time::Instant::now();
|
||||
let stage = if can_use_small_put_eager
|
||||
&& should_use_small_put_eager_path(size, eager_max_bytes, body_plan.should_compress(), encryption_enabled_for_put)
|
||||
{
|
||||
small_object_eager_stage = true;
|
||||
debug!(
|
||||
"Plain PUT is using the eager small-object path (bucket={}, key={}, size={}, eager_max={})",
|
||||
bucket, key, size, eager_max_bytes
|
||||
);
|
||||
build_small_put_eager_hash_stage(
|
||||
body,
|
||||
size,
|
||||
bytes_pool.clone(),
|
||||
hash_values,
|
||||
&request_context.headers,
|
||||
request_context.trailing_headers.clone(),
|
||||
)
|
||||
.await?
|
||||
} else if body_plan.should_compress() {
|
||||
transform_stage.mark_compression();
|
||||
let algorithm = CompressionAlgorithm::default();
|
||||
insert_str(&mut metadata, SUFFIX_COMPRESSION, algorithm.to_string());
|
||||
insert_str(&mut metadata, SUFFIX_ACTUAL_SIZE, size.to_string());
|
||||
|
||||
let ingress_source = build_put_object_ingress_source(body, body_plan);
|
||||
let stage = build_put_object_plain_hash_stage(
|
||||
ingress_source,
|
||||
std::mem::take(&mut hash_values),
|
||||
PutObjectLegacyHashStagePlan {
|
||||
size,
|
||||
actual_size: size,
|
||||
apply_s3_checksum: true,
|
||||
ignore_s3_checksum_value: false,
|
||||
},
|
||||
&request_context.headers,
|
||||
request_context.trailing_headers.clone(),
|
||||
)
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
if stage.ingress_kind == PutObjectIngressKind::ReducedCopyCandidate {
|
||||
plain_reduced_copy_stage = true;
|
||||
}
|
||||
opts.want_checksum = stage.want_checksum;
|
||||
insert_str(&mut opts.user_defined, SUFFIX_COMPRESSION, algorithm.to_string());
|
||||
insert_str(&mut opts.user_defined, SUFFIX_ACTUAL_SIZE, size.to_string());
|
||||
|
||||
let reader: Box<dyn Reader> = Box::new(CompressReader::new(stage.reader, algorithm));
|
||||
size = HashReader::SIZE_PRESERVE_LAYER;
|
||||
hash_values.clear_for_transformed_body();
|
||||
build_put_object_legacy_hash_stage(
|
||||
reader,
|
||||
hash_values,
|
||||
PutObjectLegacyHashStagePlan {
|
||||
size,
|
||||
actual_size,
|
||||
apply_s3_checksum: size >= 0,
|
||||
ignore_s3_checksum_value: false,
|
||||
},
|
||||
&request_context.headers,
|
||||
request_context.trailing_headers.clone(),
|
||||
)
|
||||
.map_err(ApiError::from)?
|
||||
} else {
|
||||
let ingress_source = build_put_object_ingress_source(body, body_plan);
|
||||
let stage = build_put_object_plain_hash_stage(
|
||||
ingress_source,
|
||||
hash_values,
|
||||
PutObjectLegacyHashStagePlan {
|
||||
size,
|
||||
actual_size,
|
||||
apply_s3_checksum: size >= 0,
|
||||
ignore_s3_checksum_value: false,
|
||||
},
|
||||
&request_context.headers,
|
||||
request_context.trailing_headers.clone(),
|
||||
)
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
if stage.ingress_kind == PutObjectIngressKind::ReducedCopyCandidate {
|
||||
plain_reduced_copy_stage = true;
|
||||
debug!(
|
||||
"Plain PUT is using the reduced-copy Reader + BlockReadable hash path (bucket={}, key={})",
|
||||
bucket, key
|
||||
);
|
||||
}
|
||||
|
||||
stage
|
||||
};
|
||||
log_put_flow_phase(
|
||||
&bucket,
|
||||
&key,
|
||||
"build_hash_stage",
|
||||
reader_stage_start.elapsed(),
|
||||
actual_size,
|
||||
small_object_eager_stage,
|
||||
plain_reduced_copy_stage,
|
||||
transform_stage.compression_applied(),
|
||||
false,
|
||||
);
|
||||
let mut reader = stage.reader;
|
||||
if stage.want_checksum.is_some() {
|
||||
opts.want_checksum = stage.want_checksum;
|
||||
}
|
||||
|
||||
let encryption_request = EncryptionRequest {
|
||||
bucket: &bucket,
|
||||
key: &key,
|
||||
server_side_encryption: effective_sse.clone(),
|
||||
ssekms_key_id: effective_kms_key_id.clone(),
|
||||
sse_customer_algorithm: sse_customer_algorithm.clone(),
|
||||
sse_customer_key,
|
||||
sse_customer_key_md5: sse_customer_key_md5.clone(),
|
||||
content_size: actual_size,
|
||||
part_number: None,
|
||||
part_key: None,
|
||||
part_nonce: None,
|
||||
};
|
||||
|
||||
if let Some(material) = sse_encryption(encryption_request).await? {
|
||||
transform_stage.mark_encryption();
|
||||
effective_sse = Some(material.server_side_encryption.clone());
|
||||
effective_kms_key_id = material.kms_key_id.clone();
|
||||
|
||||
let encrypted_reader = material.wrap_reader(reader);
|
||||
reader = HashReader::new(encrypted_reader, HashReader::SIZE_PRESERVE_LAYER, actual_size, None, None, false)
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
let encryption_metadata = material.metadata;
|
||||
metadata.extend(encryption_metadata.clone());
|
||||
opts.user_defined.extend(encryption_metadata);
|
||||
}
|
||||
|
||||
let mut reader = ChunkNativePutData::new(reader);
|
||||
|
||||
let mt2 = metadata.clone();
|
||||
opts.user_defined.extend(metadata);
|
||||
|
||||
let repoptions =
|
||||
get_must_replicate_options(&mt2, "".to_string(), ReplicationStatusType::Empty, ReplicationType::Object, opts.clone());
|
||||
|
||||
let dsc = must_replicate(&bucket, &key, repoptions).await;
|
||||
|
||||
if dsc.replicate_any() {
|
||||
insert_str(&mut opts.user_defined, SUFFIX_REPLICATION_TIMESTAMP, jiff::Zoned::now().to_string());
|
||||
insert_str(
|
||||
&mut opts.user_defined,
|
||||
SUFFIX_REPLICATION_STATUS,
|
||||
dsc.pending_status().unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
|
||||
let store_put_start = std::time::Instant::now();
|
||||
let obj_info = store
|
||||
.put_object(&bucket, &key, &mut reader, &opts)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
log_put_flow_phase(
|
||||
&bucket,
|
||||
&key,
|
||||
"store_put_object",
|
||||
store_put_start.elapsed(),
|
||||
actual_size,
|
||||
small_object_eager_stage,
|
||||
plain_reduced_copy_stage,
|
||||
transform_stage.compression_applied(),
|
||||
transform_stage.encryption_applied(),
|
||||
);
|
||||
|
||||
maybe_enqueue_transition_immediate(&obj_info, LcEventSrc::S3PutObject).await;
|
||||
|
||||
rustfs_ecstore::data_usage::increment_bucket_usage_memory(&bucket, obj_info.size as u64).await;
|
||||
|
||||
let raw_version = obj_info.version_id.map(|v| v.to_string());
|
||||
|
||||
Self::spawn_cache_invalidation(bucket.clone(), key.clone(), raw_version.clone());
|
||||
|
||||
let put_version = if bucket_prefix_versioning_enabled(&bucket, &key).await {
|
||||
raw_version.clone()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let e_tag = obj_info.etag.clone().map(|etag| to_s3s_etag(&etag));
|
||||
|
||||
let repoptions =
|
||||
get_must_replicate_options(&mt2, "".to_string(), ReplicationStatusType::Empty, ReplicationType::Object, opts);
|
||||
|
||||
let dsc = must_replicate(&bucket, &key, repoptions).await;
|
||||
let expiration = resolve_put_object_expiration(&bucket, &obj_info).await;
|
||||
|
||||
if dsc.replicate_any() {
|
||||
schedule_replication(obj_info.clone(), store.clone(), dsc, ReplicationType::Object).await;
|
||||
}
|
||||
|
||||
let mut checksums = PutObjectChecksums {
|
||||
crc32: input.checksum_crc32,
|
||||
crc32c: input.checksum_crc32c,
|
||||
sha1: input.checksum_sha1,
|
||||
sha256: input.checksum_sha256,
|
||||
crc64nvme: input.checksum_crc64nvme,
|
||||
};
|
||||
apply_trailing_checksums(
|
||||
input.checksum_algorithm.as_ref().map(|a| a.as_str()),
|
||||
&request_context.trailing_headers,
|
||||
&mut checksums,
|
||||
);
|
||||
checksums.merge_from_map(&reader.content_crc());
|
||||
if let Some(checksum_bytes) = resolved_checksum_bytes(&checksums)
|
||||
&& obj_info
|
||||
.checksum
|
||||
.as_ref()
|
||||
.is_none_or(|stored| rustfs_rio::read_checksums(stored.as_ref(), 0).0.is_empty())
|
||||
{
|
||||
let checksum_update_opts = ObjectOptions {
|
||||
version_id: raw_version.clone(),
|
||||
resolved_checksum: Some(checksum_bytes),
|
||||
..Default::default()
|
||||
};
|
||||
let _ = store
|
||||
.put_object_metadata(&bucket, &key, &checksum_update_opts)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
}
|
||||
|
||||
let output = PutObjectOutput {
|
||||
e_tag,
|
||||
server_side_encryption: effective_sse,
|
||||
sse_customer_algorithm: sse_customer_algorithm.clone(),
|
||||
sse_customer_key_md5: sse_customer_key_md5.clone(),
|
||||
ssekms_key_id: effective_kms_key_id,
|
||||
expiration,
|
||||
checksum_crc32: checksums.crc32,
|
||||
checksum_crc32c: checksums.crc32c,
|
||||
checksum_sha1: checksums.sha1,
|
||||
checksum_sha256: checksums.sha256,
|
||||
checksum_crc64nvme: checksums.crc64nvme,
|
||||
version_id: put_version,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let manager = get_capacity_manager();
|
||||
manager.record_write_operation().await;
|
||||
|
||||
{
|
||||
let duration_ms = start_time.elapsed().as_millis() as f64;
|
||||
let fast_path_selected = plain_reduced_copy_stage || small_object_eager_stage;
|
||||
rustfs_io_metrics::record_put_object(duration_ms, size, fast_path_selected);
|
||||
let io_path = if fast_path_selected {
|
||||
rustfs_io_metrics::IoPath::Fast
|
||||
} else {
|
||||
rustfs_io_metrics::IoPath::Legacy
|
||||
};
|
||||
rustfs_io_metrics::record_io_path_selected("put", io_path);
|
||||
rustfs_io_metrics::record_put_path_selected(actual_size, io_path);
|
||||
let effective_copy_mode = transform_stage.effective_copy_mode();
|
||||
rustfs_io_metrics::record_io_copy_mode("put", effective_copy_mode, actual_size.max(0) as usize);
|
||||
rustfs_io_metrics::record_put_copy_mode(actual_size, effective_copy_mode);
|
||||
if let Some(transform_kind) = transform_stage.metric_kind() {
|
||||
rustfs_io_metrics::record_put_transform_selected(transform_kind, io_path, actual_size.max(0) as usize);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(PutObjectFlowResult {
|
||||
output,
|
||||
helper_object: obj_info,
|
||||
helper_version_id: raw_version,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use bytes::Bytes;
|
||||
use futures::{StreamExt, stream};
|
||||
use rustfs_io_core::BytesPool;
|
||||
use serial_test::serial;
|
||||
use std::sync::Arc;
|
||||
use tokio::time::{Duration, timeout};
|
||||
|
||||
#[test]
|
||||
fn small_put_eager_path_only_targets_plain_small_objects() {
|
||||
assert!(should_use_small_put_eager_path(
|
||||
64 * 1024,
|
||||
DEFAULT_SMALL_PUT_EAGER_MAX_BYTES,
|
||||
false,
|
||||
false
|
||||
));
|
||||
assert!(should_use_small_put_eager_path(
|
||||
DEFAULT_SMALL_PUT_EAGER_MAX_BYTES,
|
||||
DEFAULT_SMALL_PUT_EAGER_MAX_BYTES,
|
||||
false,
|
||||
false
|
||||
));
|
||||
assert!(!should_use_small_put_eager_path(
|
||||
DEFAULT_SMALL_PUT_EAGER_MAX_BYTES + 1,
|
||||
DEFAULT_SMALL_PUT_EAGER_MAX_BYTES,
|
||||
false,
|
||||
false
|
||||
));
|
||||
assert!(!should_use_small_put_eager_path(
|
||||
64 * 1024,
|
||||
DEFAULT_SMALL_PUT_EAGER_MAX_BYTES,
|
||||
true,
|
||||
false
|
||||
));
|
||||
assert!(!should_use_small_put_eager_path(
|
||||
64 * 1024,
|
||||
DEFAULT_SMALL_PUT_EAGER_MAX_BYTES,
|
||||
false,
|
||||
true
|
||||
));
|
||||
assert!(!should_use_small_put_eager_path(0, DEFAULT_SMALL_PUT_EAGER_MAX_BYTES, false, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clamp_small_put_eager_max_bytes_caps_inline_budget() {
|
||||
assert_eq!(
|
||||
clamp_small_put_eager_max_bytes(Some(rustfs_object_io::put::PUT_REDUCED_COPY_MIN_SIZE_BYTES as usize * 2)),
|
||||
DEFAULT_SMALL_PUT_EAGER_MAX_BYTES
|
||||
);
|
||||
assert_eq!(clamp_small_put_eager_max_bytes(Some(128 * 1024)), 128 * 1024);
|
||||
assert_eq!(clamp_small_put_eager_max_bytes(None), DEFAULT_SMALL_PUT_EAGER_MAX_BYTES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn resolved_small_put_eager_max_bytes_honors_disable_env() {
|
||||
temp_env::with_var(ENV_RUSTFS_PUT_FORCE_DISABLE_SMALL_EAGER, Some("true"), || {
|
||||
assert_eq!(resolved_small_put_eager_max_bytes(256 * 1024), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn resolved_small_put_eager_max_bytes_narrows_default_budget() {
|
||||
temp_env::with_var(ENV_RUSTFS_PUT_SMALL_EAGER_MAX_BYTES, Some("4096"), || {
|
||||
assert_eq!(resolved_small_put_eager_max_bytes(256 * 1024), 4096);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn resolved_small_put_eager_max_bytes_ignores_invalid_override() {
|
||||
temp_env::with_var(ENV_RUSTFS_PUT_SMALL_EAGER_MAX_BYTES, Some("invalid"), || {
|
||||
assert_eq!(resolved_small_put_eager_max_bytes(256 * 1024), 256 * 1024);
|
||||
});
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_small_put_body_eager_requires_exact_content_length() {
|
||||
let body = stream::iter(vec![
|
||||
Ok::<Bytes, std::io::Error>(Bytes::from_static(b"abc")),
|
||||
Ok::<Bytes, std::io::Error>(Bytes::from_static(b"def")),
|
||||
]);
|
||||
|
||||
let pool = Arc::new(BytesPool::new_tiered());
|
||||
let data = read_small_put_body_eager(body, 6, pool)
|
||||
.await
|
||||
.expect("eager read should succeed");
|
||||
assert_eq!(data.as_ref(), b"abcdef");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_small_put_body_eager_rejects_length_mismatch() {
|
||||
let body = stream::iter(vec![Ok::<Bytes, std::io::Error>(Bytes::from_static(b"abc"))]);
|
||||
let pool = Arc::new(BytesPool::new_tiered());
|
||||
|
||||
let err = read_small_put_body_eager(body, 4, pool)
|
||||
.await
|
||||
.expect_err("short eager read should fail");
|
||||
assert_eq!(err.code(), &S3ErrorCode::IncompleteBody);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_small_put_body_eager_rejects_overlong_body() {
|
||||
let body = stream::iter(vec![
|
||||
Ok::<Bytes, std::io::Error>(Bytes::from_static(b"abc")),
|
||||
Ok::<Bytes, std::io::Error>(Bytes::from_static(b"def")),
|
||||
]);
|
||||
let pool = Arc::new(BytesPool::new_tiered());
|
||||
|
||||
let err = read_small_put_body_eager(body, 5, pool)
|
||||
.await
|
||||
.expect_err("overlong eager read should fail");
|
||||
assert_eq!(err.code(), &S3ErrorCode::IncompleteBody);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_small_put_body_eager_returns_buffer_to_pool_after_drop() {
|
||||
let body = stream::iter(vec![Ok::<Bytes, std::io::Error>(Bytes::from_static(b"abc"))]);
|
||||
let pool = Arc::new(BytesPool::new_tiered());
|
||||
|
||||
let data = read_small_put_body_eager(body, 3, pool.clone())
|
||||
.await
|
||||
.expect("pooled eager read should succeed");
|
||||
assert_eq!(pool.available_buffers(), 0);
|
||||
|
||||
drop(data);
|
||||
assert_eq!(pool.available_buffers(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_small_put_body_eager_returns_after_expected_bytes_without_waiting_for_eof() {
|
||||
let body = stream::once(async { Ok::<Bytes, std::io::Error>(Bytes::from_static(b"abc")) }).chain(stream::pending());
|
||||
let pool = Arc::new(BytesPool::new_tiered());
|
||||
|
||||
let data = timeout(Duration::from_millis(50), read_small_put_body_eager(body, 3, pool))
|
||||
.await
|
||||
.expect("eager read should not wait for stream termination")
|
||||
.expect("eager read should succeed once content-length bytes are read");
|
||||
|
||||
assert_eq!(data.as_ref(), b"abc");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct GetObjectRequestContext {
|
||||
pub(super) bucket: String,
|
||||
pub(super) key: String,
|
||||
pub(super) cache_key: String,
|
||||
pub(super) version_id_for_event: String,
|
||||
pub(super) part_number: Option<usize>,
|
||||
pub(super) rs: Option<HTTPRangeSpec>,
|
||||
pub(super) opts: ObjectOptions,
|
||||
pub(super) headers: HeaderMap,
|
||||
pub(super) method: hyper::Method,
|
||||
pub(super) sse_customer_key: Option<String>,
|
||||
pub(super) sse_customer_key_md5: Option<String>,
|
||||
}
|
||||
|
||||
pub(super) type PutObjectChecksums = rustfs_object_io::put::PutObjectChecksums;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct PutObjectRequestContext {
|
||||
pub(super) headers: HeaderMap,
|
||||
pub(super) trailing_headers: Option<s3s::TrailingHeaders>,
|
||||
pub(super) uri_query: Option<String>,
|
||||
pub(super) is_post_object: bool,
|
||||
pub(super) method: hyper::Method,
|
||||
pub(super) uri: hyper::Uri,
|
||||
pub(super) extensions: http::Extensions,
|
||||
pub(super) credentials: Option<s3s::auth::Credentials>,
|
||||
pub(super) region: Option<s3s::region::Region>,
|
||||
pub(super) service: Option<String>,
|
||||
}
|
||||
|
||||
pub(super) struct PutObjectFlowResult {
|
||||
pub(super) output: PutObjectOutput,
|
||||
pub(super) helper_object: ObjectInfo,
|
||||
pub(super) helper_version_id: Option<String>,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -259,6 +259,9 @@ impl From<std::io::Error> for ApiError {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
// Check if the error is a ChecksumMismatch (BadDigest)
|
||||
if let Some(inner) = err.get_ref() {
|
||||
if let Some(storage_error) = inner.downcast_ref::<StorageError>() {
|
||||
return storage_error.clone().into();
|
||||
}
|
||||
if inner.downcast_ref::<rustfs_rio::ChecksumMismatch>().is_some() {
|
||||
return ApiError {
|
||||
code: S3ErrorCode::BadDigest,
|
||||
@@ -552,4 +555,15 @@ mod tests {
|
||||
// This is expected because ApiError is not a typical Error implementation
|
||||
assert!(error.source().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_api_error_from_io_error_unwraps_invalid_range_storage_error() {
|
||||
let io_error = std::io::Error::from(StorageError::InvalidRangeSpec("range invalid".to_string()));
|
||||
|
||||
let api_error: ApiError = io_error.into();
|
||||
|
||||
assert_eq!(api_error.code, S3ErrorCode::InvalidRange);
|
||||
assert_eq!(api_error.message, ApiError::error_code_to_message(&S3ErrorCode::InvalidRange));
|
||||
assert!(api_error.source.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,7 +426,7 @@ pub async fn start_http_server(
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#[allow(unused)]
|
||||
let socket_ref = SockRef::from(&socket);
|
||||
|
||||
// ── POST-ACCEPT SOCKET SYSCALLS ──
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
use hashbrown::HashMap;
|
||||
use moka::future::Cache;
|
||||
use rustfs_config::MI_B;
|
||||
use rustfs_object_io::get::GetObjectCacheWriteback;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -1063,6 +1064,13 @@ pub struct CachedGetObject {
|
||||
pub replication_status: Option<String>,
|
||||
/// User-defined metadata (x-amz-meta-*)
|
||||
pub user_metadata: std::collections::HashMap<String, String>,
|
||||
/// Additional checksum metadata persisted with cached GET responses
|
||||
pub checksum_crc32: Option<String>,
|
||||
pub checksum_crc32c: Option<String>,
|
||||
pub checksum_sha1: Option<String>,
|
||||
pub checksum_sha256: Option<String>,
|
||||
pub checksum_crc64nvme: Option<String>,
|
||||
pub checksum_type: Option<s3s::dto::ChecksumType>,
|
||||
/// When this object was cached (for internal use, automatically set)
|
||||
#[allow(dead_code)]
|
||||
cached_at: Option<Instant>,
|
||||
@@ -1089,6 +1097,12 @@ impl Default for CachedGetObject {
|
||||
tag_count: None,
|
||||
replication_status: None,
|
||||
user_metadata: std::collections::HashMap::new(),
|
||||
checksum_crc32: None,
|
||||
checksum_crc32c: None,
|
||||
checksum_sha1: None,
|
||||
checksum_sha256: None,
|
||||
checksum_crc64nvme: None,
|
||||
checksum_type: None,
|
||||
cached_at: None,
|
||||
access_count: Arc::new(AtomicU64::new(0)),
|
||||
}
|
||||
@@ -1109,6 +1123,35 @@ impl CachedGetObject {
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume a GET cache writeback payload into the cache-owned representation.
|
||||
pub fn from_get_object_cache_writeback(writeback: GetObjectCacheWriteback) -> Self {
|
||||
Self {
|
||||
body: writeback.body,
|
||||
content_length: writeback.content_length,
|
||||
content_type: writeback.content_type,
|
||||
e_tag: writeback.e_tag,
|
||||
last_modified: writeback.last_modified,
|
||||
expires: writeback.expires,
|
||||
cache_control: writeback.cache_control,
|
||||
content_disposition: writeback.content_disposition,
|
||||
content_encoding: writeback.content_encoding,
|
||||
content_language: writeback.content_language,
|
||||
storage_class: writeback.storage_class,
|
||||
version_id: writeback.version_id,
|
||||
delete_marker: writeback.delete_marker,
|
||||
user_metadata: writeback.user_metadata,
|
||||
checksum_crc32: writeback.checksum_crc32,
|
||||
checksum_crc32c: writeback.checksum_crc32c,
|
||||
checksum_sha1: writeback.checksum_sha1,
|
||||
checksum_sha256: writeback.checksum_sha256,
|
||||
checksum_crc64nvme: writeback.checksum_crc64nvme,
|
||||
checksum_type: writeback.checksum_type,
|
||||
cached_at: Some(Instant::now()),
|
||||
access_count: Arc::new(AtomicU64::new(0)),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder method to set content_type
|
||||
pub fn with_content_type(mut self, content_type: String) -> Self {
|
||||
self.content_type = Some(content_type);
|
||||
@@ -1881,6 +1924,57 @@ mod cached_object_tests {
|
||||
assert_eq!(obj.user_metadata.get("x-amz-meta-custom"), Some(&"value".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cached_get_object_from_get_object_cache_writeback() {
|
||||
let body = Arc::new(Bytes::from("test data"));
|
||||
let obj = CachedGetObject::from_get_object_cache_writeback(GetObjectCacheWriteback {
|
||||
body: Arc::clone(&body),
|
||||
content_length: 9,
|
||||
content_type: Some("text/plain".to_string()),
|
||||
content_encoding: Some("gzip".to_string()),
|
||||
cache_control: Some("max-age=3600".to_string()),
|
||||
content_disposition: Some("attachment".to_string()),
|
||||
content_language: Some("en-US".to_string()),
|
||||
expires: Some("2024-12-31T23:59:59Z".to_string()),
|
||||
storage_class: Some("STANDARD".to_string()),
|
||||
version_id: Some("null".to_string()),
|
||||
delete_marker: false,
|
||||
user_metadata: {
|
||||
let mut metadata = std::collections::HashMap::new();
|
||||
metadata.insert("custom-key".to_string(), "value".to_string());
|
||||
metadata
|
||||
},
|
||||
e_tag: Some("\"abc123\"".to_string()),
|
||||
last_modified: Some("2024-01-01T12:00:00Z".to_string()),
|
||||
checksum_crc32: Some("crc32".to_string()),
|
||||
checksum_crc32c: None,
|
||||
checksum_sha1: None,
|
||||
checksum_sha256: None,
|
||||
checksum_crc64nvme: None,
|
||||
checksum_type: Some(s3s::dto::ChecksumType::from_static(s3s::dto::ChecksumType::FULL_OBJECT)),
|
||||
});
|
||||
|
||||
assert_eq!(obj.content_length, 9);
|
||||
assert_eq!(obj.content_type.as_deref(), Some("text/plain"));
|
||||
assert_eq!(obj.content_encoding.as_deref(), Some("gzip"));
|
||||
assert_eq!(obj.cache_control.as_deref(), Some("max-age=3600"));
|
||||
assert_eq!(obj.content_disposition.as_deref(), Some("attachment"));
|
||||
assert_eq!(obj.content_language.as_deref(), Some("en-US"));
|
||||
assert_eq!(obj.expires.as_deref(), Some("2024-12-31T23:59:59Z"));
|
||||
assert_eq!(obj.storage_class.as_deref(), Some("STANDARD"));
|
||||
assert_eq!(obj.version_id.as_deref(), Some("null"));
|
||||
assert!(!obj.delete_marker);
|
||||
assert_eq!(obj.user_metadata.get("custom-key").map(String::as_str), Some("value"));
|
||||
assert_eq!(obj.e_tag.as_deref(), Some("\"abc123\""));
|
||||
assert_eq!(obj.last_modified.as_deref(), Some("2024-01-01T12:00:00Z"));
|
||||
assert_eq!(obj.checksum_crc32.as_deref(), Some("crc32"));
|
||||
assert_eq!(
|
||||
obj.checksum_type,
|
||||
Some(s3s::dto::ChecksumType::from_static(s3s::dto::ChecksumType::FULL_OBJECT))
|
||||
);
|
||||
assert!(Arc::ptr_eq(&obj.body, &body));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cached_get_object_size() {
|
||||
let obj = CachedGetObject::new(Bytes::from("test"), 4);
|
||||
|
||||
@@ -29,7 +29,6 @@ use rustfs_ecstore::{
|
||||
use rustfs_s3_common::{S3Operation, record_s3_op};
|
||||
use s3s::{S3, S3Error, S3ErrorCode, S3Request, S3Response, S3Result, dto::*, s3_error};
|
||||
use std::fmt::Debug;
|
||||
use tokio::io::{AsyncRead, AsyncSeek};
|
||||
use tracing::{debug, error, instrument, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -44,44 +43,6 @@ pub(crate) struct ListObjectUnorderedQuery {
|
||||
pub(crate) allow_unordered: Option<String>,
|
||||
}
|
||||
|
||||
pub(crate) struct InMemoryAsyncReader {
|
||||
cursor: std::io::Cursor<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl InMemoryAsyncReader {
|
||||
pub(crate) fn new(data: Vec<u8>) -> Self {
|
||||
Self {
|
||||
cursor: std::io::Cursor::new(data),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for InMemoryAsyncReader {
|
||||
fn poll_read(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
buf: &mut tokio::io::ReadBuf<'_>,
|
||||
) -> std::task::Poll<std::io::Result<()>> {
|
||||
let unfilled = buf.initialize_unfilled();
|
||||
let bytes_read = std::io::Read::read(&mut self.cursor, unfilled)?;
|
||||
buf.advance(bytes_read);
|
||||
std::task::Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncSeek for InMemoryAsyncReader {
|
||||
fn start_seek(mut self: std::pin::Pin<&mut Self>, position: std::io::SeekFrom) -> std::io::Result<()> {
|
||||
// std::io::Cursor natively supports negative SeekCurrent offsets
|
||||
// It will automatically handle validation and return an error if the final position would be negative
|
||||
std::io::Seek::seek(&mut self.cursor, position)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn poll_complete(self: std::pin::Pin<&mut Self>, _cx: &mut std::task::Context<'_>) -> std::task::Poll<std::io::Result<u64>> {
|
||||
std::task::Poll::Ready(Ok(self.cursor.position()))
|
||||
}
|
||||
}
|
||||
|
||||
impl FS {
|
||||
pub fn new() -> Self {
|
||||
rustfs_s3_common::init_s3_metrics();
|
||||
|
||||
Reference in New Issue
Block a user