mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-22 20:36:38 +00:00
fix(get): remove GET chunk fast path (#2507)
This commit is contained in:
@@ -14,12 +14,8 @@
|
||||
|
||||
use crate::disk::{self, DiskAPI as _, DiskStore, error::DiskError};
|
||||
use crate::erasure_coding::{BitrotReader, BitrotWriterWrapper, CustomWriter};
|
||||
use crate::store_api::{GetObjectChunkCopyMode, GetObjectChunkPath, GetObjectChunkResult};
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures_util::{StreamExt, stream};
|
||||
use rustfs_io_core::{BoxChunkStream, IoChunk};
|
||||
use bytes::Bytes;
|
||||
use rustfs_utils::HashAlgorithm;
|
||||
use std::collections::VecDeque;
|
||||
use std::io::Cursor;
|
||||
use std::time::Instant;
|
||||
use tokio::io::AsyncRead;
|
||||
@@ -27,315 +23,6 @@ use tracing::debug;
|
||||
|
||||
const BITROT_READ_OPERATION: &str = "bitrot_read";
|
||||
|
||||
fn classify_chunk_copy_mode(source_direct: bool, copied: bool) -> GetObjectChunkCopyMode {
|
||||
if copied {
|
||||
GetObjectChunkCopyMode::SingleCopy
|
||||
} else if source_direct {
|
||||
GetObjectChunkCopyMode::TrueZeroCopy
|
||||
} else {
|
||||
GetObjectChunkCopyMode::SharedBytes
|
||||
}
|
||||
}
|
||||
|
||||
struct ChunkSpan {
|
||||
bytes: Bytes,
|
||||
chunk: IoChunk,
|
||||
copied: bool,
|
||||
}
|
||||
|
||||
fn take_contiguous_chunk_span(chunk: &IoChunk, offset: usize, len: usize) -> std::io::Result<ChunkSpan> {
|
||||
match chunk {
|
||||
IoChunk::Shared(bytes) => {
|
||||
let bytes = bytes.slice(offset..offset + len);
|
||||
Ok(ChunkSpan {
|
||||
bytes: bytes.clone(),
|
||||
chunk: IoChunk::Shared(bytes),
|
||||
copied: false,
|
||||
})
|
||||
}
|
||||
IoChunk::Mapped(mapped) => {
|
||||
let chunk = IoChunk::Mapped(mapped.slice(offset, len)?);
|
||||
let bytes = chunk.as_bytes();
|
||||
Ok(ChunkSpan {
|
||||
bytes,
|
||||
chunk,
|
||||
copied: false,
|
||||
})
|
||||
}
|
||||
IoChunk::Pooled(pooled) => {
|
||||
let chunk = IoChunk::Pooled(pooled.slice(offset, len)?);
|
||||
let bytes = chunk.as_bytes();
|
||||
Ok(ChunkSpan {
|
||||
bytes,
|
||||
chunk,
|
||||
copied: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BitrotChunkSource {
|
||||
source_stream: BoxChunkStream,
|
||||
source_chunks: VecDeque<IoChunk>,
|
||||
source_chunk_offset: usize,
|
||||
source_buffered_bytes: usize,
|
||||
source_done: bool,
|
||||
}
|
||||
|
||||
struct BitrotChunkStreamState {
|
||||
source: BitrotChunkSource,
|
||||
decoded_remaining: usize,
|
||||
trim_prefix: usize,
|
||||
output_remaining: usize,
|
||||
shard_size: usize,
|
||||
checksum_algo: HashAlgorithm,
|
||||
skip_verify: bool,
|
||||
}
|
||||
|
||||
struct ChunkCursor<'a> {
|
||||
chunks: &'a [IoChunk],
|
||||
chunk_index: usize,
|
||||
chunk_offset: usize,
|
||||
consumed: usize,
|
||||
total_len: usize,
|
||||
}
|
||||
|
||||
impl<'a> ChunkCursor<'a> {
|
||||
fn new(chunks: &'a [IoChunk]) -> Self {
|
||||
Self {
|
||||
chunks,
|
||||
chunk_index: 0,
|
||||
chunk_offset: 0,
|
||||
consumed: 0,
|
||||
total_len: chunks.iter().map(IoChunk::len).sum(),
|
||||
}
|
||||
}
|
||||
|
||||
fn remaining(&self) -> usize {
|
||||
self.total_len.saturating_sub(self.consumed)
|
||||
}
|
||||
|
||||
fn skip_empty_chunks(&mut self) {
|
||||
while let Some(chunk) = self.chunks.get(self.chunk_index) {
|
||||
if self.chunk_offset < chunk.len() {
|
||||
break;
|
||||
}
|
||||
self.chunk_index += 1;
|
||||
self.chunk_offset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self, len: usize) {
|
||||
self.consumed += len;
|
||||
self.chunk_offset += len;
|
||||
self.skip_empty_chunks();
|
||||
}
|
||||
|
||||
fn take_span(&mut self, len: usize) -> std::io::Result<ChunkSpan> {
|
||||
self.skip_empty_chunks();
|
||||
if self.remaining() < len {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "truncated bitrot chunk source"));
|
||||
}
|
||||
|
||||
let Some(chunk) = self.chunks.get(self.chunk_index) else {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "missing bitrot chunk source"));
|
||||
};
|
||||
let available = chunk.len().saturating_sub(self.chunk_offset);
|
||||
|
||||
if len <= available {
|
||||
let span = take_contiguous_chunk_span(chunk, self.chunk_offset, len)?;
|
||||
self.advance(len);
|
||||
return Ok(span);
|
||||
}
|
||||
|
||||
let mut aggregate = BytesMut::with_capacity(len);
|
||||
let mut remaining = len;
|
||||
while remaining > 0 {
|
||||
self.skip_empty_chunks();
|
||||
let Some(chunk) = self.chunks.get(self.chunk_index) else {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "truncated bitrot chunk source"));
|
||||
};
|
||||
let available = chunk.len().saturating_sub(self.chunk_offset);
|
||||
let take = available.min(remaining);
|
||||
aggregate.extend_from_slice(&chunk.as_bytes()[self.chunk_offset..self.chunk_offset + take]);
|
||||
self.advance(take);
|
||||
remaining -= take;
|
||||
}
|
||||
|
||||
let bytes = aggregate.freeze();
|
||||
Ok(ChunkSpan {
|
||||
bytes: bytes.clone(),
|
||||
chunk: IoChunk::Shared(bytes),
|
||||
copied: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl BitrotChunkSource {
|
||||
fn new(source_stream: BoxChunkStream, source_chunks: VecDeque<IoChunk>, source_done: bool) -> Self {
|
||||
let source_buffered_bytes = source_chunks.iter().map(IoChunk::len).sum();
|
||||
Self {
|
||||
source_stream,
|
||||
source_chunks,
|
||||
source_chunk_offset: 0,
|
||||
source_buffered_bytes,
|
||||
source_done,
|
||||
}
|
||||
}
|
||||
|
||||
fn skip_empty_chunks(&mut self) {
|
||||
while let Some(chunk) = self.source_chunks.front() {
|
||||
if self.source_chunk_offset < chunk.len() {
|
||||
break;
|
||||
}
|
||||
self.source_chunks.pop_front();
|
||||
self.source_chunk_offset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
async fn fill(&mut self, min_bytes: usize) -> std::io::Result<()> {
|
||||
while self.source_buffered_bytes < min_bytes && !self.source_done {
|
||||
match self.source_stream.next().await {
|
||||
Some(Ok(chunk)) => {
|
||||
self.source_buffered_bytes += chunk.len();
|
||||
self.source_chunks.push_back(chunk);
|
||||
}
|
||||
Some(Err(err)) => return Err(err),
|
||||
None => self.source_done = true,
|
||||
}
|
||||
}
|
||||
|
||||
if self.source_buffered_bytes < min_bytes {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "truncated bitrot chunk source"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn advance(&mut self, len: usize) {
|
||||
self.source_buffered_bytes = self.source_buffered_bytes.saturating_sub(len);
|
||||
self.source_chunk_offset += len;
|
||||
self.skip_empty_chunks();
|
||||
}
|
||||
|
||||
fn take_span(&mut self, len: usize) -> std::io::Result<ChunkSpan> {
|
||||
self.skip_empty_chunks();
|
||||
if self.source_buffered_bytes < len {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "truncated bitrot chunk source"));
|
||||
}
|
||||
|
||||
let Some(chunk) = self.source_chunks.front() else {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "missing bitrot chunk source"));
|
||||
};
|
||||
let available = chunk.len().saturating_sub(self.source_chunk_offset);
|
||||
|
||||
if len <= available {
|
||||
let span = take_contiguous_chunk_span(chunk, self.source_chunk_offset, len)?;
|
||||
self.advance(len);
|
||||
return Ok(span);
|
||||
}
|
||||
|
||||
let mut aggregate = BytesMut::with_capacity(len);
|
||||
let mut remaining = len;
|
||||
while remaining > 0 {
|
||||
self.skip_empty_chunks();
|
||||
let Some(chunk) = self.source_chunks.front() else {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "truncated bitrot chunk source"));
|
||||
};
|
||||
let available = chunk.len().saturating_sub(self.source_chunk_offset);
|
||||
let take = available.min(remaining);
|
||||
aggregate.extend_from_slice(&chunk.as_bytes()[self.source_chunk_offset..self.source_chunk_offset + take]);
|
||||
self.advance(take);
|
||||
remaining -= take;
|
||||
}
|
||||
|
||||
let bytes = aggregate.freeze();
|
||||
Ok(ChunkSpan {
|
||||
bytes: bytes.clone(),
|
||||
chunk: IoChunk::Shared(bytes),
|
||||
copied: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl BitrotChunkStreamState {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new(
|
||||
source_stream: BoxChunkStream,
|
||||
source_chunks: VecDeque<IoChunk>,
|
||||
source_done: bool,
|
||||
decoded_remaining: usize,
|
||||
trim_prefix: usize,
|
||||
output_remaining: usize,
|
||||
shard_size: usize,
|
||||
checksum_algo: HashAlgorithm,
|
||||
skip_verify: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
source: BitrotChunkSource::new(source_stream, source_chunks, source_done),
|
||||
decoded_remaining,
|
||||
trim_prefix,
|
||||
output_remaining,
|
||||
shard_size,
|
||||
checksum_algo,
|
||||
skip_verify,
|
||||
}
|
||||
}
|
||||
|
||||
fn hash_size(&self) -> usize {
|
||||
self.checksum_algo.size()
|
||||
}
|
||||
|
||||
async fn next_verified_chunk(&mut self) -> std::io::Result<Option<IoChunk>> {
|
||||
let hash_size = self.hash_size();
|
||||
|
||||
while self.output_remaining > 0 && self.decoded_remaining > 0 {
|
||||
let data_len = self.shard_size.min(self.decoded_remaining);
|
||||
|
||||
let expected_hash = if hash_size > 0 {
|
||||
self.source.fill(hash_size).await?;
|
||||
Some(self.source.take_span(hash_size)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
self.source.fill(data_len).await?;
|
||||
let data_span = self.source.take_span(data_len)?;
|
||||
|
||||
if let Some(expected_hash) = expected_hash
|
||||
&& !self.skip_verify
|
||||
&& self.checksum_algo.hash_encode(data_span.bytes.as_ref()).as_ref() != expected_hash.bytes.as_ref()
|
||||
{
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "bitrot hash mismatch"));
|
||||
}
|
||||
|
||||
self.decoded_remaining -= data_len;
|
||||
|
||||
if self.trim_prefix >= data_len {
|
||||
self.trim_prefix -= data_len;
|
||||
continue;
|
||||
}
|
||||
|
||||
let start = self.trim_prefix;
|
||||
self.trim_prefix = 0;
|
||||
let take = (data_len - start).min(self.output_remaining);
|
||||
self.output_remaining -= take;
|
||||
|
||||
let chunk = if start == 0 && take == data_len {
|
||||
data_span.chunk
|
||||
} else {
|
||||
data_span.chunk.slice(start, take)?
|
||||
};
|
||||
|
||||
if !chunk.is_empty() {
|
||||
return Ok(Some(chunk));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a BitrotReader from either inline data or disk file stream
|
||||
///
|
||||
/// # Parameters
|
||||
@@ -399,9 +86,8 @@ pub async fn create_bitrot_reader(
|
||||
let duration_ms = start.elapsed().as_secs_f64() * 1000.0;
|
||||
|
||||
rustfs_io_metrics::record_io_path_selected(BITROT_READ_OPERATION, rustfs_io_metrics::IoPath::Fast);
|
||||
// `read_file_zero_copy()` returns a shared `Bytes` view, but it may still
|
||||
// internally aggregate multiple chunk windows. The exact chunk-native copy
|
||||
// mode is only preserved by `create_bitrot_chunk_stream()`.
|
||||
// `read_file_zero_copy()` returns a shared `Bytes` view, which preserves the
|
||||
// mmap-backed fast path without exposing chunk-native GET internals.
|
||||
rustfs_io_metrics::record_io_copy_mode(
|
||||
BITROT_READ_OPERATION,
|
||||
rustfs_io_metrics::CopyMode::SharedBytes,
|
||||
@@ -469,198 +155,6 @@ pub async fn create_bitrot_reader(
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a chunk stream from bitrot-encoded data, preserving source chunk provenance when possible.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn create_bitrot_chunk_stream(
|
||||
inline_data: Option<&[u8]>,
|
||||
disk: Option<&DiskStore>,
|
||||
bucket: &str,
|
||||
path: &str,
|
||||
offset: usize,
|
||||
length: usize,
|
||||
total_data_size: usize,
|
||||
shard_size: usize,
|
||||
checksum_algo: HashAlgorithm,
|
||||
skip_verify: bool,
|
||||
use_zero_copy: bool,
|
||||
) -> disk::error::Result<Option<GetObjectChunkResult>> {
|
||||
let fetch_start = (offset / shard_size) * shard_size;
|
||||
let fetch_end = (offset + length).div_ceil(shard_size) * shard_size;
|
||||
let fetch_end = fetch_end.min(total_data_size);
|
||||
let fetch_length = fetch_end.saturating_sub(fetch_start);
|
||||
let trim_prefix = offset.saturating_sub(fetch_start);
|
||||
let hash_size = checksum_algo.size();
|
||||
let encoded_length = fetch_length.div_ceil(shard_size) * hash_size + fetch_length;
|
||||
let encoded_offset = fetch_start.div_ceil(shard_size) * hash_size + fetch_start;
|
||||
|
||||
let mut source_done = false;
|
||||
let (source_stream, mut prefetched_chunks, source_direct) = if let Some(data) = inline_data {
|
||||
source_done = true;
|
||||
let mut chunks = VecDeque::new();
|
||||
chunks.push_back(IoChunk::Shared(
|
||||
Bytes::copy_from_slice(data).slice(encoded_offset..encoded_offset + encoded_length),
|
||||
));
|
||||
let source_stream: BoxChunkStream = Box::pin(stream::empty::<std::io::Result<IoChunk>>());
|
||||
(source_stream, chunks, false)
|
||||
} else if let Some(disk) = disk {
|
||||
if use_zero_copy {
|
||||
let mut source_stream = disk.read_file_chunks(bucket, path, encoded_offset, encoded_length).await?;
|
||||
let mut prefetched_chunks = VecDeque::new();
|
||||
let mut direct = true;
|
||||
while prefetched_chunks.len() < 2 {
|
||||
let Some(chunk) = source_stream.next().await else {
|
||||
source_done = true;
|
||||
break;
|
||||
};
|
||||
let chunk = chunk?;
|
||||
direct &= matches!(chunk, IoChunk::Mapped(_));
|
||||
prefetched_chunks.push_back(chunk);
|
||||
}
|
||||
(source_stream, prefetched_chunks, direct)
|
||||
} else {
|
||||
source_done = true;
|
||||
let bytes = disk.read_file_zero_copy(bucket, path, encoded_offset, encoded_length).await?;
|
||||
let mut chunks = VecDeque::new();
|
||||
chunks.push_back(IoChunk::Shared(bytes));
|
||||
let source_stream: BoxChunkStream = Box::pin(stream::empty::<std::io::Result<IoChunk>>());
|
||||
(source_stream, chunks, false)
|
||||
}
|
||||
} else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let copied = predicted_stream_copy(encoded_length, shard_size, checksum_algo.size(), &prefetched_chunks, source_done);
|
||||
let state = BitrotChunkStreamState::new(
|
||||
source_stream,
|
||||
std::mem::take(&mut prefetched_chunks),
|
||||
source_done,
|
||||
fetch_length,
|
||||
trim_prefix,
|
||||
length,
|
||||
shard_size,
|
||||
checksum_algo,
|
||||
skip_verify,
|
||||
);
|
||||
let stream = stream::unfold(Some(state), |state| async move {
|
||||
let mut state = match state {
|
||||
Some(state) => state,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
match state.next_verified_chunk().await {
|
||||
Ok(Some(chunk)) => {
|
||||
let next_state = if state.output_remaining == 0 { None } else { Some(state) };
|
||||
Some((Ok::<IoChunk, std::io::Error>(chunk), next_state))
|
||||
}
|
||||
Ok(None) => None,
|
||||
Err(err) => Some((Err(err), None)),
|
||||
}
|
||||
});
|
||||
Ok(Some(GetObjectChunkResult {
|
||||
stream: Box::pin(stream),
|
||||
path: GetObjectChunkPath::Direct,
|
||||
copy_mode: classify_chunk_copy_mode(source_direct, copied),
|
||||
}))
|
||||
}
|
||||
|
||||
fn predicted_stream_copy(
|
||||
encoded_length: usize,
|
||||
shard_size: usize,
|
||||
hash_size: usize,
|
||||
prefetched_chunks: &VecDeque<IoChunk>,
|
||||
source_done: bool,
|
||||
) -> bool {
|
||||
if prefetched_chunks.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if source_done && prefetched_chunks.len() == 1 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let full_frame_len = hash_size + shard_size;
|
||||
if full_frame_len == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let first_window_len = prefetched_chunks.front().map(IoChunk::len).unwrap_or(encoded_length);
|
||||
encoded_length > first_window_len && !first_window_len.is_multiple_of(full_frame_len)
|
||||
}
|
||||
|
||||
fn trim_chunk_vec(chunks: Vec<IoChunk>, offset: usize, length: usize) -> std::io::Result<Vec<IoChunk>> {
|
||||
let mut skip = offset;
|
||||
let mut remaining = length;
|
||||
let mut result = Vec::new();
|
||||
|
||||
for chunk in chunks {
|
||||
if remaining == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let chunk_len = chunk.len();
|
||||
if skip >= chunk_len {
|
||||
skip -= chunk_len;
|
||||
continue;
|
||||
}
|
||||
|
||||
let start = skip;
|
||||
let take = (chunk_len - start).min(remaining);
|
||||
result.push(chunk.slice(start, take)?);
|
||||
remaining -= take;
|
||||
skip = 0;
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn decode_bitrot_chunk_source(
|
||||
source_chunks: &[IoChunk],
|
||||
shard_size: usize,
|
||||
checksum_algo: HashAlgorithm,
|
||||
skip_verify: bool,
|
||||
) -> std::io::Result<(Vec<IoChunk>, bool)> {
|
||||
let hash_size = checksum_algo.size();
|
||||
let mut cursor = ChunkCursor::new(source_chunks);
|
||||
let mut result = Vec::new();
|
||||
let mut copied = false;
|
||||
|
||||
while cursor.remaining() > 0 {
|
||||
let expected_hash = if hash_size > 0 {
|
||||
Some(cursor.take_span(hash_size)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let data_len = shard_size.min(cursor.remaining());
|
||||
if data_len == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let data_span = cursor.take_span(data_len)?;
|
||||
copied |= data_span.copied;
|
||||
if let Some(expected_hash) = expected_hash {
|
||||
copied |= expected_hash.copied;
|
||||
if !skip_verify && checksum_algo.hash_encode(data_span.bytes.as_ref()).as_ref() != expected_hash.bytes.as_ref() {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "bitrot hash mismatch"));
|
||||
}
|
||||
}
|
||||
|
||||
result.push(data_span.chunk);
|
||||
}
|
||||
|
||||
Ok((result, copied))
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn decode_bitrot_chunk_source_for_bench(
|
||||
source_chunks: &[IoChunk],
|
||||
shard_size: usize,
|
||||
checksum_algo: HashAlgorithm,
|
||||
skip_verify: bool,
|
||||
) -> std::io::Result<(Vec<IoChunk>, bool)> {
|
||||
decode_bitrot_chunk_source(source_chunks, shard_size, checksum_algo, skip_verify)
|
||||
}
|
||||
|
||||
/// Create a new BitrotWriterWrapper based on the provided parameters
|
||||
///
|
||||
/// # Parameters
|
||||
@@ -705,7 +199,6 @@ pub async fn create_bitrot_writer(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use futures_util::StreamExt;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_bitrot_reader_with_inline_data() {
|
||||
@@ -756,246 +249,6 @@ mod tests {
|
||||
assert!(result.unwrap().is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_bitrot_chunk_stream_with_inline_data() {
|
||||
let shard_size = 4;
|
||||
let checksum_algo = HashAlgorithm::HighwayHash256S;
|
||||
let shard1 = b"abcd";
|
||||
let shard2 = b"ef";
|
||||
|
||||
let mut encoded = Vec::new();
|
||||
encoded.extend_from_slice(checksum_algo.hash_encode(shard1).as_ref());
|
||||
encoded.extend_from_slice(shard1);
|
||||
encoded.extend_from_slice(checksum_algo.hash_encode(shard2).as_ref());
|
||||
encoded.extend_from_slice(shard2);
|
||||
|
||||
let mut stream = create_bitrot_chunk_stream(
|
||||
Some(&encoded),
|
||||
None,
|
||||
"test-bucket",
|
||||
"test-path",
|
||||
0,
|
||||
shard1.len() + shard2.len(),
|
||||
shard1.len() + shard2.len(),
|
||||
shard_size,
|
||||
checksum_algo,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.stream;
|
||||
|
||||
let mut collected = Vec::new();
|
||||
while let Some(chunk) = stream.next().await {
|
||||
collected.extend_from_slice(&chunk.unwrap().as_bytes());
|
||||
}
|
||||
|
||||
assert_eq!(collected, b"abcdef");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_bitrot_chunk_stream_detects_hash_mismatch() {
|
||||
let shard_size = 4;
|
||||
let checksum_algo = HashAlgorithm::HighwayHash256S;
|
||||
let shard = b"abcd";
|
||||
|
||||
let mut encoded = Vec::new();
|
||||
let mut bad_hash = checksum_algo.hash_encode(shard).as_ref().to_vec();
|
||||
bad_hash[0] ^= 0xFF;
|
||||
encoded.extend_from_slice(&bad_hash);
|
||||
encoded.extend_from_slice(shard);
|
||||
|
||||
let result = create_bitrot_chunk_stream(
|
||||
Some(&encoded),
|
||||
None,
|
||||
"test-bucket",
|
||||
"test-path",
|
||||
0,
|
||||
shard.len(),
|
||||
shard.len(),
|
||||
shard_size,
|
||||
checksum_algo,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut stream = result.unwrap().unwrap().stream;
|
||||
let err = stream.next().await.unwrap().unwrap_err();
|
||||
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
|
||||
assert!(err.to_string().contains("bitrot hash mismatch"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_bitrot_chunk_stream_trims_range_after_decode() {
|
||||
let shard_size = 4;
|
||||
let checksum_algo = HashAlgorithm::HighwayHash256S;
|
||||
let shard1 = b"abcd";
|
||||
let shard2 = b"efgh";
|
||||
|
||||
let mut encoded = Vec::new();
|
||||
encoded.extend_from_slice(checksum_algo.hash_encode(shard1).as_ref());
|
||||
encoded.extend_from_slice(shard1);
|
||||
encoded.extend_from_slice(checksum_algo.hash_encode(shard2).as_ref());
|
||||
encoded.extend_from_slice(shard2);
|
||||
|
||||
let mut stream = create_bitrot_chunk_stream(
|
||||
Some(&encoded),
|
||||
None,
|
||||
"test-bucket",
|
||||
"test-path",
|
||||
1,
|
||||
5,
|
||||
shard1.len() + shard2.len(),
|
||||
shard_size,
|
||||
checksum_algo,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.stream;
|
||||
|
||||
let mut collected = Vec::new();
|
||||
while let Some(chunk) = stream.next().await {
|
||||
collected.extend_from_slice(&chunk.unwrap().as_bytes());
|
||||
}
|
||||
|
||||
assert_eq!(collected, b"bcdef");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_bitrot_chunk_source_preserves_aligned_multi_chunk_slices() {
|
||||
let shard_size = 4;
|
||||
let checksum_algo = HashAlgorithm::Md5;
|
||||
let shard1 = b"abcd";
|
||||
let shard2 = b"efgh";
|
||||
|
||||
let mut encoded_chunk_one = Vec::new();
|
||||
encoded_chunk_one.extend_from_slice(checksum_algo.hash_encode(shard1).as_ref());
|
||||
encoded_chunk_one.extend_from_slice(shard1);
|
||||
|
||||
let mut encoded_chunk_two = Vec::new();
|
||||
encoded_chunk_two.extend_from_slice(checksum_algo.hash_encode(shard2).as_ref());
|
||||
encoded_chunk_two.extend_from_slice(shard2);
|
||||
|
||||
let source_chunks = vec![
|
||||
IoChunk::Shared(Bytes::from(encoded_chunk_one)),
|
||||
IoChunk::Shared(Bytes::from(encoded_chunk_two)),
|
||||
];
|
||||
let (decoded, copied) = decode_bitrot_chunk_source(&source_chunks, shard_size, checksum_algo, false).unwrap();
|
||||
|
||||
assert!(!copied, "frame-aligned multi-chunk source should not require aggregate copies");
|
||||
assert_eq!(decoded.len(), 2);
|
||||
assert_eq!(decoded[0].as_bytes(), Bytes::from_static(b"abcd"));
|
||||
assert_eq!(decoded[1].as_bytes(), Bytes::from_static(b"efgh"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_bitrot_chunk_source_marks_cross_chunk_frame_as_copied() {
|
||||
let shard_size = 4;
|
||||
let checksum_algo = HashAlgorithm::Md5;
|
||||
let shard1 = b"abcd";
|
||||
let shard2 = b"efgh";
|
||||
|
||||
let hash1 = checksum_algo.hash_encode(shard1).as_ref().to_vec();
|
||||
let hash2 = checksum_algo.hash_encode(shard2).as_ref().to_vec();
|
||||
let mut encoded = Vec::new();
|
||||
encoded.extend_from_slice(&hash1);
|
||||
encoded.extend_from_slice(shard1);
|
||||
encoded.extend_from_slice(&hash2);
|
||||
encoded.extend_from_slice(shard2);
|
||||
|
||||
let split = hash1.len() + 2;
|
||||
let source_chunks = vec![
|
||||
IoChunk::Shared(Bytes::copy_from_slice(&encoded[..split])),
|
||||
IoChunk::Shared(Bytes::copy_from_slice(&encoded[split..])),
|
||||
];
|
||||
let (decoded, copied) = decode_bitrot_chunk_source(&source_chunks, shard_size, checksum_algo, false).unwrap();
|
||||
|
||||
assert!(copied, "cross-chunk frame should be classified as requiring a copy");
|
||||
assert_eq!(decoded.len(), 2);
|
||||
assert_eq!(decoded[0].as_bytes(), Bytes::from_static(b"abcd"));
|
||||
assert_eq!(decoded[1].as_bytes(), Bytes::from_static(b"efgh"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_bitrot_chunk_source_preserves_pooled_single_chunk_slice() {
|
||||
let shard_size = 4;
|
||||
let checksum_algo = HashAlgorithm::Md5;
|
||||
let shard = b"abcd";
|
||||
|
||||
let mut encoded = Vec::new();
|
||||
encoded.extend_from_slice(checksum_algo.hash_encode(shard).as_ref());
|
||||
encoded.extend_from_slice(shard);
|
||||
|
||||
let source_chunks = vec![IoChunk::Pooled(rustfs_io_core::PooledChunk::from_vec(encoded))];
|
||||
let (decoded, copied) = decode_bitrot_chunk_source(&source_chunks, shard_size, checksum_algo, false).unwrap();
|
||||
|
||||
assert!(!copied, "single pooled chunk slice should preserve provenance without copy");
|
||||
assert_eq!(decoded.len(), 1);
|
||||
assert!(matches!(&decoded[0], IoChunk::Pooled(_)));
|
||||
assert_eq!(decoded[0].as_bytes(), Bytes::from_static(b"abcd"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_bitrot_chunk_source_marks_cross_chunk_take_as_copied() {
|
||||
let source_stream: BoxChunkStream = Box::pin(stream::iter(vec![
|
||||
Ok(IoChunk::Shared(Bytes::from_static(b"ab"))),
|
||||
Ok(IoChunk::Shared(Bytes::from_static(b"cd"))),
|
||||
]));
|
||||
let mut source = BitrotChunkSource::new(source_stream, VecDeque::new(), false);
|
||||
|
||||
source.fill(4).await.expect("source fill should succeed");
|
||||
let span = source.take_span(4).expect("cross-chunk take should succeed");
|
||||
|
||||
assert!(span.copied, "cross-chunk take should be classified as copied");
|
||||
assert_eq!(span.bytes, Bytes::from_static(b"abcd"));
|
||||
assert_eq!(span.chunk.as_bytes(), Bytes::from_static(b"abcd"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_bitrot_chunk_stream_state_yields_verified_prefix_before_later_truncation() {
|
||||
let shard_size = 4;
|
||||
let checksum_algo = HashAlgorithm::Md5;
|
||||
let shard1 = b"abcd";
|
||||
let shard2 = b"efgh";
|
||||
|
||||
let mut first_frame = Vec::new();
|
||||
first_frame.extend_from_slice(checksum_algo.hash_encode(shard1).as_ref());
|
||||
first_frame.extend_from_slice(shard1);
|
||||
|
||||
let mut second_frame_prefix = Vec::new();
|
||||
second_frame_prefix.extend_from_slice(checksum_algo.hash_encode(shard2).as_ref());
|
||||
second_frame_prefix.extend_from_slice(&shard2[..2]);
|
||||
|
||||
let source_stream: BoxChunkStream = Box::pin(stream::iter(vec![
|
||||
Ok(IoChunk::Shared(Bytes::from(first_frame))),
|
||||
Ok(IoChunk::Shared(Bytes::from(second_frame_prefix))),
|
||||
]));
|
||||
let mut state = BitrotChunkStreamState::new(
|
||||
source_stream,
|
||||
VecDeque::new(),
|
||||
false,
|
||||
shard1.len() + shard2.len(),
|
||||
0,
|
||||
shard1.len() + shard2.len(),
|
||||
shard_size,
|
||||
checksum_algo,
|
||||
false,
|
||||
);
|
||||
|
||||
let first = state.next_verified_chunk().await.unwrap().unwrap();
|
||||
assert_eq!(first.as_bytes(), Bytes::from_static(b"abcd"));
|
||||
|
||||
let err = state.next_verified_chunk().await.unwrap_err();
|
||||
assert_eq!(err.kind(), std::io::ErrorKind::UnexpectedEof);
|
||||
assert!(err.to_string().contains("truncated bitrot chunk source"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_bitrot_reader_with_inline_offset_starts_at_requested_shard() {
|
||||
let shard_size = 4;
|
||||
|
||||
@@ -249,9 +249,6 @@ mod read;
|
||||
mod replication;
|
||||
mod write;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub use read::collect_direct_data_shard_chunks_for_benchmark;
|
||||
|
||||
/// Get lock acquire timeout from environment variable RUSTFS_LOCK_ACQUIRE_TIMEOUT (in seconds)
|
||||
/// Defaults to 30 seconds if not set or invalid
|
||||
pub fn get_lock_acquire_timeout() -> Duration {
|
||||
|
||||
@@ -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<_>>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
use crate::disk::error_reduce::count_errs;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::store_api::{GetObjectChunkResult, ListPartsInfo, ObjectInfoOrErr, WalkOptions};
|
||||
use crate::store_api::{ListPartsInfo, ObjectInfoOrErr, WalkOptions};
|
||||
use crate::{
|
||||
disk::{
|
||||
DiskAPI, DiskInfo, DiskOption, DiskStore,
|
||||
@@ -287,19 +287,6 @@ impl Sets {
|
||||
self.get_disks(self.get_hashed_set_index(key))
|
||||
}
|
||||
|
||||
pub async fn get_object_chunks(
|
||||
&self,
|
||||
bucket: &str,
|
||||
object: &str,
|
||||
range: Option<HTTPRangeSpec>,
|
||||
h: HeaderMap,
|
||||
opts: &ObjectOptions,
|
||||
) -> Result<GetObjectChunkResult> {
|
||||
self.get_disks_by_key(object)
|
||||
.get_object_chunks(bucket, object, range, h, opts)
|
||||
.await
|
||||
}
|
||||
|
||||
fn get_hashed_set_index(&self, input: &str) -> usize {
|
||||
match self.distribution_algo {
|
||||
DistributionAlgoVersion::V1 => crc_hash(input, self.disk_set.len()),
|
||||
|
||||
@@ -60,9 +60,9 @@ use crate::{
|
||||
sets::Sets,
|
||||
store_api::{
|
||||
BucketInfo, BucketOperations, BucketOptions, ChunkNativePutData, CompletePart, DeleteBucketOptions, DeletedObject,
|
||||
GetObjectChunkResult, GetObjectReader, HTTPRangeSpec, HealOperations, ListObjectsV2Info, ListOperations,
|
||||
MakeBucketOptions, MultipartOperations, MultipartUploadResult, ObjectInfo, ObjectOperations, ObjectOptions,
|
||||
ObjectToDelete, PartInfo, StorageAPI,
|
||||
GetObjectReader, HTTPRangeSpec, HealOperations, ListObjectsV2Info, ListOperations, MakeBucketOptions,
|
||||
MultipartOperations, MultipartUploadResult, ObjectInfo, ObjectOperations, ObjectOptions, ObjectToDelete, PartInfo,
|
||||
StorageAPI,
|
||||
},
|
||||
store_init,
|
||||
};
|
||||
@@ -271,20 +271,6 @@ impl ObjectIO for ECStore {
|
||||
}
|
||||
}
|
||||
|
||||
impl ECStore {
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
pub async fn get_object_chunks(
|
||||
&self,
|
||||
bucket: &str,
|
||||
object: &str,
|
||||
range: Option<HTTPRangeSpec>,
|
||||
h: HeaderMap,
|
||||
opts: &ObjectOptions,
|
||||
) -> Result<GetObjectChunkResult> {
|
||||
self.handle_get_object_chunks(bucket, object, range, h, opts).await
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref enableObjcetLockConfig: ObjectLockConfiguration = ObjectLockConfiguration {
|
||||
object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)),
|
||||
|
||||
@@ -13,8 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
use super::*;
|
||||
use crate::store_api::GetObjectChunkResult;
|
||||
|
||||
fn select_data_movement_target_pool(
|
||||
existing_pool_idx: Result<usize>,
|
||||
src_pool_idx: usize,
|
||||
@@ -214,34 +212,6 @@ impl ECStore {
|
||||
.await
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
pub(super) async fn handle_get_object_chunks(
|
||||
&self,
|
||||
bucket: &str,
|
||||
object: &str,
|
||||
range: Option<HTTPRangeSpec>,
|
||||
h: HeaderMap,
|
||||
opts: &ObjectOptions,
|
||||
) -> Result<GetObjectChunkResult> {
|
||||
check_get_obj_args(bucket, object)?;
|
||||
|
||||
let object = encode_dir_object(object);
|
||||
|
||||
if self.single_pool() {
|
||||
return self.pools[0].get_object_chunks(bucket, object.as_str(), range, h, opts).await;
|
||||
}
|
||||
|
||||
let mut opts = opts.clone();
|
||||
opts.no_lock = true;
|
||||
|
||||
let (_, idx) = self
|
||||
.get_latest_accessible_object_info_with_idx(bucket, &object, &opts)
|
||||
.await?;
|
||||
self.pools[idx]
|
||||
.get_object_chunks(bucket, object.as_str(), range, h, &opts)
|
||||
.await
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self, data))]
|
||||
pub(super) async fn handle_put_object(
|
||||
&self,
|
||||
|
||||
@@ -31,7 +31,6 @@ use rustfs_filemeta::{
|
||||
RestoreStatusOps as _, VersionPurgeStatusType, parse_restore_obj_status, replication_statuses_map,
|
||||
version_purge_statuses_map,
|
||||
};
|
||||
use rustfs_io_core::BoxChunkStream;
|
||||
use rustfs_lock::NamespaceLockWrapper;
|
||||
use rustfs_madmin::heal_commands::HealResultItem;
|
||||
use rustfs_rio::Checksum;
|
||||
|
||||
@@ -189,26 +189,6 @@ pub struct GetObjectReader {
|
||||
pub object_info: ObjectInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum GetObjectChunkPath {
|
||||
Direct,
|
||||
Bridge,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum GetObjectChunkCopyMode {
|
||||
TrueZeroCopy,
|
||||
SharedBytes,
|
||||
SingleCopy,
|
||||
Reconstructed,
|
||||
}
|
||||
|
||||
pub struct GetObjectChunkResult {
|
||||
pub stream: BoxChunkStream,
|
||||
pub path: GetObjectChunkPath,
|
||||
pub copy_mode: GetObjectChunkCopyMode,
|
||||
}
|
||||
|
||||
impl GetObjectReader {
|
||||
#[tracing::instrument(level = "debug", skip(reader, rs, opts, _h))]
|
||||
pub fn new(
|
||||
|
||||
Reference in New Issue
Block a user