mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-25 21:46:50 +00:00
fix(get): remove GET chunk fast path (#2507)
This commit is contained in:
@@ -13,14 +13,11 @@
|
||||
// limitations under the License.
|
||||
|
||||
use super::*;
|
||||
use crate::bitrot::create_bitrot_chunk_stream;
|
||||
use crate::erasure_coding::decode::ErasureChunkDecoder;
|
||||
use crate::erasure_coding::{calc_shard_size, calc_shard_size_legacy};
|
||||
use crate::store_api::{GetObjectChunkCopyMode, GetObjectChunkPath, GetObjectChunkResult};
|
||||
use bytes::BytesMut;
|
||||
use futures_util::{Stream, StreamExt, stream};
|
||||
use futures_util::Stream;
|
||||
use rustfs_config::{DEFAULT_OBJECT_ZERO_COPY_ENABLE, ENV_OBJECT_ZERO_COPY_ENABLE};
|
||||
use rustfs_io_core::{BoxChunkStream, IoChunk};
|
||||
use rustfs_io_core::IoChunk;
|
||||
use std::io;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Mutex;
|
||||
@@ -40,56 +37,6 @@ impl ChannelChunkStream {
|
||||
}
|
||||
}
|
||||
|
||||
struct DirectShardCursor {
|
||||
stream: BoxChunkStream,
|
||||
current_chunk: Option<IoChunk>,
|
||||
current_offset: usize,
|
||||
}
|
||||
|
||||
impl DirectShardCursor {
|
||||
fn new(stream: BoxChunkStream) -> Self {
|
||||
Self {
|
||||
stream,
|
||||
current_chunk: None,
|
||||
current_offset: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn current_remaining(&self) -> usize {
|
||||
self.current_chunk
|
||||
.as_ref()
|
||||
.map(|chunk| chunk.len().saturating_sub(self.current_offset))
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn consume_current(&mut self, len: usize) {
|
||||
self.current_offset += len;
|
||||
if self.current_remaining() == 0 {
|
||||
self.current_chunk = None;
|
||||
self.current_offset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_chunk(&mut self, shard_index: usize) -> io::Result<bool> {
|
||||
if self.current_chunk.is_some() && self.current_remaining() > 0 {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
match self.stream.next().await {
|
||||
Some(Ok(chunk)) => {
|
||||
self.current_chunk = Some(chunk);
|
||||
self.current_offset = 0;
|
||||
Ok(true)
|
||||
}
|
||||
Some(Err(err)) => Err(err),
|
||||
None => {
|
||||
debug!(shard_index, "direct shard cursor reached EOF");
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for ChannelChunkStream {
|
||||
type Item = io::Result<IoChunk>;
|
||||
|
||||
@@ -157,17 +104,6 @@ impl AsyncWrite for ChannelChunkWriter {
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_chunk_copy_mode(current: GetObjectChunkCopyMode, next: GetObjectChunkCopyMode) -> GetObjectChunkCopyMode {
|
||||
use GetObjectChunkCopyMode::{Reconstructed, SharedBytes, SingleCopy, TrueZeroCopy};
|
||||
|
||||
match (current, next) {
|
||||
(Reconstructed, _) | (_, Reconstructed) => Reconstructed,
|
||||
(SingleCopy, _) | (_, SingleCopy) => SingleCopy,
|
||||
(SharedBytes, _) | (_, SharedBytes) => SharedBytes,
|
||||
(TrueZeroCopy, TrueZeroCopy) => TrueZeroCopy,
|
||||
}
|
||||
}
|
||||
|
||||
fn multipart_logical_part_size(fi: &FileInfo, part_index: usize) -> usize {
|
||||
let part = &fi.parts[part_index];
|
||||
if part.actual_size > 0 {
|
||||
@@ -277,616 +213,7 @@ fn direct_block_shard_size(
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn send_direct_data_shard_chunks(
|
||||
sender: UnboundedSender<io::Result<IoChunk>>,
|
||||
shard_streams: Vec<BoxChunkStream>,
|
||||
data_shards: usize,
|
||||
block_size: usize,
|
||||
total_size: usize,
|
||||
uses_legacy: bool,
|
||||
offset: usize,
|
||||
length: usize,
|
||||
) {
|
||||
if length == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let start_block = offset / block_size;
|
||||
let end_block = offset.saturating_add(length.saturating_sub(1)) / block_size;
|
||||
let mut shard_cursors = shard_streams.into_iter().map(DirectShardCursor::new).collect::<Vec<_>>();
|
||||
|
||||
for block_index in start_block..=end_block {
|
||||
let (block_offset, block_length) = block_window(offset, length, block_size, block_index, start_block, end_block);
|
||||
if block_length == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let shard_block_size = direct_block_shard_size(total_size, block_size, data_shards, block_index, uses_legacy);
|
||||
let mut write_left = block_length;
|
||||
let mut skip = block_offset;
|
||||
|
||||
for (shard_index, shard_cursor) in shard_cursors.iter_mut().enumerate().take(data_shards) {
|
||||
let mut shard_block_left = shard_block_size;
|
||||
|
||||
while shard_block_left > 0 {
|
||||
let has_chunk = match shard_cursor.ensure_chunk(shard_index).await {
|
||||
Ok(has_chunk) => has_chunk,
|
||||
Err(err) => {
|
||||
let _ = sender.send(Err(err));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if !has_chunk {
|
||||
let _ = sender.send(Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
format!("missing chunk for data shard {shard_index}"),
|
||||
)));
|
||||
return;
|
||||
}
|
||||
|
||||
let chunk = shard_cursor.current_chunk.as_ref().expect("chunk should exist after ensure");
|
||||
let chunk_remaining = chunk.len().saturating_sub(shard_cursor.current_offset);
|
||||
let take_from_chunk = chunk_remaining.min(shard_block_left);
|
||||
if skip >= take_from_chunk {
|
||||
skip -= take_from_chunk;
|
||||
shard_cursor.consume_current(take_from_chunk);
|
||||
shard_block_left -= take_from_chunk;
|
||||
continue;
|
||||
}
|
||||
|
||||
let start = shard_cursor.current_offset + skip;
|
||||
let available = take_from_chunk.saturating_sub(skip);
|
||||
let take = available.min(write_left);
|
||||
let out_chunk = if start == shard_cursor.current_offset && take == take_from_chunk {
|
||||
chunk.slice(start, take).expect("full remaining slice should succeed")
|
||||
} else {
|
||||
match chunk.slice(start, take) {
|
||||
Ok(chunk) => chunk,
|
||||
Err(err) => {
|
||||
let _ = sender.send(Err(err));
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let consumed = skip + take;
|
||||
skip = 0;
|
||||
shard_cursor.consume_current(consumed);
|
||||
shard_block_left -= consumed;
|
||||
if sender.send(Ok(out_chunk)).is_err() {
|
||||
return;
|
||||
}
|
||||
write_left -= take;
|
||||
|
||||
if write_left == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if write_left == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if write_left != 0 {
|
||||
let _ = sender.send(Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
"not enough decoded shard data for requested block",
|
||||
)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub async fn collect_direct_data_shard_chunks_for_benchmark(
|
||||
shard_streams: Vec<BoxChunkStream>,
|
||||
data_shards: usize,
|
||||
block_size: usize,
|
||||
total_size: usize,
|
||||
uses_legacy: bool,
|
||||
offset: usize,
|
||||
length: usize,
|
||||
) -> io::Result<Vec<IoChunk>> {
|
||||
let (tx, rx) = unbounded_channel();
|
||||
send_direct_data_shard_chunks(tx, shard_streams, data_shards, block_size, total_size, uses_legacy, offset, length).await;
|
||||
|
||||
let mut stream = ChannelChunkStream::new(rx);
|
||||
let mut chunks = Vec::new();
|
||||
while let Some(chunk) = stream.next().await {
|
||||
chunks.push(chunk?);
|
||||
}
|
||||
|
||||
Ok(chunks)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn build_reconstructed_part_stream(
|
||||
bucket: &str,
|
||||
object: &str,
|
||||
part_number: usize,
|
||||
part_offset: usize,
|
||||
part_length: usize,
|
||||
part_size: usize,
|
||||
read_offset: usize,
|
||||
till_offset: usize,
|
||||
files: &[FileInfo],
|
||||
disks: &[Option<DiskStore>],
|
||||
erasure: &erasure_coding::Erasure,
|
||||
checksum_algo: rustfs_utils::HashAlgorithm,
|
||||
skip_verify_bitrot: bool,
|
||||
use_zero_copy: bool,
|
||||
) -> Result<Option<BoxChunkStream>> {
|
||||
let shard_length = till_offset.saturating_sub(read_offset);
|
||||
let mut readers = Vec::with_capacity(disks.len());
|
||||
let mut errors = Vec::with_capacity(disks.len());
|
||||
for (idx, disk_op) in disks.iter().enumerate() {
|
||||
match create_bitrot_reader(
|
||||
files[idx].data.as_deref(),
|
||||
disk_op.as_ref(),
|
||||
bucket,
|
||||
&format!("{}/{}/part.{}", object, files[idx].data_dir.unwrap_or_default(), part_number),
|
||||
read_offset,
|
||||
shard_length,
|
||||
erasure.shard_size(),
|
||||
checksum_algo.clone(),
|
||||
skip_verify_bitrot,
|
||||
use_zero_copy,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Some(reader)) => {
|
||||
readers.push(Some(reader));
|
||||
errors.push(None);
|
||||
}
|
||||
Ok(None) => {
|
||||
readers.push(None);
|
||||
errors.push(Some(DiskError::DiskNotFound));
|
||||
}
|
||||
Err(err) => {
|
||||
readers.push(None);
|
||||
errors.push(Some(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let available_shards = errors.iter().filter(|error| error.is_none()).count();
|
||||
if available_shards < erasure.data_shards {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let missing_shards = readers.len().saturating_sub(available_shards);
|
||||
if missing_shards > 0 {
|
||||
debug!(
|
||||
bucket,
|
||||
object,
|
||||
part_number,
|
||||
missing_shards,
|
||||
available_shards,
|
||||
data_shards = erasure.data_shards,
|
||||
parity_shards = erasure.parity_shards,
|
||||
"using reconstructed part stream for missing shards"
|
||||
);
|
||||
}
|
||||
|
||||
let (tx, rx) = unbounded_channel();
|
||||
let bucket = bucket.to_string();
|
||||
let object = object.to_string();
|
||||
let erasure = erasure.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut decoder = match ErasureChunkDecoder::new(erasure, readers, part_offset, part_length, part_size) {
|
||||
Ok(decoder) => decoder,
|
||||
Err(err) => {
|
||||
let _ = tx.send(Err(err));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
loop {
|
||||
match decoder.next_chunks().await {
|
||||
Ok(Some(chunks)) => {
|
||||
for chunk in chunks {
|
||||
if tx.send(Ok(chunk)).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => break,
|
||||
Err(err) => {
|
||||
let _ = tx.send(Err(err));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(err) = decoder.finish_error() {
|
||||
let _ = tx.send(Err(err));
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(disk_err) = decoder.take_healable_error() {
|
||||
let allow_heal_only =
|
||||
decoder.written() == part_length && matches!(disk_err, DiskError::FileNotFound | DiskError::FileCorrupt);
|
||||
if !allow_heal_only {
|
||||
let _ = tx.send(Err(io::Error::other(disk_err.to_string())));
|
||||
return;
|
||||
}
|
||||
|
||||
debug!(
|
||||
bucket,
|
||||
object,
|
||||
part_number,
|
||||
bytes_written = decoder.written(),
|
||||
error = %disk_err,
|
||||
"reconstructed part completed with healable shard error"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Some(Box::pin(ChannelChunkStream::new(rx))))
|
||||
}
|
||||
|
||||
impl SetDisks {
|
||||
#[tracing::instrument(level = "debug", skip(self, h, opts))]
|
||||
pub(crate) async fn get_object_chunks(
|
||||
&self,
|
||||
bucket: &str,
|
||||
object: &str,
|
||||
range: Option<HTTPRangeSpec>,
|
||||
h: HeaderMap,
|
||||
opts: &ObjectOptions,
|
||||
) -> Result<GetObjectChunkResult> {
|
||||
let lock_optimization_enabled = is_lock_optimization_enabled();
|
||||
|
||||
let read_lock_guard = if !opts.no_lock {
|
||||
let acquire_start = Instant::now();
|
||||
|
||||
if is_deadlock_detection_enabled() {
|
||||
debug!(
|
||||
lock_id = format!("{}:{}", bucket, object),
|
||||
lock_type = "read",
|
||||
resource = format!("{}/{}", bucket, object),
|
||||
"Waiting for read lock"
|
||||
);
|
||||
}
|
||||
|
||||
let guard = self
|
||||
.new_ns_lock(bucket, object)
|
||||
.await?
|
||||
.get_read_lock(get_lock_acquire_timeout())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::other(format!(
|
||||
"Failed to acquire read lock: {}",
|
||||
self.format_lock_error_from_error(bucket, object, "read", &e)
|
||||
))
|
||||
})?;
|
||||
|
||||
let _lock_id = record_lock_acquire(bucket, object, "read");
|
||||
metrics::counter!("rustfs.lock.acquire.total", "type" => "read").increment(1);
|
||||
metrics::histogram!("rustfs.lock.acquire.duration.seconds").record(acquire_start.elapsed().as_secs_f64());
|
||||
|
||||
Some(guard)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (fi, files, disks) = self
|
||||
.get_object_fileinfo(bucket, object, opts, true)
|
||||
.await
|
||||
.map_err(|err| to_object_err(err, vec![bucket, object]))?;
|
||||
let object_info = ObjectInfo::from_file_info(&fi, bucket, object, opts.versioned || opts.version_suspended);
|
||||
|
||||
if object_info.delete_marker {
|
||||
if opts.version_id.is_none() {
|
||||
return Err(to_object_err(Error::FileNotFound, vec![bucket, object]));
|
||||
}
|
||||
return Err(to_object_err(Error::MethodNotAllowed, vec![bucket, object]));
|
||||
}
|
||||
|
||||
if object_info.size == 0 {
|
||||
return Ok(GetObjectChunkResult {
|
||||
stream: Box::pin(stream::iter(Vec::<io::Result<IoChunk>>::new())),
|
||||
path: GetObjectChunkPath::Direct,
|
||||
copy_mode: GetObjectChunkCopyMode::SharedBytes,
|
||||
});
|
||||
}
|
||||
|
||||
let (bridge_offset, bridge_length) = if fi.parts.is_empty() {
|
||||
(0, fi.size)
|
||||
} else {
|
||||
let total_size = multipart_logical_total_size(&fi);
|
||||
if let Some(range) = &range {
|
||||
let (offset, length) = range
|
||||
.get_offset_length(total_size as i64)
|
||||
.map_err(|err| to_object_err(err, vec![bucket, object]))?;
|
||||
(offset, length)
|
||||
} else {
|
||||
(0, total_size as i64)
|
||||
}
|
||||
};
|
||||
|
||||
if object_info.is_remote() {
|
||||
let mut opts = opts.clone();
|
||||
if object_info.parts.len() == 1 {
|
||||
opts.part_number = Some(1);
|
||||
}
|
||||
let gr = get_transitioned_object_reader(bucket, object, &range, &h, &object_info, &opts).await?;
|
||||
let stream = ReaderStream::new(gr.stream).map(|result| result.map(IoChunk::Shared));
|
||||
return Ok(GetObjectChunkResult {
|
||||
stream: Box::pin(stream),
|
||||
path: GetObjectChunkPath::Bridge,
|
||||
copy_mode: GetObjectChunkCopyMode::SingleCopy,
|
||||
});
|
||||
}
|
||||
|
||||
if fi.erasure.data_blocks > 0 {
|
||||
let (disks, files) = Self::shuffle_disks_and_parts_metadata_by_index(&disks, &files, &fi);
|
||||
let total_size = multipart_logical_total_size(&fi);
|
||||
let requested_length = if let Some(range) = &range {
|
||||
let (offset, length) = range
|
||||
.get_offset_length(total_size as i64)
|
||||
.map_err(|err| to_object_err(err, vec![bucket, object]))?;
|
||||
(offset, length as usize)
|
||||
} else {
|
||||
(0, total_size)
|
||||
};
|
||||
|
||||
let (part_index, mut part_offset) = multipart_to_logical_part_offset(&fi, requested_length.0)?;
|
||||
let mut end_offset = requested_length.0;
|
||||
if requested_length.1 > 0 {
|
||||
end_offset += requested_length.1 - 1;
|
||||
}
|
||||
let (last_part_index, _) = multipart_to_logical_part_offset(&fi, end_offset)?;
|
||||
|
||||
let use_zero_copy = rustfs_utils::get_env_bool(ENV_OBJECT_ZERO_COPY_ENABLE, DEFAULT_OBJECT_ZERO_COPY_ENABLE);
|
||||
let single_shard_file = if fi.erasure.data_blocks == 1 {
|
||||
Some(
|
||||
files
|
||||
.first()
|
||||
.ok_or_else(|| Error::other("single-shard multipart metadata missing"))?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let single_shard_disk = if fi.erasure.data_blocks == 1 {
|
||||
Some(
|
||||
disks
|
||||
.first()
|
||||
.ok_or_else(|| Error::other("single-shard multipart disk slot missing"))?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut part_streams = Vec::new();
|
||||
let mut part_total_read = 0usize;
|
||||
let mut merged_copy_mode = GetObjectChunkCopyMode::TrueZeroCopy;
|
||||
|
||||
for current_part in part_index..=last_part_index {
|
||||
let part_number = fi.parts[current_part].number;
|
||||
let part_size = multipart_logical_part_size(&fi, current_part);
|
||||
let mut part_length = part_size - part_offset;
|
||||
if part_length > (requested_length.1 - part_total_read) {
|
||||
part_length = requested_length.1 - part_total_read;
|
||||
}
|
||||
let checksum_info = fi.erasure.get_checksum_info(part_number);
|
||||
let checksum_algo =
|
||||
if fi.uses_legacy_checksum && checksum_info.algorithm == rustfs_utils::HashAlgorithm::HighwayHash256S {
|
||||
rustfs_utils::HashAlgorithm::HighwayHash256SLegacy
|
||||
} else {
|
||||
checksum_info.algorithm.clone()
|
||||
};
|
||||
|
||||
if fi.erasure.data_blocks == 1 {
|
||||
let single_shard_file = single_shard_file.expect("single-shard multipart metadata must exist");
|
||||
let single_shard_disk = single_shard_disk.expect("single-shard multipart disk slot must exist");
|
||||
let data_dir = single_shard_file
|
||||
.data_dir
|
||||
.as_ref()
|
||||
.map(uuid::Uuid::to_string)
|
||||
.unwrap_or_default();
|
||||
let data_path = format!("{}/{}/part.{}", object, data_dir, part_number);
|
||||
let chunk_result = create_bitrot_chunk_stream(
|
||||
single_shard_file.data.as_deref(),
|
||||
single_shard_disk.as_ref(),
|
||||
bucket,
|
||||
&data_path,
|
||||
part_offset,
|
||||
part_length,
|
||||
part_size,
|
||||
fi.erasure.shard_size(),
|
||||
checksum_algo,
|
||||
opts.skip_verify_bitrot,
|
||||
use_zero_copy,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let Some(chunk_result) = chunk_result else {
|
||||
part_streams.clear();
|
||||
break;
|
||||
};
|
||||
merged_copy_mode = merge_chunk_copy_mode(merged_copy_mode, chunk_result.copy_mode);
|
||||
part_streams.push(chunk_result.stream);
|
||||
} else {
|
||||
let erasure = erasure_coding::Erasure::new_with_options(
|
||||
fi.erasure.data_blocks,
|
||||
fi.erasure.parity_blocks,
|
||||
fi.erasure.block_size,
|
||||
fi.uses_legacy_checksum,
|
||||
);
|
||||
let read_offset = (part_offset / erasure.block_size) * erasure.shard_size();
|
||||
let till_offset = erasure.shard_file_offset(part_offset, part_length, part_size);
|
||||
let shard_length = till_offset.saturating_sub(read_offset);
|
||||
let shard_total_size = erasure.shard_file_size(part_size as i64) as usize;
|
||||
let mut shard_streams = Vec::with_capacity(erasure.data_shards);
|
||||
let mut part_copy_mode = GetObjectChunkCopyMode::TrueZeroCopy;
|
||||
let mut needs_reconstruct = false;
|
||||
|
||||
for shard_index in 0..erasure.data_shards {
|
||||
let data_path =
|
||||
format!("{}/{}/part.{}", object, files[shard_index].data_dir.unwrap_or_default(), part_number);
|
||||
let chunk_result = match create_bitrot_chunk_stream(
|
||||
files[shard_index].data.as_deref(),
|
||||
disks[shard_index].as_ref(),
|
||||
bucket,
|
||||
&data_path,
|
||||
read_offset,
|
||||
shard_length,
|
||||
shard_total_size,
|
||||
erasure.shard_size(),
|
||||
checksum_algo.clone(),
|
||||
opts.skip_verify_bitrot,
|
||||
use_zero_copy,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Some(chunk_result)) => chunk_result,
|
||||
Ok(None) => {
|
||||
needs_reconstruct = true;
|
||||
shard_streams.clear();
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
debug!(
|
||||
bucket,
|
||||
object,
|
||||
part_number,
|
||||
shard_index,
|
||||
error = %err,
|
||||
"multi-shard direct chunk path unavailable, falling back to decoded read path"
|
||||
);
|
||||
needs_reconstruct = true;
|
||||
shard_streams.clear();
|
||||
break;
|
||||
}
|
||||
};
|
||||
part_copy_mode = merge_chunk_copy_mode(part_copy_mode, chunk_result.copy_mode);
|
||||
shard_streams.push(chunk_result.stream);
|
||||
}
|
||||
|
||||
if needs_reconstruct {
|
||||
let reconstructed_stream = match build_reconstructed_part_stream(
|
||||
bucket,
|
||||
object,
|
||||
part_number,
|
||||
part_offset,
|
||||
part_length,
|
||||
part_size,
|
||||
read_offset,
|
||||
till_offset,
|
||||
&files,
|
||||
&disks,
|
||||
&erasure,
|
||||
checksum_algo,
|
||||
opts.skip_verify_bitrot,
|
||||
use_zero_copy,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some(stream) => stream,
|
||||
None => {
|
||||
part_streams.clear();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
merged_copy_mode = merge_chunk_copy_mode(merged_copy_mode, GetObjectChunkCopyMode::Reconstructed);
|
||||
part_streams.push(reconstructed_stream);
|
||||
part_total_read += part_length;
|
||||
part_offset = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if shard_streams.len() != erasure.data_shards {
|
||||
part_streams.clear();
|
||||
break;
|
||||
}
|
||||
|
||||
let (tx, rx) = unbounded_channel();
|
||||
tokio::spawn(send_direct_data_shard_chunks(
|
||||
tx,
|
||||
shard_streams,
|
||||
erasure.data_shards,
|
||||
erasure.block_size,
|
||||
part_size,
|
||||
fi.uses_legacy_checksum,
|
||||
part_offset,
|
||||
part_length,
|
||||
));
|
||||
|
||||
merged_copy_mode = merge_chunk_copy_mode(merged_copy_mode, part_copy_mode);
|
||||
part_streams.push(Box::pin(ChannelChunkStream::new(rx)));
|
||||
}
|
||||
part_total_read += part_length;
|
||||
part_offset = 0;
|
||||
}
|
||||
|
||||
if !part_streams.is_empty() {
|
||||
return Ok(GetObjectChunkResult {
|
||||
stream: Box::pin(stream::iter(part_streams).flatten()),
|
||||
path: GetObjectChunkPath::Direct,
|
||||
copy_mode: merged_copy_mode,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let read_lock_guard = if lock_optimization_enabled {
|
||||
if read_lock_guard.is_some() {
|
||||
let lock_id = format!("{}:{}", bucket, object);
|
||||
record_lock_release(bucket, object, &lock_id, "read");
|
||||
metrics::counter!("rustfs.lock.release.early.total", "type" => "read").increment(1);
|
||||
}
|
||||
drop(read_lock_guard);
|
||||
debug!(bucket, object, "Lock optimization: released read lock after metadata read");
|
||||
None
|
||||
} else {
|
||||
read_lock_guard
|
||||
};
|
||||
|
||||
let chunk_size = get_duplex_buffer_size();
|
||||
let bucket = bucket.to_owned();
|
||||
let object = object.to_owned();
|
||||
let set_index = self.set_index;
|
||||
let pool_index = self.pool_index;
|
||||
let skip_verify = opts.skip_verify_bitrot;
|
||||
let (tx, rx) = unbounded_channel();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let _guard = read_lock_guard;
|
||||
let mut writer = ChannelChunkWriter::new(tx, chunk_size);
|
||||
if let Err(err) = Self::get_object_with_fileinfo(
|
||||
&bucket,
|
||||
&object,
|
||||
bridge_offset,
|
||||
bridge_length,
|
||||
&mut writer,
|
||||
fi,
|
||||
files,
|
||||
&disks,
|
||||
set_index,
|
||||
pool_index,
|
||||
skip_verify,
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("get_object_with_fileinfo {bucket}/{object} err {:?}", err);
|
||||
writer.send_error(io::Error::other(err.to_string()));
|
||||
}
|
||||
|
||||
if let Err(err) = writer.finish() {
|
||||
debug!(bucket, object, error = %err, "failed to flush chunk writer");
|
||||
}
|
||||
});
|
||||
|
||||
Ok(GetObjectChunkResult {
|
||||
stream: Box::pin(ChannelChunkStream::new(rx)),
|
||||
path: GetObjectChunkPath::Bridge,
|
||||
copy_mode: GetObjectChunkCopyMode::SingleCopy,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) async fn read_parts(
|
||||
disks: &[Option<DiskStore>],
|
||||
bucket: &str,
|
||||
@@ -1724,103 +1051,3 @@ impl SetDisks {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use bytes::Bytes;
|
||||
use futures_util::StreamExt;
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_direct_data_shard_chunks_reassembles_multi_block_range() {
|
||||
let data_shards = 4;
|
||||
let block_size = 16;
|
||||
let shard_streams: Vec<BoxChunkStream> = vec![
|
||||
Box::pin(stream::iter(vec![
|
||||
Ok(IoChunk::Shared(Bytes::copy_from_slice(&[0, 1, 2, 3]))),
|
||||
Ok(IoChunk::Shared(Bytes::copy_from_slice(&[16, 17, 18, 19]))),
|
||||
])),
|
||||
Box::pin(stream::iter(vec![
|
||||
Ok(IoChunk::Shared(Bytes::copy_from_slice(&[4, 5, 6, 7]))),
|
||||
Ok(IoChunk::Shared(Bytes::copy_from_slice(&[20, 21, 22, 23]))),
|
||||
])),
|
||||
Box::pin(stream::iter(vec![
|
||||
Ok(IoChunk::Shared(Bytes::copy_from_slice(&[8, 9, 10, 11]))),
|
||||
Ok(IoChunk::Shared(Bytes::copy_from_slice(&[24, 25, 26, 27]))),
|
||||
])),
|
||||
Box::pin(stream::iter(vec![
|
||||
Ok(IoChunk::Shared(Bytes::copy_from_slice(&[12, 13, 14, 15]))),
|
||||
Ok(IoChunk::Shared(Bytes::copy_from_slice(&[28, 29, 30, 31]))),
|
||||
])),
|
||||
];
|
||||
let (tx, rx) = unbounded_channel();
|
||||
|
||||
send_direct_data_shard_chunks(tx, shard_streams, data_shards, block_size, 32, false, 3, 18).await;
|
||||
|
||||
let mut stream = ChannelChunkStream::new(rx);
|
||||
let mut collected = Vec::new();
|
||||
while let Some(chunk) = stream.next().await {
|
||||
collected.extend_from_slice(&chunk.unwrap().as_bytes());
|
||||
}
|
||||
|
||||
assert_eq!(collected, (3u8..21).collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_direct_data_shard_chunks_keeps_block_boundaries_with_cross_block_chunks() {
|
||||
let data_shards = 4;
|
||||
let block_size = 16;
|
||||
let shard_streams: Vec<BoxChunkStream> = vec![
|
||||
Box::pin(stream::iter(vec![Ok(IoChunk::Shared(Bytes::copy_from_slice(&[
|
||||
0, 1, 2, 3, 16, 17, 18, 19,
|
||||
])))])),
|
||||
Box::pin(stream::iter(vec![Ok(IoChunk::Shared(Bytes::copy_from_slice(&[
|
||||
4, 5, 6, 7, 20, 21, 22, 23,
|
||||
])))])),
|
||||
Box::pin(stream::iter(vec![Ok(IoChunk::Shared(Bytes::copy_from_slice(&[
|
||||
8, 9, 10, 11, 24, 25, 26, 27,
|
||||
])))])),
|
||||
Box::pin(stream::iter(vec![Ok(IoChunk::Shared(Bytes::copy_from_slice(&[
|
||||
12, 13, 14, 15, 28, 29, 30, 31,
|
||||
])))])),
|
||||
];
|
||||
let (tx, rx) = unbounded_channel();
|
||||
|
||||
send_direct_data_shard_chunks(tx, shard_streams, data_shards, block_size, 32, false, 3, 18).await;
|
||||
|
||||
let mut stream = ChannelChunkStream::new(rx);
|
||||
let mut collected = Vec::new();
|
||||
while let Some(chunk) = stream.next().await {
|
||||
collected.extend_from_slice(&chunk.unwrap().as_bytes());
|
||||
}
|
||||
|
||||
assert_eq!(collected, (3u8..21).collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_direct_data_shard_chunks_keeps_final_full_block_when_length_is_block_aligned() {
|
||||
let data_shards = 2;
|
||||
let block_size = 16;
|
||||
let shard_streams: Vec<BoxChunkStream> = vec![
|
||||
Box::pin(stream::iter(vec![
|
||||
Ok(IoChunk::Shared(Bytes::copy_from_slice(&[0, 1, 2, 3, 4, 5, 6, 7]))),
|
||||
Ok(IoChunk::Shared(Bytes::copy_from_slice(&[16, 17, 18, 19, 20, 21, 22, 23]))),
|
||||
])),
|
||||
Box::pin(stream::iter(vec![
|
||||
Ok(IoChunk::Shared(Bytes::copy_from_slice(&[8, 9, 10, 11, 12, 13, 14, 15]))),
|
||||
Ok(IoChunk::Shared(Bytes::copy_from_slice(&[24, 25, 26, 27, 28, 29, 30, 31]))),
|
||||
])),
|
||||
];
|
||||
let (tx, rx) = unbounded_channel();
|
||||
|
||||
send_direct_data_shard_chunks(tx, shard_streams, data_shards, block_size, 32, false, 0, 32).await;
|
||||
|
||||
let mut stream = ChannelChunkStream::new(rx);
|
||||
let mut collected = Vec::new();
|
||||
while let Some(chunk) = stream.next().await {
|
||||
collected.extend_from_slice(&chunk.unwrap().as_bytes());
|
||||
}
|
||||
|
||||
assert_eq!(collected, (0u8..32).collect::<Vec<_>>());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user