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:
houseme
2026-04-07 08:33:46 +08:00
committed by GitHub
parent 8d27170ce4
commit 32bf8f5bf3
84 changed files with 15932 additions and 3592 deletions
+9
View File
@@ -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
+784 -74
View File
@@ -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
+12
View File
@@ -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