mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-10 15:16:56 +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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user