mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-11 15:46:53 +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:
+780
-10
@@ -14,13 +14,328 @@
|
||||
|
||||
use crate::disk::{self, DiskAPI as _, DiskStore, error::DiskError};
|
||||
use crate::erasure_coding::{BitrotReader, BitrotWriterWrapper, CustomWriter};
|
||||
use bytes::Bytes;
|
||||
use crate::store_api::{GetObjectChunkCopyMode, GetObjectChunkPath, GetObjectChunkResult};
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures_util::{StreamExt, stream};
|
||||
use rustfs_io_core::{BoxChunkStream, IoChunk};
|
||||
use rustfs_utils::HashAlgorithm;
|
||||
use std::collections::VecDeque;
|
||||
use std::io::Cursor;
|
||||
use std::time::Instant;
|
||||
use tokio::io::AsyncRead;
|
||||
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
|
||||
@@ -65,20 +380,39 @@ pub async fn create_bitrot_reader(
|
||||
} else if let Some(disk) = disk {
|
||||
// Read from disk
|
||||
if use_zero_copy {
|
||||
if !disk.is_local() {
|
||||
rustfs_io_metrics::record_io_path_selected(BITROT_READ_OPERATION, rustfs_io_metrics::IoPath::Legacy);
|
||||
rustfs_io_metrics::record_io_fallback(
|
||||
rustfs_io_metrics::IoStage::ReadSetup,
|
||||
rustfs_io_metrics::FallbackReason::NonLocalBackend,
|
||||
);
|
||||
|
||||
let rd = disk.read_file_stream(bucket, path, offset, length).await?;
|
||||
let reader = BitrotReader::new(rd, shard_size, checksum_algo, skip_verify);
|
||||
return Ok(Some(reader));
|
||||
}
|
||||
|
||||
// Try zero-copy read first (uses mmap on Unix)
|
||||
let start = Instant::now();
|
||||
match disk.read_file_zero_copy(bucket, path, offset, length).await {
|
||||
Ok(bytes) => {
|
||||
let duration_ms = start.elapsed().as_secs_f64() * 1000.0;
|
||||
|
||||
// Record zero-copy metrics
|
||||
rustfs_io_metrics::record_zero_copy_read(bytes.len(), duration_ms);
|
||||
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()`.
|
||||
rustfs_io_metrics::record_io_copy_mode(
|
||||
BITROT_READ_OPERATION,
|
||||
rustfs_io_metrics::CopyMode::SharedBytes,
|
||||
bytes.len(),
|
||||
);
|
||||
|
||||
// Log successful zero-copy read
|
||||
debug!(
|
||||
size = bytes.len(),
|
||||
duration_ms,
|
||||
path = %path,
|
||||
"zero_copy_read_success"
|
||||
"bitrot_fast_read_success"
|
||||
);
|
||||
|
||||
// Wrap Bytes in Cursor for AsyncRead
|
||||
@@ -93,14 +427,16 @@ pub async fn create_bitrot_reader(
|
||||
Ok(Some(reader))
|
||||
}
|
||||
Err(e) => {
|
||||
// Record zero-copy fallback
|
||||
rustfs_io_metrics::record_zero_copy_fallback(&format!("{:?}", e));
|
||||
rustfs_io_metrics::record_io_path_selected(BITROT_READ_OPERATION, rustfs_io_metrics::IoPath::Legacy);
|
||||
rustfs_io_metrics::record_io_fallback(
|
||||
rustfs_io_metrics::IoStage::ReadSetup,
|
||||
rustfs_io_metrics::FallbackReason::Unknown,
|
||||
);
|
||||
|
||||
// Log zero-copy fallback
|
||||
debug!(
|
||||
reason = %format!("{:?}", e),
|
||||
reason = %e,
|
||||
path = %path,
|
||||
"zero_copy_fallback"
|
||||
"bitrot_fast_read_fallback"
|
||||
);
|
||||
|
||||
// Fall back to regular stream read on error
|
||||
@@ -117,6 +453,7 @@ pub async fn create_bitrot_reader(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rustfs_io_metrics::record_io_path_selected(BITROT_READ_OPERATION, rustfs_io_metrics::IoPath::Legacy);
|
||||
// Use regular stream read
|
||||
match disk.read_file_stream(bucket, path, offset, length).await {
|
||||
Ok(rd) => {
|
||||
@@ -132,6 +469,198 @@ 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
|
||||
@@ -176,6 +705,7 @@ 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() {
|
||||
@@ -226,6 +756,246 @@ 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;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
use crate::bucket::metadata::BUCKET_METADATA_FILE;
|
||||
use crate::bucket::replication::{decode_resync_file, encode_resync_file};
|
||||
use crate::disk::{BUCKET_META_PREFIX, MIGRATING_META_BUCKET, RUSTFS_META_BUCKET};
|
||||
use crate::store_api::{BucketOptions, ObjectOptions, PutObjReader, StorageAPI};
|
||||
use crate::store_api::{BucketOptions, ChunkNativePutData, ObjectOptions, StorageAPI};
|
||||
use http::HeaderMap;
|
||||
use rustfs_policy::auth::UserIdentity;
|
||||
use rustfs_policy::policy::PolicyDoc;
|
||||
@@ -263,10 +263,8 @@ async fn migrate_one_if_missing<S: StorageAPI>(
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = store
|
||||
.put_object(RUSTFS_META_BUCKET, path, &mut PutObjReader::from_vec(data), opts)
|
||||
.await
|
||||
{
|
||||
let mut put_data = ChunkNativePutData::from_vec(data);
|
||||
if let Err(e) = store.put_object(RUSTFS_META_BUCKET, path, &mut put_data, opts).await {
|
||||
warn!("write {label}: {e}");
|
||||
} else {
|
||||
info!("Migrated {label}");
|
||||
@@ -343,10 +341,8 @@ pub async fn try_migrate_iam_config<S: StorageAPI>(store: Arc<S>) {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if let Err(e) = store
|
||||
.put_object(RUSTFS_META_BUCKET, path, &mut PutObjReader::from_vec(data), &opts)
|
||||
.await
|
||||
{
|
||||
let mut put_data = ChunkNativePutData::from_vec(data);
|
||||
if let Err(e) = store.put_object(RUSTFS_META_BUCKET, path, &mut put_data, &opts).await {
|
||||
warn!("write IAM config {path}: {e}");
|
||||
} else {
|
||||
info!("Migrated IAM config: {path}");
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::config::{Config, GLOBAL_STORAGE_CLASS, KVS, audit, notify, oidc, stor
|
||||
use crate::disk::{MIGRATING_META_BUCKET, RUSTFS_META_BUCKET};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::global::is_first_cluster_node_local;
|
||||
use crate::store_api::{ObjectInfo, ObjectOptions, PutObjReader, StorageAPI};
|
||||
use crate::store_api::{ChunkNativePutData, ObjectInfo, ObjectOptions, StorageAPI};
|
||||
use http::HeaderMap;
|
||||
use rustfs_config::audit::{AUDIT_MQTT_KEYS, AUDIT_MQTT_SUB_SYS, AUDIT_WEBHOOK_KEYS, AUDIT_WEBHOOK_SUB_SYS};
|
||||
use rustfs_config::notify::{NOTIFY_MQTT_KEYS, NOTIFY_MQTT_SUB_SYS, NOTIFY_WEBHOOK_KEYS, NOTIFY_WEBHOOK_SUB_SYS};
|
||||
@@ -128,10 +128,8 @@ pub async fn delete_config<S: StorageAPI>(api: Arc<S>, file: &str) -> Result<()>
|
||||
}
|
||||
|
||||
pub async fn save_config_with_opts<S: StorageAPI>(api: Arc<S>, file: &str, data: Vec<u8>, opts: &ObjectOptions) -> Result<()> {
|
||||
if let Err(err) = api
|
||||
.put_object(RUSTFS_META_BUCKET, file, &mut PutObjReader::from_vec(data), opts)
|
||||
.await
|
||||
{
|
||||
let mut put_data = ChunkNativePutData::from_vec(data);
|
||||
if let Err(err) = api.put_object(RUSTFS_META_BUCKET, file, &mut put_data, opts).await {
|
||||
error!("save_config_with_opts: err: {:?}, file: {}", err, file);
|
||||
return Err(err);
|
||||
}
|
||||
@@ -1078,10 +1076,10 @@ mod tests {
|
||||
use crate::global::{is_dist_erasure, is_erasure, is_erasure_sd, update_erasure_type};
|
||||
use crate::set_disk::SetDisks;
|
||||
use crate::store_api::{
|
||||
BucketInfo, BucketOperations, BucketOptions, CompletePart, DeleteBucketOptions, DeletedObject, GetObjectReader,
|
||||
HTTPRangeSpec, HealOperations, ListMultipartsInfo, ListObjectVersionsInfo, ListObjectsV2Info, ListOperations,
|
||||
MakeBucketOptions, MultipartInfo, MultipartOperations, MultipartUploadResult, ObjectIO, ObjectInfo, ObjectOperations,
|
||||
ObjectOptions, ObjectToDelete, PartInfo, PutObjReader, StorageAPI, WalkOptions,
|
||||
BucketInfo, BucketOperations, BucketOptions, ChunkNativePutData, CompletePart, DeleteBucketOptions, DeletedObject,
|
||||
GetObjectReader, HTTPRangeSpec, HealOperations, ListMultipartsInfo, ListObjectVersionsInfo, ListObjectsV2Info,
|
||||
ListOperations, MakeBucketOptions, MultipartInfo, MultipartOperations, MultipartUploadResult, ObjectIO, ObjectInfo,
|
||||
ObjectOperations, ObjectOptions, ObjectToDelete, PartInfo, StorageAPI, WalkOptions,
|
||||
};
|
||||
use http::HeaderMap;
|
||||
use rustfs_config::audit::{AUDIT_MQTT_SUB_SYS, AUDIT_WEBHOOK_SUB_SYS};
|
||||
@@ -1304,7 +1302,7 @@ mod tests {
|
||||
&self,
|
||||
_bucket: &str,
|
||||
_object: &str,
|
||||
_data: &mut PutObjReader,
|
||||
_data: &mut ChunkNativePutData,
|
||||
_opts: &ObjectOptions,
|
||||
) -> Result<ObjectInfo> {
|
||||
panic!("unused in test")
|
||||
@@ -1491,7 +1489,7 @@ mod tests {
|
||||
_object: &str,
|
||||
_upload_id: &str,
|
||||
_part_id: usize,
|
||||
_data: &mut PutObjReader,
|
||||
_data: &mut ChunkNativePutData,
|
||||
_opts: &ObjectOptions,
|
||||
) -> Result<PartInfo> {
|
||||
panic!("unused in test")
|
||||
|
||||
@@ -150,18 +150,7 @@ impl Config {
|
||||
return false;
|
||||
}
|
||||
|
||||
let shard_size = shard_size as usize;
|
||||
|
||||
let mut inline_block = DEFAULT_INLINE_BLOCK;
|
||||
if self.initialized {
|
||||
inline_block = self.inline_block;
|
||||
}
|
||||
|
||||
if versioned {
|
||||
shard_size <= inline_block / 8
|
||||
} else {
|
||||
shard_size <= inline_block
|
||||
}
|
||||
shard_size as usize <= self.inline_shard_limit_bytes(versioned)
|
||||
}
|
||||
|
||||
pub fn inline_block(&self) -> usize {
|
||||
@@ -172,6 +161,15 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inline_shard_limit_bytes(&self, versioned: bool) -> usize {
|
||||
let inline_block = self.inline_block();
|
||||
if versioned { inline_block / 8 } else { inline_block }
|
||||
}
|
||||
|
||||
pub fn inline_object_limit_bytes(&self, data_shards: usize, versioned: bool) -> usize {
|
||||
self.inline_shard_limit_bytes(versioned).saturating_mul(data_shards.max(1))
|
||||
}
|
||||
|
||||
pub fn capacity_optimized(&self) -> bool {
|
||||
if !self.initialized {
|
||||
false
|
||||
@@ -336,3 +334,32 @@ pub fn validate_parity_inner(ss_parity: usize, rrs_parity: usize, set_drive_coun
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn inline_object_limit_matches_default_non_versioned_budget() {
|
||||
let cfg = Config {
|
||||
initialized: true,
|
||||
inline_block: DEFAULT_INLINE_BLOCK,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(cfg.inline_shard_limit_bytes(false), DEFAULT_INLINE_BLOCK);
|
||||
assert_eq!(cfg.inline_object_limit_bytes(8, false), DEFAULT_INLINE_BLOCK * 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_object_limit_scales_down_for_versioned_objects() {
|
||||
let cfg = Config {
|
||||
initialized: true,
|
||||
inline_block: DEFAULT_INLINE_BLOCK,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(cfg.inline_shard_limit_bytes(true), DEFAULT_INLINE_BLOCK / 8);
|
||||
assert_eq!(cfg.inline_object_limit_bytes(8, true), DEFAULT_INLINE_BLOCK);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,11 @@
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::store::ECStore;
|
||||
use crate::store_api::{CompletePart, GetObjectReader, MultipartOperations, ObjectIO, ObjectInfo, ObjectOptions, PutObjReader};
|
||||
use crate::store_api::{
|
||||
ChunkNativePutData, CompletePart, GetObjectReader, MultipartOperations, ObjectIO, ObjectInfo, ObjectOptions,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use rustfs_rio::{EtagResolvable, HashReader, HashReaderDetector, Index, TryGetIndex};
|
||||
use rustfs_rio::{BlockReadable, BoxReadBlockFuture, EtagResolvable, HashReader, HashReaderDetector, Index, TryGetIndex};
|
||||
use std::io::Cursor;
|
||||
use std::pin::Pin;
|
||||
use std::sync::{
|
||||
@@ -54,6 +56,11 @@ impl<R: AsyncRead + Unpin + Send + Sync> TryGetIndex for IndexedDataMovementRead
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: AsyncRead + Unpin + Send + Sync> BlockReadable for IndexedDataMovementReader<R> {
|
||||
fn read_block<'a>(&'a mut self, buf: &'a mut [u8]) -> BoxReadBlockFuture<'a> {
|
||||
Box::pin(rustfs_utils::read_full(self, buf))
|
||||
}
|
||||
}
|
||||
pub fn decode_part_index(index: Option<&Bytes>) -> Option<Index> {
|
||||
let bytes = index?;
|
||||
let mut decoded = Index::new();
|
||||
@@ -64,7 +71,7 @@ pub fn decode_part_index(index: Option<&Bytes>) -> Option<Index> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn put_obj_reader_from_chunk(chunk: Vec<u8>, size: i64, actual_size: i64, index: Option<Index>) -> Result<PutObjReader> {
|
||||
pub fn put_data_from_chunk(chunk: Vec<u8>, size: i64, actual_size: i64, index: Option<Index>) -> Result<ChunkNativePutData> {
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
let sha256hex = if !chunk.is_empty() {
|
||||
@@ -74,8 +81,8 @@ pub fn put_obj_reader_from_chunk(chunk: Vec<u8>, size: i64, actual_size: i64, in
|
||||
};
|
||||
|
||||
let reader = IndexedDataMovementReader::new(Cursor::new(chunk), index);
|
||||
let hash_reader = HashReader::from_stream(reader, size, actual_size, None, sha256hex, false)?;
|
||||
Ok(PutObjReader::new(hash_reader))
|
||||
let hash_reader = HashReader::from_reader(reader, size, actual_size, None, sha256hex, false)?;
|
||||
Ok(ChunkNativePutData::new(hash_reader))
|
||||
}
|
||||
|
||||
pub fn new_multipart_abort_flag() -> Arc<AtomicBool> {
|
||||
@@ -172,7 +179,7 @@ pub(crate) async fn migrate_object(
|
||||
let part_size = i64::try_from(part.size).map_err(|_| Error::other("part size overflow"))?;
|
||||
let part_actual_size = if part.actual_size > 0 { part.actual_size } else { part_size };
|
||||
let index = decode_part_index(part.index.as_ref());
|
||||
let mut data = put_obj_reader_from_chunk(chunk, part_size, part_actual_size, index)?;
|
||||
let mut data = put_data_from_chunk(chunk, part_size, part_actual_size, index)?;
|
||||
|
||||
let pi = match store
|
||||
.put_object_part(
|
||||
@@ -254,8 +261,8 @@ pub(crate) async fn migrate_object(
|
||||
.first()
|
||||
.and_then(|part| decode_part_index(part.index.as_ref()));
|
||||
let reader = IndexedDataMovementReader::new(BufReader::new(rd.stream), index);
|
||||
let hrd = HashReader::from_stream(reader, object_info.size, actual_size, object_info.etag.clone(), None, false)?;
|
||||
let mut data = PutObjReader::new(hrd);
|
||||
let hrd = HashReader::from_reader(reader, object_info.size, actual_size, object_info.etag.clone(), None, false)?;
|
||||
let mut data = ChunkNativePutData::new(hrd);
|
||||
|
||||
if let Err(err) = store
|
||||
.put_object(
|
||||
|
||||
@@ -21,6 +21,7 @@ use crate::disk::{
|
||||
use crate::global::GLOBAL_LOCAL_DISK_ID_MAP;
|
||||
use bytes::Bytes;
|
||||
use rustfs_filemeta::{FileInfo, ObjectPartInfo, RawFileInfo};
|
||||
use rustfs_io_core::BoxChunkStream;
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
@@ -738,6 +739,14 @@ impl DiskAPI for LocalDiskWrapper {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn read_file_chunks(&self, volume: &str, path: &str, offset: usize, length: usize) -> Result<BoxChunkStream> {
|
||||
self.track_disk_health(
|
||||
|| async { self.disk.read_file_chunks(volume, path, offset, length).await },
|
||||
get_max_timeout_duration(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn append_file(&self, volume: &str, path: &str) -> Result<crate::disk::FileWriter> {
|
||||
self.track_disk_health(|| async { self.disk.append_file(volume, path).await }, Duration::ZERO)
|
||||
.await
|
||||
|
||||
@@ -30,12 +30,19 @@ use crate::disk::{
|
||||
};
|
||||
use crate::erasure_coding::bitrot_verify;
|
||||
use crate::global::{GLOBAL_IsErasureSD, GLOBAL_RootDiskThreshold};
|
||||
use bytes::Bytes;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures_util::{StreamExt, stream};
|
||||
use parking_lot::RwLock as ParkingLotRwLock;
|
||||
use rustfs_config::{
|
||||
DEFAULT_OBJECT_ZERO_COPY_ENABLE, DEFAULT_OBJECT_ZERO_COPY_MAX_ACTIVE_MMAP_BYTES, DEFAULT_OBJECT_ZERO_COPY_MMAP_WINDOW_BYTES,
|
||||
DEFAULT_OBJECT_ZERO_COPY_MODE, ENV_OBJECT_ZERO_COPY_ENABLE, ENV_OBJECT_ZERO_COPY_MAX_ACTIVE_MMAP_BYTES,
|
||||
ENV_OBJECT_ZERO_COPY_MMAP_WINDOW_BYTES, ENV_OBJECT_ZERO_COPY_MODE,
|
||||
};
|
||||
use rustfs_filemeta::{
|
||||
Cache, FileInfo, FileInfoOpts, FileMeta, MetaCacheEntry, MetacacheWriter, ObjectPartInfo, Opts, RawFileInfo, UpdateFn,
|
||||
get_file_info, read_xl_meta_no_data,
|
||||
};
|
||||
use rustfs_io_core::{BoxChunkStream, BytesPool, IoChunk, MappedChunk, PooledChunk};
|
||||
use rustfs_utils::HashAlgorithm;
|
||||
use rustfs_utils::os::get_info;
|
||||
use rustfs_utils::path::{
|
||||
@@ -46,7 +53,7 @@ use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Debug;
|
||||
use std::io::SeekFrom;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
@@ -61,6 +68,11 @@ use tokio::time::interval;
|
||||
use tracing::{debug, error, info, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[cfg(test)]
|
||||
use serial_test::serial;
|
||||
#[cfg(test)]
|
||||
use temp_env::with_var;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FormatInfo {
|
||||
pub id: Option<Uuid>,
|
||||
@@ -97,6 +109,410 @@ pub struct LocalDisk {
|
||||
exit_signal: Option<tokio::sync::broadcast::Sender<()>>,
|
||||
}
|
||||
|
||||
const LOCAL_CHUNK_FAST_PATH_MIN_BYTES: usize = 64 * 1024;
|
||||
const LOCAL_DISK_POOLED_SOURCE_FALLBACK: &str = "fallback";
|
||||
const LOCAL_DISK_POOLED_SOURCE_COMPAT_COLLECT: &str = "compat_collect";
|
||||
const LOCAL_DISK_POOLED_SOURCE_COMPAT_DIRECT: &str = "compat_direct";
|
||||
const ACTIVE_MMAP_WINDOW_BUDGET_EXCEEDED_MESSAGE: &str = "active mmap window budget exceeded";
|
||||
|
||||
#[cfg(unix)]
|
||||
const LOCAL_CHUNK_COMPAT_MAX_MAPPED_WINDOWS: usize = 1;
|
||||
|
||||
static LOCAL_CHUNK_FALLBACK_POOL: OnceLock<BytesPool> = OnceLock::new();
|
||||
|
||||
fn local_chunk_fallback_pool() -> &'static BytesPool {
|
||||
LOCAL_CHUNK_FALLBACK_POOL.get_or_init(BytesPool::new_tiered)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum LocalChunkZeroCopyMode {
|
||||
Off,
|
||||
Conservative,
|
||||
Balanced,
|
||||
Aggressive,
|
||||
}
|
||||
|
||||
impl LocalChunkZeroCopyMode {
|
||||
fn from_env() -> Self {
|
||||
match rustfs_utils::get_env_str(ENV_OBJECT_ZERO_COPY_MODE, DEFAULT_OBJECT_ZERO_COPY_MODE)
|
||||
.trim()
|
||||
.to_ascii_lowercase()
|
||||
.as_str()
|
||||
{
|
||||
"off" => Self::Off,
|
||||
"conservative" => Self::Conservative,
|
||||
"aggressive" => Self::Aggressive,
|
||||
_ => Self::Balanced,
|
||||
}
|
||||
}
|
||||
|
||||
fn effective() -> Self {
|
||||
if !rustfs_utils::get_env_bool(ENV_OBJECT_ZERO_COPY_ENABLE, DEFAULT_OBJECT_ZERO_COPY_ENABLE) {
|
||||
return Self::Off;
|
||||
}
|
||||
|
||||
Self::from_env()
|
||||
}
|
||||
|
||||
const fn fast_path_min_bytes(self) -> usize {
|
||||
match self {
|
||||
Self::Aggressive => 1,
|
||||
Self::Off | Self::Conservative | Self::Balanced => LOCAL_CHUNK_FAST_PATH_MIN_BYTES,
|
||||
}
|
||||
}
|
||||
|
||||
const fn allows_multi_window(self) -> bool {
|
||||
matches!(self, Self::Balanced | Self::Aggressive)
|
||||
}
|
||||
|
||||
const fn is_disabled(self) -> bool {
|
||||
matches!(self, Self::Off)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
static ACTIVE_LOCAL_MMAP_BYTES: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[cfg(unix)]
|
||||
#[derive(Debug)]
|
||||
struct ActiveMmapWindow {
|
||||
mmap: memmap2::Mmap,
|
||||
accounted_len: usize,
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl AsRef<[u8]> for ActiveMmapWindow {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.mmap[..]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl Drop for ActiveMmapWindow {
|
||||
fn drop(&mut self) {
|
||||
let remaining = ACTIVE_LOCAL_MMAP_BYTES
|
||||
.fetch_sub(self.accounted_len, Ordering::AcqRel)
|
||||
.saturating_sub(self.accounted_len);
|
||||
rustfs_io_metrics::record_local_disk_active_mmap_bytes(remaining);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[allow(unsafe_code)]
|
||||
fn mmap_page_size() -> usize {
|
||||
static PAGE_SIZE: OnceLock<usize> = OnceLock::new();
|
||||
|
||||
*PAGE_SIZE.get_or_init(|| {
|
||||
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
|
||||
if page_size <= 0 { 4096 } else { page_size as usize }
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn configured_local_chunk_window_bytes() -> usize {
|
||||
let page_size = mmap_page_size();
|
||||
rustfs_utils::get_env_usize(ENV_OBJECT_ZERO_COPY_MMAP_WINDOW_BYTES, DEFAULT_OBJECT_ZERO_COPY_MMAP_WINDOW_BYTES)
|
||||
.max(page_size)
|
||||
.div_ceil(page_size)
|
||||
* page_size
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn configured_local_chunk_max_active_mmap_bytes() -> usize {
|
||||
rustfs_utils::get_env_usize(ENV_OBJECT_ZERO_COPY_MAX_ACTIVE_MMAP_BYTES, DEFAULT_OBJECT_ZERO_COPY_MAX_ACTIVE_MMAP_BYTES)
|
||||
.max(configured_local_chunk_window_bytes())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn should_prefer_pooled_zero_copy_compat(mode: LocalChunkZeroCopyMode, length: usize, window_bytes: usize) -> bool {
|
||||
if mode.is_disabled() || !mode.allows_multi_window() || length < mode.fast_path_min_bytes() || window_bytes == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
length.div_ceil(window_bytes) > LOCAL_CHUNK_COMPAT_MAX_MAPPED_WINDOWS
|
||||
}
|
||||
|
||||
fn fallback_reason_for_local_mmap_error(err: &DiskError) -> rustfs_io_metrics::FallbackReason {
|
||||
match err {
|
||||
DiskError::Io(io_error) if io_error.to_string().contains(ACTIVE_MMAP_WINDOW_BUDGET_EXCEEDED_MESSAGE) => {
|
||||
rustfs_io_metrics::FallbackReason::WindowLimitExceeded
|
||||
}
|
||||
_ => rustfs_io_metrics::FallbackReason::MmapUnavailable,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn try_reserve_active_mmap_bytes(accounted_len: usize, max_active_bytes: usize) -> bool {
|
||||
loop {
|
||||
let current = ACTIVE_LOCAL_MMAP_BYTES.load(Ordering::Acquire);
|
||||
let Some(next) = current.checked_add(accounted_len) else {
|
||||
return false;
|
||||
};
|
||||
if next > max_active_bytes {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ACTIVE_LOCAL_MMAP_BYTES
|
||||
.compare_exchange_weak(current, next, Ordering::AcqRel, Ordering::Acquire)
|
||||
.is_ok()
|
||||
{
|
||||
rustfs_io_metrics::record_local_disk_active_mmap_bytes(next);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[allow(unsafe_code)]
|
||||
fn map_file_region_bytes(file_path: &Path, offset: usize, length: usize, max_active_bytes: usize) -> Result<Bytes> {
|
||||
use memmap2::MmapOptions;
|
||||
|
||||
let aligned_offset = offset / mmap_page_size() * mmap_page_size();
|
||||
let logical_offset = offset - aligned_offset;
|
||||
let map_length = logical_offset.checked_add(length).ok_or(DiskError::FileCorrupt)?;
|
||||
let visible_end = logical_offset.checked_add(length).ok_or(DiskError::FileCorrupt)?;
|
||||
if !try_reserve_active_mmap_bytes(map_length, max_active_bytes) {
|
||||
return Err(DiskError::other(ACTIVE_MMAP_WINDOW_BUDGET_EXCEEDED_MESSAGE));
|
||||
}
|
||||
let file = std::fs::File::open(file_path).map_err(DiskError::from)?;
|
||||
|
||||
let mmap_result =
|
||||
unsafe { MmapOptions::new().offset(aligned_offset as u64).len(map_length).map(&file) }.map_err(DiskError::other);
|
||||
let mmap = match mmap_result {
|
||||
Ok(mmap) => mmap,
|
||||
Err(err) => {
|
||||
let remaining = ACTIVE_LOCAL_MMAP_BYTES
|
||||
.fetch_sub(map_length, Ordering::AcqRel)
|
||||
.saturating_sub(map_length);
|
||||
rustfs_io_metrics::record_local_disk_active_mmap_bytes(remaining);
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
let bytes = Bytes::from_owner(ActiveMmapWindow {
|
||||
mmap,
|
||||
accounted_len: map_length,
|
||||
});
|
||||
|
||||
Ok(bytes.slice(logical_offset..visible_end))
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[allow(unsafe_code)]
|
||||
fn map_file_region_chunk(file_path: &Path, offset: usize, length: usize, max_active_bytes: usize) -> Result<MappedChunk> {
|
||||
let bytes = map_file_region_bytes(file_path, offset, length, max_active_bytes)?;
|
||||
MappedChunk::new(bytes, 0, length).map_err(DiskError::other)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[derive(Debug)]
|
||||
struct LocalMappedChunkStreamState {
|
||||
file_path: PathBuf,
|
||||
next_offset: usize,
|
||||
remaining: usize,
|
||||
window_bytes: usize,
|
||||
}
|
||||
|
||||
async fn read_file_pooled_chunk_from_path(file_path: PathBuf, offset: usize, length: usize) -> std::io::Result<IoChunk> {
|
||||
read_file_pooled_chunk_from_path_with_source(file_path, offset, length, LOCAL_DISK_POOLED_SOURCE_FALLBACK).await
|
||||
}
|
||||
|
||||
async fn read_file_pooled_chunk_from_path_with_source(
|
||||
file_path: PathBuf,
|
||||
offset: usize,
|
||||
length: usize,
|
||||
metric_source: &'static str,
|
||||
) -> std::io::Result<IoChunk> {
|
||||
let mut file = File::open(file_path).await?;
|
||||
if offset > 0 {
|
||||
file.seek(SeekFrom::Start(offset as u64)).await?;
|
||||
}
|
||||
|
||||
let mut buffer = local_chunk_fallback_pool().acquire_buffer(length).await;
|
||||
buffer.resize(length, 0);
|
||||
file.read_exact(&mut buffer[..length]).await?;
|
||||
rustfs_io_metrics::record_local_disk_pooled_chunk(metric_source, length);
|
||||
Ok(IoChunk::Pooled(PooledChunk::new(buffer, length).map_err(std::io::Error::other)?))
|
||||
}
|
||||
|
||||
async fn prepare_read_file_request(disk: &LocalDisk, volume: &str, path: &str) -> Result<(PathBuf, PathBuf, Metadata)> {
|
||||
let volume_dir = disk.get_bucket_path(volume)?;
|
||||
if !skip_access_checks(volume) {
|
||||
access(&volume_dir)
|
||||
.await
|
||||
.map_err(|e| to_access_error(e, DiskError::VolumeAccessDenied))?;
|
||||
}
|
||||
|
||||
let file_path = disk.get_object_path(volume, path)?;
|
||||
check_path_length(file_path.to_string_lossy().as_ref())?;
|
||||
|
||||
let file_path_clone = file_path.clone();
|
||||
let meta = tokio::task::spawn_blocking(move || std::fs::metadata(&file_path_clone).map_err(DiskError::from))
|
||||
.await
|
||||
.map_err(DiskError::from)??;
|
||||
|
||||
Ok((volume_dir, file_path, meta))
|
||||
}
|
||||
|
||||
fn validate_read_file_bounds(meta: &Metadata, offset: usize, length: usize) -> Result<()> {
|
||||
let end_offset = offset.checked_add(length).ok_or(DiskError::FileCorrupt)?;
|
||||
if meta.len() < end_offset as u64 {
|
||||
error!(
|
||||
"read_file: file size is less than offset + length {} + {} = {}",
|
||||
offset,
|
||||
length,
|
||||
meta.len()
|
||||
);
|
||||
return Err(DiskError::FileCorrupt);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn build_lazy_mapped_chunk_stream(
|
||||
file_path: PathBuf,
|
||||
offset: usize,
|
||||
length: usize,
|
||||
window_bytes: usize,
|
||||
max_active_bytes: usize,
|
||||
) -> BoxChunkStream {
|
||||
let state = LocalMappedChunkStreamState {
|
||||
file_path,
|
||||
next_offset: offset,
|
||||
remaining: length,
|
||||
window_bytes,
|
||||
};
|
||||
|
||||
Box::pin(stream::unfold(Some(state), move |state| async move {
|
||||
let mut state = match state {
|
||||
Some(state) => state,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
if state.remaining == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let visible_len = state.remaining.min(state.window_bytes);
|
||||
let window_offset = state.next_offset;
|
||||
let file_path = state.file_path.clone();
|
||||
let mmap_result =
|
||||
tokio::task::spawn_blocking(move || map_file_region_chunk(&file_path, window_offset, visible_len, max_active_bytes))
|
||||
.await;
|
||||
|
||||
match mmap_result {
|
||||
Ok(Ok(chunk)) => {
|
||||
state.next_offset += visible_len;
|
||||
state.remaining -= visible_len;
|
||||
let next_state = if state.remaining == 0 { None } else { Some(state) };
|
||||
Some((Ok(IoChunk::Mapped(chunk)), next_state))
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
rustfs_io_metrics::record_io_fallback(
|
||||
rustfs_io_metrics::IoStage::LocalDiskChunk,
|
||||
fallback_reason_for_local_mmap_error(&err),
|
||||
);
|
||||
debug!(
|
||||
error = %err,
|
||||
offset = window_offset,
|
||||
len = visible_len,
|
||||
"local disk lazy mmap window failed, falling back to buffered remainder"
|
||||
);
|
||||
let fallback =
|
||||
read_file_pooled_chunk_from_path(state.file_path.clone(), state.next_offset, state.remaining).await;
|
||||
Some((fallback, None))
|
||||
}
|
||||
Err(err) => {
|
||||
rustfs_io_metrics::record_io_fallback(
|
||||
rustfs_io_metrics::IoStage::LocalDiskChunk,
|
||||
rustfs_io_metrics::FallbackReason::MmapUnavailable,
|
||||
);
|
||||
debug!(
|
||||
error = %err,
|
||||
offset = window_offset,
|
||||
len = visible_len,
|
||||
"local disk lazy mmap task failed, falling back to buffered remainder"
|
||||
);
|
||||
let fallback =
|
||||
read_file_pooled_chunk_from_path(state.file_path.clone(), state.next_offset, state.remaining).await;
|
||||
Some((fallback, None))
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
async fn read_file_pooled_chunk_fallback(
|
||||
disk: &LocalDisk,
|
||||
volume_dir: &Path,
|
||||
file_path: PathBuf,
|
||||
offset: usize,
|
||||
length: usize,
|
||||
) -> Result<IoChunk> {
|
||||
read_file_pooled_chunk_fallback_with_source(disk, volume_dir, file_path, offset, length, LOCAL_DISK_POOLED_SOURCE_FALLBACK)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn read_file_pooled_chunk_fallback_with_source(
|
||||
disk: &LocalDisk,
|
||||
volume_dir: &Path,
|
||||
file_path: PathBuf,
|
||||
offset: usize,
|
||||
length: usize,
|
||||
metric_source: &'static str,
|
||||
) -> Result<IoChunk> {
|
||||
let mut f = disk.open_file(file_path, O_RDONLY, volume_dir).await?;
|
||||
|
||||
if offset > 0 {
|
||||
f.seek(SeekFrom::Start(offset as u64)).await?;
|
||||
}
|
||||
|
||||
let mut buffer = local_chunk_fallback_pool().acquire_buffer(length).await;
|
||||
buffer.resize(length, 0);
|
||||
f.read_exact(&mut buffer[..length]).await?;
|
||||
rustfs_io_metrics::record_local_disk_pooled_chunk(metric_source, length);
|
||||
Ok(IoChunk::Pooled(PooledChunk::new(buffer, length).map_err(DiskError::other)?))
|
||||
}
|
||||
|
||||
async fn collect_chunk_stream_bytes(mut stream: BoxChunkStream, expected_len: usize) -> Result<Bytes> {
|
||||
let Some(first) = stream.next().await else {
|
||||
return Ok(Bytes::new());
|
||||
};
|
||||
let first = first.map_err(DiskError::from)?;
|
||||
let first_len = first.len();
|
||||
if matches!(first, IoChunk::Pooled(_)) {
|
||||
rustfs_io_metrics::record_local_disk_pooled_chunk(LOCAL_DISK_POOLED_SOURCE_COMPAT_COLLECT, first_len);
|
||||
}
|
||||
let first_bytes = first.as_bytes();
|
||||
|
||||
let Some(second) = stream.next().await else {
|
||||
return Ok(first_bytes);
|
||||
};
|
||||
let second = second.map_err(DiskError::from)?;
|
||||
let second_len = second.len();
|
||||
if matches!(second, IoChunk::Pooled(_)) {
|
||||
rustfs_io_metrics::record_local_disk_pooled_chunk(LOCAL_DISK_POOLED_SOURCE_COMPAT_COLLECT, second_len);
|
||||
}
|
||||
let mut chunk_count = 2usize;
|
||||
let mut total_bytes = first_len + second_len;
|
||||
let mut buffer = BytesMut::with_capacity(expected_len);
|
||||
buffer.extend_from_slice(first_bytes.as_ref());
|
||||
buffer.extend_from_slice(second.as_bytes().as_ref());
|
||||
|
||||
while let Some(chunk) = stream.next().await {
|
||||
let chunk = chunk.map_err(DiskError::from)?;
|
||||
let chunk_len = chunk.len();
|
||||
if matches!(chunk, IoChunk::Pooled(_)) {
|
||||
rustfs_io_metrics::record_local_disk_pooled_chunk(LOCAL_DISK_POOLED_SOURCE_COMPAT_COLLECT, chunk_len);
|
||||
}
|
||||
chunk_count += 1;
|
||||
total_bytes += chunk_len;
|
||||
buffer.extend_from_slice(chunk.as_bytes().as_ref());
|
||||
}
|
||||
|
||||
rustfs_io_metrics::record_local_disk_compat_collect(chunk_count, total_bytes);
|
||||
Ok(buffer.freeze())
|
||||
}
|
||||
|
||||
impl Drop for LocalDisk {
|
||||
fn drop(&mut self) {
|
||||
if let Some(exit_signal) = self.exit_signal.take() {
|
||||
@@ -1835,87 +2251,96 @@ impl DiskAPI for LocalDisk {
|
||||
use std::time::Instant;
|
||||
|
||||
let start = Instant::now();
|
||||
let volume_dir = self.get_bucket_path(volume)?;
|
||||
if !skip_access_checks(volume) {
|
||||
access(&volume_dir)
|
||||
.await
|
||||
.map_err(|e| to_access_error(e, DiskError::VolumeAccessDenied))?;
|
||||
}
|
||||
|
||||
let file_path = self.get_object_path(volume, path)?;
|
||||
check_path_length(file_path.to_string_lossy().as_ref())?;
|
||||
|
||||
// Verify file exists and get metadata
|
||||
let file_path_clone = file_path.clone();
|
||||
let meta = tokio::task::spawn_blocking(move || std::fs::metadata(&file_path_clone).map_err(DiskError::from))
|
||||
.await
|
||||
.map_err(DiskError::from)??;
|
||||
|
||||
let end_offset = offset.checked_add(length).ok_or(DiskError::FileCorrupt)?;
|
||||
if meta.len() < end_offset as u64 {
|
||||
error!(
|
||||
"read_file_zero_copy: file size is less than offset + length {} + {} = {}",
|
||||
offset,
|
||||
length,
|
||||
meta.len()
|
||||
);
|
||||
return Err(DiskError::FileCorrupt);
|
||||
}
|
||||
|
||||
// Unix: use mmap to read the data (copies into Bytes for safe ownership)
|
||||
// Non-Unix: fall back to efficient read
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use memmap2::MmapOptions;
|
||||
let file_path_clone = file_path.clone();
|
||||
let offset_u64 = offset as u64;
|
||||
|
||||
let bytes = tokio::task::spawn_blocking(move || {
|
||||
let file = std::fs::File::open(&file_path_clone).map_err(DiskError::from)?;
|
||||
|
||||
// Create memory map for the specified region
|
||||
// SAFETY: The file is opened as read-only, and we're mapping a region
|
||||
// that we've already verified exists and is within file bounds.
|
||||
let mmap = unsafe { MmapOptions::new().offset(offset_u64).len(length).map(&file) }.map_err(DiskError::other)?;
|
||||
|
||||
// Copy the mapped region into a Bytes buffer. This avoids undefined
|
||||
// behavior from treating OS-managed mmap memory as allocator-managed
|
||||
// Vec storage, at the cost of an extra copy.
|
||||
Ok::<Bytes, DiskError>(Bytes::copy_from_slice(&mmap))
|
||||
})
|
||||
.await
|
||||
.map_err(DiskError::from)??;
|
||||
|
||||
// Log successful mmap read metrics
|
||||
let duration_ms = start.elapsed().as_secs_f64() * 1000.0;
|
||||
|
||||
// Record mmap read metrics
|
||||
rustfs_io_metrics::record_zero_copy_read(length, duration_ms);
|
||||
|
||||
debug!(size = length, duration_ms = duration_ms, "mmap_read_success");
|
||||
|
||||
return Ok(bytes);
|
||||
let zero_copy_mode = LocalChunkZeroCopyMode::effective();
|
||||
let window_bytes = configured_local_chunk_window_bytes();
|
||||
if should_prefer_pooled_zero_copy_compat(zero_copy_mode, length, window_bytes) {
|
||||
let (volume_dir, file_path, meta) = prepare_read_file_request(self, volume, path).await?;
|
||||
validate_read_file_bounds(&meta, offset, length)?;
|
||||
let chunk = read_file_pooled_chunk_fallback_with_source(
|
||||
self,
|
||||
&volume_dir,
|
||||
file_path,
|
||||
offset,
|
||||
length,
|
||||
LOCAL_DISK_POOLED_SOURCE_COMPAT_DIRECT,
|
||||
)
|
||||
.await?;
|
||||
let bytes = collect_chunk_stream_bytes(Box::pin(stream::iter(vec![Ok(chunk)])), length).await?;
|
||||
debug!(
|
||||
size = bytes.len(),
|
||||
duration_ms = start.elapsed().as_secs_f64() * 1000.0,
|
||||
"chunk_compat_read_pooled_success"
|
||||
);
|
||||
return Ok(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
// Non-Unix fallback: efficient read into Bytes
|
||||
#[cfg(not(unix))]
|
||||
let bytes = collect_chunk_stream_bytes(self.read_file_chunks(volume, path, offset, length).await?, length).await?;
|
||||
debug!(
|
||||
size = bytes.len(),
|
||||
duration_ms = start.elapsed().as_secs_f64() * 1000.0,
|
||||
"chunk_compat_read_success"
|
||||
);
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
#[tracing::instrument(level = "debug", skip(self))]
|
||||
async fn read_file_chunks(&self, volume: &str, path: &str, offset: usize, length: usize) -> Result<BoxChunkStream> {
|
||||
let (volume_dir, file_path, meta) = prepare_read_file_request(self, volume, path).await?;
|
||||
validate_read_file_bounds(&meta, offset, length)?;
|
||||
|
||||
let zero_copy_mode = LocalChunkZeroCopyMode::effective();
|
||||
if zero_copy_mode.is_disabled() {
|
||||
rustfs_io_metrics::record_io_fallback(
|
||||
rustfs_io_metrics::IoStage::LocalDiskChunk,
|
||||
rustfs_io_metrics::FallbackReason::MmapDisabled,
|
||||
);
|
||||
let chunk = read_file_pooled_chunk_fallback(self, &volume_dir, file_path, offset, length).await?;
|
||||
return Ok(Box::pin(stream::iter(vec![Ok(chunk)])));
|
||||
}
|
||||
|
||||
if length < zero_copy_mode.fast_path_min_bytes() {
|
||||
rustfs_io_metrics::record_io_fallback(
|
||||
rustfs_io_metrics::IoStage::LocalDiskChunk,
|
||||
rustfs_io_metrics::FallbackReason::SmallObject,
|
||||
);
|
||||
let chunk = read_file_pooled_chunk_fallback(self, &volume_dir, file_path, offset, length).await?;
|
||||
return Ok(Box::pin(stream::iter(vec![Ok(chunk)])));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
// Record zero-copy fallback
|
||||
rustfs_io_metrics::record_zero_copy_fallback("non_unix_platform");
|
||||
let window_bytes = configured_local_chunk_window_bytes();
|
||||
|
||||
debug!(reason = "non_unix_platform", "zero_copy_fallback");
|
||||
|
||||
let mut f = self.open_file(file_path, O_RDONLY, volume_dir).await?;
|
||||
|
||||
if offset > 0 {
|
||||
f.seek(SeekFrom::Start(offset as u64)).await?;
|
||||
if !zero_copy_mode.allows_multi_window() && length > window_bytes {
|
||||
rustfs_io_metrics::record_io_fallback(
|
||||
rustfs_io_metrics::IoStage::LocalDiskChunk,
|
||||
rustfs_io_metrics::FallbackReason::WindowLimitExceeded,
|
||||
);
|
||||
let chunk = read_file_pooled_chunk_fallback(self, &volume_dir, file_path, offset, length).await?;
|
||||
return Ok(Box::pin(stream::iter(vec![Ok(chunk)])));
|
||||
}
|
||||
|
||||
let mut buffer = Vec::with_capacity(length);
|
||||
buffer.resize(length, 0);
|
||||
f.read_exact(&mut buffer).await?;
|
||||
return Ok(build_lazy_mapped_chunk_stream(
|
||||
file_path,
|
||||
offset,
|
||||
length,
|
||||
window_bytes,
|
||||
configured_local_chunk_max_active_mmap_bytes(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Bytes::from(buffer))
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
rustfs_io_metrics::record_io_fallback(
|
||||
rustfs_io_metrics::IoStage::LocalDiskChunk,
|
||||
rustfs_io_metrics::FallbackReason::MmapUnavailable,
|
||||
);
|
||||
let chunk = read_file_pooled_chunk_fallback(self, &volume_dir, file_path, offset, length).await?;
|
||||
Ok(Box::pin(stream::iter(vec![Ok(chunk)])))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2674,6 +3099,7 @@ async fn get_disk_info(drive_path: PathBuf) -> Result<(rustfs_utils::os::DiskInf
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use futures_util::StreamExt;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_skip_access_checks() {
|
||||
@@ -2960,6 +3386,22 @@ mod test {
|
||||
assert!(matches!(result, Err(DiskError::FileCorrupt)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_read_file_zero_copy_supports_non_zero_offset() {
|
||||
use tempfile::tempdir;
|
||||
|
||||
let dir = tempdir().unwrap();
|
||||
let endpoint = Endpoint::try_from(dir.path().to_str().unwrap()).unwrap();
|
||||
let disk = LocalDisk::new(&endpoint, false).await.unwrap();
|
||||
|
||||
disk.make_volume("test-volume").await.unwrap();
|
||||
let content = Bytes::from_static(b"0123456789abcdef");
|
||||
disk.write_all("test-volume", "test-file.txt", content.clone()).await.unwrap();
|
||||
|
||||
let result = disk.read_file_zero_copy("test-volume", "test-file.txt", 3, 7).await.unwrap();
|
||||
assert_eq!(result, Bytes::from_static(b"3456789"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_valid_volname() {
|
||||
// Valid volume names (length >= 3)
|
||||
@@ -3057,6 +3499,274 @@ mod test {
|
||||
let _ = fs::remove_file(test_file).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_read_file_chunks_returns_pooled_chunk_for_local_fallback() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let bucket = "chunk-bucket";
|
||||
let object = "obj.txt";
|
||||
let content = b"chunk-data";
|
||||
|
||||
fs::create_dir_all(dir.path().join(bucket)).await.unwrap();
|
||||
fs::write(dir.path().join(bucket).join(object), content).await.unwrap();
|
||||
|
||||
let endpoint = Endpoint::try_from(dir.path().to_str().unwrap()).unwrap();
|
||||
let disk = LocalDisk::new(&endpoint, false).await.unwrap();
|
||||
|
||||
let mut stream = disk.read_file_chunks(bucket, object, 0, content.len()).await.unwrap();
|
||||
let first = stream.next().await.unwrap().unwrap();
|
||||
assert!(matches!(first, IoChunk::Pooled(_)));
|
||||
assert_eq!(first.as_bytes(), Bytes::from_static(content));
|
||||
assert!(stream.next().await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_read_file_chunks_prefers_mapped_chunk_when_eligible() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let bucket = "chunk-bucket";
|
||||
let object = "obj-large.txt";
|
||||
let content = vec![7u8; LOCAL_CHUNK_FAST_PATH_MIN_BYTES];
|
||||
|
||||
fs::create_dir_all(dir.path().join(bucket)).await.unwrap();
|
||||
fs::write(dir.path().join(bucket).join(object), &content).await.unwrap();
|
||||
|
||||
let endpoint = Endpoint::try_from(dir.path().to_str().unwrap()).unwrap();
|
||||
let disk = LocalDisk::new(&endpoint, false).await.unwrap();
|
||||
|
||||
let mut stream = disk.read_file_chunks(bucket, object, 0, content.len()).await.unwrap();
|
||||
let first = stream.next().await.unwrap().unwrap();
|
||||
#[cfg(unix)]
|
||||
assert!(matches!(first, IoChunk::Mapped(_)));
|
||||
#[cfg(not(unix))]
|
||||
assert!(matches!(first, IoChunk::Pooled(_)));
|
||||
assert_eq!(first.as_bytes(), Bytes::from(content));
|
||||
assert!(stream.next().await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_read_file_chunks_falls_back_to_pooled_for_small_object() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let bucket = "chunk-bucket";
|
||||
let object = "obj-small.txt";
|
||||
let content = b"small-object";
|
||||
|
||||
fs::create_dir_all(dir.path().join(bucket)).await.unwrap();
|
||||
fs::write(dir.path().join(bucket).join(object), content).await.unwrap();
|
||||
|
||||
let endpoint = Endpoint::try_from(dir.path().to_str().unwrap()).unwrap();
|
||||
let disk = LocalDisk::new(&endpoint, false).await.unwrap();
|
||||
|
||||
let mut stream = disk.read_file_chunks(bucket, object, 0, content.len()).await.unwrap();
|
||||
let first = stream.next().await.unwrap().unwrap();
|
||||
assert!(matches!(first, IoChunk::Pooled(_)));
|
||||
assert_eq!(first.as_bytes(), Bytes::from_static(content));
|
||||
assert!(stream.next().await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_read_file_chunks_supports_non_zero_offset() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let bucket = "chunk-bucket";
|
||||
let object = "obj-offset.txt";
|
||||
let content = vec![3u8; LOCAL_CHUNK_FAST_PATH_MIN_BYTES + 16];
|
||||
|
||||
fs::create_dir_all(dir.path().join(bucket)).await.unwrap();
|
||||
fs::write(dir.path().join(bucket).join(object), &content).await.unwrap();
|
||||
|
||||
let endpoint = Endpoint::try_from(dir.path().to_str().unwrap()).unwrap();
|
||||
let disk = LocalDisk::new(&endpoint, false).await.unwrap();
|
||||
|
||||
let mut stream = disk
|
||||
.read_file_chunks(bucket, object, 1, LOCAL_CHUNK_FAST_PATH_MIN_BYTES)
|
||||
.await
|
||||
.unwrap();
|
||||
let first = stream.next().await.unwrap().unwrap();
|
||||
#[cfg(unix)]
|
||||
assert!(matches!(first, IoChunk::Mapped(_)));
|
||||
#[cfg(not(unix))]
|
||||
assert!(matches!(first, IoChunk::Pooled(_)));
|
||||
assert_eq!(first.as_bytes(), Bytes::copy_from_slice(&content[1..1 + LOCAL_CHUNK_FAST_PATH_MIN_BYTES]));
|
||||
assert!(stream.next().await.is_none());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_read_file_chunks_splits_large_reads_into_multiple_windows() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let bucket = "chunk-bucket";
|
||||
let object = "obj-windowed.txt";
|
||||
let content = vec![5u8; DEFAULT_OBJECT_ZERO_COPY_MMAP_WINDOW_BYTES + 32];
|
||||
|
||||
fs::create_dir_all(dir.path().join(bucket)).await.unwrap();
|
||||
fs::write(dir.path().join(bucket).join(object), &content).await.unwrap();
|
||||
|
||||
let endpoint = Endpoint::try_from(dir.path().to_str().unwrap()).unwrap();
|
||||
let disk = LocalDisk::new(&endpoint, false).await.unwrap();
|
||||
|
||||
let mut stream = disk.read_file_chunks(bucket, object, 0, content.len()).await.unwrap();
|
||||
let first = stream.next().await.unwrap().unwrap();
|
||||
let second = stream.next().await.unwrap().unwrap();
|
||||
|
||||
assert!(matches!(first, IoChunk::Mapped(_)));
|
||||
assert!(matches!(second, IoChunk::Mapped(_)));
|
||||
assert_eq!(first.len(), DEFAULT_OBJECT_ZERO_COPY_MMAP_WINDOW_BYTES);
|
||||
assert_eq!(second.len(), 32);
|
||||
assert_eq!(first.as_bytes(), Bytes::from(vec![5u8; DEFAULT_OBJECT_ZERO_COPY_MMAP_WINDOW_BYTES]));
|
||||
assert_eq!(second.as_bytes(), Bytes::from(vec![5u8; 32]));
|
||||
assert!(stream.next().await.is_none());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_read_file_zero_copy_collects_multi_window_chunk_stream() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let bucket = "chunk-bucket";
|
||||
let object = "obj-zero-copy-compat.txt";
|
||||
let content = vec![6u8; DEFAULT_OBJECT_ZERO_COPY_MMAP_WINDOW_BYTES + 48];
|
||||
|
||||
fs::create_dir_all(dir.path().join(bucket)).await.unwrap();
|
||||
fs::write(dir.path().join(bucket).join(object), &content).await.unwrap();
|
||||
|
||||
let endpoint = Endpoint::try_from(dir.path().to_str().unwrap()).unwrap();
|
||||
let disk = LocalDisk::new(&endpoint, false).await.unwrap();
|
||||
|
||||
let bytes = disk.read_file_zero_copy(bucket, object, 0, content.len()).await.unwrap();
|
||||
assert_eq!(bytes, Bytes::from(content));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_read_file_chunks_lazy_windows_reuse_single_window_budget() {
|
||||
let page_size = mmap_page_size();
|
||||
let window_bytes = page_size.to_string();
|
||||
let max_active_bytes = page_size.to_string();
|
||||
|
||||
with_var(ENV_OBJECT_ZERO_COPY_ENABLE, Some("true"), || {
|
||||
with_var(ENV_OBJECT_ZERO_COPY_MODE, Some("aggressive"), || {
|
||||
with_var(ENV_OBJECT_ZERO_COPY_MMAP_WINDOW_BYTES, Some(window_bytes.clone()), || {
|
||||
with_var(ENV_OBJECT_ZERO_COPY_MAX_ACTIVE_MMAP_BYTES, Some(max_active_bytes.clone()), || {
|
||||
ACTIVE_LOCAL_MMAP_BYTES.store(0, Ordering::Release);
|
||||
|
||||
let runtime = tokio::runtime::Runtime::new().unwrap();
|
||||
runtime.block_on(async {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let bucket = "chunk-bucket";
|
||||
let object = "obj-budgeted.txt";
|
||||
let content = vec![9u8; page_size * 2];
|
||||
|
||||
fs::create_dir_all(dir.path().join(bucket)).await.unwrap();
|
||||
fs::write(dir.path().join(bucket).join(object), &content).await.unwrap();
|
||||
|
||||
let endpoint = Endpoint::try_from(dir.path().to_str().unwrap()).unwrap();
|
||||
let disk = LocalDisk::new(&endpoint, false).await.unwrap();
|
||||
|
||||
let mut stream = disk.read_file_chunks(bucket, object, 0, content.len()).await.unwrap();
|
||||
let first = stream.next().await.unwrap().unwrap();
|
||||
assert!(matches!(first, IoChunk::Mapped(_)));
|
||||
assert_eq!(first.len(), page_size);
|
||||
assert_eq!(ACTIVE_LOCAL_MMAP_BYTES.load(Ordering::Acquire), page_size);
|
||||
|
||||
drop(first);
|
||||
assert_eq!(ACTIVE_LOCAL_MMAP_BYTES.load(Ordering::Acquire), 0);
|
||||
|
||||
let second = stream.next().await.unwrap().unwrap();
|
||||
assert!(matches!(second, IoChunk::Mapped(_)));
|
||||
assert_eq!(second.len(), page_size);
|
||||
assert_eq!(ACTIVE_LOCAL_MMAP_BYTES.load(Ordering::Acquire), page_size);
|
||||
|
||||
drop(second);
|
||||
assert_eq!(ACTIVE_LOCAL_MMAP_BYTES.load(Ordering::Acquire), 0);
|
||||
assert!(stream.next().await.is_none());
|
||||
});
|
||||
|
||||
assert_eq!(ACTIVE_LOCAL_MMAP_BYTES.load(Ordering::Acquire), 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_local_chunk_zero_copy_mode_respects_enable_and_mode_env() {
|
||||
with_var(ENV_OBJECT_ZERO_COPY_ENABLE, Some("false"), || {
|
||||
assert_eq!(LocalChunkZeroCopyMode::effective(), LocalChunkZeroCopyMode::Off);
|
||||
});
|
||||
|
||||
with_var(ENV_OBJECT_ZERO_COPY_ENABLE, Some("true"), || {
|
||||
with_var(ENV_OBJECT_ZERO_COPY_MODE, Some("aggressive"), || {
|
||||
assert_eq!(LocalChunkZeroCopyMode::effective(), LocalChunkZeroCopyMode::Aggressive);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_should_prefer_pooled_zero_copy_compat_for_multi_window_requests() {
|
||||
let window_bytes = 1024;
|
||||
let balanced_multi_window_len = LOCAL_CHUNK_FAST_PATH_MIN_BYTES.max(window_bytes * 2);
|
||||
|
||||
assert!(!should_prefer_pooled_zero_copy_compat(
|
||||
LocalChunkZeroCopyMode::Off,
|
||||
window_bytes * 2,
|
||||
window_bytes
|
||||
));
|
||||
assert!(!should_prefer_pooled_zero_copy_compat(
|
||||
LocalChunkZeroCopyMode::Conservative,
|
||||
window_bytes * 2,
|
||||
window_bytes
|
||||
));
|
||||
assert!(!should_prefer_pooled_zero_copy_compat(
|
||||
LocalChunkZeroCopyMode::Balanced,
|
||||
window_bytes,
|
||||
window_bytes
|
||||
));
|
||||
assert!(should_prefer_pooled_zero_copy_compat(
|
||||
LocalChunkZeroCopyMode::Balanced,
|
||||
balanced_multi_window_len,
|
||||
window_bytes
|
||||
));
|
||||
assert!(should_prefer_pooled_zero_copy_compat(
|
||||
LocalChunkZeroCopyMode::Aggressive,
|
||||
window_bytes * 2,
|
||||
window_bytes
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fallback_reason_for_local_mmap_error_distinguishes_budget_limit() {
|
||||
let budget_err = DiskError::other(ACTIVE_MMAP_WINDOW_BUDGET_EXCEEDED_MESSAGE);
|
||||
let generic_err = DiskError::other("mmap failed");
|
||||
|
||||
assert_eq!(
|
||||
fallback_reason_for_local_mmap_error(&budget_err),
|
||||
rustfs_io_metrics::FallbackReason::WindowLimitExceeded
|
||||
);
|
||||
assert_eq!(
|
||||
fallback_reason_for_local_mmap_error(&generic_err),
|
||||
rustfs_io_metrics::FallbackReason::MmapUnavailable
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_configured_local_chunk_window_bytes_aligns_to_page_size() {
|
||||
with_var(ENV_OBJECT_ZERO_COPY_MMAP_WINDOW_BYTES, Some("12345"), || {
|
||||
let page_size = mmap_page_size();
|
||||
let window_bytes = configured_local_chunk_window_bytes();
|
||||
assert!(window_bytes >= 12345);
|
||||
assert_eq!(window_bytes % page_size, 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_root_path() {
|
||||
// Unix root path
|
||||
|
||||
@@ -41,6 +41,7 @@ use error::DiskError;
|
||||
use error::{Error, Result};
|
||||
use local::LocalDisk;
|
||||
use rustfs_filemeta::{FileInfo, ObjectPartInfo, RawFileInfo};
|
||||
use rustfs_io_core::BoxChunkStream;
|
||||
use rustfs_madmin::info_commands::DiskMetrics;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt::Debug, path::PathBuf, sync::Arc};
|
||||
@@ -295,6 +296,14 @@ impl DiskAPI for Disk {
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
async fn read_file_chunks(&self, volume: &str, path: &str, offset: usize, length: usize) -> Result<BoxChunkStream> {
|
||||
match self {
|
||||
Disk::Local(local_disk) => local_disk.read_file_chunks(volume, path, offset, length).await,
|
||||
Disk::Remote(remote_disk) => remote_disk.read_file_chunks(volume, path, offset, length).await,
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
async fn append_file(&self, volume: &str, path: &str) -> Result<FileWriter> {
|
||||
match self {
|
||||
@@ -505,6 +514,9 @@ pub trait DiskAPI: Debug + Send + Sync + 'static {
|
||||
/// On other platforms, falls back to efficient read operations.
|
||||
async fn read_file_zero_copy(&self, volume: &str, path: &str, offset: usize, length: usize) -> Result<Bytes>;
|
||||
|
||||
/// Chunk-based file read compatibility layer for the zero-copy data plane.
|
||||
async fn read_file_chunks(&self, volume: &str, path: &str, offset: usize, length: usize) -> Result<BoxChunkStream>;
|
||||
|
||||
async fn append_file(&self, volume: &str, path: &str) -> Result<FileWriter>;
|
||||
async fn create_file(&self, origvolume: &str, volume: &str, path: &str, file_size: i64) -> Result<FileWriter>;
|
||||
// ReadFileStream
|
||||
|
||||
@@ -172,6 +172,52 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl BitrotWriter<CustomWriter> {
|
||||
fn write_inline_sync(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
if buf.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
if self.finished {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "bitrot writer already finished"));
|
||||
}
|
||||
|
||||
if buf.len() > self.shard_size {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("data size {} exceeds shard size {}", buf.len(), self.shard_size),
|
||||
));
|
||||
}
|
||||
|
||||
if buf.len() < self.shard_size {
|
||||
self.finished = true;
|
||||
}
|
||||
|
||||
match &mut self.inner {
|
||||
CustomWriter::InlineBuffer(data) => {
|
||||
if self.hash_algo.size() > 0 {
|
||||
let hash = self.hash_algo.hash_encode(buf);
|
||||
if hash.as_ref().is_empty() {
|
||||
error!("bitrot writer write hash error: hash is empty");
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "hash is empty"));
|
||||
}
|
||||
data.extend_from_slice(hash.as_ref());
|
||||
}
|
||||
data.extend_from_slice(buf);
|
||||
Ok(buf.len())
|
||||
}
|
||||
CustomWriter::Other(_) => Err(std::io::Error::other("inline sync write requires inline buffer writer")),
|
||||
}
|
||||
}
|
||||
|
||||
fn shutdown_inline_sync(&mut self) -> std::io::Result<()> {
|
||||
match self.inner {
|
||||
CustomWriter::InlineBuffer(_) => Ok(()),
|
||||
CustomWriter::Other(_) => Err(std::io::Error::other("inline sync shutdown requires inline buffer writer")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn write_all_vectored<W>(writer: &mut W, hash: &[u8], data: &[u8]) -> std::io::Result<()>
|
||||
where
|
||||
W: AsyncWrite + Unpin,
|
||||
@@ -280,6 +326,10 @@ impl CustomWriter {
|
||||
Self::Other(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_inline_buffer(&self) -> bool {
|
||||
matches!(self, Self::InlineBuffer(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for CustomWriter {
|
||||
@@ -397,6 +447,24 @@ impl BitrotWriterWrapper {
|
||||
self.bitrot_writer.shutdown().await
|
||||
}
|
||||
|
||||
pub fn is_inline_buffer(&self) -> bool {
|
||||
matches!(self.writer_type, WriterType::InlineBuffer)
|
||||
}
|
||||
|
||||
pub fn write_inline_sync(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
if !self.is_inline_buffer() {
|
||||
return Err(std::io::Error::other("inline sync write requires inline buffer writer"));
|
||||
}
|
||||
self.bitrot_writer.write_inline_sync(buf)
|
||||
}
|
||||
|
||||
pub fn shutdown_inline_sync(&mut self) -> std::io::Result<()> {
|
||||
if !self.is_inline_buffer() {
|
||||
return Err(std::io::Error::other("inline sync shutdown requires inline buffer writer"));
|
||||
}
|
||||
self.bitrot_writer.shutdown_inline_sync()
|
||||
}
|
||||
|
||||
/// Extract the inline buffer data, consuming the wrapper
|
||||
pub fn into_inline_data(self) -> Option<Vec<u8>> {
|
||||
match self.writer_type {
|
||||
|
||||
@@ -17,6 +17,7 @@ use crate::disk::error_reduce::reduce_errs;
|
||||
use crate::erasure_coding::{BitrotReader, Erasure};
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use pin_project_lite::pin_project;
|
||||
use rustfs_io_core::{IoChunk, PooledChunk};
|
||||
use std::io;
|
||||
use std::io::ErrorKind;
|
||||
use tokio::io::AsyncRead;
|
||||
@@ -155,6 +156,68 @@ fn get_data_block_len(shards: &[Option<Vec<u8>>], data_blocks: usize) -> usize {
|
||||
size
|
||||
}
|
||||
|
||||
fn block_window(
|
||||
offset: usize,
|
||||
length: usize,
|
||||
block_size: usize,
|
||||
block_index: usize,
|
||||
start_block: usize,
|
||||
end_block: usize,
|
||||
) -> (usize, usize) {
|
||||
let end_remainder = offset.saturating_add(length) % block_size;
|
||||
if start_block == end_block {
|
||||
(offset % block_size, length)
|
||||
} else if block_index == start_block {
|
||||
(offset % block_size, block_size - (offset % block_size))
|
||||
} else if block_index == end_block {
|
||||
(0, if end_remainder == 0 { block_size } else { end_remainder })
|
||||
} else {
|
||||
(0, block_size)
|
||||
}
|
||||
}
|
||||
|
||||
fn take_data_blocks_as_chunks(
|
||||
shards: &mut [Option<Vec<u8>>],
|
||||
data_blocks: usize,
|
||||
mut offset: usize,
|
||||
length: usize,
|
||||
) -> io::Result<Vec<IoChunk>> {
|
||||
if get_data_block_len(shards, data_blocks) < length {
|
||||
error!("take_data_blocks_as_chunks get_data_block_len < length");
|
||||
return Err(io::Error::new(ErrorKind::UnexpectedEof, "Not enough data blocks to write"));
|
||||
}
|
||||
|
||||
let mut chunks = Vec::new();
|
||||
let mut remaining = length;
|
||||
for block_op in shards.iter_mut().take(data_blocks) {
|
||||
let Some(block) = block_op.take() else {
|
||||
error!("take_data_blocks_as_chunks block_op.is_none()");
|
||||
return Err(io::Error::new(ErrorKind::UnexpectedEof, "Missing data block"));
|
||||
};
|
||||
|
||||
if offset >= block.len() {
|
||||
offset -= block.len();
|
||||
continue;
|
||||
}
|
||||
|
||||
let start = offset;
|
||||
offset = 0;
|
||||
let take = (block.len() - start).min(remaining);
|
||||
let chunk = if start == 0 && take == block.len() {
|
||||
IoChunk::Pooled(PooledChunk::from_vec(block))
|
||||
} else {
|
||||
IoChunk::Pooled(PooledChunk::from_vec(block).slice(start, take)?)
|
||||
};
|
||||
chunks.push(chunk);
|
||||
remaining -= take;
|
||||
if remaining == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(chunks)
|
||||
}
|
||||
|
||||
/// Write data blocks from encoded blocks to target, supporting offset and length
|
||||
async fn write_data_blocks<W>(
|
||||
writer: &mut W,
|
||||
@@ -213,6 +276,134 @@ where
|
||||
Ok(total_written)
|
||||
}
|
||||
|
||||
pub(crate) struct ErasureChunkDecoder<R> {
|
||||
erasure: Erasure,
|
||||
reader: ParallelReader<R>,
|
||||
offset: usize,
|
||||
length: usize,
|
||||
start_block: usize,
|
||||
end_block: usize,
|
||||
current_block: usize,
|
||||
written: usize,
|
||||
healable_error: Option<Error>,
|
||||
finished: bool,
|
||||
}
|
||||
|
||||
impl<R> ErasureChunkDecoder<R>
|
||||
where
|
||||
R: AsyncRead + Unpin + Send + Sync,
|
||||
{
|
||||
pub(crate) fn new(
|
||||
erasure: Erasure,
|
||||
readers: Vec<Option<BitrotReader<R>>>,
|
||||
offset: usize,
|
||||
length: usize,
|
||||
total_length: usize,
|
||||
) -> io::Result<Self> {
|
||||
if readers.len() != erasure.data_shards + erasure.parity_shards {
|
||||
return Err(io::Error::new(ErrorKind::InvalidInput, "Invalid number of readers"));
|
||||
}
|
||||
|
||||
let end_offset = offset
|
||||
.checked_add(length)
|
||||
.ok_or_else(|| io::Error::new(ErrorKind::InvalidInput, "offset + length exceeds total length"))?;
|
||||
if end_offset > total_length {
|
||||
return Err(io::Error::new(ErrorKind::InvalidInput, "offset + length exceeds total length"));
|
||||
}
|
||||
|
||||
let start_block = offset / erasure.block_size;
|
||||
let end_block = if length == 0 {
|
||||
start_block
|
||||
} else {
|
||||
end_offset.saturating_sub(1) / erasure.block_size
|
||||
};
|
||||
let reader = ParallelReader::new(readers, erasure.clone(), offset, total_length);
|
||||
|
||||
Ok(Self {
|
||||
erasure,
|
||||
reader,
|
||||
offset,
|
||||
length,
|
||||
start_block,
|
||||
end_block,
|
||||
current_block: start_block,
|
||||
written: 0,
|
||||
healable_error: None,
|
||||
finished: length == 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn next_chunks(&mut self) -> io::Result<Option<Vec<IoChunk>>> {
|
||||
if self.finished {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if self.current_block > self.end_block {
|
||||
self.finished = true;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let block_index = self.current_block;
|
||||
self.current_block += 1;
|
||||
|
||||
let (block_offset, block_length) = block_window(
|
||||
self.offset,
|
||||
self.length,
|
||||
self.erasure.block_size,
|
||||
block_index,
|
||||
self.start_block,
|
||||
self.end_block,
|
||||
);
|
||||
if block_length == 0 {
|
||||
self.finished = true;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let (mut shards, errs) = self.reader.read().await;
|
||||
|
||||
if self.healable_error.is_none()
|
||||
&& let (_, Some(err)) = reduce_errs(&errs, &[])
|
||||
&& (err == Error::FileNotFound || err == Error::FileCorrupt)
|
||||
{
|
||||
self.healable_error = Some(err);
|
||||
}
|
||||
|
||||
if !self.reader.can_decode(&shards) {
|
||||
self.finished = true;
|
||||
error!("reconstructed chunk decoder can_decode errs: {:?}", &errs);
|
||||
return Err(Error::ErasureReadQuorum.into());
|
||||
}
|
||||
|
||||
if let Err(err) = self.erasure.decode_data(&mut shards) {
|
||||
self.finished = true;
|
||||
error!("reconstructed chunk decoder decode_data err: {:?}", err);
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
let chunks = take_data_blocks_as_chunks(&mut shards, self.erasure.data_shards, block_offset, block_length)?;
|
||||
self.written += chunks.iter().map(IoChunk::len).sum::<usize>();
|
||||
Ok(Some(chunks))
|
||||
}
|
||||
|
||||
pub(crate) fn written(&self) -> usize {
|
||||
self.written
|
||||
}
|
||||
|
||||
pub(crate) fn finish_error(&self) -> Option<io::Error> {
|
||||
if self.written < self.length {
|
||||
Some(Error::LessData.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn take_healable_error(&mut self) -> Option<Error> {
|
||||
self.healable_error.take()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type ReconstructedChunkDecoder<R> = ErasureChunkDecoder<R>;
|
||||
|
||||
impl Erasure {
|
||||
pub async fn decode<W, R>(
|
||||
&self,
|
||||
@@ -230,7 +421,10 @@ impl Erasure {
|
||||
return (0, Some(io::Error::new(ErrorKind::InvalidInput, "Invalid number of readers")));
|
||||
}
|
||||
|
||||
if offset + length > total_length {
|
||||
let Some(end_offset) = offset.checked_add(length) else {
|
||||
return (0, Some(io::Error::new(ErrorKind::InvalidInput, "offset + length exceeds total length")));
|
||||
};
|
||||
if end_offset > total_length {
|
||||
return (0, Some(io::Error::new(ErrorKind::InvalidInput, "offset + length exceeds total length")));
|
||||
}
|
||||
|
||||
@@ -245,7 +439,7 @@ impl Erasure {
|
||||
let mut reader = ParallelReader::new(readers, self.clone(), offset, total_length);
|
||||
|
||||
let start = offset / self.block_size;
|
||||
let end = (offset + length) / self.block_size;
|
||||
let end = end_offset.saturating_sub(1) / self.block_size;
|
||||
|
||||
for i in start..=end {
|
||||
let (block_offset, block_length) = if start == end {
|
||||
@@ -253,7 +447,8 @@ impl Erasure {
|
||||
} else if i == start {
|
||||
(offset % self.block_size, self.block_size - (offset % self.block_size))
|
||||
} else if i == end {
|
||||
(0, (offset + length) % self.block_size)
|
||||
let end_remainder = end_offset % self.block_size;
|
||||
(0, if end_remainder == 0 { self.block_size } else { end_remainder })
|
||||
} else {
|
||||
(0, self.block_size)
|
||||
};
|
||||
@@ -316,6 +511,7 @@ mod tests {
|
||||
disk::error::DiskError,
|
||||
erasure_coding::{BitrotReader, BitrotWriter},
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use rustfs_utils::HashAlgorithm;
|
||||
use std::io::Cursor;
|
||||
|
||||
@@ -456,4 +652,47 @@ mod tests {
|
||||
let reader_cursor = Cursor::new(buf);
|
||||
BitrotReader::new(reader_cursor, shard_size, hash_algo.clone(), false)
|
||||
}
|
||||
|
||||
async fn create_bitrot_reader_from_shard(
|
||||
shard: Bytes,
|
||||
shard_size: usize,
|
||||
hash_algo: &HashAlgorithm,
|
||||
) -> BitrotReader<Cursor<Vec<u8>>> {
|
||||
let writer = Cursor::new(Vec::new());
|
||||
let mut writer = BitrotWriter::new(writer, shard_size, hash_algo.clone());
|
||||
writer.write(shard.as_ref()).await.unwrap();
|
||||
let reader_cursor = Cursor::new(writer.into_inner().into_inner());
|
||||
BitrotReader::new(reader_cursor, shard_size, hash_algo.clone(), false)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_erasure_chunk_decoder_reconstructs_missing_data_shard_as_pooled_chunks() {
|
||||
let erasure = Erasure::new(2, 1, 4);
|
||||
let original = b"abcd";
|
||||
let encoded = erasure.encode_data(original).unwrap();
|
||||
let shard_size = erasure.shard_size();
|
||||
let hash_algo = HashAlgorithm::None;
|
||||
|
||||
let readers = vec![
|
||||
None,
|
||||
Some(create_bitrot_reader_from_shard(encoded[1].clone(), shard_size, &hash_algo).await),
|
||||
Some(create_bitrot_reader_from_shard(encoded[2].clone(), shard_size, &hash_algo).await),
|
||||
];
|
||||
|
||||
let mut decoder = ErasureChunkDecoder::new(erasure, readers, 0, original.len(), original.len()).unwrap();
|
||||
let first_batch = decoder.next_chunks().await.unwrap().unwrap();
|
||||
assert!(
|
||||
first_batch.iter().all(|chunk| matches!(chunk, IoChunk::Pooled(_))),
|
||||
"reconstructed decoder should produce pooled chunks"
|
||||
);
|
||||
let collected = first_batch
|
||||
.into_iter()
|
||||
.flat_map(|chunk| chunk.as_bytes().to_vec())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(collected, original);
|
||||
assert!(decoder.next_chunks().await.unwrap().is_none());
|
||||
assert_eq!(decoder.written(), original.len());
|
||||
assert!(decoder.finish_error().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,12 @@ use crate::disk::error_reduce::count_errs;
|
||||
use crate::disk::error_reduce::{OBJECT_OP_IGNORED_ERRS, reduce_write_quorum_errs};
|
||||
use crate::erasure_coding::BitrotWriterWrapper;
|
||||
use crate::erasure_coding::Erasure;
|
||||
use crate::erasure_coding::erasure::{EncodeBlockBuffer, EncodedShardBlock, EncodedShardBufferPool};
|
||||
use bytes::Bytes;
|
||||
use futures::StreamExt;
|
||||
use futures::stream::FuturesUnordered;
|
||||
use rustfs_rio::BlockReadable;
|
||||
use std::sync::Arc;
|
||||
use std::vec;
|
||||
use tokio::io::AsyncRead;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::error;
|
||||
@@ -32,6 +33,164 @@ pub(crate) struct MultiWriter<'a> {
|
||||
errs: Vec<Option<Error>>,
|
||||
}
|
||||
|
||||
pub(crate) struct BlockAssembler<R> {
|
||||
reader: R,
|
||||
block_buffer: EncodeBlockBuffer,
|
||||
total_bytes: usize,
|
||||
}
|
||||
|
||||
impl<R> BlockAssembler<R>
|
||||
where
|
||||
R: AsyncRead + BlockReadable + Send + Sync + Unpin + 'static,
|
||||
{
|
||||
pub(crate) fn new(reader: R, block_size: usize) -> Self {
|
||||
Self {
|
||||
reader,
|
||||
block_buffer: EncodeBlockBuffer::new(block_size),
|
||||
total_bytes: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn next_block(&mut self) -> std::io::Result<Option<Vec<u8>>> {
|
||||
match self.block_buffer.read_from_block(&mut self.reader).await {
|
||||
Ok(n) if n > 0 => {
|
||||
self.total_bytes += n;
|
||||
Ok(Some(self.block_buffer.filled(n).to_vec()))
|
||||
}
|
||||
Ok(_) => Ok(None),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
|
||||
if let Some(inner) = e.get_ref()
|
||||
&& rustfs_rio::is_checksum_mismatch(inner)
|
||||
{
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn total_bytes(&self) -> usize {
|
||||
self.total_bytes
|
||||
}
|
||||
|
||||
pub(crate) fn into_inner(self) -> R {
|
||||
self.reader
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ErasureChunkEncoder {
|
||||
erasure: Arc<Erasure>,
|
||||
buffer_pool: EncodedShardBufferPool,
|
||||
}
|
||||
|
||||
impl ErasureChunkEncoder {
|
||||
pub(crate) async fn new(erasure: Arc<Erasure>) -> Self {
|
||||
let reusable_capacity = erasure.shard_size() * erasure.total_shard_count();
|
||||
Self {
|
||||
erasure,
|
||||
buffer_pool: EncodedShardBufferPool::with_prefill(reusable_capacity, 2).await,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn encode_block(&self, block: &[u8]) -> std::io::Result<EncodedShardBlock> {
|
||||
let reusable_buffer = self.buffer_pool.acquire().await;
|
||||
self.erasure.encode_data_block_with_buffer(block, reusable_buffer)
|
||||
}
|
||||
|
||||
pub(crate) async fn release(&self, block: EncodedShardBlock) {
|
||||
self.buffer_pool.release(block).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ErasureWritePipeline {
|
||||
erasure: Arc<Erasure>,
|
||||
write_quorum: usize,
|
||||
}
|
||||
|
||||
impl ErasureWritePipeline {
|
||||
pub(crate) fn new(erasure: Arc<Erasure>, write_quorum: usize) -> Self {
|
||||
Self { erasure, write_quorum }
|
||||
}
|
||||
|
||||
pub(crate) async fn run<R>(&self, reader: R, writers: &mut [Option<BitrotWriterWrapper>]) -> std::io::Result<(R, usize)>
|
||||
where
|
||||
R: AsyncRead + BlockReadable + Send + Sync + Unpin + 'static,
|
||||
{
|
||||
let (tx, mut rx) = mpsc::channel::<EncodedShardBlock>(8);
|
||||
let producer = ErasureChunkEncoder::new(self.erasure.clone()).await;
|
||||
let writer_pool = producer.clone();
|
||||
let block_size = self.erasure.block_size;
|
||||
|
||||
let task = tokio::spawn(async move {
|
||||
let mut assembler = BlockAssembler::new(reader, block_size);
|
||||
while let Some(block) = assembler.next_block().await? {
|
||||
let res = producer.encode_block(&block).await?;
|
||||
if let Err(err) = tx.send(res).await {
|
||||
return Err(std::io::Error::other(format!("Failed to send encoded data : {err}")));
|
||||
}
|
||||
}
|
||||
|
||||
let total = assembler.total_bytes();
|
||||
Ok((assembler.into_inner(), total))
|
||||
});
|
||||
|
||||
let mut writers = MultiWriter::new(writers, self.write_quorum);
|
||||
let mut write_err = None;
|
||||
|
||||
while let Some(block) = rx.recv().await {
|
||||
if block.is_empty() {
|
||||
break;
|
||||
}
|
||||
let write_result = writers.write(&block).await;
|
||||
writer_pool.release(block).await;
|
||||
if let Err(err) = write_result {
|
||||
write_err = Some(err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(err) = write_err {
|
||||
task.abort();
|
||||
let _ = task.await;
|
||||
if let Err(shutdown_err) = writers.shutdown().await {
|
||||
error!("failed to shutdown erasure writers after write error: {:?}", shutdown_err);
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
let (reader, total) = task.await??;
|
||||
writers.shutdown().await?;
|
||||
Ok((reader, total))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait ShardSource {
|
||||
fn shard_count(&self) -> usize;
|
||||
fn shard(&self, idx: usize) -> Bytes;
|
||||
}
|
||||
|
||||
impl ShardSource for EncodedShardBlock {
|
||||
fn shard_count(&self) -> usize {
|
||||
self.shard_count()
|
||||
}
|
||||
|
||||
fn shard(&self, idx: usize) -> Bytes {
|
||||
self.shard(idx)
|
||||
}
|
||||
}
|
||||
|
||||
impl ShardSource for Vec<Bytes> {
|
||||
fn shard_count(&self) -> usize {
|
||||
self.len()
|
||||
}
|
||||
|
||||
fn shard(&self, idx: usize) -> Bytes {
|
||||
self[idx].clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MultiWriter<'a> {
|
||||
pub fn new(writers: &'a mut [Option<BitrotWriterWrapper>], write_quorum: usize) -> Self {
|
||||
let length = writers.len();
|
||||
@@ -42,10 +201,10 @@ impl<'a> MultiWriter<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn write_shard(writer_opt: &mut Option<BitrotWriterWrapper>, err: &mut Option<Error>, shard: &Bytes) {
|
||||
async fn write_shard(writer_opt: &mut Option<BitrotWriterWrapper>, err: &mut Option<Error>, shard: Bytes) {
|
||||
match writer_opt {
|
||||
Some(writer) => {
|
||||
match writer.write(shard).await {
|
||||
match writer.write(&shard).await {
|
||||
Ok(n) => {
|
||||
if n < shard.len() {
|
||||
*err = Some(Error::ShortWrite);
|
||||
@@ -65,16 +224,40 @@ impl<'a> MultiWriter<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn write(&mut self, data: Vec<Bytes>) -> std::io::Result<()> {
|
||||
assert_eq!(data.len(), self.writers.len());
|
||||
fn write_shard_inline(writer_opt: &mut Option<BitrotWriterWrapper>, err: &mut Option<Error>, shard: Bytes) {
|
||||
match writer_opt {
|
||||
Some(writer) => match writer.write_inline_sync(&shard) {
|
||||
Ok(n) => {
|
||||
if n < shard.len() {
|
||||
*err = Some(Error::ShortWrite);
|
||||
*writer_opt = None;
|
||||
} else {
|
||||
*err = None;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
*err = Some(Error::from(e));
|
||||
}
|
||||
},
|
||||
None => {
|
||||
*err = Some(Error::DiskNotFound);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn write<T>(&mut self, data: &T) -> std::io::Result<()>
|
||||
where
|
||||
T: ShardSource,
|
||||
{
|
||||
assert_eq!(data.shard_count(), self.writers.len());
|
||||
|
||||
{
|
||||
let mut futures = FuturesUnordered::new();
|
||||
for ((writer_opt, err), shard) in self.writers.iter_mut().zip(self.errs.iter_mut()).zip(data.iter()) {
|
||||
for (idx, (writer_opt, err)) in self.writers.iter_mut().zip(self.errs.iter_mut()).enumerate() {
|
||||
if err.is_some() {
|
||||
continue; // Skip if we already have an error for this writer
|
||||
}
|
||||
futures.push(Self::write_shard(writer_opt, err, shard));
|
||||
futures.push(Self::write_shard(writer_opt, err, data.shard(idx)));
|
||||
}
|
||||
while let Some(()) = futures.next().await {}
|
||||
}
|
||||
@@ -112,6 +295,45 @@ impl<'a> MultiWriter<'a> {
|
||||
)))
|
||||
}
|
||||
|
||||
pub fn write_inline<T>(&mut self, data: &T) -> std::io::Result<()>
|
||||
where
|
||||
T: ShardSource,
|
||||
{
|
||||
assert_eq!(data.shard_count(), self.writers.len());
|
||||
|
||||
for (idx, (writer_opt, err)) in self.writers.iter_mut().zip(self.errs.iter_mut()).enumerate() {
|
||||
if err.is_some() {
|
||||
continue;
|
||||
}
|
||||
Self::write_shard_inline(writer_opt, err, data.shard(idx));
|
||||
}
|
||||
|
||||
let nil_count = self.errs.iter().filter(|&e| e.is_none()).count();
|
||||
if nil_count >= self.write_quorum {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(write_err) = reduce_write_quorum_errs(&self.errs, OBJECT_OP_IGNORED_ERRS, self.write_quorum) {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"Failed to write inline data: {} (offline-disks={}/{})",
|
||||
write_err,
|
||||
count_errs(&self.errs, &Error::DiskNotFound),
|
||||
self.writers.len()
|
||||
)));
|
||||
}
|
||||
|
||||
Err(std::io::Error::other(format!(
|
||||
"Failed to write inline data: (offline-disks={}/{}): {}",
|
||||
count_errs(&self.errs, &Error::DiskNotFound),
|
||||
self.writers.len(),
|
||||
self.errs
|
||||
.iter()
|
||||
.map(|e| e.as_ref().map_or("<nil>".to_string(), |e| e.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)))
|
||||
}
|
||||
|
||||
async fn shutdown_writer(writer_opt: &mut Option<BitrotWriterWrapper>, err: &mut Option<Error>) {
|
||||
match writer_opt {
|
||||
Some(writer) => match writer.shutdown().await {
|
||||
@@ -129,6 +351,23 @@ impl<'a> MultiWriter<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn shutdown_writer_inline(writer_opt: &mut Option<BitrotWriterWrapper>, err: &mut Option<Error>) {
|
||||
match writer_opt {
|
||||
Some(writer) => match writer.shutdown_inline_sync() {
|
||||
Ok(()) => {
|
||||
*err = None;
|
||||
}
|
||||
Err(e) => {
|
||||
*err = Some(Error::from(e));
|
||||
*writer_opt = None;
|
||||
}
|
||||
},
|
||||
None => {
|
||||
*err = Some(Error::DiskNotFound);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn shutdown(&mut self) -> std::io::Result<()> {
|
||||
{
|
||||
let mut futures = FuturesUnordered::new();
|
||||
@@ -173,66 +412,53 @@ impl<'a> MultiWriter<'a> {
|
||||
.join(", ")
|
||||
)))
|
||||
}
|
||||
|
||||
pub fn shutdown_inline(&mut self) -> std::io::Result<()> {
|
||||
for (writer_opt, err) in self.writers.iter_mut().zip(self.errs.iter_mut()) {
|
||||
if err.is_some() {
|
||||
continue;
|
||||
}
|
||||
Self::shutdown_writer_inline(writer_opt, err);
|
||||
}
|
||||
|
||||
let nil_count = self.errs.iter().filter(|&e| e.is_none()).count();
|
||||
if nil_count >= self.write_quorum {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(write_err) = reduce_write_quorum_errs(&self.errs, OBJECT_OP_IGNORED_ERRS, self.write_quorum) {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"Failed to shutdown inline writers: {} (offline-disks={}/{})",
|
||||
write_err,
|
||||
count_errs(&self.errs, &Error::DiskNotFound),
|
||||
self.writers.len()
|
||||
)));
|
||||
}
|
||||
|
||||
Err(std::io::Error::other(format!(
|
||||
"Failed to shutdown inline writers: (offline-disks={}/{}): {}",
|
||||
count_errs(&self.errs, &Error::DiskNotFound),
|
||||
self.writers.len(),
|
||||
self.errs
|
||||
.iter()
|
||||
.map(|e| e.as_ref().map_or("<nil>".to_string(), |e| e.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Erasure {
|
||||
pub async fn encode<R>(
|
||||
self: Arc<Self>,
|
||||
mut reader: R,
|
||||
reader: R,
|
||||
writers: &mut [Option<BitrotWriterWrapper>],
|
||||
quorum: usize,
|
||||
) -> std::io::Result<(R, usize)>
|
||||
where
|
||||
R: AsyncRead + Send + Sync + Unpin + 'static,
|
||||
R: AsyncRead + BlockReadable + Send + Sync + Unpin + 'static,
|
||||
{
|
||||
let (tx, mut rx) = mpsc::channel::<Vec<Bytes>>(8);
|
||||
|
||||
let task = tokio::spawn(async move {
|
||||
let block_size = self.block_size;
|
||||
let mut total = 0;
|
||||
let mut buf = vec![0u8; block_size];
|
||||
loop {
|
||||
match rustfs_utils::read_full(&mut reader, &mut buf).await {
|
||||
Ok(n) if n > 0 => {
|
||||
total += n;
|
||||
let res = self.encode_data(&buf[..n])?;
|
||||
if let Err(err) = tx.send(res).await {
|
||||
return Err(std::io::Error::other(format!("Failed to send encoded data : {err}")));
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
break;
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
|
||||
// Check if the inner error is a checksum mismatch - if so, propagate it
|
||||
if let Some(inner) = e.get_ref()
|
||||
&& rustfs_rio::is_checksum_mismatch(inner)
|
||||
{
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()));
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((reader, total))
|
||||
});
|
||||
|
||||
let mut writers = MultiWriter::new(writers, quorum);
|
||||
|
||||
while let Some(block) = rx.recv().await {
|
||||
if block.is_empty() {
|
||||
break;
|
||||
}
|
||||
writers.write(block).await?;
|
||||
}
|
||||
|
||||
let (reader, total) = task.await??;
|
||||
writers.shutdown().await?;
|
||||
Ok((reader, total))
|
||||
ErasureWritePipeline::new(self, quorum).run(reader, writers).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,6 +467,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::erasure_coding::{BitrotWriterWrapper, CustomWriter};
|
||||
use rustfs_utils::HashAlgorithm;
|
||||
use std::io::Cursor;
|
||||
use std::pin::Pin;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::task::{Context, Poll};
|
||||
@@ -296,4 +523,28 @@ mod tests {
|
||||
assert_eq!(written, b"small payload".len());
|
||||
assert!(!committed.lock().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn block_assembler_splits_input_into_erasure_blocks() {
|
||||
let reader = tokio::io::BufReader::new(Cursor::new(b"abcdefghijkl".to_vec()));
|
||||
let mut assembler = BlockAssembler::new(reader, 4);
|
||||
|
||||
assert_eq!(assembler.next_block().await.unwrap(), Some(b"abcd".to_vec()));
|
||||
assert_eq!(assembler.next_block().await.unwrap(), Some(b"efgh".to_vec()));
|
||||
assert_eq!(assembler.next_block().await.unwrap(), Some(b"ijkl".to_vec()));
|
||||
assert_eq!(assembler.next_block().await.unwrap(), None);
|
||||
assert_eq!(assembler.total_bytes(), 12);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn erasure_chunk_encoder_produces_full_shard_block() {
|
||||
let erasure = Arc::new(Erasure::new(2, 1, 4));
|
||||
let encoder = ErasureChunkEncoder::new(erasure.clone()).await;
|
||||
let block = encoder.encode_block(b"abcd").await.unwrap();
|
||||
|
||||
assert_eq!(block.shard_count(), 3);
|
||||
assert_eq!(block.shard(0).len(), erasure.shard_size());
|
||||
|
||||
encoder.release(block).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,12 +19,131 @@
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use reed_solomon_erasure::galois_8::ReedSolomon;
|
||||
use reed_solomon_simd;
|
||||
use rustfs_rio::BlockReadable;
|
||||
use smallvec::SmallVec;
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
use tokio::io::AsyncRead;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) struct EncodeBlockBuffer {
|
||||
buf: Vec<u8>,
|
||||
}
|
||||
|
||||
impl EncodeBlockBuffer {
|
||||
pub(crate) fn new(block_size: usize) -> Self {
|
||||
Self {
|
||||
buf: vec![0u8; block_size],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn read_from<R>(&mut self, reader: &mut R) -> io::Result<usize>
|
||||
where
|
||||
R: AsyncRead + Send + Sync + Unpin,
|
||||
{
|
||||
rustfs_utils::read_full(&mut *reader, &mut self.buf).await
|
||||
}
|
||||
|
||||
pub(crate) async fn read_from_block<R>(&mut self, reader: &mut R) -> io::Result<usize>
|
||||
where
|
||||
R: BlockReadable + Send + Sync + Unpin,
|
||||
{
|
||||
reader.read_block(&mut self.buf).await
|
||||
}
|
||||
|
||||
pub(crate) fn filled(&self, len: usize) -> &[u8] {
|
||||
&self.buf[..len]
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EncodedShardBlock {
|
||||
data: Bytes,
|
||||
shard_size: usize,
|
||||
shard_count: usize,
|
||||
}
|
||||
|
||||
impl EncodedShardBlock {
|
||||
pub(crate) fn new(data: Bytes, shard_size: usize, shard_count: usize) -> Self {
|
||||
Self {
|
||||
data,
|
||||
shard_size,
|
||||
shard_count,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shard_count(&self) -> usize {
|
||||
self.shard_count
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.shard_count
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.shard_count == 0
|
||||
}
|
||||
|
||||
pub fn shard(&self, idx: usize) -> Bytes {
|
||||
let start = idx * self.shard_size;
|
||||
let end = start + self.shard_size;
|
||||
self.data.slice(start..end)
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = Bytes> + '_ {
|
||||
(0..self.shard_count).map(|idx| self.shard(idx))
|
||||
}
|
||||
|
||||
pub fn into_vec(self) -> Vec<Bytes> {
|
||||
(0..self.shard_count).map(|idx| self.shard(idx)).collect()
|
||||
}
|
||||
|
||||
pub fn into_reusable_buffer(self) -> BytesMut {
|
||||
match self.data.try_into_mut() {
|
||||
Ok(mut buf) => {
|
||||
buf.clear();
|
||||
buf
|
||||
}
|
||||
Err(data) => BytesMut::with_capacity(data.len()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct EncodedShardBufferPool {
|
||||
capacity: usize,
|
||||
free: Arc<Mutex<Vec<BytesMut>>>,
|
||||
}
|
||||
|
||||
impl EncodedShardBufferPool {
|
||||
pub(crate) async fn with_prefill(capacity: usize, initial: usize) -> Self {
|
||||
let mut free = Vec::with_capacity(initial);
|
||||
for _ in 0..initial {
|
||||
free.push(BytesMut::with_capacity(capacity));
|
||||
}
|
||||
|
||||
Self {
|
||||
capacity,
|
||||
free: Arc::new(Mutex::new(free)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn acquire(&self) -> BytesMut {
|
||||
let mut free = self.free.lock().await;
|
||||
free.pop().unwrap_or_else(|| BytesMut::with_capacity(self.capacity))
|
||||
}
|
||||
|
||||
pub(crate) async fn release(&self, block: EncodedShardBlock) {
|
||||
let mut free = self.free.lock().await;
|
||||
let mut buf = block.into_reusable_buffer();
|
||||
if buf.capacity() < self.capacity {
|
||||
buf.reserve(self.capacity - buf.capacity());
|
||||
}
|
||||
free.push(buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// Legacy calc_shard_size formula: (block_size.div_ceil(data_shards) + 1) & !1
|
||||
/// Matches main branch and filemeta::ErasureInfo for old-version files.
|
||||
pub fn calc_shard_size_legacy(block_size: usize, data_shards: usize) -> usize {
|
||||
@@ -351,6 +470,25 @@ impl Erasure {
|
||||
/// A vector of encoded shards as `Bytes`.
|
||||
#[tracing::instrument(level = "debug", skip_all, fields(data_len=data.len()))]
|
||||
pub fn encode_data(&self, data: &[u8]) -> io::Result<Vec<Bytes>> {
|
||||
Ok(self.encode_data_block(data)?.into_vec())
|
||||
}
|
||||
|
||||
/// Encode one logical block into an `EncodedShardBlock` using a caller-provided backing buffer.
|
||||
///
|
||||
/// This is the explicit reuse-oriented variant for non-hot paths that want to
|
||||
/// thread a reusable `BytesMut` across multiple encode calls.
|
||||
#[tracing::instrument(level = "debug", skip_all, fields(data_len=data.len()))]
|
||||
pub fn encode_data_with_buffer(&self, data: &[u8], data_buffer: BytesMut) -> io::Result<EncodedShardBlock> {
|
||||
self.encode_data_block_with_buffer(data, data_buffer)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all, fields(data_len=data.len()))]
|
||||
pub(crate) fn encode_data_block(&self, data: &[u8]) -> io::Result<EncodedShardBlock> {
|
||||
self.encode_data_block_with_buffer(data, BytesMut::with_capacity(self.shard_size() * self.total_shard_count()))
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all, fields(data_len=data.len()))]
|
||||
pub(crate) fn encode_data_block_with_buffer(&self, data: &[u8], mut data_buffer: BytesMut) -> io::Result<EncodedShardBlock> {
|
||||
let shard_size_fn = if self.uses_legacy {
|
||||
calc_shard_size_legacy
|
||||
} else {
|
||||
@@ -359,7 +497,10 @@ impl Erasure {
|
||||
let per_shard_size = shard_size_fn(data.len(), self.data_shards);
|
||||
let need_total_size = per_shard_size * self.total_shard_count();
|
||||
|
||||
let mut data_buffer = BytesMut::with_capacity(need_total_size);
|
||||
data_buffer.clear();
|
||||
if data_buffer.capacity() < need_total_size {
|
||||
data_buffer.reserve(need_total_size - data_buffer.capacity());
|
||||
}
|
||||
data_buffer.extend_from_slice(data);
|
||||
data_buffer.resize(need_total_size, 0u8);
|
||||
|
||||
@@ -382,14 +523,7 @@ impl Erasure {
|
||||
}
|
||||
|
||||
// Zero-copy split, all shards reference data_buffer
|
||||
let mut data_buffer = data_buffer.freeze();
|
||||
let mut shards = Vec::with_capacity(self.total_shard_count());
|
||||
for _ in 0..self.total_shard_count() {
|
||||
let shard = data_buffer.split_to(per_shard_size);
|
||||
shards.push(shard);
|
||||
}
|
||||
|
||||
Ok(shards)
|
||||
Ok(EncodedShardBlock::new(data_buffer.freeze(), per_shard_size, self.total_shard_count()))
|
||||
}
|
||||
|
||||
/// Decode and reconstruct missing shards in-place.
|
||||
@@ -478,8 +612,8 @@ impl Erasure {
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `reader` - An async reader implementing AsyncRead + Send + Sync + Unpin
|
||||
/// * `mut on_block` - Async callback that receives encoded blocks and returns a Result
|
||||
/// * `F` - Callback type: FnMut(Result<Vec<Bytes>, std::io::Error>) -> Future<Output=Result<(), E>> + Send
|
||||
/// * `mut on_block` - Async callback that receives encoded blocks and returns the block for reuse
|
||||
/// * `F` - Callback type: FnMut(Result<EncodedShardBlock, std::io::Error>) -> Future<Output=Result<Option<EncodedShardBlock>, E>> + Send
|
||||
/// * `Fut` - Future type returned by the callback
|
||||
/// * `E` - Error type returned by the callback
|
||||
/// * `R` - Reader type implementing AsyncRead + Send + Sync + Unpin
|
||||
@@ -489,26 +623,31 @@ impl Erasure {
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns error if reading from reader fails or if callback returns error
|
||||
pub async fn encode_stream_callback_async<F, Fut, E, R>(
|
||||
pub(crate) async fn encode_stream_callback_async<F, Fut, E, R>(
|
||||
self: std::sync::Arc<Self>,
|
||||
reader: &mut R,
|
||||
mut on_block: F,
|
||||
) -> Result<usize, E>
|
||||
where
|
||||
R: AsyncRead + Send + Sync + Unpin,
|
||||
F: FnMut(std::io::Result<Vec<Bytes>>) -> Fut + Send,
|
||||
Fut: std::future::Future<Output = Result<(), E>> + Send,
|
||||
F: FnMut(std::io::Result<EncodedShardBlock>) -> Fut + Send,
|
||||
Fut: std::future::Future<Output = Result<Option<EncodedShardBlock>, E>> + Send,
|
||||
{
|
||||
let block_size = self.block_size;
|
||||
let mut total = 0;
|
||||
let mut block_buffer = EncodeBlockBuffer::new(block_size);
|
||||
let reusable_capacity = self.shard_size() * self.total_shard_count();
|
||||
let buffer_pool = EncodedShardBufferPool::with_prefill(reusable_capacity, 1).await;
|
||||
loop {
|
||||
let mut buf = vec![0u8; block_size];
|
||||
match rustfs_utils::read_full(&mut *reader, &mut buf).await {
|
||||
match block_buffer.read_from(&mut *reader).await {
|
||||
Ok(n) if n > 0 => {
|
||||
warn!("encode_stream_callback_async read n={}", n);
|
||||
total += n;
|
||||
let res = self.encode_data(&buf[..n]);
|
||||
on_block(res).await?
|
||||
let reusable_buffer = buffer_pool.acquire().await;
|
||||
let res = self.encode_data_block_with_buffer(block_buffer.filled(n), reusable_buffer);
|
||||
if let Some(block) = on_block(res).await? {
|
||||
buffer_pool.release(block).await;
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
warn!("encode_stream_callback_async read unexpected ok");
|
||||
@@ -520,11 +659,10 @@ impl Erasure {
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("encode_stream_callback_async read error={:?}", e);
|
||||
on_block(Err(e)).await?;
|
||||
let _ = on_block(Err(e)).await?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
buf.clear();
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
@@ -747,8 +885,8 @@ mod tests {
|
||||
let tx = tx.clone();
|
||||
async move {
|
||||
let shards = res.unwrap();
|
||||
tx.send(shards).await.unwrap();
|
||||
Ok(())
|
||||
tx.send(shards.iter().collect()).await.unwrap();
|
||||
Ok(Some(shards))
|
||||
}
|
||||
})
|
||||
.await
|
||||
@@ -760,6 +898,36 @@ mod tests {
|
||||
assert_eq!(collected_shards.len(), data_shards + parity_shards);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_data_with_buffer_supports_explicit_reuse() {
|
||||
let erasure = Erasure::new(4, 2, 1024);
|
||||
let reusable_capacity = erasure.shard_size() * erasure.total_shard_count();
|
||||
|
||||
let first_data = b"explicit reusable buffer path".repeat(32);
|
||||
let first_block = erasure
|
||||
.encode_data_with_buffer(&first_data, BytesMut::with_capacity(reusable_capacity))
|
||||
.expect("first encode should succeed");
|
||||
let reusable_buffer = first_block.into_reusable_buffer();
|
||||
assert!(reusable_buffer.capacity() >= reusable_capacity);
|
||||
|
||||
let second_data = b"second encode through same reusable buffer".repeat(24);
|
||||
let second_block = erasure
|
||||
.encode_data_with_buffer(&second_data, reusable_buffer)
|
||||
.expect("second encode should succeed");
|
||||
|
||||
let mut shards_opt: Vec<Option<Vec<u8>>> = second_block.iter().map(|shard| Some(shard.to_vec())).collect();
|
||||
shards_opt[1] = None;
|
||||
shards_opt[5] = None;
|
||||
erasure.decode_data(&mut shards_opt).expect("decode should succeed");
|
||||
|
||||
let mut recovered = Vec::new();
|
||||
for shard in shards_opt.iter().take(erasure.data_shards) {
|
||||
recovered.extend_from_slice(shard.as_ref().expect("data shard should exist after decode"));
|
||||
}
|
||||
recovered.truncate(second_data.len());
|
||||
assert_eq!(&recovered, &second_data);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_encode_stream_callback_async_channel_decode() {
|
||||
use std::io::Cursor;
|
||||
@@ -786,8 +954,8 @@ mod tests {
|
||||
let tx = tx.clone();
|
||||
async move {
|
||||
let shards = res.unwrap();
|
||||
tx.send(shards).await.unwrap();
|
||||
Ok(())
|
||||
tx.send(shards.iter().collect()).await.unwrap();
|
||||
Ok(Some(shards))
|
||||
}
|
||||
})
|
||||
.await
|
||||
@@ -800,8 +968,8 @@ mod tests {
|
||||
|
||||
// Test decode using the old API that operates in-place
|
||||
let mut decode_input: Vec<Option<Vec<u8>>> = vec![None; data_shards + parity_shards];
|
||||
for i in 0..data_shards {
|
||||
decode_input[i] = Some(shards[i].to_vec());
|
||||
for (i, shard) in shards.iter().enumerate().take(data_shards) {
|
||||
decode_input[i] = Some(shard.to_vec());
|
||||
}
|
||||
erasure.decode_data(&mut decode_input).unwrap();
|
||||
|
||||
@@ -1198,8 +1366,8 @@ mod tests {
|
||||
let tx = tx.clone();
|
||||
async move {
|
||||
let shards = res.unwrap();
|
||||
tx.send(shards).await.unwrap();
|
||||
Ok(())
|
||||
tx.send(shards.iter().collect()).await.unwrap();
|
||||
Ok(Some(shards))
|
||||
}
|
||||
})
|
||||
.await
|
||||
@@ -1233,5 +1401,63 @@ mod tests {
|
||||
recovered.truncate(data_clone.len());
|
||||
assert_eq!(&recovered, &data_clone);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn stress_simd_stream_callback_reuses_backing_buffers_across_many_blocks() {
|
||||
use std::io::Cursor;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
let data_shards = 4;
|
||||
let parity_shards = 2;
|
||||
let block_size = 1024;
|
||||
let erasure = Arc::new(Erasure::new(data_shards, parity_shards, block_size));
|
||||
|
||||
let sample =
|
||||
b"SIMD stress callback test payload that intentionally spans many blocks to exercise reusable backing buffers.";
|
||||
let data = sample.repeat((4 * 1024 * 1024 / sample.len()).max(1));
|
||||
let data_clone = data.clone();
|
||||
let mut reader = Cursor::new(data);
|
||||
|
||||
let recovered = Arc::new(Mutex::new(Vec::with_capacity(data_clone.len())));
|
||||
let block_count = Arc::new(AtomicUsize::new(0));
|
||||
let erasure_for_callback = erasure.clone();
|
||||
let recovered_for_callback = recovered.clone();
|
||||
let block_count_for_callback = block_count.clone();
|
||||
|
||||
erasure
|
||||
.clone()
|
||||
.encode_stream_callback_async::<_, _, (), _>(&mut reader, move |res| {
|
||||
let erasure_for_callback = erasure_for_callback.clone();
|
||||
let recovered_for_callback = recovered_for_callback.clone();
|
||||
let block_count_for_callback = block_count_for_callback.clone();
|
||||
async move {
|
||||
let shards = res.unwrap();
|
||||
block_count_for_callback.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
let mut shards_opt: Vec<Option<Vec<u8>>> = shards.iter().map(|b| Some(b.to_vec())).collect();
|
||||
shards_opt[1] = None;
|
||||
shards_opt[5] = None;
|
||||
erasure_for_callback.decode_data(&mut shards_opt).unwrap();
|
||||
|
||||
let mut recovered = recovered_for_callback.lock().await;
|
||||
for shard in shards_opt.iter().take(data_shards) {
|
||||
recovered.extend_from_slice(shard.as_ref().unwrap());
|
||||
}
|
||||
|
||||
Ok(Some(shards))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(block_count.load(Ordering::Relaxed) > 1024);
|
||||
|
||||
let mut recovered = recovered.lock().await;
|
||||
recovered.truncate(data_clone.len());
|
||||
assert_eq!(&*recovered, &data_clone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ impl super::Erasure {
|
||||
let available_writers = writers.iter().filter(|w| w.is_some()).count();
|
||||
let write_quorum = available_writers.max(1); // At least 1 writer must succeed
|
||||
let mut writers = MultiWriter::new(writers, write_quorum);
|
||||
writers.write(shards).await?;
|
||||
writers.write(&shards).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -30,8 +30,10 @@ use crate::{
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use futures::lock::Mutex;
|
||||
use futures_util::StreamExt;
|
||||
use http::{HeaderMap, HeaderValue, Method, header::CONTENT_TYPE};
|
||||
use rustfs_filemeta::{FileInfo, ObjectPartInfo, RawFileInfo};
|
||||
use rustfs_io_core::{BoxChunkStream, IoChunk};
|
||||
use rustfs_protos::proto_gen::node_service::RenamePartRequest;
|
||||
use rustfs_protos::proto_gen::node_service::{
|
||||
CheckPartsRequest, DeletePathsRequest, DeleteRequest, DeleteVersionRequest, DeleteVersionsRequest, DeleteVolumeRequest,
|
||||
@@ -40,7 +42,7 @@ use rustfs_protos::proto_gen::node_service::{
|
||||
RenameFileRequest, StatVolumeRequest, UpdateMetadataRequest, VerifyFileRequest, WriteAllRequest, WriteMetadataRequest,
|
||||
node_service_client::NodeServiceClient,
|
||||
};
|
||||
use rustfs_rio::{HttpReader, HttpWriter};
|
||||
use rustfs_rio::{HttpReader, HttpWriter, open_http_byte_stream};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use std::{
|
||||
io::Cursor,
|
||||
@@ -1071,6 +1073,33 @@ impl DiskAPI for RemoteDisk {
|
||||
Ok(Bytes::from(buffer))
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self))]
|
||||
async fn read_file_chunks(&self, volume: &str, path: &str, offset: usize, length: usize) -> Result<BoxChunkStream> {
|
||||
if self.health.is_faulty() {
|
||||
return Err(DiskError::FaultyDisk);
|
||||
}
|
||||
let disk = self.disk_ref().await;
|
||||
|
||||
let url = format!(
|
||||
"{}/rustfs/rpc/read_file_stream?disk={}&volume={}&path={}&offset={}&length={}",
|
||||
self.endpoint.grid_host(),
|
||||
urlencoding::encode(&disk),
|
||||
urlencoding::encode(volume),
|
||||
urlencoding::encode(path),
|
||||
offset,
|
||||
length
|
||||
);
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
|
||||
build_auth_headers(&url, &Method::GET, &mut headers);
|
||||
|
||||
let stream = open_http_byte_stream(url, Method::GET, headers, None)
|
||||
.await?
|
||||
.map(|result| result.map(IoChunk::Shared));
|
||||
Ok(Box::pin(stream))
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self))]
|
||||
async fn append_file(&self, volume: &str, path: &str) -> Result<FileWriter> {
|
||||
info!("append_file {}/{}", volume, path);
|
||||
|
||||
+183
-39
@@ -52,9 +52,9 @@ use crate::{
|
||||
event_notification::{EventArgs, send_event},
|
||||
global::{GLOBAL_LOCAL_DISK_MAP, GLOBAL_LOCAL_DISK_SET_DRIVES, get_global_deployment_id, is_dist_erasure},
|
||||
store_api::{
|
||||
BucketInfo, BucketOperations, BucketOptions, CompletePart, DeleteBucketOptions, DeletedObject, GetObjectReader,
|
||||
HTTPRangeSpec, HealOperations, ListMultipartsInfo, ListObjectsV2Info, ListOperations, MakeBucketOptions, MultipartInfo,
|
||||
MultipartOperations, MultipartUploadResult, ObjectIO, ObjectInfo, ObjectOperations, PartInfo, PutObjReader, StorageAPI,
|
||||
BucketInfo, BucketOperations, BucketOptions, ChunkNativePutData, CompletePart, DeleteBucketOptions, DeletedObject,
|
||||
GetObjectReader, HTTPRangeSpec, HealOperations, ListMultipartsInfo, ListObjectsV2Info, ListOperations, MakeBucketOptions,
|
||||
MultipartInfo, MultipartOperations, MultipartUploadResult, ObjectIO, ObjectInfo, ObjectOperations, PartInfo, StorageAPI,
|
||||
},
|
||||
store_init::load_format_erasure,
|
||||
};
|
||||
@@ -118,6 +118,66 @@ use tokio::{
|
||||
time::{interval, timeout},
|
||||
};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
const ENV_RUSTFS_PUT_INLINE_OBJECT_MAX_BYTES: &str = "RUSTFS_PUT_INLINE_OBJECT_MAX_BYTES";
|
||||
const ENV_RUSTFS_PUT_FORCE_DISABLE_INLINE: &str = "RUSTFS_PUT_FORCE_DISABLE_INLINE";
|
||||
const SLOW_PUT_STORAGE_PHASE_DEBUG_THRESHOLD_MS: u64 = 100;
|
||||
const SLOW_PUT_STORAGE_PHASE_WARN_THRESHOLD_MS: u64 = 1_000;
|
||||
const SLOW_PUT_STORAGE_PHASE_ERROR_THRESHOLD_MS: u64 = 5_000;
|
||||
|
||||
fn env_flag_enabled(name: &str) -> bool {
|
||||
rustfs_utils::get_env_bool(name, false)
|
||||
}
|
||||
|
||||
fn env_non_negative_usize(name: &str) -> Option<usize> {
|
||||
rustfs_utils::get_env_opt_usize(name)
|
||||
}
|
||||
|
||||
fn resolved_put_inline_buffer_enabled(object_size: i64, inline_by_topology: bool) -> bool {
|
||||
if !inline_by_topology || object_size < 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
if env_flag_enabled(ENV_RUSTFS_PUT_FORCE_DISABLE_INLINE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
env_non_negative_usize(ENV_RUSTFS_PUT_INLINE_OBJECT_MAX_BYTES)
|
||||
.map(|value| usize::try_from(object_size).is_ok_and(|size| size <= value))
|
||||
.unwrap_or(inline_by_topology)
|
||||
}
|
||||
|
||||
fn log_put_storage_phase(
|
||||
bucket: &str,
|
||||
object: &str,
|
||||
phase: &str,
|
||||
elapsed: Duration,
|
||||
object_size: i64,
|
||||
inline_selected: bool,
|
||||
write_quorum: usize,
|
||||
) {
|
||||
let duration_ms = elapsed.as_millis() as u64;
|
||||
if duration_ms < SLOW_PUT_STORAGE_PHASE_DEBUG_THRESHOLD_MS {
|
||||
return;
|
||||
}
|
||||
|
||||
if duration_ms >= SLOW_PUT_STORAGE_PHASE_ERROR_THRESHOLD_MS {
|
||||
error!(
|
||||
phase,
|
||||
duration_ms, object_size, inline_selected, write_quorum, bucket, object, "PUT storage phase is critically slow"
|
||||
);
|
||||
} else if duration_ms >= SLOW_PUT_STORAGE_PHASE_WARN_THRESHOLD_MS {
|
||||
warn!(
|
||||
phase,
|
||||
duration_ms, object_size, inline_selected, write_quorum, bucket, object, "PUT storage phase is slow"
|
||||
);
|
||||
} else {
|
||||
debug!(
|
||||
phase,
|
||||
duration_ms, object_size, inline_selected, write_quorum, bucket, object, "PUT storage phase exceeded debug threshold"
|
||||
);
|
||||
}
|
||||
}
|
||||
use tracing::error;
|
||||
use tracing::{debug, info, warn};
|
||||
use uuid::Uuid;
|
||||
@@ -151,6 +211,9 @@ 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 {
|
||||
@@ -692,7 +755,13 @@ impl ObjectIO for SetDisks {
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, data,))]
|
||||
async fn put_object(&self, bucket: &str, object: &str, data: &mut PutObjReader, opts: &ObjectOptions) -> Result<ObjectInfo> {
|
||||
async fn put_object(
|
||||
&self,
|
||||
bucket: &str,
|
||||
object: &str,
|
||||
data: &mut ChunkNativePutData,
|
||||
opts: &ObjectOptions,
|
||||
) -> Result<ObjectInfo> {
|
||||
let disks = self.get_disks_internal().await;
|
||||
|
||||
let mut object_lock_guard = None;
|
||||
@@ -774,13 +843,18 @@ impl ObjectIO for SetDisks {
|
||||
let erasure = erasure_coding::Erasure::new(fi.erasure.data_blocks, fi.erasure.parity_blocks, fi.erasure.block_size);
|
||||
|
||||
let is_inline_buffer = {
|
||||
if let Some(sc) = GLOBAL_STORAGE_CLASS.get() {
|
||||
let inline_by_topology = if let Some(sc) = GLOBAL_STORAGE_CLASS.get() {
|
||||
sc.should_inline(erasure.shard_file_size(data.size()), opts.versioned)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
resolved_put_inline_buffer_enabled(data.size(), inline_by_topology)
|
||||
};
|
||||
if is_inline_buffer {
|
||||
rustfs_io_metrics::record_put_inline_selected(data.size(), opts.versioned);
|
||||
}
|
||||
|
||||
let writer_setup_start = Instant::now();
|
||||
let mut writers = Vec::with_capacity(shuffle_disks.len());
|
||||
let mut errors = Vec::with_capacity(shuffle_disks.len());
|
||||
for disk_op in shuffle_disks.iter() {
|
||||
@@ -814,6 +888,15 @@ impl ObjectIO for SetDisks {
|
||||
writers.push(None);
|
||||
}
|
||||
}
|
||||
log_put_storage_phase(
|
||||
bucket,
|
||||
object,
|
||||
"writer_setup",
|
||||
writer_setup_start.elapsed(),
|
||||
data.size(),
|
||||
is_inline_buffer,
|
||||
write_quorum,
|
||||
);
|
||||
|
||||
let nil_count = errors.iter().filter(|&e| e.is_none()).count();
|
||||
if nil_count < write_quorum {
|
||||
@@ -825,23 +908,33 @@ impl ObjectIO for SetDisks {
|
||||
return Err(Error::other(format!("not enough disks to write: {errors:?}")));
|
||||
}
|
||||
|
||||
let stream = mem::replace(
|
||||
&mut data.stream,
|
||||
HashReader::from_stream(Cursor::new(Vec::new()), 0, 0, None, None, false)?,
|
||||
);
|
||||
|
||||
let (reader, w_size) = match Arc::new(erasure).encode(stream, &mut writers, write_quorum).await {
|
||||
Ok((r, w)) => (r, w),
|
||||
let object_size = data.size();
|
||||
let encode_write_start = Instant::now();
|
||||
let w_size = match Self::write_chunk_native_put_data(data, Arc::new(erasure), &mut writers, write_quorum).await {
|
||||
Ok(written) => written,
|
||||
Err(e) => {
|
||||
log_put_storage_phase(
|
||||
bucket,
|
||||
object,
|
||||
"encode_write",
|
||||
encode_write_start.elapsed(),
|
||||
object_size,
|
||||
is_inline_buffer,
|
||||
write_quorum,
|
||||
);
|
||||
error!("encode err {:?}", e);
|
||||
return Err(e.into());
|
||||
}
|
||||
}; // TODO: delete temporary directory on error
|
||||
|
||||
let _ = mem::replace(&mut data.stream, reader);
|
||||
// if let Err(err) = close_bitrot_writers(&mut writers).await {
|
||||
// error!("close_bitrot_writers err {:?}", err);
|
||||
// }
|
||||
log_put_storage_phase(
|
||||
bucket,
|
||||
object,
|
||||
"encode_write",
|
||||
encode_write_start.elapsed(),
|
||||
data.size(),
|
||||
is_inline_buffer,
|
||||
write_quorum,
|
||||
);
|
||||
|
||||
if (w_size as i64) < data.size() {
|
||||
warn!("put_object write size < data.size(), w_size={}, data.size={}", w_size, data.size());
|
||||
@@ -856,11 +949,11 @@ impl ObjectIO for SetDisks {
|
||||
insert_str(&mut user_defined, SUFFIX_COMPRESSION_SIZE, w_size.to_string());
|
||||
}
|
||||
|
||||
let index_op = data.stream.try_get_index().map(|v| v.clone().into_vec());
|
||||
let index_op = data.index_bytes();
|
||||
|
||||
//TODO: userDefined
|
||||
|
||||
let etag = data.stream.try_resolve_etag().unwrap_or_default();
|
||||
let etag = data.resolve_etag().unwrap_or_default();
|
||||
|
||||
user_defined.insert("etag".to_owned(), etag.clone());
|
||||
|
||||
@@ -877,9 +970,9 @@ impl ObjectIO for SetDisks {
|
||||
}
|
||||
|
||||
if fi.checksum.is_none()
|
||||
&& let Some(content_hash) = data.as_hash_reader().content_hash()
|
||||
&& let Some(content_hash) = data.content_hash_bytes()?
|
||||
{
|
||||
fi.checksum = Some(content_hash.to_bytes(&[]));
|
||||
fi.checksum = Some(content_hash);
|
||||
}
|
||||
|
||||
if let Some(sc) = user_defined.get(AMZ_STORAGE_CLASS)
|
||||
@@ -917,6 +1010,7 @@ impl ObjectIO for SetDisks {
|
||||
|
||||
drop(writers); // drop writers to close all files, this is to prevent FileAccessDenied errors when renaming data
|
||||
|
||||
let post_write_lock_start = Instant::now();
|
||||
if !opts.no_lock && object_lock_guard.is_none() {
|
||||
let ns_lock = self.new_ns_lock(bucket, object).await?;
|
||||
object_lock_guard = Some(ns_lock.get_write_lock(get_lock_acquire_timeout()).await.map_err(|e| {
|
||||
@@ -926,7 +1020,17 @@ impl ObjectIO for SetDisks {
|
||||
))
|
||||
})?);
|
||||
}
|
||||
log_put_storage_phase(
|
||||
bucket,
|
||||
object,
|
||||
"post_write_lock",
|
||||
post_write_lock_start.elapsed(),
|
||||
data.size(),
|
||||
is_inline_buffer,
|
||||
write_quorum,
|
||||
);
|
||||
|
||||
let finalize_start = Instant::now();
|
||||
let (online_disks, _, op_old_dir) = Self::rename_data(
|
||||
&shuffle_disks,
|
||||
RUSTFS_META_TMP_BUCKET,
|
||||
@@ -936,7 +1040,18 @@ impl ObjectIO for SetDisks {
|
||||
object,
|
||||
write_quorum,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.inspect_err(|_| {
|
||||
log_put_storage_phase(
|
||||
bucket,
|
||||
object,
|
||||
"finalize",
|
||||
finalize_start.elapsed(),
|
||||
data.size(),
|
||||
is_inline_buffer,
|
||||
write_quorum,
|
||||
);
|
||||
})?;
|
||||
|
||||
if let Some(old_dir) = op_old_dir {
|
||||
self.commit_rename_data_dir(&online_disks, bucket, object, &old_dir.to_string(), write_quorum)
|
||||
@@ -946,6 +1061,15 @@ impl ObjectIO for SetDisks {
|
||||
drop(object_lock_guard); // drop object lock guard to release the lock
|
||||
|
||||
self.delete_all(RUSTFS_META_TMP_BUCKET, &tmp_dir).await?;
|
||||
log_put_storage_phase(
|
||||
bucket,
|
||||
object,
|
||||
"finalize",
|
||||
finalize_start.elapsed(),
|
||||
data.size(),
|
||||
is_inline_buffer,
|
||||
write_quorum,
|
||||
);
|
||||
|
||||
for (i, op_disk) in online_disks.iter().enumerate() {
|
||||
if let Some(disk) = op_disk
|
||||
@@ -1864,6 +1988,9 @@ impl ObjectOperations for SetDisks {
|
||||
if let Some(ref version_id) = opts.version_id {
|
||||
fi.version_id = Uuid::parse_str(version_id).ok();
|
||||
}
|
||||
if let Some(checksum) = &opts.resolved_checksum {
|
||||
fi.checksum = Some(checksum.clone());
|
||||
}
|
||||
|
||||
self.update_object_meta(bucket, object, fi.clone(), &online_disks)
|
||||
.await
|
||||
@@ -2090,7 +2217,7 @@ impl ObjectOperations for SetDisks {
|
||||
let gr = gr.unwrap();
|
||||
let reader = BufReader::new(gr.stream);
|
||||
let hash_reader = HashReader::from_stream(reader, gr.object_info.size, gr.object_info.size, None, None, false)?;
|
||||
let mut p_reader = PutObjReader::new(hash_reader);
|
||||
let mut p_reader = ChunkNativePutData::new(hash_reader);
|
||||
return match self_.clone().put_object(bucket, object, &mut p_reader, &ropts).await {
|
||||
Ok(restored_info) => {
|
||||
send_event(EventArgs {
|
||||
@@ -2158,7 +2285,7 @@ impl ObjectOperations for SetDisks {
|
||||
};
|
||||
let reader = BufReader::new(gr.stream);
|
||||
let hash_reader = HashReader::from_stream(reader, part_info.actual_size, part_info.actual_size, None, None, false)?;
|
||||
let mut p_reader = PutObjReader::new(hash_reader);
|
||||
let mut p_reader = ChunkNativePutData::new(hash_reader);
|
||||
let p_info = self_
|
||||
.clone()
|
||||
.put_object_part(bucket, object, &res.upload_id, part_info.number, &mut p_reader, &ObjectOptions::default())
|
||||
@@ -2382,7 +2509,7 @@ impl MultipartOperations for SetDisks {
|
||||
object: &str,
|
||||
upload_id: &str,
|
||||
part_id: usize,
|
||||
data: &mut PutObjReader,
|
||||
data: &mut ChunkNativePutData,
|
||||
opts: &ObjectOptions,
|
||||
) -> Result<PartInfo> {
|
||||
let upload_id_path = Self::get_upload_id_dir(bucket, object, upload_id);
|
||||
@@ -2394,9 +2521,8 @@ impl MultipartOperations for SetDisks {
|
||||
if let Some(checksum) = fi.metadata.get(rustfs_rio::RUSTFS_MULTIPART_CHECKSUM)
|
||||
&& !checksum.is_empty()
|
||||
&& data
|
||||
.as_hash_reader()
|
||||
.content_crc_type()
|
||||
.is_none_or(|v| v.to_string() != *checksum)
|
||||
.is_none_or(|v: rustfs_rio::ChecksumType| v.to_string() != *checksum)
|
||||
{
|
||||
return Err(Error::other(format!("checksum mismatch: {checksum}")));
|
||||
}
|
||||
@@ -2461,14 +2587,7 @@ impl MultipartOperations for SetDisks {
|
||||
return Err(Error::other(format!("not enough disks to write: {errors:?}")));
|
||||
}
|
||||
|
||||
let stream = mem::replace(
|
||||
&mut data.stream,
|
||||
HashReader::from_stream(Cursor::new(Vec::new()), 0, 0, None, None, false)?,
|
||||
);
|
||||
|
||||
let (reader, w_size) = Arc::new(erasure).encode(stream, &mut writers, write_quorum).await?; // TODO: delete temporary directory on error
|
||||
|
||||
let _ = mem::replace(&mut data.stream, reader);
|
||||
let w_size = Self::write_chunk_native_put_data(data, Arc::new(erasure), &mut writers, write_quorum).await?; // TODO: delete temporary directory on error
|
||||
|
||||
if (w_size as i64) < data.size() {
|
||||
warn!("put_object_part write size < data.size(), w_size={}, data.size={}", w_size, data.size());
|
||||
@@ -2479,9 +2598,9 @@ impl MultipartOperations for SetDisks {
|
||||
)));
|
||||
}
|
||||
|
||||
let index_op = data.stream.try_get_index().map(|v| v.clone().into_vec());
|
||||
let index_op = data.index_bytes();
|
||||
|
||||
let mut etag = data.stream.try_resolve_etag().unwrap_or_default();
|
||||
let mut etag = data.resolve_etag().unwrap_or_default();
|
||||
|
||||
if let Some(ref tag) = opts.preserve_etag {
|
||||
etag = tag.clone();
|
||||
@@ -2495,7 +2614,7 @@ impl MultipartOperations for SetDisks {
|
||||
}
|
||||
}
|
||||
|
||||
let checksums = data.as_hash_reader().content_crc();
|
||||
let checksums = data.content_crc();
|
||||
|
||||
let part_info = ObjectPartInfo {
|
||||
etag: etag.clone(),
|
||||
@@ -4212,6 +4331,31 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn resolved_put_inline_buffer_enabled_honors_disable_env() {
|
||||
temp_env::with_var(ENV_RUSTFS_PUT_FORCE_DISABLE_INLINE, Some("true"), || {
|
||||
assert!(!resolved_put_inline_buffer_enabled(4096, true));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn resolved_put_inline_buffer_enabled_honors_max_bytes_override() {
|
||||
temp_env::with_var(ENV_RUSTFS_PUT_INLINE_OBJECT_MAX_BYTES, Some("4096"), || {
|
||||
assert!(resolved_put_inline_buffer_enabled(4096, true));
|
||||
assert!(!resolved_put_inline_buffer_enabled(4097, true));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn resolved_put_inline_buffer_enabled_ignores_invalid_override() {
|
||||
temp_env::with_var(ENV_RUSTFS_PUT_INLINE_OBJECT_MAX_BYTES, Some("invalid"), || {
|
||||
assert!(resolved_put_inline_buffer_enabled(4096, true));
|
||||
});
|
||||
}
|
||||
|
||||
async fn current_setup_type() -> SetupType {
|
||||
if is_dist_erasure().await {
|
||||
SetupType::DistErasure
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,12 +13,87 @@
|
||||
// limitations under the License.
|
||||
|
||||
use super::*;
|
||||
use crate::store_api::ChunkNativePutData;
|
||||
|
||||
impl SetDisks {
|
||||
fn all_inline_bitrot_writers(writers: &[Option<crate::erasure_coding::BitrotWriterWrapper>]) -> bool {
|
||||
writers.iter().all(|writer| {
|
||||
writer
|
||||
.as_ref()
|
||||
.is_some_and(crate::erasure_coding::BitrotWriterWrapper::is_inline_buffer)
|
||||
})
|
||||
}
|
||||
|
||||
async fn write_chunk_native_put_data_inline(
|
||||
data: &mut ChunkNativePutData,
|
||||
erasure: Arc<erasure_coding::Erasure>,
|
||||
writers: &mut [Option<crate::erasure_coding::BitrotWriterWrapper>],
|
||||
write_quorum: usize,
|
||||
) -> std::io::Result<usize> {
|
||||
let stream = data.take_stream()?;
|
||||
let mut assembler = erasure_coding::encode::BlockAssembler::new(stream, erasure.block_size);
|
||||
let encoder = erasure_coding::encode::ErasureChunkEncoder::new(erasure).await;
|
||||
let mut writer_group = erasure_coding::encode::MultiWriter::new(writers, write_quorum);
|
||||
|
||||
loop {
|
||||
let block = match assembler.next_block().await {
|
||||
Ok(Some(block)) => block,
|
||||
Ok(None) => break,
|
||||
Err(err) => {
|
||||
data.restore_stream(assembler.into_inner());
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
|
||||
let encoded = match encoder.encode_block(&block).await {
|
||||
Ok(encoded) => encoded,
|
||||
Err(err) => {
|
||||
data.restore_stream(assembler.into_inner());
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = writer_group.write_inline(&encoded) {
|
||||
encoder.release(encoded).await;
|
||||
data.restore_stream(assembler.into_inner());
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
encoder.release(encoded).await;
|
||||
}
|
||||
|
||||
let total_bytes = assembler.total_bytes();
|
||||
let stream = assembler.into_inner();
|
||||
if let Err(err) = writer_group.shutdown_inline() {
|
||||
data.restore_stream(stream);
|
||||
return Err(err);
|
||||
}
|
||||
data.restore_stream(stream);
|
||||
Ok(total_bytes)
|
||||
}
|
||||
|
||||
pub(super) fn default_read_quorum(&self) -> usize {
|
||||
self.set_drive_count - self.default_parity_count
|
||||
}
|
||||
|
||||
pub(super) async fn write_chunk_native_put_data(
|
||||
data: &mut ChunkNativePutData,
|
||||
erasure: Arc<erasure_coding::Erasure>,
|
||||
writers: &mut [Option<crate::erasure_coding::BitrotWriterWrapper>],
|
||||
write_quorum: usize,
|
||||
) -> std::io::Result<usize> {
|
||||
if Self::all_inline_bitrot_writers(writers) {
|
||||
return Self::write_chunk_native_put_data_inline(data, erasure, writers, write_quorum).await;
|
||||
}
|
||||
|
||||
let stream = data.take_stream()?;
|
||||
let (stream, written) = erasure_coding::encode::ErasureWritePipeline::new(erasure, write_quorum)
|
||||
.run(stream, writers)
|
||||
.await?;
|
||||
data.restore_stream(stream);
|
||||
Ok(written)
|
||||
}
|
||||
|
||||
pub(super) fn default_write_quorum(&self) -> usize {
|
||||
let mut data_count = self.set_drive_count - self.default_parity_count;
|
||||
if data_count == self.default_parity_count {
|
||||
@@ -627,3 +702,60 @@ impl SetDisks {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::erasure_coding::{BitrotWriterWrapper, CustomWriter};
|
||||
use crate::store_api::ChunkNativePutData;
|
||||
|
||||
#[tokio::test]
|
||||
async fn write_chunk_native_put_data_restores_reader_state_after_encoding() {
|
||||
let payload = b"chunk-native-put-payload".repeat(8);
|
||||
let erasure = Arc::new(erasure_coding::Erasure::new(2, 1, 8));
|
||||
let mut reader = ChunkNativePutData::from_vec(payload.clone());
|
||||
let mut writers: Vec<Option<BitrotWriterWrapper>> = (0..erasure.total_shard_count())
|
||||
.map(|_| {
|
||||
Some(BitrotWriterWrapper::new(
|
||||
CustomWriter::new_inline_buffer(),
|
||||
erasure.shard_size(),
|
||||
HashAlgorithm::HighwayHash256S,
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let written = SetDisks::write_chunk_native_put_data(&mut reader, erasure.clone(), &mut writers, 2)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(written, payload.len());
|
||||
assert_eq!(reader.size(), payload.len() as i64);
|
||||
assert_eq!(reader.actual_size(), payload.len() as i64);
|
||||
assert!(
|
||||
reader.resolve_etag().is_some(),
|
||||
"restored reader should preserve computed etag state after chunk-native encode"
|
||||
);
|
||||
|
||||
let inline_lengths: Vec<usize> = writers
|
||||
.into_iter()
|
||||
.map(|writer| writer.expect("writer").into_inline_data().expect("inline data").len())
|
||||
.collect();
|
||||
assert!(inline_lengths.iter().all(|len| *len > 0));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn write_chunk_native_put_data_detects_inline_writer_set() {
|
||||
let erasure = Arc::new(erasure_coding::Erasure::new(2, 1, 8));
|
||||
let writers: Vec<Option<BitrotWriterWrapper>> = (0..erasure.total_shard_count())
|
||||
.map(|_| {
|
||||
Some(BitrotWriterWrapper::new(
|
||||
CustomWriter::new_inline_buffer(),
|
||||
erasure.shard_size(),
|
||||
HashAlgorithm::HighwayHash256S,
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert!(SetDisks::all_inline_bitrot_writers(&writers));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
use crate::disk::error_reduce::count_errs;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::store_api::{ListPartsInfo, ObjectInfoOrErr, WalkOptions};
|
||||
use crate::store_api::{GetObjectChunkResult, ListPartsInfo, ObjectInfoOrErr, WalkOptions};
|
||||
use crate::{
|
||||
disk::{
|
||||
DiskAPI, DiskInfo, DiskOption, DiskStore,
|
||||
@@ -28,10 +28,10 @@ use crate::{
|
||||
global::{GLOBAL_LOCAL_DISK_SET_DRIVES, get_global_lock_clients, is_dist_erasure},
|
||||
set_disk::SetDisks,
|
||||
store_api::{
|
||||
BucketInfo, BucketOperations, BucketOptions, CompletePart, DeleteBucketOptions, DeletedObject, GetObjectReader,
|
||||
HTTPRangeSpec, HealOperations, ListMultipartsInfo, ListObjectVersionsInfo, ListObjectsV2Info, ListOperations,
|
||||
MakeBucketOptions, MultipartInfo, MultipartOperations, MultipartUploadResult, ObjectIO, ObjectInfo, ObjectOperations,
|
||||
ObjectOptions, ObjectToDelete, PartInfo, PutObjReader, StorageAPI,
|
||||
BucketInfo, BucketOperations, BucketOptions, ChunkNativePutData, CompletePart, DeleteBucketOptions, DeletedObject,
|
||||
GetObjectReader, HTTPRangeSpec, HealOperations, ListMultipartsInfo, ListObjectVersionsInfo, ListObjectsV2Info,
|
||||
ListOperations, MakeBucketOptions, MultipartInfo, MultipartOperations, MultipartUploadResult, ObjectIO, ObjectInfo,
|
||||
ObjectOperations, ObjectOptions, ObjectToDelete, PartInfo, StorageAPI,
|
||||
},
|
||||
store_init::{check_format_erasure_values, get_format_erasure_in_quorum, load_format_erasure_all, save_format_file},
|
||||
};
|
||||
@@ -287,6 +287,19 @@ 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()),
|
||||
@@ -375,7 +388,13 @@ impl ObjectIO for Sets {
|
||||
.await
|
||||
}
|
||||
#[tracing::instrument(level = "debug", skip(self, data))]
|
||||
async fn put_object(&self, bucket: &str, object: &str, data: &mut PutObjReader, opts: &ObjectOptions) -> Result<ObjectInfo> {
|
||||
async fn put_object(
|
||||
&self,
|
||||
bucket: &str,
|
||||
object: &str,
|
||||
data: &mut ChunkNativePutData,
|
||||
opts: &ObjectOptions,
|
||||
) -> Result<ObjectInfo> {
|
||||
self.get_disks_by_key(object).put_object(bucket, object, data, opts).await
|
||||
}
|
||||
}
|
||||
@@ -688,7 +707,7 @@ impl MultipartOperations for Sets {
|
||||
object: &str,
|
||||
upload_id: &str,
|
||||
part_id: usize,
|
||||
data: &mut PutObjReader,
|
||||
data: &mut ChunkNativePutData,
|
||||
opts: &ObjectOptions,
|
||||
) -> Result<PartInfo> {
|
||||
self.get_disks_by_key(object)
|
||||
|
||||
@@ -59,9 +59,10 @@ use crate::{
|
||||
rpc::S3PeerSys,
|
||||
sets::Sets,
|
||||
store_api::{
|
||||
BucketInfo, BucketOperations, BucketOptions, CompletePart, DeleteBucketOptions, DeletedObject, GetObjectReader,
|
||||
HTTPRangeSpec, HealOperations, ListObjectsV2Info, ListOperations, MakeBucketOptions, MultipartOperations,
|
||||
MultipartUploadResult, ObjectInfo, ObjectOperations, ObjectOptions, ObjectToDelete, PartInfo, PutObjReader, StorageAPI,
|
||||
BucketInfo, BucketOperations, BucketOptions, ChunkNativePutData, CompletePart, DeleteBucketOptions, DeletedObject,
|
||||
GetObjectChunkResult, GetObjectReader, HTTPRangeSpec, HealOperations, ListObjectsV2Info, ListOperations,
|
||||
MakeBucketOptions, MultipartOperations, MultipartUploadResult, ObjectInfo, ObjectOperations, ObjectOptions,
|
||||
ObjectToDelete, PartInfo, StorageAPI,
|
||||
},
|
||||
store_init,
|
||||
};
|
||||
@@ -259,11 +260,31 @@ impl ObjectIO for ECStore {
|
||||
self.handle_get_object_reader(bucket, object, range, h, opts).await
|
||||
}
|
||||
#[instrument(level = "debug", skip(self, data))]
|
||||
async fn put_object(&self, bucket: &str, object: &str, data: &mut PutObjReader, opts: &ObjectOptions) -> Result<ObjectInfo> {
|
||||
async fn put_object(
|
||||
&self,
|
||||
bucket: &str,
|
||||
object: &str,
|
||||
data: &mut ChunkNativePutData,
|
||||
opts: &ObjectOptions,
|
||||
) -> Result<ObjectInfo> {
|
||||
enqueue_transition_after_write(self.handle_put_object(bucket, object, data, opts).await, LcEventSrc::S3PutObject).await
|
||||
}
|
||||
}
|
||||
|
||||
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)),
|
||||
@@ -495,7 +516,7 @@ impl MultipartOperations for ECStore {
|
||||
object: &str,
|
||||
upload_id: &str,
|
||||
part_id: usize,
|
||||
data: &mut PutObjReader,
|
||||
data: &mut ChunkNativePutData,
|
||||
opts: &ObjectOptions,
|
||||
) -> Result<PartInfo> {
|
||||
self.handle_put_object_part(bucket, object, upload_id, part_id, data, opts)
|
||||
|
||||
@@ -176,7 +176,7 @@ impl ECStore {
|
||||
object: &str,
|
||||
upload_id: &str,
|
||||
part_id: usize,
|
||||
data: &mut PutObjReader,
|
||||
data: &mut ChunkNativePutData,
|
||||
opts: &ObjectOptions,
|
||||
) -> Result<PartInfo> {
|
||||
check_put_object_part_args(bucket, object, upload_id)?;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use super::*;
|
||||
use crate::store_api::GetObjectChunkResult;
|
||||
|
||||
fn select_data_movement_target_pool(
|
||||
existing_pool_idx: Result<usize>,
|
||||
@@ -213,12 +214,40 @@ 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,
|
||||
bucket: &str,
|
||||
object: &str,
|
||||
data: &mut PutObjReader,
|
||||
data: &mut ChunkNativePutData,
|
||||
opts: &ObjectOptions,
|
||||
) -> Result<ObjectInfo> {
|
||||
check_put_object_args(bucket, object)?;
|
||||
|
||||
@@ -31,6 +31,7 @@ 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;
|
||||
|
||||
@@ -1,7 +1,101 @@
|
||||
use super::*;
|
||||
use rustfs_rio::TryGetIndex;
|
||||
|
||||
pub struct ChunkNativePutData {
|
||||
stream: Option<HashReader>,
|
||||
size: i64,
|
||||
actual_size: i64,
|
||||
}
|
||||
|
||||
impl Debug for ChunkNativePutData {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ChunkNativePutData").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl ChunkNativePutData {
|
||||
pub fn new(stream: HashReader) -> Self {
|
||||
let size = stream.size();
|
||||
let actual_size = stream.actual_size();
|
||||
Self {
|
||||
stream: Some(stream),
|
||||
size,
|
||||
actual_size,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_vec(data: Vec<u8>) -> Self {
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
let content_length = data.len() as i64;
|
||||
let sha256hex = if content_length > 0 {
|
||||
Some(hex_simd::encode_to_string(Sha256::digest(&data), hex_simd::AsciiCase::Lower))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Self::new(HashReader::from_stream(Cursor::new(data), content_length, content_length, None, sha256hex, false).unwrap())
|
||||
}
|
||||
|
||||
pub fn take_stream(&mut self) -> std::io::Result<HashReader> {
|
||||
self.stream
|
||||
.take()
|
||||
.ok_or_else(|| std::io::Error::other("ChunkNativePutData stream already taken"))
|
||||
}
|
||||
|
||||
pub fn restore_stream(&mut self, stream: HashReader) {
|
||||
self.size = stream.size();
|
||||
self.actual_size = stream.actual_size();
|
||||
self.stream = Some(stream);
|
||||
}
|
||||
|
||||
pub fn as_hash_reader(&self) -> Option<&HashReader> {
|
||||
self.stream.as_ref()
|
||||
}
|
||||
|
||||
pub fn as_hash_reader_mut(&mut self) -> Option<&mut HashReader> {
|
||||
self.stream.as_mut()
|
||||
}
|
||||
|
||||
pub fn index_bytes(&self) -> Option<Bytes> {
|
||||
self.as_hash_reader()
|
||||
.and_then(|reader| reader.try_get_index().map(|index| index.clone().into_vec()))
|
||||
}
|
||||
|
||||
pub fn resolve_etag(&mut self) -> Option<String> {
|
||||
self.as_hash_reader_mut()
|
||||
.and_then(rustfs_rio::EtagResolvable::try_resolve_etag)
|
||||
}
|
||||
|
||||
pub fn content_hash_bytes(&mut self) -> std::io::Result<Option<Bytes>> {
|
||||
let Some(reader) = self.as_hash_reader_mut() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
Ok(reader
|
||||
.finalize_content_hash()?
|
||||
.as_ref()
|
||||
.map(|checksum| checksum.to_bytes(&[])))
|
||||
}
|
||||
|
||||
pub fn content_crc_type(&self) -> Option<rustfs_rio::ChecksumType> {
|
||||
self.as_hash_reader().and_then(HashReader::content_crc_type)
|
||||
}
|
||||
|
||||
pub fn content_crc(&self) -> HashMap<String, String> {
|
||||
self.as_hash_reader().map_or_else(HashMap::new, HashReader::content_crc)
|
||||
}
|
||||
|
||||
pub fn size(&self) -> i64 {
|
||||
self.size
|
||||
}
|
||||
|
||||
pub fn actual_size(&self) -> i64 {
|
||||
self.actual_size
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PutObjReader {
|
||||
pub stream: HashReader,
|
||||
data: ChunkNativePutData,
|
||||
}
|
||||
|
||||
impl Debug for PutObjReader {
|
||||
@@ -12,32 +106,81 @@ impl Debug for PutObjReader {
|
||||
|
||||
impl PutObjReader {
|
||||
pub fn new(stream: HashReader) -> Self {
|
||||
PutObjReader { stream }
|
||||
Self {
|
||||
data: ChunkNativePutData::new(stream),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_hash_reader(&self) -> &HashReader {
|
||||
&self.stream
|
||||
pub fn chunk_native_data(&self) -> &ChunkNativePutData {
|
||||
&self.data
|
||||
}
|
||||
|
||||
pub fn chunk_native_data_mut(&mut self) -> &mut ChunkNativePutData {
|
||||
&mut self.data
|
||||
}
|
||||
|
||||
pub fn take_stream(&mut self) -> std::io::Result<HashReader> {
|
||||
self.data.take_stream()
|
||||
}
|
||||
|
||||
pub fn restore_stream(&mut self, stream: HashReader) {
|
||||
self.data.restore_stream(stream);
|
||||
}
|
||||
|
||||
pub fn as_hash_reader(&self) -> Option<&HashReader> {
|
||||
self.data.as_hash_reader()
|
||||
}
|
||||
|
||||
pub fn as_hash_reader_mut(&mut self) -> Option<&mut HashReader> {
|
||||
self.data.as_hash_reader_mut()
|
||||
}
|
||||
|
||||
pub fn index_bytes(&self) -> Option<Bytes> {
|
||||
self.data.index_bytes()
|
||||
}
|
||||
|
||||
pub fn resolve_etag(&mut self) -> Option<String> {
|
||||
self.data.resolve_etag()
|
||||
}
|
||||
|
||||
pub fn content_hash_bytes(&mut self) -> std::io::Result<Option<Bytes>> {
|
||||
self.data.content_hash_bytes()
|
||||
}
|
||||
|
||||
pub fn content_crc_type(&self) -> Option<rustfs_rio::ChecksumType> {
|
||||
self.data.content_crc_type()
|
||||
}
|
||||
|
||||
pub fn content_crc(&self) -> HashMap<String, String> {
|
||||
self.data.content_crc()
|
||||
}
|
||||
|
||||
pub fn from_vec(data: Vec<u8>) -> Self {
|
||||
use sha2::{Digest, Sha256};
|
||||
let content_length = data.len() as i64;
|
||||
let sha256hex = if content_length > 0 {
|
||||
Some(hex_simd::encode_to_string(Sha256::digest(&data), hex_simd::AsciiCase::Lower))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
PutObjReader {
|
||||
stream: HashReader::from_stream(Cursor::new(data), content_length, content_length, None, sha256hex, false).unwrap(),
|
||||
Self {
|
||||
data: ChunkNativePutData::from_vec(data),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(&self) -> i64 {
|
||||
self.stream.size()
|
||||
self.data.size()
|
||||
}
|
||||
|
||||
pub fn actual_size(&self) -> i64 {
|
||||
self.stream.actual_size()
|
||||
self.data.actual_size()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for PutObjReader {
|
||||
type Target = ChunkNativePutData;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for PutObjReader {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +189,26 @@ 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(
|
||||
@@ -63,31 +226,34 @@ impl GetObjectReader {
|
||||
rs = HTTPRangeSpec::from_object_info(oi, part_number);
|
||||
}
|
||||
|
||||
// TODO:Encrypted
|
||||
let logical_size = oi.get_actual_size()?;
|
||||
let encrypted_object = oi.user_defined.contains_key("x-rustfs-encryption-key")
|
||||
|| oi
|
||||
.user_defined
|
||||
.contains_key("x-amz-server-side-encryption-customer-algorithm");
|
||||
|
||||
let (algo, is_compressed) = oi.is_compressed_ok()?;
|
||||
|
||||
// TODO: check TRANSITION
|
||||
|
||||
if is_compressed {
|
||||
let actual_size = oi.get_actual_size()?;
|
||||
let (off, length, dec_off, dec_length) = if let Some(rs) = rs {
|
||||
// Support range requests for compressed objects
|
||||
let (dec_off, dec_length) = rs.get_offset_length(actual_size)?;
|
||||
let (dec_off, dec_length) = rs.get_offset_length(logical_size)?;
|
||||
(0, oi.size, dec_off, dec_length)
|
||||
} else {
|
||||
(0, oi.size, 0, actual_size)
|
||||
(0, oi.size, 0, logical_size)
|
||||
};
|
||||
|
||||
let dec_reader = DecompressReader::new(reader, algo);
|
||||
|
||||
let actual_size_usize = if actual_size > 0 {
|
||||
actual_size as usize
|
||||
let actual_size_usize = if logical_size > 0 {
|
||||
logical_size as usize
|
||||
} else {
|
||||
return Err(Error::other(format!("invalid decompressed size {actual_size}")));
|
||||
return Err(Error::other(format!("invalid decompressed size {logical_size}")));
|
||||
};
|
||||
|
||||
let final_reader: Box<dyn AsyncRead + Unpin + Send + Sync> = if dec_off > 0 || dec_length != actual_size {
|
||||
let final_reader: Box<dyn AsyncRead + Unpin + Send + Sync> = if dec_off > 0 || dec_length != logical_size {
|
||||
// Use RangedDecompressReader for streaming range processing
|
||||
// The new implementation supports any offset size by streaming and skipping data
|
||||
match RangedDecompressReader::new(dec_reader, dec_off, dec_length, actual_size_usize) {
|
||||
@@ -122,8 +288,19 @@ impl GetObjectReader {
|
||||
));
|
||||
}
|
||||
|
||||
if encrypted_object && rs.is_none() {
|
||||
return Ok((
|
||||
GetObjectReader {
|
||||
stream: reader,
|
||||
object_info: oi.clone(),
|
||||
},
|
||||
0,
|
||||
oi.size,
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(rs) = rs {
|
||||
let (off, length) = rs.get_offset_length(oi.size)?;
|
||||
let (off, length) = rs.get_offset_length(logical_size)?;
|
||||
|
||||
Ok((
|
||||
GetObjectReader {
|
||||
@@ -140,7 +317,7 @@ impl GetObjectReader {
|
||||
object_info: oi.clone(),
|
||||
},
|
||||
0,
|
||||
oi.size,
|
||||
logical_size,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,13 @@ pub trait ObjectIO: Send + Sync + Debug + 'static {
|
||||
opts: &ObjectOptions,
|
||||
) -> Result<GetObjectReader>;
|
||||
|
||||
async fn put_object(&self, bucket: &str, object: &str, data: &mut PutObjReader, opts: &ObjectOptions) -> Result<ObjectInfo>;
|
||||
async fn put_object(
|
||||
&self,
|
||||
bucket: &str,
|
||||
object: &str,
|
||||
data: &mut ChunkNativePutData,
|
||||
opts: &ObjectOptions,
|
||||
) -> Result<ObjectInfo>;
|
||||
}
|
||||
|
||||
/// Bucket-level storage operations.
|
||||
@@ -126,7 +132,7 @@ pub trait MultipartOperations: Send + Sync + Debug {
|
||||
object: &str,
|
||||
upload_id: &str,
|
||||
part_id: usize,
|
||||
data: &mut PutObjReader,
|
||||
data: &mut ChunkNativePutData,
|
||||
opts: &ObjectOptions,
|
||||
) -> Result<PartInfo>;
|
||||
async fn get_multipart_info(
|
||||
|
||||
@@ -70,6 +70,7 @@ pub struct ObjectOptions {
|
||||
|
||||
pub eval_metadata: Option<HashMap<String, String>>,
|
||||
|
||||
pub resolved_checksum: Option<Bytes>,
|
||||
pub want_checksum: Option<Checksum>,
|
||||
pub skip_verify_bitrot: bool,
|
||||
}
|
||||
@@ -283,7 +284,7 @@ pub struct ObjectInfo {
|
||||
pub expires: Option<OffsetDateTime>,
|
||||
pub num_versions: usize,
|
||||
pub successor_mod_time: Option<OffsetDateTime>,
|
||||
pub put_object_reader: Option<PutObjReader>,
|
||||
pub put_object_reader: Option<ChunkNativePutData>,
|
||||
pub etag: Option<String>,
|
||||
pub inlined: bool,
|
||||
pub metadata_only: bool,
|
||||
@@ -509,6 +510,18 @@ impl ObjectInfo {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let actual_size = fi
|
||||
.parts
|
||||
.iter()
|
||||
.map(|part| {
|
||||
if part.actual_size > 0 {
|
||||
part.actual_size
|
||||
} else {
|
||||
i64::try_from(part.size).unwrap_or_default()
|
||||
}
|
||||
})
|
||||
.sum();
|
||||
|
||||
// TODO: part checksums
|
||||
|
||||
ObjectInfo {
|
||||
@@ -521,6 +534,7 @@ impl ObjectInfo {
|
||||
delete_marker: fi.deleted,
|
||||
mod_time: fi.mod_time,
|
||||
size: fi.size,
|
||||
actual_size,
|
||||
parts,
|
||||
is_latest: fi.is_latest,
|
||||
user_tags,
|
||||
|
||||
@@ -51,7 +51,7 @@ use crate::{
|
||||
disk::{MIGRATING_META_BUCKET, RUSTFS_META_BUCKET},
|
||||
global::is_first_cluster_node_local,
|
||||
store::ECStore,
|
||||
store_api::{ObjectIO as _, ObjectOptions, PutObjReader},
|
||||
store_api::{ChunkNativePutData, ObjectIO as _, ObjectOptions},
|
||||
};
|
||||
use rustfs_rio::HashReader;
|
||||
use rustfs_utils::path::{SLASH_SEPARATOR, path_join};
|
||||
@@ -1046,9 +1046,8 @@ impl TierConfigMgr {
|
||||
opts: &ObjectOptions,
|
||||
) -> std::result::Result<(), std::io::Error> {
|
||||
debug!("save tier config:{}", file);
|
||||
let _ = api
|
||||
.put_object(RUSTFS_META_BUCKET, file, &mut PutObjReader::from_vec(data.to_vec()), opts)
|
||||
.await?;
|
||||
let mut put_data = ChunkNativePutData::from_vec(data.to_vec());
|
||||
let _ = api.put_object(RUSTFS_META_BUCKET, file, &mut put_data, opts).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1104,11 +1103,12 @@ async fn load_tier_config(api: Arc<ECStore>) -> std::result::Result<TierConfigMg
|
||||
Ok(data) => {
|
||||
let cfg = TierConfigMgr::unmarshal(&data)?;
|
||||
let normalized = encode_external_tiering_config_blob(&cfg)?;
|
||||
let mut put_data = ChunkNativePutData::from_vec(normalized.to_vec());
|
||||
let _ = api
|
||||
.put_object(
|
||||
RUSTFS_META_BUCKET,
|
||||
&config_file,
|
||||
&mut PutObjReader::from_vec(normalized.to_vec()),
|
||||
&mut put_data,
|
||||
&ObjectOptions {
|
||||
max_parity: true,
|
||||
..Default::default()
|
||||
@@ -1158,10 +1158,11 @@ async fn read_tier_config_from_bucket<S: StorageAPI>(
|
||||
}
|
||||
|
||||
async fn write_tier_config_to_rustfs<S: StorageAPI>(api: Arc<S>, path: &str, data: Bytes) -> io::Result<()> {
|
||||
let mut put_data = ChunkNativePutData::from_vec(data.to_vec());
|
||||
api.put_object(
|
||||
RUSTFS_META_BUCKET,
|
||||
path,
|
||||
&mut PutObjReader::from_vec(data.to_vec()),
|
||||
&mut put_data,
|
||||
&ObjectOptions {
|
||||
max_parity: true,
|
||||
..Default::default()
|
||||
|
||||
Reference in New Issue
Block a user