fix: address correctness, safety, and concurrency issues (#2327)

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
houseme
2026-03-30 00:30:57 +08:00
committed by GitHub
parent 860a37d3a8
commit 7172e151de
90 changed files with 20397 additions and 1228 deletions
+56
View File
@@ -0,0 +1,56 @@
# Changelog
All notable changes to the rustfs-io-core and rustfs-io-metrics crates will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.0.5] - 2025-01-XX
### Added
#### rustfs-io-core
- **IoScheduler**: Adaptive I/O scheduler with buffer size calculation
- **IoPriorityQueue**: Priority queue with starvation prevention
- **BackpressureMonitor**: System overload protection with dual watermark
- **DeadlockDetector**: Wait-for graph based deadlock detection
- **LockOptimizer**: Adaptive spin lock optimization
- **RequestTimeoutWrapper**: Dynamic timeout calculation
- **Buffer size functions**: `calculate_optimal_buffer_size`, `get_buffer_size_for_media`, etc.
- **Configuration types**: `IoSchedulerConfig`, `BackpressureConfig`, `DeadlockDetectorConfig`, etc.
#### rustfs-io-metrics
- **CacheConfig**: L1/L2 tiered cache configuration
- **AdaptiveTTL**: Dynamic TTL adjustment based on access frequency
- **AccessTracker**: Cache item access pattern tracking
- **Metrics recording functions**: I/O, cache, backpressure, deadlock, lock, timeout metrics
- **Unified configuration**: `IoConfig`, `CacheSettings`, `IoSchedulerSettings`, etc.
- **Bandwidth monitoring**: Real-time bandwidth observation
### Changed
- Migrated core I/O scheduling algorithms from `rustfs::storage::concurrency` to `rustfs-io-core`
- Migrated metrics and configuration to `rustfs-io-metrics`
- Updated `rustfs::storage::concurrency::mod.rs` to re-export new module types
- Added API compatibility tests
### Fixed
- Improved buffer size calculation for different storage media
- Enhanced deadlock detection with cycle detection algorithm
- Better backpressure state transitions
### Documentation
- Added comprehensive README.md for both crates
- Added design documentation for I/O scheduler, backpressure, deadlock detection
- Added metrics guide and configuration reference
- Added runnable example code
### Migration Notes
- All original APIs in `rustfs::storage::concurrency` are preserved
- New types are re-exported for gradual migration
- No breaking changes to existing code
## [0.0.4] - Previous Version
### Note
This changelog starts with version 0.0.5 which includes the concurrency module migration.
For previous versions, see the git history.
+41
View File
@@ -0,0 +1,41 @@
# Copyright 2024 RustFS Team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
[package]
name = "rustfs-io-core"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
homepage.workspace = true
description = "Zero-copy core reader and writer implementations for RustFS"
keywords = ["zero-copy", "reader", "writer", "rustfs"]
categories = ["development-tools", "filesystem"]
[lints]
workspace = true
[dependencies]
bytes = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["io-util", "fs", "rt", "sync"] }
memmap2 = { workspace = true }
rustfs-io-metrics = { workspace = true }
[dev-dependencies]
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
[lib]
doctest = false
+280
View File
@@ -0,0 +1,280 @@
# rustfs-io-core
<p align="center">
<a href="https://github.com/rustfs/rustfs/actions/workflows/ci.yml">
<img src="https://github.com/rustfs/rustfs/actions/workflows/ci.yml/badge.svg" alt="CI Status" />
</a>
<a href="https://docs.rs/rustfs-io-core">
<img src="https://docs.rs/rustfs-io-core/badge.svg" alt="Documentation" />
</a>
<a href="https://crates.io/crates/rustfs-io-core">
<img src="https://img.shields.io/crates/v/rustfs-io-core.svg" alt="Crates.io" />
</a>
</p>
<p align="center">
· <a href="https://github.com/rustfs/rustfs">Home</a>
· <a href="https://docs.rs/rustfs-io-core">Docs</a>
· <a href="https://github.com/rustfs/rustfs/issues">Issues</a>
· <a href="https://github.com/rustfs/rustfs/discussions">Discussions</a>
</p>
---
## Overview
**rustfs-io-core** is the core I/O scheduling module for [RustFS](https://rustfs.com), a distributed object storage system. It provides:
- **I/O Scheduler**: Adaptive buffer size calculation and load management
- **Priority Queue**: Request priority scheduling with starvation prevention
- **Backpressure Control**: System overload protection with graceful degradation
- **Deadlock Detection**: Wait-for graph based deadlock detection algorithm
- **Lock Optimizer**: Adaptive spin lock optimization
- **Timeout Wrapper**: Dynamic timeout calculation and operation progress tracking
## Features
### I/O Scheduler
Adaptive I/O scheduling with dynamic buffer size calculation based on file size, access pattern, and system load:
```rust
use rustfs_io_core::{IoScheduler, IoSchedulerConfig, IoLoadLevel};
use rustfs_io_core::io_profile::{StorageMedia, AccessPattern};
// Create scheduler
let config = IoSchedulerConfig {
max_concurrent_reads: 64,
base_buffer_size: 64 * 1024, // 64 KB
max_buffer_size: 1024 * 1024, // 1 MB
..Default::default()
};
let scheduler = IoScheduler::new(config);
// Calculate optimal buffer size
let buffer_size = calculate_optimal_buffer_size(
10 * 1024 * 1024, // 10 MB file
64 * 1024, // base buffer
true, // sequential access
4, // concurrent requests
StorageMedia::Ssd,
IoLoadLevel::Low,
);
```
### Priority Queue
Priority queue with starvation prevention:
```rust
use rustfs_io_core::{IoPriorityQueue, IoPriority, IoQueueStatus};
let queue = IoPriorityQueue::<()>::new(100);
// Enqueue request
let request_id = queue.enqueue(IoPriority::High, (), 1024);
// Dequeue request
if let Some((priority, data)) = queue.dequeue() {
println!("Processing priority {:?} request", priority);
}
// Check queue status
let status = queue.status();
println!("High priority waiting: {}", status.high_priority_waiting);
```
### Backpressure Control
System overload protection:
```rust
use rustfs_io_core::{BackpressureMonitor, BackpressureState, BackpressureConfig};
let config = BackpressureConfig {
high_watermark: 0.8, // 80% triggers backpressure
low_watermark: 0.5, // 50% releases backpressure
..Default::default()
};
let monitor = BackpressureMonitor::new(config);
// Check state
match monitor.state() {
BackpressureState::Normal => println!("System normal"),
BackpressureState::Warning => println!("System warning"),
BackpressureState::Critical => println!("System overloaded"),
}
```
### Deadlock Detection
Wait-for graph based deadlock detection:
```rust
use rustfs_io_core::{DeadlockDetector, LockType};
let detector = DeadlockDetector::with_defaults();
// Register locks
let lock1 = detector.register_lock(LockType::Mutex);
let lock2 = detector.register_lock(LockType::RwLockWrite);
// Record lock acquisition
detector.record_acquire(lock1, 1); // Thread 1 acquires lock1
detector.record_wait(lock2, 1); // Thread 1 waits for lock2
// Detect deadlock
if let Some(deadlock) = detector.detect_deadlock() {
println!("Deadlock detected: {:?}", deadlock);
}
```
### Lock Optimizer
Adaptive spin lock optimization:
```rust
use rustfs_io_core::{LockOptimizer, LockOptimizeConfig};
let optimizer = LockOptimizer::with_defaults();
// Record lock operations
optimizer.on_acquire();
// ... do work ...
optimizer.on_release(std::time::Duration::from_millis(10));
// View statistics
let stats = optimizer.stats();
println!("Locks acquired: {}", stats.total_acquired());
```
### Timeout Wrapper
Dynamic timeout calculation:
```rust
use rustfs_io_core::{RequestTimeoutWrapper, TimeoutConfig};
use std::time::Duration;
let config = TimeoutConfig {
base_timeout: Duration::from_secs(5),
timeout_per_mb: Duration::from_millis(100),
max_timeout: Duration::from_secs(300),
..Default::default()
};
let wrapper = RequestTimeoutWrapper::new(config);
// Calculate operation timeout
let timeout = wrapper.calculate_timeout(10 * 1024 * 1024); // 10 MB
```
## Buffer Size Calculation
Multiple buffer size calculation functions are provided:
```rust
use rustfs_io_core::{
get_concurrency_aware_buffer_size,
get_advanced_buffer_size,
get_buffer_size_for_media,
calculate_optimal_buffer_size,
KI_B, MI_B,
};
use rustfs_io_core::io_profile::StorageMedia;
// Basic calculation
let size1 = get_concurrency_aware_buffer_size(1024 * 1024, 64 * 1024);
// Advanced calculation (considering access pattern)
let size2 = get_advanced_buffer_size(10 * 1024 * 1024, 64 * 1024, true);
// Media type optimization
let size3 = get_buffer_size_for_media(64 * 1024, StorageMedia::Ssd);
// Comprehensive calculation
let size4 = calculate_optimal_buffer_size(
100 * 1024 * 1024, // 100 MB file
64 * 1024, // base buffer
true, // sequential access
4, // concurrent requests
StorageMedia::Nvme,
IoLoadLevel::Low,
);
```
## Configuration
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `RUSTFS_MAX_CONCURRENT_READS` | Max concurrent reads | 64 |
| `RUSTFS_BASE_BUFFER_SIZE` | Base buffer size | 65536 |
| `RUSTFS_MAX_BUFFER_SIZE` | Max buffer size | 1048576 |
| `RUSTFS_IO_TIMEOUT_SECS` | I/O timeout seconds | 30 |
### Code Configuration
```rust
use rustfs_io_core::IoSchedulerConfig;
let config = IoSchedulerConfig {
max_concurrent_reads: 128,
base_buffer_size: 128 * 1024,
max_buffer_size: 4 * 1024 * 1024,
high_priority_threshold: 64 * 1024,
low_priority_threshold: 4 * 1024 * 1024,
..Default::default()
};
// Validate configuration
if let Err(e) = config.validate() {
panic!("Invalid configuration: {}", e);
}
```
## Module Structure
```
rustfs-io-core/
├── src/
│ ├── lib.rs # Module entry
│ ├── config.rs # Configuration types
│ ├── scheduler.rs # I/O scheduler
│ ├── io_priority_queue.rs # Priority queue
│ ├── backpressure.rs # Backpressure control
│ ├── deadlock_detector.rs # Deadlock detection
│ ├── lock_optimizer.rs # Lock optimization
│ ├── timeout_wrapper.rs # Timeout wrapper
│ └── io_profile.rs # I/O profile
└── Cargo.toml
```
## Testing
```bash
# Run all tests
cargo test --package rustfs-io-core
# Run specific tests
cargo test --package rustfs-io-core --lib scheduler
# Run benchmarks
cargo bench --package rustfs-io-core
```
## Documentation
- [API Documentation](https://docs.rs/rustfs-io-core)
- [I/O Scheduler Design](./docs/scheduler-design.md)
- [Backpressure Control Design](./docs/backpressure-design.md)
- [Deadlock Detection Algorithm](./docs/deadlock-detection.md)
## Related Modules
- **rustfs-io-metrics**: Metrics collection and configuration
- **rustfs**: Main storage service
## License
Apache License 2.0
+304
View File
@@ -0,0 +1,304 @@
# rustfs-io-core
<p align="center">
<a href="https://github.com/rustfs/rustfs/actions/workflows/ci.yml">
<img src="https://github.com/rustfs/rustfs/actions/workflows/ci.yml/badge.svg" alt="CI Status" />
</a>
<a href="https://docs.rs/rustfs-io-core">
<img src="https://docs.rs/rustfs-io-core/badge.svg" alt="Documentation" />
</a>
<a href="https://crates.io/crates/rustfs-io-core">
<img src="https://img.shields.io/crates/v/rustfs-io-core.svg" alt="Crates.io" />
</a>
</p>
<p align="center">
· <a href="https://github.com/rustfs/rustfs">🏠 主页</a>
· <a href="https://docs.rs/rustfs-io-core">📚 文档</a>
· <a href="https://github.com/rustfs/rustfs/issues">🐛 问题</a>
· <a href="https://github.com/rustfs/rustfs/discussions">💬 讨论</a>
</p>
---
## 📖 概述
**rustfs-io-core** 是 [RustFS](https://rustfs.com) 分布式对象存储系统的核心 I/O 调度模块。它提供了:
- **I/O 调度器**:自适应缓冲区大小计算和负载管理
- **优先级队列**:支持饥饿预防的请求优先级调度
- **背压控制**:系统过载保护和优雅降级
- **死锁检测**:基于等待图的死锁检测算法
- **锁优化**:自适应自旋锁优化
- **超时包装器**:动态超时计算和操作进度追踪
## ✨ 核心功能
### I/O 调度器 (IoScheduler)
自适应 I/O 调度,根据文件大小、访问模式和系统负载动态调整缓冲区大小:
```rust
use rustfs_io_core::{IoScheduler, IoSchedulerConfig, IoLoadLevel};
use rustfs_io_core::io_profile::{StorageMedia, AccessPattern};
// 创建调度器
let config = IoSchedulerConfig {
max_concurrent_reads: 64,
base_buffer_size: 64 * 1024, // 64 KB
max_buffer_size: 1024 * 1024, // 1 MB
..Default::default()
};
let scheduler = IoScheduler::new(config);
// 计算最优缓冲区大小
let buffer_size = scheduler.calculate_buffer_size(
10 * 1024 * 1024, // 10 MB 文件
true, // 顺序访问
StorageMedia::Ssd,
IoLoadLevel::Low,
);
println!("缓冲区大小: {} bytes", buffer_size);
```
### 优先级队列 (IoPriorityQueue)
支持饥饿预防的优先级队列:
```rust
use rustfs_io_core::{IoPriorityQueue, IoPriority, IoQueueStatus};
let queue = IoPriorityQueue::<()>::new(100);
// 入队请求
let request_id = queue.enqueue(
IoPriority::High,
(), // 请求数据
1024, // 请求大小
);
// 出队请求
if let Some((priority, data)) = queue.dequeue() {
println!("处理优先级 {:?} 的请求", priority);
}
// 检查队列状态
let status = queue.status();
println!("高优先级等待: {}", status.high_priority_waiting);
println!("低优先级等待: {}", status.low_priority_waiting);
```
### 背压控制 (BackpressureMonitor)
系统过载保护:
```rust
use rustfs_io_core::{BackpressureMonitor, BackpressureState, BackpressureConfig};
let config = BackpressureConfig {
high_watermark: 0.8, // 80% 触发背压
low_watermark: 0.5, // 50% 解除背压
..Default::default()
};
let monitor = BackpressureMonitor::new(config);
// 检查状态
match monitor.state() {
BackpressureState::Normal => println!("系统正常"),
BackpressureState::Warning => println!("系统警告"),
BackpressureState::Critical => println!("系统过载"),
}
// 更新负载
monitor.update_load(75, 100); // 当前 75,最大 100
```
### 死锁检测 (DeadlockDetector)
基于等待图的死锁检测:
```rust
use rustfs_io_core::{DeadlockDetector, LockType};
let detector = DeadlockDetector::with_defaults();
// 注册锁
let lock1 = detector.register_lock(LockType::Mutex);
let lock2 = detector.register_lock(LockType::RwLockWrite);
// 记录锁获取
detector.record_acquire(lock1, 1); // 线程 1 获取 lock1
detector.record_wait(lock2, 1); // 线程 1 等待 lock2
// 检测死锁
if let Some(deadlock) = detector.detect_deadlock() {
println!("检测到死锁: {:?}", deadlock);
}
// 清理
detector.unregister_lock(lock1);
detector.unregister_lock(lock2);
```
### 锁优化 (LockOptimizer)
自适应自旋锁优化:
```rust
use rustfs_io_core::{LockOptimizer, LockOptimizeConfig};
let config = LockOptimizeConfig {
max_spin_iterations: 1000,
spin_backoff_factor: 2.0,
..Default::default()
};
let optimizer = LockOptimizer::new(config);
// 获取锁守卫
let guard = optimizer.acquire_lock("my_lock");
// 守卫释放时自动记录统计
drop(guard);
// 查看统计
let stats = optimizer.stats();
println!("获取锁次数: {}", stats.locks_acquired.load(std::sync::atomic::Ordering::Relaxed));
```
### 超时包装器 (RequestTimeoutWrapper)
动态超时计算:
```rust
use rustfs_io_core::{RequestTimeoutWrapper, TimeoutConfig};
use std::time::Duration;
let config = TimeoutConfig {
base_timeout: Duration::from_secs(5),
timeout_per_mb: Duration::from_millis(100),
max_timeout: Duration::from_secs(300),
..Default::default()
};
let wrapper = RequestTimeoutWrapper::new(config);
// 计算操作超时
let timeout = wrapper.calculate_timeout(10 * 1024 * 1024); // 10 MB
println!("超时时间: {:?}", timeout);
// 执行带超时的操作
let result = wrapper.execute_with_timeout(async {
// 异步操作
Ok::<_, std::io::Error>(())
}, timeout).await;
```
## 📊 缓冲区大小计算
模块提供了多种缓冲区大小计算函数:
```rust
use rustfs_io_core::{
get_concurrency_aware_buffer_size,
get_advanced_buffer_size,
get_buffer_size_for_media,
calculate_optimal_buffer_size,
KI_B, MI_B,
};
use rustfs_io_core::io_profile::StorageMedia;
// 基础计算
let size1 = get_concurrency_aware_buffer_size(1024 * 1024, 64 * 1024);
// 高级计算(考虑访问模式)
let size2 = get_advanced_buffer_size(10 * 1024 * 1024, 64 * 1024, true);
// 媒体类型优化
let size3 = get_buffer_size_for_media(64 * 1024, StorageMedia::Ssd);
// 综合计算
let size4 = calculate_optimal_buffer_size(
100 * 1024 * 1024, // 100 MB 文件
64 * 1024, // 基础缓冲区
true, // 顺序访问
4, // 并发请求数
StorageMedia::Nvme,
IoLoadLevel::Low,
);
```
## 🔧 配置
### 环境变量
| 变量名 | 描述 | 默认值 |
|--------|------|--------|
| `RUSTFS_MAX_CONCURRENT_READS` | 最大并发读数 | 64 |
| `RUSTFS_BASE_BUFFER_SIZE` | 基础缓冲区大小 | 65536 |
| `RUSTFS_MAX_BUFFER_SIZE` | 最大缓冲区大小 | 1048576 |
| `RUSTFS_IO_TIMEOUT_SECS` | I/O 超时秒数 | 30 |
### 代码配置
```rust
use rustfs_io_core::IoSchedulerConfig;
let config = IoSchedulerConfig {
max_concurrent_reads: 128,
base_buffer_size: 128 * 1024,
max_buffer_size: 4 * 1024 * 1024,
high_priority_threshold: 64 * 1024,
low_priority_threshold: 4 * 1024 * 1024,
..Default::default()
};
// 验证配置
if let Err(e) = config.validate() {
panic!("配置无效: {}", e);
}
```
## 📁 模块结构
```
rustfs-io-core/
├── src/
│ ├── lib.rs # 模块入口
│ ├── config.rs # 配置类型
│ ├── scheduler.rs # I/O 调度器
│ ├── io_priority_queue.rs # 优先级队列
│ ├── backpressure.rs # 背压控制
│ ├── deadlock_detector.rs # 死锁检测
│ ├── lock_optimizer.rs # 锁优化
│ ├── timeout_wrapper.rs # 超时包装器
│ └── io_profile.rs # I/O 配置文件
└── Cargo.toml
```
## 🧪 测试
```bash
# 运行所有测试
cargo test --package rustfs-io-core
# 运行特定测试
cargo test --package rustfs-io-core --lib scheduler
# 运行基准测试
cargo bench --package rustfs-io-core
```
## 📚 文档
- [API 文档](https://docs.rs/rustfs-io-core)
- [I/O 调度器设计](./docs/scheduler-design.md)
- [背压控制原理](./docs/backpressure-design.md)
- [死锁检测算法](./docs/deadlock-detection.md)
## 🔗 相关模块
- **rustfs-io-metrics**: 指标收集和配置管理
- **rustfs**: 主存储服务
## 📄 许可证
Apache License 2.0
@@ -0,0 +1,190 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Example demonstrating I/O scheduler usage.
use rustfs_io_core::io_profile::StorageMedia;
use rustfs_io_core::{
BackpressureMonitor, BackpressureState, DeadlockDetector, IoLoadLevel, IoScheduler, IoSchedulerConfig, KI_B, LockOptimizer,
LockType, MI_B, calculate_optimal_buffer_size, get_buffer_size_for_media,
};
use std::time::Duration;
fn main() {
println!("=== rustfs-io-core Example ===\n");
// 1. I/O scheduler example
io_scheduler_example();
// 2. Buffer size calculation example
buffer_size_example();
// 3. Backpressure control example
backpressure_example();
// 4. Deadlock detection example
deadlock_detection_example();
// 5. Lock optimizer example
lock_optimizer_example();
}
fn io_scheduler_example() {
println!("--- I/O Scheduler ---");
// Create scheduler with configuration
let config = IoSchedulerConfig {
max_concurrent_reads: 64,
base_buffer_size: 64 * KI_B,
max_buffer_size: MI_B,
..Default::default()
};
let scheduler = IoScheduler::new(config);
println!(" Max concurrent reads: {}", scheduler.config().max_concurrent_reads);
println!(" Base buffer size: {} KB", scheduler.config().base_buffer_size / KI_B);
println!(" Max buffer size: {} KB", scheduler.config().max_buffer_size / KI_B);
// Calculate buffer sizes for different scenarios
let scenarios = [
("Small file", 10 * KI_B as i64, true, StorageMedia::Ssd),
("Medium file", MI_B as i64, true, StorageMedia::Ssd),
("Large sequential", 100 * MI_B as i64, true, StorageMedia::Ssd),
("Large random", 100 * MI_B as i64, false, StorageMedia::Ssd),
("NVMe large", 100 * MI_B as i64, true, StorageMedia::Nvme),
("HDD large", 100 * MI_B as i64, true, StorageMedia::Hdd),
];
for (name, size, sequential, media) in scenarios {
let buffer = calculate_optimal_buffer_size(size, 64 * KI_B, sequential, 4, media, IoLoadLevel::Low);
println!(" {}: {} bytes ({} KB)", name, buffer, buffer / KI_B);
}
println!();
}
fn buffer_size_example() {
println!("--- Buffer Size Calculation ---");
// Comprehensive calculation
let size1 = calculate_optimal_buffer_size(10 * MI_B as i64, 64 * KI_B, true, 4, StorageMedia::Ssd, IoLoadLevel::Low);
println!(" Comprehensive (10MB, sequential, SSD): {} KB", size1 / KI_B);
// Media type optimization
let media_types = [
StorageMedia::Nvme,
StorageMedia::Ssd,
StorageMedia::Hdd,
StorageMedia::Unknown,
];
for media in media_types {
let size = get_buffer_size_for_media(64 * KI_B, media);
println!(" {} optimized: {} KB", media.as_str(), size / KI_B);
}
println!();
}
fn backpressure_example() {
println!("--- Backpressure Control ---");
let monitor = BackpressureMonitor::with_defaults();
// Check initial state
let state = monitor.state();
let state_str = match state {
BackpressureState::Normal => "Normal",
BackpressureState::Warning => "Warning",
BackpressureState::Critical => "Critical",
};
println!(" Initial state: {}", state_str);
// Check if active
let is_active = monitor.is_active();
println!(" Backpressure active: {}", is_active);
// Try to acquire permit
if monitor.try_acquire() {
println!(" Successfully acquired permit");
monitor.release();
println!(" Released permit");
}
// View statistics
println!(" Total processed: {}", monitor.total_processed());
println!(" Total rejected: {}", monitor.total_rejected());
println!();
}
fn deadlock_detection_example() {
println!("--- Deadlock Detection ---");
let detector = DeadlockDetector::with_defaults();
// Register locks
let mutex1 = detector.register_lock(LockType::Mutex);
let mutex2 = detector.register_lock(LockType::Mutex);
println!(" Registered locks: mutex1={}, mutex2={}", mutex1, mutex2);
// Simulate normal operation
detector.record_acquire(mutex1, 1); // Thread 1 acquires mutex1
detector.record_acquire(mutex2, 2); // Thread 2 acquires mutex2
println!(" Normal operation: no deadlock");
// Detect deadlock
if detector.detect_deadlock().is_none() {
println!(" Detection result: no deadlock");
}
// Simulate deadlock scenario
detector.record_wait(mutex2, 1); // Thread 1 waits for mutex2
detector.record_wait(mutex1, 2); // Thread 2 waits for mutex1
// Detect deadlock
if let Some(deadlock) = detector.detect_deadlock() {
println!(" Detection result: deadlock found {:?}", deadlock);
}
// Cleanup
detector.unregister_lock(mutex1);
detector.unregister_lock(mutex2);
println!();
}
fn lock_optimizer_example() {
println!("--- Lock Optimizer ---");
let optimizer = LockOptimizer::with_defaults();
// Simulate lock operations
for _i in 0..5 {
optimizer.on_acquire();
// Simulate work
std::thread::sleep(Duration::from_millis(10));
optimizer.on_release(Duration::from_millis(10));
}
// View statistics
let stats = optimizer.stats();
let acquired = stats.total_acquired();
let avg_hold = stats.avg_hold_time();
let contention = stats.contention_rate();
println!(" Locks acquired: {}", acquired);
println!(" Average hold time: {:?}", avg_hold);
println!(" Contention rate: {:.2}%", contention * 100.0);
println!();
}
+394
View File
@@ -0,0 +1,394 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Backpressure management for I/O operations.
//!
//! This module provides backpressure mechanisms to prevent system overload
//! and maintain stability under high load conditions.
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::time::{Duration, Instant};
/// Backpressure configuration.
#[derive(Debug, Clone)]
pub struct BackpressureConfig {
/// Maximum concurrent operations.
pub max_concurrent: usize,
/// High water mark (percentage of max_concurrent).
pub high_water_mark: f64,
/// Low water mark (percentage of max_concurrent).
pub low_water_mark: f64,
/// Cooldown period after applying backpressure.
pub cooldown: Duration,
/// Whether backpressure is enabled.
pub enabled: bool,
}
impl Default for BackpressureConfig {
fn default() -> Self {
Self {
max_concurrent: 32,
high_water_mark: 0.8,
low_water_mark: 0.5,
cooldown: Duration::from_millis(100),
enabled: true,
}
}
}
impl BackpressureConfig {
/// Create new configuration.
pub fn new() -> Self {
Self::default()
}
/// Get the high water mark threshold.
pub fn high_threshold(&self) -> usize {
(self.max_concurrent as f64 * self.high_water_mark) as usize
}
/// Get the low water mark threshold.
pub fn low_threshold(&self) -> usize {
(self.max_concurrent as f64 * self.low_water_mark) as usize
}
/// Validate the configuration.
pub fn validate(&self) -> Result<(), BackpressureError> {
if self.max_concurrent == 0 {
return Err(BackpressureError::InvalidConfig("max_concurrent must be > 0".to_string()));
}
if self.high_water_mark <= self.low_water_mark || self.high_water_mark > 1.0 {
return Err(BackpressureError::InvalidConfig(
"high_water_mark must be > low_water_mark and <= 1.0".to_string(),
));
}
if self.low_water_mark < 0.0 {
return Err(BackpressureError::InvalidConfig("low_water_mark must be >= 0.0".to_string()));
}
Ok(())
}
}
/// Backpressure error.
#[derive(Debug, Clone, thiserror::Error)]
pub enum BackpressureError {
/// Invalid configuration.
#[error("Invalid backpressure config: {0}")]
InvalidConfig(String),
}
/// Backpressure state.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BackpressureState {
/// Normal operation.
#[default]
Normal,
/// Warning: approaching high water mark.
Warning,
/// Critical: backpressure applied.
Critical,
}
impl BackpressureState {
/// Get state as string.
pub fn as_str(&self) -> &'static str {
match self {
BackpressureState::Normal => "normal",
BackpressureState::Warning => "warning",
BackpressureState::Critical => "critical",
}
}
}
/// Backpressure monitor.
pub struct BackpressureMonitor {
/// Configuration.
config: BackpressureConfig,
/// Current concurrent operations.
current: AtomicUsize,
/// Total operations processed.
total_processed: AtomicU64,
/// Total operations rejected.
total_rejected: AtomicU64,
/// Current state.
state: std::sync::Mutex<BackpressureState>,
/// Last state change time.
last_state_change: std::sync::Mutex<Option<Instant>>,
/// Whether backpressure is currently active.
active: AtomicBool,
}
impl BackpressureMonitor {
/// Create a new backpressure monitor.
pub fn new(config: BackpressureConfig) -> Self {
Self {
config,
current: AtomicUsize::new(0),
total_processed: AtomicU64::new(0),
total_rejected: AtomicU64::new(0),
state: std::sync::Mutex::new(BackpressureState::Normal),
last_state_change: std::sync::Mutex::new(None),
active: AtomicBool::new(false),
}
}
/// Create with default configuration.
pub fn with_defaults() -> Self {
Self::new(BackpressureConfig::default())
}
/// Get the configuration.
pub fn config(&self) -> &BackpressureConfig {
&self.config
}
/// Get current concurrent operations.
pub fn current(&self) -> usize {
self.current.load(Ordering::Relaxed)
}
/// Get current state.
pub fn state(&self) -> BackpressureState {
if let Ok(state) = self.state.lock() {
*state
} else {
BackpressureState::Normal
}
}
/// Check if backpressure is active.
pub fn is_active(&self) -> bool {
self.active.load(Ordering::Relaxed)
}
/// Try to acquire a slot for a new operation.
///
/// Returns true if the operation should proceed, false if it should be rejected.
pub fn try_acquire(&self) -> bool {
if !self.config.enabled {
self.current.fetch_add(1, Ordering::Relaxed);
self.total_processed.fetch_add(1, Ordering::Relaxed);
return true;
}
let high_threshold = self.config.high_threshold();
// Use a CAS loop to ensure we never exceed `max_concurrent` under contention.
loop {
let current = self.current.load(Ordering::Relaxed);
if current >= self.config.max_concurrent {
// At capacity: reject
self.total_rejected.fetch_add(1, Ordering::Relaxed);
return false;
}
let new = current + 1;
match self
.current
.compare_exchange_weak(current, new, Ordering::Relaxed, Ordering::Relaxed)
{
Ok(_) => {
// Successfully acquired a slot.
self.total_processed.fetch_add(1, Ordering::Relaxed);
// Update state if needed, based on the pre-increment value `current`.
if current >= high_threshold {
self.set_state(BackpressureState::Critical);
self.active.store(true, Ordering::Relaxed);
} else if current >= self.config.low_threshold() {
self.set_state(BackpressureState::Warning);
}
return true;
}
Err(_) => {
// Another thread raced with us; retry with the updated value.
continue;
}
}
}
}
/// Release a slot after operation completes.
pub fn release(&self) {
let prev = self.current.fetch_sub(1, Ordering::Relaxed);
let low_threshold = self.config.low_threshold();
// Update state if needed
if prev <= low_threshold + 1 {
self.set_state(BackpressureState::Normal);
self.active.store(false, Ordering::Relaxed);
}
}
/// Set the state.
fn set_state(&self, new_state: BackpressureState) {
if let Ok(mut state) = self.state.lock()
&& *state != new_state
{
*state = new_state;
if let Ok(mut last) = self.last_state_change.lock() {
*last = Some(Instant::now());
}
}
}
/// Get total processed operations.
pub fn total_processed(&self) -> u64 {
self.total_processed.load(Ordering::Relaxed)
}
/// Get total rejected operations.
pub fn total_rejected(&self) -> u64 {
self.total_rejected.load(Ordering::Relaxed)
}
/// Get rejection rate.
pub fn rejection_rate(&self) -> f64 {
let processed = self.total_processed.load(Ordering::Relaxed);
let rejected = self.total_rejected.load(Ordering::Relaxed);
let total = processed + rejected;
if total == 0 { 0.0 } else { rejected as f64 / total as f64 }
}
/// Check if we should apply backpressure based on cooldown.
pub fn should_apply_backpressure(&self) -> bool {
if !self.config.enabled {
return false;
}
let current = self.current.load(Ordering::Relaxed);
if current < self.config.high_threshold() {
return false;
}
// Check cooldown
if let Ok(last) = self.last_state_change.lock()
&& let Some(last_time) = *last
&& last_time.elapsed() < self.config.cooldown
{
return false;
}
true
}
/// Reset statistics.
pub fn reset_stats(&self) {
self.total_processed.store(0, Ordering::Relaxed);
self.total_rejected.store(0, Ordering::Relaxed);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_backpressure_config() {
let config = BackpressureConfig::default();
assert!(config.validate().is_ok());
assert_eq!(config.high_threshold(), 25); // 32 * 0.8
assert_eq!(config.low_threshold(), 16); // 32 * 0.5
}
#[test]
fn test_backpressure_config_validation() {
let config = BackpressureConfig {
max_concurrent: 0,
..Default::default()
};
assert!(config.validate().is_err());
let config = BackpressureConfig {
high_water_mark: 0.3,
low_water_mark: 0.5,
..Default::default()
};
assert!(config.validate().is_err());
}
#[test]
fn test_backpressure_monitor() {
let config = BackpressureConfig {
max_concurrent: 4,
high_water_mark: 0.75, // high threshold = 3
low_water_mark: 0.5, // low threshold = 2
..Default::default()
};
let monitor = BackpressureMonitor::new(config);
// Acquire slots - current = 1 after acquire
assert!(monitor.try_acquire());
// State is Normal (1 < low_threshold=2)
// current = 2 after acquire
assert!(monitor.try_acquire());
// State should be Warning (2 >= low_threshold=2)
// current = 3 after acquire
assert!(monitor.try_acquire());
// State should be Critical (3 >= high_threshold=3)
// current = 4 after acquire
assert!(monitor.try_acquire());
// At capacity now
assert!(!monitor.try_acquire()); // Should be rejected
// Release slots - current = 3 after release
monitor.release();
// State is still Critical (3 >= high_threshold=3)
// current = 2 after release
monitor.release();
// State should be Warning (2 >= low_threshold=2 but < high_threshold=3)
// current = 1 after release
monitor.release();
// State should be Normal (1 < low_threshold=2)
assert_eq!(monitor.state(), BackpressureState::Normal);
}
#[test]
fn test_rejection_rate() {
let config = BackpressureConfig {
max_concurrent: 2,
..Default::default()
};
let monitor = BackpressureMonitor::new(config);
assert!(monitor.try_acquire());
assert!(monitor.try_acquire());
assert!(!monitor.try_acquire()); // Rejected
assert!((monitor.rejection_rate() - 0.3333333333333333).abs() < 0.01);
}
#[test]
fn test_disabled_backpressure() {
let config = BackpressureConfig {
max_concurrent: 1,
enabled: false,
..Default::default()
};
let monitor = BackpressureMonitor::new(config);
// Should always succeed when disabled
assert!(monitor.try_acquire());
assert!(monitor.try_acquire());
assert!(monitor.try_acquire());
}
}
+227
View File
@@ -0,0 +1,227 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! BufReader layer optimizer for minimizing redundant buffering layers.
//!
//! This module provides optimization for BufReader usage in data paths,
//! including layer count limiting and dynamic buffer size adjustment.
use std::sync::atomic::{AtomicU64, Ordering};
/// BufReader optimization configuration.
#[derive(Debug, Clone)]
pub struct BufReaderConfig {
/// Maximum number of nested BufReader layers (default: 2)
pub max_layers: u32,
/// Buffer size for small files (default: 8KB)
pub small_file_buffer: usize,
/// Buffer size for large files (default: 64KB)
pub large_file_buffer: usize,
/// Threshold for large file classification (default: 1MB)
pub large_file_threshold: usize,
}
impl Default for BufReaderConfig {
fn default() -> Self {
Self {
max_layers: 2,
small_file_buffer: 8 * 1024, // 8KB
large_file_buffer: 64 * 1024, // 64KB
large_file_threshold: 1024 * 1024, // 1MB
}
}
}
/// BufReader optimization statistics.
#[derive(Debug, Default)]
pub struct BufReaderStats {
/// Total number of readers created
pub total_readers: AtomicU64,
/// Number of redundant layers eliminated
pub eliminated_layers: AtomicU64,
/// Number of buffer size adjustments
pub buffer_size_adjustments: AtomicU64,
}
/// BufReader layer optimizer.
///
/// Analyzes and optimizes BufReader nesting in data paths,
/// dynamically adjusting buffer sizes based on data characteristics.
pub struct BufReaderOptimizer {
config: BufReaderConfig,
stats: BufReaderStats,
}
impl BufReaderOptimizer {
/// Create a new BufReader optimizer with the given configuration.
pub fn new(config: BufReaderConfig) -> Self {
Self {
config,
stats: BufReaderStats::default(),
}
}
/// Create a new BufReader optimizer with default configuration.
pub fn with_defaults() -> Self {
Self::new(BufReaderConfig::default())
}
/// Calculate the optimal buffer size based on data size.
///
/// Returns the appropriate buffer size based on whether the data
/// is classified as a small or large file.
pub fn optimal_buffer_size(&self, data_size: Option<usize>) -> usize {
match data_size {
Some(size) if size >= self.config.large_file_threshold => self.config.large_file_buffer,
Some(_) => self.config.small_file_buffer,
None => self.config.small_file_buffer,
}
}
/// Optimize a reader by wrapping it with an appropriately sized BufReader.
///
/// This method applies the optimal buffer size based on the expected
/// data size and tracks statistics.
pub fn optimize<R: tokio::io::AsyncRead + Unpin>(&self, reader: R, data_size: Option<usize>) -> tokio::io::BufReader<R> {
let buffer_size = self.optimal_buffer_size(data_size);
self.stats.total_readers.fetch_add(1, Ordering::Relaxed);
tokio::io::BufReader::with_capacity(buffer_size, reader)
}
/// Get the statistics for this optimizer.
pub fn stats(&self) -> &BufReaderStats {
&self.stats
}
/// Get the configuration for this optimizer.
pub fn config(&self) -> &BufReaderConfig {
&self.config
}
}
/// Marker trait for buffered sources.
///
/// Types implementing this trait are considered already buffered
/// and should not be wrapped with additional BufReader layers.
pub trait BufferedSource: tokio::io::AsyncRead {}
impl BufReaderOptimizer {
/// Check if a reader is already a buffered source.
///
/// Returns true if the reader implements `BufferedSource`,
/// indicating it should not be wrapped with BufReader.
pub fn is_buffered_source<R: BufferedSource + ?Sized>(&self, _reader: &R) -> bool {
true
}
/// Eliminate redundant BufReader layers if possible.
///
/// This method attempts to reduce the nesting depth of BufReader
/// layers to improve performance.
pub fn eliminate_redundant_layers<R: tokio::io::AsyncRead + Unpin>(&self, reader: R) -> R {
// For now, just return the reader as-is
// Future implementation could detect and unwrap nested BufReaders
self.stats.eliminated_layers.fetch_add(0, Ordering::Relaxed);
reader
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::AsyncReadExt;
#[test]
fn test_default_config() {
let config = BufReaderConfig::default();
assert_eq!(config.max_layers, 2);
assert_eq!(config.small_file_buffer, 8 * 1024);
assert_eq!(config.large_file_buffer, 64 * 1024);
assert_eq!(config.large_file_threshold, 1024 * 1024);
}
#[test]
fn test_optimal_buffer_size_small_file() {
let optimizer = BufReaderOptimizer::with_defaults();
// Small file (< 1MB)
assert_eq!(optimizer.optimal_buffer_size(Some(100)), 8 * 1024);
assert_eq!(optimizer.optimal_buffer_size(Some(1024)), 8 * 1024);
assert_eq!(optimizer.optimal_buffer_size(Some(512 * 1024)), 8 * 1024);
}
#[test]
fn test_optimal_buffer_size_large_file() {
let optimizer = BufReaderOptimizer::with_defaults();
// Large file (>= 1MB)
assert_eq!(optimizer.optimal_buffer_size(Some(1024 * 1024)), 64 * 1024);
assert_eq!(optimizer.optimal_buffer_size(Some(10 * 1024 * 1024)), 64 * 1024);
}
#[test]
fn test_optimal_buffer_size_unknown() {
let optimizer = BufReaderOptimizer::with_defaults();
// Unknown size
assert_eq!(optimizer.optimal_buffer_size(None), 8 * 1024);
}
#[tokio::test]
async fn test_optimize_creates_bufreader() {
let optimizer = BufReaderOptimizer::with_defaults();
let data = vec![1u8, 2, 3, 4, 5];
let cursor = std::io::Cursor::new(data.clone());
let mut reader = optimizer.optimize(cursor, Some(5));
let mut buf = vec![0u8; 5];
let n = reader.read(&mut buf).await.unwrap();
assert_eq!(n, 5);
assert_eq!(buf, data);
}
#[test]
fn test_stats_tracking() {
let optimizer = BufReaderOptimizer::with_defaults();
assert_eq!(optimizer.stats().total_readers.load(Ordering::Relaxed), 0);
let cursor = std::io::Cursor::new(vec![1u8, 2, 3]);
let _reader = optimizer.optimize(cursor, Some(3));
assert_eq!(optimizer.stats().total_readers.load(Ordering::Relaxed), 1);
}
#[test]
fn test_custom_config() {
let config = BufReaderConfig {
max_layers: 3,
small_file_buffer: 4 * 1024,
large_file_buffer: 128 * 1024,
large_file_threshold: 2 * 1024 * 1024,
};
let optimizer = BufReaderOptimizer::new(config);
assert_eq!(optimizer.optimal_buffer_size(Some(1024 * 1024)), 4 * 1024);
assert_eq!(optimizer.optimal_buffer_size(Some(3 * 1024 * 1024)), 128 * 1024);
}
}
+283
View File
@@ -0,0 +1,283 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! I/O scheduler configuration types.
//!
//! This module provides configuration types for the I/O scheduler,
//! including priority thresholds, queue capacities, and load thresholds.
use std::time::Duration;
/// I/O scheduler configuration.
#[derive(Debug, Clone, PartialEq)]
pub struct IoSchedulerConfig {
/// Maximum concurrent disk reads.
pub max_concurrent_reads: usize,
/// High priority size threshold in bytes.
pub high_priority_size_threshold: usize,
/// Low priority size threshold in bytes.
pub low_priority_size_threshold: usize,
/// High priority queue capacity.
pub queue_high_capacity: usize,
/// Normal priority queue capacity.
pub queue_normal_capacity: usize,
/// Low priority queue capacity.
pub queue_low_capacity: usize,
/// Starvation prevention check interval in milliseconds.
pub starvation_prevention_interval_ms: u64,
/// Starvation threshold in seconds.
pub starvation_threshold_secs: u64,
/// Load sampling window size.
pub load_sample_window: usize,
/// High load wait time threshold in milliseconds.
pub load_high_threshold_ms: u64,
/// Low load wait time threshold in milliseconds.
pub load_low_threshold_ms: u64,
/// Whether priority scheduling is enabled.
pub enable_priority: bool,
// Enhanced scheduling configuration fields
/// Storage media detection enabled.
pub storage_detection_enabled: bool,
/// Sequential detection enabled.
pub sequential_detection_enabled: bool,
/// Bandwidth monitoring enabled.
pub bandwidth_monitoring_enabled: bool,
/// Adaptive buffer sizing enabled.
pub adaptive_buffer_enabled: bool,
/// Base buffer size for I/O operations.
pub base_buffer_size: usize,
/// Maximum buffer size.
pub max_buffer_size: usize,
/// Minimum buffer size.
pub min_buffer_size: usize,
}
impl Default for IoSchedulerConfig {
fn default() -> Self {
Self {
max_concurrent_reads: 32,
high_priority_size_threshold: 64 * 1024, // 64KB
low_priority_size_threshold: 4 * 1024 * 1024, // 4MB
queue_high_capacity: 100,
queue_normal_capacity: 500,
queue_low_capacity: 200,
starvation_prevention_interval_ms: 100,
starvation_threshold_secs: 5,
load_sample_window: 10,
load_high_threshold_ms: 50,
load_low_threshold_ms: 5,
enable_priority: true,
storage_detection_enabled: true,
sequential_detection_enabled: true,
bandwidth_monitoring_enabled: true,
adaptive_buffer_enabled: true,
base_buffer_size: 128 * 1024, // 128KB
max_buffer_size: 1024 * 1024, // 1MB
min_buffer_size: 4 * 1024, // 4KB
}
}
}
impl IoSchedulerConfig {
/// Create a new configuration with default values.
pub fn new() -> Self {
Self::default()
}
/// Validate the configuration.
///
/// # Errors
///
/// Returns an error if any configuration value is invalid.
pub fn validate(&self) -> Result<(), ConfigError> {
if self.max_concurrent_reads == 0 {
return Err(ConfigError::InvalidValue("max_concurrent_reads must be > 0".to_string()));
}
if self.high_priority_size_threshold >= self.low_priority_size_threshold {
return Err(ConfigError::InvalidValue(
"high_priority_size_threshold must be < low_priority_size_threshold".to_string(),
));
}
if self.min_buffer_size > self.max_buffer_size {
return Err(ConfigError::InvalidValue("min_buffer_size must be <= max_buffer_size".to_string()));
}
if self.base_buffer_size < self.min_buffer_size || self.base_buffer_size > self.max_buffer_size {
return Err(ConfigError::InvalidValue(
"base_buffer_size must be between min_buffer_size and max_buffer_size".to_string(),
));
}
Ok(())
}
/// Get the starvation prevention interval as a Duration.
pub fn starvation_prevention_interval(&self) -> Duration {
Duration::from_millis(self.starvation_prevention_interval_ms)
}
/// Get the starvation threshold as a Duration.
pub fn starvation_threshold(&self) -> Duration {
Duration::from_secs(self.starvation_threshold_secs)
}
/// Get the high load threshold as a Duration.
pub fn load_high_threshold(&self) -> Duration {
Duration::from_millis(self.load_high_threshold_ms)
}
/// Get the low load threshold as a Duration.
pub fn load_low_threshold(&self) -> Duration {
Duration::from_millis(self.load_low_threshold_ms)
}
/// Builder pattern: set max concurrent reads.
pub fn with_max_concurrent_reads(mut self, value: usize) -> Self {
self.max_concurrent_reads = value;
self
}
/// Builder pattern: set priority thresholds.
pub fn with_priority_thresholds(mut self, high: usize, low: usize) -> Self {
self.high_priority_size_threshold = high;
self.low_priority_size_threshold = low;
self
}
/// Builder pattern: set buffer sizes.
pub fn with_buffer_sizes(mut self, base: usize, min: usize, max: usize) -> Self {
self.base_buffer_size = base;
self.min_buffer_size = min;
self.max_buffer_size = max;
self
}
/// Builder pattern: enable/disable priority scheduling.
pub fn with_priority_enabled(mut self, enabled: bool) -> Self {
self.enable_priority = enabled;
self
}
}
/// Configuration error type.
#[derive(Debug, Clone, thiserror::Error)]
pub enum ConfigError {
/// Invalid configuration value.
#[error("Invalid configuration: {0}")]
InvalidValue(String),
}
/// I/O priority queue configuration.
#[derive(Debug, Clone, PartialEq)]
pub struct IoPriorityQueueConfig {
/// High priority queue capacity.
pub high_capacity: usize,
/// Normal priority queue capacity.
pub normal_capacity: usize,
/// Low priority queue capacity.
pub low_capacity: usize,
/// Starvation prevention interval.
pub starvation_interval: Duration,
/// Starvation threshold.
pub starvation_threshold: Duration,
}
impl Default for IoPriorityQueueConfig {
fn default() -> Self {
Self {
high_capacity: 100,
normal_capacity: 500,
low_capacity: 200,
starvation_interval: Duration::from_millis(100),
starvation_threshold: Duration::from_secs(5),
}
}
}
impl IoPriorityQueueConfig {
/// Create from IoSchedulerConfig.
pub fn from_scheduler_config(config: &IoSchedulerConfig) -> Self {
Self {
high_capacity: config.queue_high_capacity,
normal_capacity: config.queue_normal_capacity,
low_capacity: config.queue_low_capacity,
starvation_interval: config.starvation_prevention_interval(),
starvation_threshold: config.starvation_threshold(),
}
}
/// Get total capacity across all queues.
pub fn total_capacity(&self) -> usize {
self.high_capacity + self.normal_capacity + self.low_capacity
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = IoSchedulerConfig::default();
assert!(config.validate().is_ok());
assert!(config.enable_priority);
assert!(config.adaptive_buffer_enabled);
}
#[test]
fn test_config_validation() {
let config = IoSchedulerConfig::new().with_max_concurrent_reads(0);
assert!(config.validate().is_err());
let config = IoSchedulerConfig::new().with_priority_thresholds(1024 * 1024, 1024);
assert!(config.validate().is_err());
let config = IoSchedulerConfig::new().with_buffer_sizes(1024, 4096, 512);
assert!(config.validate().is_err());
}
#[test]
fn test_builder_pattern() {
let config = IoSchedulerConfig::new()
.with_max_concurrent_reads(64)
.with_priority_thresholds(32 * 1024, 8 * 1024 * 1024)
.with_buffer_sizes(256 * 1024, 8 * 1024, 2 * 1024 * 1024)
.with_priority_enabled(false);
assert_eq!(config.max_concurrent_reads, 64);
assert_eq!(config.high_priority_size_threshold, 32 * 1024);
assert!(!config.enable_priority);
assert!(config.validate().is_ok());
}
#[test]
fn test_priority_queue_config() {
let config = IoSchedulerConfig::default();
let pq_config = IoPriorityQueueConfig::from_scheduler_config(&config);
assert_eq!(pq_config.high_capacity, config.queue_high_capacity);
assert_eq!(pq_config.normal_capacity, config.queue_normal_capacity);
assert_eq!(pq_config.low_capacity, config.queue_low_capacity);
assert!(pq_config.total_capacity() > 0);
}
#[test]
fn test_duration_helpers() {
let config = IoSchedulerConfig::default();
assert_eq!(config.starvation_prevention_interval(), Duration::from_millis(100));
assert_eq!(config.starvation_threshold(), Duration::from_secs(5));
assert_eq!(config.load_high_threshold(), Duration::from_millis(50));
assert_eq!(config.load_low_threshold(), Duration::from_millis(5));
}
}
+447
View File
@@ -0,0 +1,447 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Deadlock detection for concurrent operations.
//!
//! This module provides deadlock detection mechanisms using wait-for graphs
//! to identify potential circular dependencies between locks.
use std::collections::{HashMap, HashSet};
use std::sync::Mutex;
use std::time::{Duration, Instant};
/// Lock type identifier.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LockType {
/// Mutex lock.
Mutex,
/// RwLock (read).
RwLockRead,
/// RwLock (write).
RwLockWrite,
/// Semaphore.
Semaphore,
}
impl LockType {
/// Get as string.
pub fn as_str(&self) -> &'static str {
match self {
LockType::Mutex => "mutex",
LockType::RwLockRead => "rwlock_read",
LockType::RwLockWrite => "rwlock_write",
LockType::Semaphore => "semaphore",
}
}
}
/// Lock information.
#[derive(Debug, Clone)]
pub struct LockInfo {
/// Lock ID.
pub id: u64,
/// Lock type.
pub lock_type: LockType,
/// Owner thread ID (if held).
pub owner: Option<u64>,
/// Waiters (thread IDs).
pub waiters: Vec<u64>,
/// Acquisition time.
pub acquired_at: Option<Instant>,
}
impl LockInfo {
/// Create new lock info.
pub fn new(id: u64, lock_type: LockType) -> Self {
Self {
id,
lock_type,
owner: None,
waiters: Vec::new(),
acquired_at: None,
}
}
/// Check if the lock is held.
pub fn is_held(&self) -> bool {
self.owner.is_some()
}
/// Get hold duration.
pub fn hold_duration(&self) -> Option<Duration> {
self.acquired_at.map(|t| t.elapsed())
}
}
/// Wait graph edge (thread A waits for thread B).
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct WaitGraphEdge {
/// Waiting thread ID.
pub waiter: u64,
/// Resource/thread being waited for.
pub waited_for: u64,
/// Lock ID involved.
pub lock_id: u64,
}
/// Deadlock detector configuration.
#[derive(Debug, Clone)]
pub struct DeadlockDetectorConfig {
/// Detection interval.
pub detection_interval: Duration,
/// Maximum lock hold time before warning.
pub max_hold_time: Duration,
/// Whether detection is enabled.
pub enabled: bool,
}
impl Default for DeadlockDetectorConfig {
fn default() -> Self {
Self {
detection_interval: Duration::from_secs(1),
max_hold_time: Duration::from_secs(30),
enabled: true,
}
}
}
/// Deadlock detector.
pub struct DeadlockDetector {
/// Configuration.
config: DeadlockDetectorConfig,
/// Registered locks.
locks: Mutex<HashMap<u64, LockInfo>>,
/// Wait graph edges.
wait_graph: Mutex<Vec<WaitGraphEdge>>,
/// Tracked requests (request_id -> thread_id).
requests: Mutex<HashMap<String, u64>>,
/// Next lock ID.
next_lock_id: Mutex<u64>,
}
impl DeadlockDetector {
/// Create a new deadlock detector.
pub fn new(config: DeadlockDetectorConfig) -> Self {
Self {
config,
locks: Mutex::new(HashMap::new()),
wait_graph: Mutex::new(Vec::new()),
requests: Mutex::new(HashMap::new()),
next_lock_id: Mutex::new(0),
}
}
/// Create with default configuration.
pub fn with_defaults() -> Self {
Self::new(DeadlockDetectorConfig::default())
}
/// Get the configuration.
pub fn config(&self) -> &DeadlockDetectorConfig {
&self.config
}
/// Register a new lock.
pub fn register_lock(&self, lock_type: LockType) -> u64 {
let id = {
let mut next = self.next_lock_id.lock().unwrap();
*next += 1;
*next
};
let info = LockInfo::new(id, lock_type);
if let Ok(mut locks) = self.locks.lock() {
locks.insert(id, info);
}
id
}
/// Unregister a lock.
pub fn unregister_lock(&self, lock_id: u64) {
if let Ok(mut locks) = self.locks.lock() {
locks.remove(&lock_id);
}
}
/// Record lock acquisition.
pub fn record_acquire(&self, lock_id: u64, thread_id: u64) {
if !self.config.enabled {
return;
}
if let Ok(mut locks) = self.locks.lock()
&& let Some(info) = locks.get_mut(&lock_id)
{
info.owner = Some(thread_id);
info.acquired_at = Some(Instant::now());
info.waiters.retain(|&w| w != thread_id);
}
// Remove wait edge
if let Ok(mut graph) = self.wait_graph.lock() {
graph.retain(|e| !(e.waiter == thread_id && e.lock_id == lock_id));
}
}
/// Record lock release.
pub fn record_release(&self, lock_id: u64) {
if !self.config.enabled {
return;
}
if let Ok(mut locks) = self.locks.lock()
&& let Some(info) = locks.get_mut(&lock_id)
{
info.owner = None;
info.acquired_at = None;
}
}
/// Record a wait for lock.
pub fn record_wait(&self, lock_id: u64, thread_id: u64) {
if !self.config.enabled {
return;
}
// Add to waiters list
if let Ok(mut locks) = self.locks.lock()
&& let Some(info) = locks.get_mut(&lock_id)
{
if !info.waiters.contains(&thread_id) {
info.waiters.push(thread_id);
}
// Add edge to wait graph
if let Some(owner) = info.owner
&& owner != thread_id
&& let Ok(mut graph) = self.wait_graph.lock()
{
graph.push(WaitGraphEdge {
waiter: thread_id,
waited_for: owner,
lock_id,
});
}
}
}
/// Detect deadlocks using cycle detection in wait graph.
pub fn detect_deadlock(&self) -> Option<Vec<u64>> {
if !self.config.enabled {
return None;
}
let graph = self.wait_graph.lock().unwrap();
// Build adjacency list
let mut adj: HashMap<u64, Vec<u64>> = HashMap::new();
for edge in graph.iter() {
adj.entry(edge.waiter).or_default().push(edge.waited_for);
}
// DFS for cycle detection
let mut visited: HashSet<u64> = HashSet::new();
let mut rec_stack: HashSet<u64> = HashSet::new();
let mut path: Vec<u64> = Vec::new();
for &node in adj.keys() {
if self.dfs_cycle(node, &adj, &mut visited, &mut rec_stack, &mut path) {
return Some(path);
}
}
None
}
/// DFS helper for cycle detection.
fn dfs_cycle(
&self,
node: u64,
adj: &HashMap<u64, Vec<u64>>,
visited: &mut HashSet<u64>,
rec_stack: &mut HashSet<u64>,
path: &mut Vec<u64>,
) -> bool {
if rec_stack.contains(&node) {
// Found cycle, extract cycle from path
if let Some(start) = path.iter().position(|&n| n == node) {
*path = path[start..].to_vec();
}
path.push(node);
return true;
}
if visited.contains(&node) {
return false;
}
visited.insert(node);
rec_stack.insert(node);
path.push(node);
if let Some(neighbors) = adj.get(&node) {
for &neighbor in neighbors {
if self.dfs_cycle(neighbor, adj, visited, rec_stack, path) {
return true;
}
}
}
rec_stack.remove(&node);
path.pop();
false
}
/// Check for long-held locks.
pub fn check_long_held(&self) -> Vec<(u64, Duration)> {
if !self.config.enabled {
return Vec::new();
}
let locks = self.locks.lock().unwrap();
let mut result = Vec::new();
for (&id, info) in locks.iter() {
if let Some(duration) = info.hold_duration()
&& duration > self.config.max_hold_time
{
result.push((id, duration));
}
}
result
}
/// Register a request for tracking.
pub fn register_request(&self, request_id: &str, thread_id: u64) {
if let Ok(mut requests) = self.requests.lock() {
requests.insert(request_id.to_string(), thread_id);
}
}
/// Unregister a request.
pub fn unregister_request(&self, request_id: &str) {
if let Ok(mut requests) = self.requests.lock() {
requests.remove(request_id);
}
}
/// Get number of tracked requests.
pub fn tracked_count(&self) -> usize {
if let Ok(requests) = self.requests.lock() {
requests.len()
} else {
0
}
}
/// Get lock info.
pub fn get_lock_info(&self, lock_id: u64) -> Option<LockInfo> {
let locks = self.locks.lock().unwrap();
locks.get(&lock_id).cloned()
}
/// Get total number of registered locks.
pub fn lock_count(&self) -> usize {
let locks = self.locks.lock().unwrap();
locks.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lock_info() {
let info = LockInfo::new(1, LockType::Mutex);
assert!(!info.is_held());
assert!(info.hold_duration().is_none());
}
#[test]
fn test_register_lock() {
let detector = DeadlockDetector::with_defaults();
let id1 = detector.register_lock(LockType::Mutex);
let id2 = detector.register_lock(LockType::RwLockWrite);
assert_ne!(id1, id2);
assert_eq!(detector.lock_count(), 2);
detector.unregister_lock(id1);
assert_eq!(detector.lock_count(), 1);
}
#[test]
fn test_acquire_release() {
let detector = DeadlockDetector::with_defaults();
let lock_id = detector.register_lock(LockType::Mutex);
detector.record_acquire(lock_id, 1);
let info = detector.get_lock_info(lock_id).unwrap();
assert!(info.is_held());
assert_eq!(info.owner, Some(1));
detector.record_release(lock_id);
let info = detector.get_lock_info(lock_id).unwrap();
assert!(!info.is_held());
}
#[test]
fn test_request_tracking() {
let detector = DeadlockDetector::with_defaults();
detector.register_request("req-1", 1);
detector.register_request("req-2", 2);
assert_eq!(detector.tracked_count(), 2);
detector.unregister_request("req-1");
assert_eq!(detector.tracked_count(), 1);
}
#[test]
fn test_no_deadlock() {
let detector = DeadlockDetector::with_defaults();
let lock1 = detector.register_lock(LockType::Mutex);
let lock2 = detector.register_lock(LockType::Mutex);
// Thread 1 holds lock1, waits for lock2
detector.record_acquire(lock1, 1);
detector.record_wait(lock2, 1);
// Thread 2 holds lock2
detector.record_acquire(lock2, 2);
// No deadlock
assert!(detector.detect_deadlock().is_none());
}
#[test]
fn test_disabled_detector() {
let config = DeadlockDetectorConfig {
enabled: false,
..Default::default()
};
let detector = DeadlockDetector::new(config);
let lock_id = detector.register_lock(LockType::Mutex);
detector.record_acquire(lock_id, 1);
// Should not track when disabled
assert!(detector.detect_deadlock().is_none());
}
}
+294
View File
@@ -0,0 +1,294 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Aligned pread-based file reader.
//!
//! This module provides an aligned, position-based file reader that uses
//! `pread`/`FileExt::read_at` for I/O operations. It performs reads at
//! 512-byte-aligned offsets and sizes, making it suitable as a foundation
//! for workloads where alignment matters.
//!
//! Note: This reader does **not** set the `O_DIRECT` flag and therefore does
//! not bypass the OS page cache. It is an aligned `pread`-based reader, not
//! true Direct I/O. To implement true O_DIRECT on Linux, the file must be
//! opened with `O_DIRECT` via `libc::open`.
//!
//! # Platform Support
//!
//! The `read_at` implementation is only available on Unix-like platforms.
//! On other platforms, this reader will return an error.
use std::io::{self};
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::io::{AsyncRead, ReadBuf};
/// Errors that can occur during aligned pread operations.
#[derive(Debug, Clone)]
pub enum DirectIoError {
/// Platform doesn't support `read_at`-based I/O
UnsupportedPlatform,
/// File descriptor doesn't support this reader
UnsupportedFile,
/// I/O error occurred
Io(String),
/// Invalid alignment (reads require 512-byte-aligned offset and size)
AlignmentError { offset: u64, size: usize },
}
impl std::fmt::Display for DirectIoError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnsupportedPlatform => write!(f, "Aligned pread not supported on this platform"),
Self::UnsupportedFile => write!(f, "File doesn't support this reader"),
Self::Io(msg) => write!(f, "I/O error: {}", msg),
Self::AlignmentError { offset, size } => {
write!(f, "Alignment error: offset={}, size={}", offset, size)
}
}
}
}
impl std::error::Error for DirectIoError {}
impl From<io::Error> for DirectIoError {
fn from(err: io::Error) -> Self {
Self::Io(err.to_string())
}
}
/// Aligned pread-based file reader for Unix platforms.
///
/// This reader performs I/O using `pread`/`FileExt::read_at` at
/// 512-byte-aligned offsets and sizes, without modifying the file's
/// current position.
///
/// **Note:** This reader does **not** set the `O_DIRECT` flag and therefore
/// does **not** bypass the OS page cache. It is an aligned `pread`-based
/// reader. To implement true O_DIRECT, the file must be opened with
/// `O_DIRECT` via `libc::open`.
///
/// # Platform Support
///
/// Only available on Linux (uses `FileExt::read_at`). On other platforms,
/// use `ZeroCopyObjectReader` with memory mapping instead.
///
/// # Alignment Requirements
///
/// Reads have strict alignment requirements:
/// - File offset must be aligned to 512 bytes
/// - Buffer size must be a multiple of 512 bytes
/// - Buffer address must be aligned (handled internally)
///
/// # Example
///
/// ```ignore
/// use rustfs_io_core::DirectIoReader;
///
/// // Linux only
/// #[cfg(target_os = "linux")]
/// let reader = DirectIoReader::new(file, offset, size)?;
/// ```
#[cfg(target_os = "linux")]
pub struct DirectIoReader {
/// Underlying file handle used for aligned pread I/O
file: std::fs::File,
/// Current read position
pos: u64,
/// Remaining bytes to read
remaining: usize,
/// Buffer for aligned reads
buffer: Vec<u8>,
/// Current position in the buffer
buffer_pos: usize,
/// Amount of data in the buffer
buffer_len: usize,
}
#[cfg(target_os = "linux")]
impl DirectIoReader {
/// Alignment requirement for reads (512 bytes for most systems)
pub const ALIGNMENT: usize = 512;
/// Create a new aligned pread-based reader.
///
/// # Arguments
///
/// * `file` - File to read from
/// * `offset` - Starting offset in the file (must be 512-byte aligned)
/// * `size` - Number of bytes to read (must be 512-byte aligned)
///
/// # Returns
///
/// A `DirectIoReader` that reads the file at the given offset.
///
/// # Errors
///
/// Returns an error if offset or size are not 512-byte aligned.
pub fn new(file: std::fs::File, offset: u64, size: usize) -> Result<Self, DirectIoError> {
// Check alignment
if !offset.is_multiple_of(Self::ALIGNMENT as u64) {
return Err(DirectIoError::AlignmentError { offset, size });
}
if !size.is_multiple_of(Self::ALIGNMENT) {
return Err(DirectIoError::AlignmentError { offset, size });
}
Ok(Self {
file,
pos: offset,
remaining: size,
buffer: Vec::new(),
buffer_pos: 0,
buffer_len: 0,
})
}
/// Read a chunk of data using Direct I/O.
///
/// This method performs aligned reads and handles the buffering
/// required for Direct I/O operations.
fn read_chunk(&mut self, buf: &mut [u8]) -> io::Result<usize> {
// If buffer is exhausted, read more data
if self.buffer_pos >= self.buffer_len {
if self.remaining == 0 {
return Ok(0);
}
// Allocate aligned buffer
let chunk_size = (self.remaining).min(64 * 1024); // 64KB chunks
let aligned_size = chunk_size.div_ceil(Self::ALIGNMENT) * Self::ALIGNMENT;
self.buffer = vec![0u8; aligned_size];
// Use pread for atomic read at position (no file offset modification)
use std::os::unix::fs::FileExt;
let n = self.file.read_at(&mut self.buffer, self.pos)?;
self.buffer_pos = 0;
self.buffer_len = n;
self.pos += n as u64;
self.remaining -= n;
if n == 0 {
return Ok(0);
}
}
// Copy from buffer to user buffer
let available = self.buffer_len - self.buffer_pos;
let to_copy = buf.len().min(available);
buf[..to_copy].copy_from_slice(&self.buffer[self.buffer_pos..self.buffer_pos + to_copy]);
self.buffer_pos += to_copy;
Ok(to_copy)
}
}
#[cfg(target_os = "linux")]
impl AsyncRead for DirectIoReader {
fn poll_read(mut self: Pin<&mut Self>, _cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll<io::Result<()>> {
let filled = buf.filled().len();
let mut remaining = buf.initialize_unfilled();
while !remaining.is_empty() {
match self.read_chunk(remaining) {
Ok(0) => break,
Ok(n) => {
remaining = &mut remaining[n..];
}
Err(e) => return Poll::Ready(Err(e)),
}
}
let _n_read = buf.filled().len() - filled;
Poll::Ready(Ok(()))
}
}
/// Aligned pread reader stub for non-Linux platforms.
///
/// On non-Linux platforms, `read_at`-based I/O is not available through this
/// type. This stub exists to provide a consistent API across platforms.
#[cfg(not(target_os = "linux"))]
pub struct DirectIoReader {
_priv: (),
}
#[cfg(not(target_os = "linux"))]
impl DirectIoReader {
/// Create a new aligned pread reader (not supported on this platform).
///
/// Always returns an error on non-Linux platforms.
pub fn new(_file: std::fs::File, _offset: u64, _size: usize) -> Result<Self, DirectIoError> {
Err(DirectIoError::UnsupportedPlatform)
}
}
#[cfg(not(target_os = "linux"))]
impl AsyncRead for DirectIoReader {
fn poll_read(self: Pin<&mut Self>, _cx: &mut Context<'_>, _buf: &mut ReadBuf<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Err(io::Error::new(
io::ErrorKind::Unsupported,
"Aligned pread-based I/O not supported on this platform",
)))
}
}
impl std::fmt::Debug for DirectIoReader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
#[cfg(target_os = "linux")]
{
f.debug_struct("DirectIoReader")
.field("pos", &self.pos)
.field("remaining", &self.remaining)
.field("buffer_len", &self.buffer_len)
.finish()
}
#[cfg(not(target_os = "linux"))]
{
f.debug_struct("DirectIoReader").field("platform", &"unsupported").finish()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_alignment_check() {
#[cfg(target_os = "linux")]
{
// Valid alignment
let file = std::fs::File::open("/dev/zero").unwrap();
assert!(DirectIoReader::new(file, 0, 512).is_ok(), "Should succeed with aligned offset and size");
// Invalid offset
let file = std::fs::File::open("/dev/zero").unwrap();
assert!(DirectIoReader::new(file, 1, 512).is_err(), "Should fail with unaligned offset");
// Invalid size
let file = std::fs::File::open("/dev/zero").unwrap();
assert!(DirectIoReader::new(file, 0, 511).is_err(), "Should fail with unaligned size");
}
#[cfg(not(target_os = "linux"))]
{
// Non-Linux should return UnsupportedPlatform
let file = std::fs::File::open("/dev/null").unwrap();
assert!(matches!(DirectIoReader::new(file, 0, 512), Err(DirectIoError::UnsupportedPlatform)));
}
}
}
+381
View File
@@ -0,0 +1,381 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! I/O priority queue for scheduling I/O operations.
//!
//! This module provides a priority queue implementation for I/O operations
//! with support for starvation prevention and fair scheduling.
use crate::config::IoPriorityQueueConfig;
use crate::scheduler::IoPriority;
use std::collections::VecDeque;
use std::time::{Duration, Instant};
/// A queued I/O request.
#[derive(Debug, Clone)]
pub struct IoRequest {
/// Request ID.
pub id: u64,
/// Request priority.
pub priority: IoPriority,
/// Request size in bytes.
pub size: usize,
/// Queue time.
pub queued_at: Instant,
/// Whether this is a sequential read.
pub is_sequential: bool,
}
impl IoRequest {
/// Create a new I/O request.
pub fn new(id: u64, priority: IoPriority, size: usize, is_sequential: bool) -> Self {
Self {
id,
priority,
size,
queued_at: Instant::now(),
is_sequential,
}
}
/// Get the wait time in the queue.
pub fn wait_time(&self) -> Duration {
self.queued_at.elapsed()
}
}
/// Queue status for a priority level.
#[derive(Debug, Clone, Default)]
pub struct IoQueueStatus {
/// Number of requests in the queue.
pub count: usize,
/// Total size of all requests.
pub total_size: usize,
/// Oldest request wait time.
pub oldest_wait: Option<Duration>,
/// Number of requests processed.
pub processed: u64,
}
impl IoQueueStatus {
/// Create new queue status.
pub fn new() -> Self {
Self::default()
}
}
/// I/O priority queue.
pub struct IoPriorityQueue {
/// Queue configuration.
config: IoPriorityQueueConfig,
/// High priority queue.
high: VecDeque<IoRequest>,
/// Normal priority queue.
normal: VecDeque<IoRequest>,
/// Low priority queue.
low: VecDeque<IoRequest>,
/// Next request ID.
next_id: u64,
/// Last dequeue time for each priority (for starvation prevention).
last_dequeue: [Option<Instant>; 3],
/// Statistics for each queue.
stats: [IoQueueStatus; 3],
}
impl IoPriorityQueue {
/// Create a new priority queue with the given configuration.
pub fn new(config: IoPriorityQueueConfig) -> Self {
Self {
config,
high: VecDeque::with_capacity(100),
normal: VecDeque::with_capacity(500),
low: VecDeque::with_capacity(200),
next_id: 0,
last_dequeue: [None, None, None],
stats: [IoQueueStatus::new(), IoQueueStatus::new(), IoQueueStatus::new()],
}
}
/// Create with default configuration.
pub fn with_defaults() -> Self {
Self::new(IoPriorityQueueConfig::default())
}
/// Get the configuration.
pub fn config(&self) -> &IoPriorityQueueConfig {
&self.config
}
/// Enqueue a request.
pub fn enqueue(&mut self, priority: IoPriority, size: usize, is_sequential: bool) -> u64 {
let id = self.next_id;
self.next_id += 1;
let request = IoRequest::new(id, priority, size, is_sequential);
match priority {
IoPriority::High => {
if self.high.len() < self.config.high_capacity {
self.high.push_back(request);
}
}
IoPriority::Normal => {
if self.normal.len() < self.config.normal_capacity {
self.normal.push_back(request);
}
}
IoPriority::Low => {
if self.low.len() < self.config.low_capacity {
self.low.push_back(request);
}
}
}
id
}
/// Dequeue the next request.
///
/// Uses weighted fair queuing with starvation prevention.
pub fn dequeue(&mut self) -> Option<IoRequest> {
let now = Instant::now();
// Check for starvation: if a lower priority queue hasn't been served in a while,
// give it priority
let normal_starved = self.is_starved(IoPriority::Normal, now);
let low_starved = self.is_starved(IoPriority::Low, now);
// Priority order with starvation consideration
// Check conditions first, then dequeue
let dequeue_high = !self.high.is_empty() && !low_starved && !normal_starved;
let dequeue_normal = !self.normal.is_empty() && !low_starved;
let dequeue_low = !self.low.is_empty();
let dequeue_high_fallback = !self.high.is_empty();
let dequeue_normal_fallback = !self.normal.is_empty();
if dequeue_high {
let request = self.high.pop_front();
if request.is_some() {
self.last_dequeue[0] = Some(Instant::now());
self.stats[0].processed += 1;
}
request
} else if dequeue_normal {
let request = self.normal.pop_front();
if request.is_some() {
self.last_dequeue[1] = Some(Instant::now());
self.stats[1].processed += 1;
}
request
} else if dequeue_low {
let request = self.low.pop_front();
if request.is_some() {
self.last_dequeue[2] = Some(Instant::now());
self.stats[2].processed += 1;
}
request
} else if dequeue_high_fallback {
let request = self.high.pop_front();
if request.is_some() {
self.last_dequeue[0] = Some(Instant::now());
self.stats[0].processed += 1;
}
request
} else if dequeue_normal_fallback {
let request = self.normal.pop_front();
if request.is_some() {
self.last_dequeue[1] = Some(Instant::now());
self.stats[1].processed += 1;
}
request
} else {
None
}
}
/// Check if a priority level is starved.
fn is_starved(&self, priority: IoPriority, now: Instant) -> bool {
let idx = match priority {
IoPriority::High => 0,
IoPriority::Normal => 1,
IoPriority::Low => 2,
};
if let Some(last) = self.last_dequeue[idx] {
now.duration_since(last) > self.config.starvation_threshold
} else {
false
}
}
/// Get the total number of queued requests.
pub fn len(&self) -> usize {
self.high.len() + self.normal.len() + self.low.len()
}
/// Check if the queue is empty.
pub fn is_empty(&self) -> bool {
self.high.is_empty() && self.normal.is_empty() && self.low.is_empty()
}
/// Get queue status for a priority level.
pub fn status(&self, priority: IoPriority) -> IoQueueStatus {
let (queue, idx) = match priority {
IoPriority::High => (&self.high, 0),
IoPriority::Normal => (&self.normal, 1),
IoPriority::Low => (&self.low, 2),
};
let mut status = self.stats[idx].clone();
status.count = queue.len();
status.total_size = queue.iter().map(|r| r.size).sum();
status.oldest_wait = queue.front().map(|r| r.wait_time());
status
}
/// Get the total queue status.
pub fn total_status(&self) -> IoQueueStatus {
let mut total = IoQueueStatus::new();
total.count = self.len();
total.total_size = self
.high
.iter()
.chain(self.normal.iter())
.chain(self.low.iter())
.map(|r| r.size)
.sum();
total.processed = self.stats.iter().map(|s| s.processed).sum();
total.oldest_wait = self
.high
.front()
.map(|r| r.wait_time())
.or_else(|| self.normal.front().map(|r| r.wait_time()))
.or_else(|| self.low.front().map(|r| r.wait_time()));
total
}
/// Clear all queues.
pub fn clear(&mut self) {
self.high.clear();
self.normal.clear();
self.low.clear();
}
/// Peek at the next request without removing it.
pub fn peek(&self) -> Option<&IoRequest> {
if !self.high.is_empty() {
self.high.front()
} else if !self.normal.is_empty() {
self.normal.front()
} else {
self.low.front()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_enqueue_dequeue() {
let mut queue = IoPriorityQueue::with_defaults();
let id1 = queue.enqueue(IoPriority::High, 1024, true);
let id2 = queue.enqueue(IoPriority::Normal, 2048, false);
let id3 = queue.enqueue(IoPriority::Low, 4096, true);
assert_eq!(queue.len(), 3);
// High priority should be dequeued first
let req1 = queue.dequeue().unwrap();
assert_eq!(req1.id, id1);
assert_eq!(req1.priority, IoPriority::High);
let req2 = queue.dequeue().unwrap();
assert_eq!(req2.id, id2);
assert_eq!(req2.priority, IoPriority::Normal);
let req3 = queue.dequeue().unwrap();
assert_eq!(req3.id, id3);
assert_eq!(req3.priority, IoPriority::Low);
assert!(queue.is_empty());
}
#[test]
fn test_queue_status() {
let mut queue = IoPriorityQueue::with_defaults();
queue.enqueue(IoPriority::High, 1024, true);
queue.enqueue(IoPriority::High, 2048, true);
queue.enqueue(IoPriority::Normal, 4096, false);
let high_status = queue.status(IoPriority::High);
assert_eq!(high_status.count, 2);
assert_eq!(high_status.total_size, 3072);
let normal_status = queue.status(IoPriority::Normal);
assert_eq!(normal_status.count, 1);
assert_eq!(normal_status.total_size, 4096);
let total = queue.total_status();
assert_eq!(total.count, 3);
assert_eq!(total.total_size, 7168);
}
#[test]
fn test_queue_capacity() {
let config = IoPriorityQueueConfig {
high_capacity: 2,
normal_capacity: 2,
low_capacity: 2,
..Default::default()
};
let mut queue = IoPriorityQueue::new(config);
queue.enqueue(IoPriority::High, 1024, true);
queue.enqueue(IoPriority::High, 1024, true);
queue.enqueue(IoPriority::High, 1024, true); // Should be dropped
assert_eq!(queue.status(IoPriority::High).count, 2);
}
#[test]
fn test_clear() {
let mut queue = IoPriorityQueue::with_defaults();
queue.enqueue(IoPriority::High, 1024, true);
queue.enqueue(IoPriority::Normal, 2048, false);
queue.enqueue(IoPriority::Low, 4096, true);
assert_eq!(queue.len(), 3);
queue.clear();
assert!(queue.is_empty());
}
#[test]
fn test_peek() {
let mut queue = IoPriorityQueue::with_defaults();
queue.enqueue(IoPriority::Normal, 2048, false);
queue.enqueue(IoPriority::High, 1024, true);
let peeked = queue.peek().unwrap();
assert_eq!(peeked.priority, IoPriority::High);
// Peek shouldn't remove the item
assert_eq!(queue.len(), 2);
}
}
+462
View File
@@ -0,0 +1,462 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! I/O profile helpers for adaptive scheduling.
use std::collections::VecDeque;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum StorageMedia {
Nvme,
Ssd,
Hdd,
Unknown,
}
impl StorageMedia {
#[allow(dead_code)]
pub fn as_str(&self) -> &'static str {
match self {
Self::Nvme => "nvme",
Self::Ssd => "ssd",
Self::Hdd => "hdd",
Self::Unknown => "unknown",
}
}
}
impl FromStr for StorageMedia {
type Err = ();
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.trim().to_ascii_lowercase().as_str() {
"nvme" => Ok(Self::Nvme),
"ssd" => Ok(Self::Ssd),
"hdd" => Ok(Self::Hdd),
"unknown" => Ok(Self::Unknown),
_ => Err(()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AccessPattern {
Sequential,
Random,
Mixed,
Unknown,
}
impl AccessPattern {
#[allow(dead_code)]
pub fn as_str(&self) -> &'static str {
match self {
Self::Sequential => "sequential",
Self::Random => "random",
Self::Mixed => "mixed",
Self::Unknown => "unknown",
}
}
/// Check if this is a sequential access pattern.
#[allow(dead_code)]
pub fn is_sequential(&self) -> bool {
matches!(self, Self::Sequential)
}
/// Check if this is a random access pattern.
#[allow(dead_code)]
pub fn is_random(&self) -> bool {
matches!(self, Self::Random)
}
/// Check if this is a mixed access pattern.
#[allow(dead_code)]
pub fn is_mixed(&self) -> bool {
matches!(self, Self::Mixed)
}
/// Check if this pattern is unknown.
#[allow(dead_code)]
pub fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StorageProfile {
pub media: StorageMedia,
pub buffer_cap: usize,
pub sequential_boost_multiplier: f64,
pub random_penalty_multiplier: f64,
pub prefers_readahead: bool,
}
impl StorageProfile {
pub fn for_media(media: StorageMedia, nvme_buffer_cap: usize, ssd_buffer_cap: usize, hdd_buffer_cap: usize) -> Self {
match media {
StorageMedia::Nvme => Self {
media,
buffer_cap: nvme_buffer_cap,
sequential_boost_multiplier: 1.35,
random_penalty_multiplier: 0.9,
prefers_readahead: true,
},
StorageMedia::Ssd => Self {
media,
buffer_cap: ssd_buffer_cap,
sequential_boost_multiplier: 1.2,
random_penalty_multiplier: 0.8,
prefers_readahead: true,
},
StorageMedia::Hdd => Self {
media,
buffer_cap: hdd_buffer_cap,
sequential_boost_multiplier: 1.1,
random_penalty_multiplier: 0.65,
prefers_readahead: false,
},
StorageMedia::Unknown => Self {
media,
buffer_cap: ssd_buffer_cap,
sequential_boost_multiplier: 1.0,
random_penalty_multiplier: 0.8,
prefers_readahead: true,
},
}
}
}
#[derive(Debug, Clone)]
pub struct IoPatternDetector {
history_size: usize,
sequential_step_tolerance_bytes: u64,
history: VecDeque<(u64, u64)>,
}
impl IoPatternDetector {
pub fn new(history_size: usize, sequential_step_tolerance_bytes: u64) -> Self {
Self {
history_size: history_size.max(2),
sequential_step_tolerance_bytes,
history: VecDeque::with_capacity(history_size.max(2)),
}
}
pub fn record(&mut self, offset: u64, len: u64) {
if self.history.len() == self.history_size {
self.history.pop_front();
}
self.history.push_back((offset, len));
}
pub fn current_pattern(&self) -> AccessPattern {
if self.history.len() < 2 {
return AccessPattern::Unknown;
}
let history = self.history.iter().copied().collect::<Vec<_>>();
let mut sequential = 0usize;
let mut random = 0usize;
for window in history.windows(2) {
let (prev_offset, prev_len) = window[0];
let (curr_offset, _) = window[1];
let prev_end = prev_offset.saturating_add(prev_len);
if curr_offset.abs_diff(prev_end) <= self.sequential_step_tolerance_bytes {
sequential += 1;
} else {
random += 1;
}
}
match (sequential, random) {
(0, 0) => AccessPattern::Unknown,
(_, 0) => AccessPattern::Sequential,
(0, _) => AccessPattern::Random,
_ => AccessPattern::Mixed,
}
}
}
pub fn detect_storage_media(storage_detection_enabled: bool, storage_media_override: &str) -> StorageMedia {
if let Ok(media) = StorageMedia::from_str(storage_media_override) {
return media;
}
if !storage_detection_enabled {
return StorageMedia::Unknown;
}
// Try platform-specific detection
#[cfg(target_os = "linux")]
{
if let Ok(media) = detect_linux_storage_media()
&& media != StorageMedia::Unknown
{
return media;
}
}
#[cfg(target_os = "macos")]
{
if let Ok(media) = detect_macos_storage_media()
&& media != StorageMedia::Unknown
{
return media;
}
}
StorageMedia::Unknown
}
#[cfg(target_os = "linux")]
fn detect_linux_storage_media() -> Result<StorageMedia, std::io::Error> {
use std::path::Path;
// Try to detect NVMe devices first
if Path::new("/sys/class/nvme").exists() {
// Check if there are any NVMe devices
if let Ok(entries) = std::fs::read_dir("/sys/class/nvme") {
for entry in entries.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str.starts_with("nvme") {
return Ok(StorageMedia::Nvme);
}
}
}
}
// Check rotational flag for common block devices (sda, sdb, etc.)
for device in &["sda", "sdb", "nvme0n1", "vda"] {
let rotational_path = format!("/sys/block/{}/queue/rotational", device);
if let Ok(content) = std::fs::read_to_string(&rotational_path) {
let rotational = content.trim().parse::<u32>().unwrap_or(1);
if rotational == 0 {
// Non-rotating = SSD/NVMe
// If device name starts with "nvme", it's NVMe
if device.starts_with("nvme") {
return Ok(StorageMedia::Nvme);
}
return Ok(StorageMedia::Ssd);
} else {
// Rotating = HDD
return Ok(StorageMedia::Hdd);
}
}
}
Ok(StorageMedia::Unknown)
}
#[cfg(target_os = "macos")]
fn detect_macos_storage_media() -> Result<StorageMedia, std::io::Error> {
use std::process::Command;
// Use diskutil to get disk information
let output = Command::new("diskutil").args(["info", "/"]).output()?;
if !output.status.success() {
return Ok(StorageMedia::Unknown);
}
let info = String::from_utf8_lossy(&output.stdout);
// Check for NVMe
if info.contains("NVMe") || info.contains("nvme") {
return Ok(StorageMedia::Nvme);
}
// Check for SSD indicators
if info.contains("Solid State") || info.contains("SSD") || info.contains("Solid-State") {
return Ok(StorageMedia::Ssd);
}
// Check for HDD/rotational indicators
// Note: macOS typically doesn't explicitly say "HDD", so we assume HDD if not SSD/NVMe
// when detection is enabled
if info.contains("Rotational") || info.contains("HDD") {
return Ok(StorageMedia::Hdd);
}
// Default to SSD for modern Macs (most are SSD-based)
// This is a reasonable default for macOS systems
Ok(StorageMedia::Ssd)
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn detect_platform_storage_media() -> Result<StorageMedia, std::io::Error> {
Ok(StorageMedia::Unknown)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_storage_media_override() {
// Override should always take precedence
assert_eq!(detect_storage_media(true, "nvme"), StorageMedia::Nvme);
assert_eq!(detect_storage_media(false, "ssd"), StorageMedia::Ssd);
assert_eq!(detect_storage_media(true, "hdd"), StorageMedia::Hdd);
assert_eq!(detect_storage_media(false, "unknown"), StorageMedia::Unknown);
}
#[test]
fn test_storage_media_from_str() {
assert_eq!(StorageMedia::from_str("nvme"), Ok(StorageMedia::Nvme));
assert_eq!(StorageMedia::from_str("NVMe"), Ok(StorageMedia::Nvme));
assert_eq!(StorageMedia::from_str("ssd"), Ok(StorageMedia::Ssd));
assert_eq!(StorageMedia::from_str("SSD"), Ok(StorageMedia::Ssd));
assert_eq!(StorageMedia::from_str("hdd"), Ok(StorageMedia::Hdd));
assert_eq!(StorageMedia::from_str("HDD"), Ok(StorageMedia::Hdd));
assert_eq!(StorageMedia::from_str("unknown"), Ok(StorageMedia::Unknown));
assert_eq!(StorageMedia::from_str("invalid"), Err(()));
assert_eq!(StorageMedia::from_str(""), Err(()));
}
#[test]
fn test_storage_media_as_str() {
assert_eq!(StorageMedia::Nvme.as_str(), "nvme");
assert_eq!(StorageMedia::Ssd.as_str(), "ssd");
assert_eq!(StorageMedia::Hdd.as_str(), "hdd");
assert_eq!(StorageMedia::Unknown.as_str(), "unknown");
}
#[test]
fn test_storage_detection_disabled() {
// When detection is disabled and no override, should return Unknown
assert_eq!(detect_storage_media(false, ""), StorageMedia::Unknown);
}
#[test]
fn test_pattern_detector_sequential() {
let mut detector = IoPatternDetector::new(4, 1024);
detector.record(0, 4096);
detector.record(4096, 4096);
detector.record(8192, 4096);
assert_eq!(detector.current_pattern(), AccessPattern::Sequential);
}
#[test]
fn test_pattern_detector_random() {
let mut detector = IoPatternDetector::new(4, 1024);
detector.record(0, 4096);
detector.record(65536, 4096);
detector.record(4096, 4096);
assert_eq!(detector.current_pattern(), AccessPattern::Random);
}
#[test]
fn test_pattern_detector_mixed() {
let mut detector = IoPatternDetector::new(10, 1024);
detector.record(0, 4096); // Sequential to 4096
detector.record(4096, 4096); // Sequential to 8192
detector.record(65536, 4096); // Random jump
detector.record(98304, 4096); // Sequential from random position
assert_eq!(detector.current_pattern(), AccessPattern::Mixed);
}
#[test]
fn test_pattern_detector_insufficient_history() {
let detector = IoPatternDetector::new(10, 1024);
// No records yet
assert_eq!(detector.current_pattern(), AccessPattern::Unknown);
// Only one record
let mut detector = IoPatternDetector::new(10, 1024);
detector.record(0, 4096);
assert_eq!(detector.current_pattern(), AccessPattern::Unknown);
}
#[test]
fn test_access_pattern_helpers() {
assert!(AccessPattern::Sequential.is_sequential());
assert!(!AccessPattern::Sequential.is_random());
assert!(!AccessPattern::Sequential.is_mixed());
assert!(!AccessPattern::Sequential.is_unknown());
assert!(AccessPattern::Random.is_random());
assert!(!AccessPattern::Random.is_sequential());
assert!(AccessPattern::Mixed.is_mixed());
assert!(!AccessPattern::Mixed.is_sequential());
assert!(!AccessPattern::Mixed.is_random());
assert!(AccessPattern::Unknown.is_unknown());
assert!(!AccessPattern::Unknown.is_sequential());
}
#[test]
fn test_storage_profile_for_media() {
let nvme_cap = 2 * 1024 * 1024;
let ssd_cap = 1024 * 1024;
let hdd_cap = 512 * 1024;
let nvme_profile = StorageProfile::for_media(StorageMedia::Nvme, nvme_cap, ssd_cap, hdd_cap);
assert_eq!(nvme_profile.media, StorageMedia::Nvme);
assert_eq!(nvme_profile.buffer_cap, nvme_cap);
assert_eq!(nvme_profile.sequential_boost_multiplier, 1.35);
assert_eq!(nvme_profile.random_penalty_multiplier, 0.9);
assert!(nvme_profile.prefers_readahead);
let ssd_profile = StorageProfile::for_media(StorageMedia::Ssd, nvme_cap, ssd_cap, hdd_cap);
assert_eq!(ssd_profile.media, StorageMedia::Ssd);
assert_eq!(ssd_profile.buffer_cap, ssd_cap);
assert_eq!(ssd_profile.sequential_boost_multiplier, 1.2);
assert_eq!(ssd_profile.random_penalty_multiplier, 0.8);
let hdd_profile = StorageProfile::for_media(StorageMedia::Hdd, nvme_cap, ssd_cap, hdd_cap);
assert_eq!(hdd_profile.media, StorageMedia::Hdd);
assert_eq!(hdd_profile.buffer_cap, hdd_cap);
assert_eq!(hdd_profile.sequential_boost_multiplier, 1.1);
assert_eq!(hdd_profile.random_penalty_multiplier, 0.65);
assert!(!hdd_profile.prefers_readahead);
let unknown_profile = StorageProfile::for_media(StorageMedia::Unknown, nvme_cap, ssd_cap, hdd_cap);
assert_eq!(unknown_profile.media, StorageMedia::Unknown);
// Unknown media uses SSD cap
assert_eq!(unknown_profile.buffer_cap, ssd_cap);
assert_eq!(unknown_profile.sequential_boost_multiplier, 1.0);
}
#[cfg(target_os = "linux")]
#[test]
fn test_linux_storage_detection_exists() {
// This test just verifies the detection function exists and doesn't panic
// The actual result depends on the system it's running on
let result = detect_storage_media(true, "");
// We should get some result (not panic)
match result {
StorageMedia::Nvme | StorageMedia::Ssd | StorageMedia::Hdd | StorageMedia::Unknown => {
// All valid results
}
}
}
#[cfg(target_os = "macos")]
#[test]
fn test_macos_storage_detection_exists() {
// This test just verifies the detection function exists and doesn't panic
let result = detect_storage_media(true, "");
// We should get some result (not panic)
match result {
StorageMedia::Nvme | StorageMedia::Ssd | StorageMedia::Hdd | StorageMedia::Unknown => {
// All valid results
}
}
}
}
+101
View File
@@ -0,0 +1,101 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Zero-copy core reader and writer implementations for RustFS.
//!
//! This crate provides zero-copy readers and writers that minimize memory
//! allocations and data copying during I/O operations. It depends on
//! `rustfs-io-metrics` for metrics reporting and is designed to avoid
//! introducing cyclic dependencies in the RustFS crate graph.
//!
//! # Features
//!
//! - Memory-mapped file reading (mmap) on Unix platforms
//! - Bytes-based zero-copy wrapping
//! - AsyncRead trait implementations
//! - Tiered BytesPool for buffer management
//! - Optional Direct I/O support (Linux only)
//!
//! # Example
//!
//! ```ignore
//! use rustfs_io_core::{ZeroCopyObjectReader, BytesPool};
//! use bytes::Bytes;
//!
//! // Create from existing bytes (zero-copy)
//! let data = Bytes::from("hello world");
//! let reader = ZeroCopyObjectReader::from_bytes(data);
//!
//! // Create from file using mmap (Unix only)
//! #[cfg(unix)]
//! let reader = ZeroCopyObjectReader::from_file_mmap(&file, 0, 1024).await?;
//!
//! // Use BytesPool
//! let pool = BytesPool::new_tiered();
//! let mut buffer = pool.acquire_buffer(8192).await;
//! ```
pub mod backpressure;
pub mod bufreader_optimizer;
pub mod config;
pub mod deadlock_detector;
pub mod direct_io;
pub mod io_priority_queue;
pub mod io_profile;
pub mod lock_optimizer;
pub mod pool;
pub mod reader;
pub mod scheduler;
pub mod shared_memory;
pub mod timeout_wrapper;
pub mod writer;
#[cfg(target_os = "linux")]
pub use direct_io::{DirectIoError, DirectIoReader};
pub use pool::{BytesPool, BytesPoolConfig, BytesPoolMetrics, PooledBuffer};
pub use reader::{ZeroCopyObjectReader, ZeroCopyReadError};
pub use writer::{ZeroCopyObjectWriter, ZeroCopyWriteError};
// BufReader optimizer exports
pub use bufreader_optimizer::{BufReaderConfig, BufReaderOptimizer, BufReaderStats, BufferedSource};
// Shared memory exports
pub use shared_memory::{ArcData, ArcMetadata, SharedMemoryConfig, SharedMemoryPool, SharedMemoryStats};
// Config exports
pub use config::{ConfigError, IoPriorityQueueConfig, IoSchedulerConfig};
// Scheduler exports
pub use scheduler::{
BandwidthTier, IoLoadLevel, IoLoadMetrics, IoPriority, IoScheduler, IoSchedulingContext, IoStrategy, KI_B, MI_B,
calculate_optimal_buffer_size, get_advanced_buffer_size, get_buffer_size_for_media, get_concurrency_aware_buffer_size,
};
// Priority queue exports
pub use io_priority_queue::{IoPriorityQueue, IoQueueStatus, IoRequest};
// Backpressure exports
pub use backpressure::{BackpressureConfig, BackpressureError, BackpressureMonitor, BackpressureState};
// Deadlock detector exports
pub use deadlock_detector::{DeadlockDetector, DeadlockDetectorConfig, LockInfo, LockType, WaitGraphEdge};
// Lock optimizer exports
pub use lock_optimizer::{LockGuard, LockOptimizeConfig, LockOptimizer, LockStats};
// Timeout wrapper exports
pub use timeout_wrapper::{
OperationProgress, RequestTimeoutWrapper, TimeoutConfig, TimeoutError, TimeoutStats, calculate_adaptive_timeout,
estimate_bytes_per_second,
};
+397
View File
@@ -0,0 +1,397 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Lock optimization utilities.
//!
//! This module provides lock optimization strategies and statistics
//! to improve concurrent performance.
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::time::{Duration, Instant};
/// Lock optimization configuration.
#[derive(Debug, Clone)]
pub struct LockOptimizeConfig {
/// Whether optimization is enabled.
pub enabled: bool,
/// Lock acquire timeout.
pub acquire_timeout: Duration,
/// Maximum hold time warning threshold.
pub max_hold_time_warning: Duration,
/// Enable adaptive spinning.
pub adaptive_spin: bool,
/// Maximum spin iterations.
pub max_spin_iterations: usize,
}
impl Default for LockOptimizeConfig {
fn default() -> Self {
Self {
enabled: true,
acquire_timeout: Duration::from_secs(5),
max_hold_time_warning: Duration::from_millis(100),
adaptive_spin: true,
max_spin_iterations: 1000,
}
}
}
/// Lock statistics.
#[derive(Debug, Default)]
pub struct LockStats {
/// Number of locks acquired.
pub locks_acquired: AtomicU64,
/// Number of locks released early (before timeout).
pub locks_released_early: AtomicU64,
/// Total hold time in nanoseconds.
pub total_hold_time_ns: AtomicU64,
/// Maximum hold time in nanoseconds.
pub max_hold_time_ns: AtomicU64,
/// Number of contention events.
pub contentions: AtomicU64,
/// Number of spin successes.
pub spin_successes: AtomicU64,
/// Number of spin failures.
pub spin_failures: AtomicU64,
}
impl LockStats {
/// Create new lock statistics.
pub fn new() -> Self {
Self::default()
}
/// Record a lock acquisition.
pub fn record_acquire(&self) {
self.locks_acquired.fetch_add(1, Ordering::Relaxed);
}
/// Record a lock release.
pub fn record_release(&self, hold_time: Duration) {
let ns = hold_time.as_nanos() as u64;
self.total_hold_time_ns.fetch_add(ns, Ordering::Relaxed);
// Update max hold time
let mut current = self.max_hold_time_ns.load(Ordering::Relaxed);
while ns > current {
match self
.max_hold_time_ns
.compare_exchange_weak(current, ns, Ordering::Relaxed, Ordering::Relaxed)
{
Ok(_) => break,
Err(actual) => current = actual,
}
}
}
/// Record an early release.
pub fn record_early_release(&self) {
self.locks_released_early.fetch_add(1, Ordering::Relaxed);
}
/// Record a contention event.
pub fn record_contention(&self) {
self.contentions.fetch_add(1, Ordering::Relaxed);
}
/// Record a spin success.
pub fn record_spin_success(&self) {
self.spin_successes.fetch_add(1, Ordering::Relaxed);
}
/// Record a spin failure.
pub fn record_spin_failure(&self) {
self.spin_failures.fetch_add(1, Ordering::Relaxed);
}
/// Get total locks acquired.
pub fn total_acquired(&self) -> u64 {
self.locks_acquired.load(Ordering::Relaxed)
}
/// Get average hold time.
pub fn avg_hold_time(&self) -> Duration {
let total = self.total_hold_time_ns.load(Ordering::Relaxed);
let count = self.locks_acquired.load(Ordering::Relaxed);
if count == 0 {
Duration::ZERO
} else {
Duration::from_nanos(total / count)
}
}
/// Get maximum hold time.
pub fn max_hold_time(&self) -> Duration {
Duration::from_nanos(self.max_hold_time_ns.load(Ordering::Relaxed))
}
/// Get contention rate.
pub fn contention_rate(&self) -> f64 {
let acquired = self.locks_acquired.load(Ordering::Relaxed);
let contentions = self.contentions.load(Ordering::Relaxed);
if acquired == 0 {
0.0
} else {
contentions as f64 / acquired as f64
}
}
/// Get spin success rate.
pub fn spin_success_rate(&self) -> f64 {
let successes = self.spin_successes.load(Ordering::Relaxed);
let failures = self.spin_failures.load(Ordering::Relaxed);
let total = successes + failures;
if total == 0 { 0.0 } else { successes as f64 / total as f64 }
}
/// Reset all statistics.
pub fn reset(&self) {
self.locks_acquired.store(0, Ordering::Relaxed);
self.locks_released_early.store(0, Ordering::Relaxed);
self.total_hold_time_ns.store(0, Ordering::Relaxed);
self.max_hold_time_ns.store(0, Ordering::Relaxed);
self.contentions.store(0, Ordering::Relaxed);
self.spin_successes.store(0, Ordering::Relaxed);
self.spin_failures.store(0, Ordering::Relaxed);
}
}
/// Lock optimizer.
pub struct LockOptimizer {
/// Configuration.
config: LockOptimizeConfig,
/// Statistics.
stats: LockStats,
/// Current spin iterations (adaptive).
current_spin: AtomicUsize,
}
impl LockOptimizer {
/// Create a new lock optimizer.
pub fn new(config: LockOptimizeConfig) -> Self {
Self {
config,
stats: LockStats::new(),
current_spin: AtomicUsize::new(100),
}
}
/// Create with default configuration.
pub fn with_defaults() -> Self {
Self::new(LockOptimizeConfig::default())
}
/// Get the configuration.
pub fn config(&self) -> &LockOptimizeConfig {
&self.config
}
/// Get the statistics.
pub fn stats(&self) -> &LockStats {
&self.stats
}
/// Record lock acquisition.
pub fn on_acquire(&self) {
if !self.config.enabled {
return;
}
self.stats.record_acquire();
}
/// Record lock release.
pub fn on_release(&self, hold_time: Duration) {
if !self.config.enabled {
return;
}
self.stats.record_release(hold_time);
// Check for early release
if hold_time < self.config.acquire_timeout / 2 {
self.stats.record_early_release();
}
}
/// Record contention.
pub fn on_contention(&self) {
if !self.config.enabled {
return;
}
self.stats.record_contention();
}
/// Perform adaptive spin.
///
/// Returns true if the lock was acquired during spinning.
pub fn try_spin<F>(&self, mut try_acquire: F) -> bool
where
F: FnMut() -> bool,
{
if !self.config.enabled || !self.config.adaptive_spin {
return false;
}
let spin_count = self.current_spin.load(Ordering::Relaxed).min(self.config.max_spin_iterations);
for _ in 0..spin_count {
if try_acquire() {
self.stats.record_spin_success();
self.adapt_spin(true);
return true;
}
// Hint to the CPU that we're spinning
std::hint::spin_loop();
}
self.stats.record_spin_failure();
self.adapt_spin(false);
false
}
/// Adapt spin count based on success/failure.
fn adapt_spin(&self, success: bool) {
let current = self.current_spin.load(Ordering::Relaxed);
let new_count = if success {
// Increase spin count on success (up to max)
(current * 2).min(self.config.max_spin_iterations)
} else {
// Decrease spin count on failure (down to min)
(current / 2).max(10)
};
self.current_spin.store(new_count, Ordering::Relaxed);
}
/// Get current spin count.
pub fn current_spin_count(&self) -> usize {
self.current_spin.load(Ordering::Relaxed)
}
/// Check if hold time is excessive.
pub fn is_hold_time_excessive(&self, hold_time: Duration) -> bool {
hold_time > self.config.max_hold_time_warning
}
/// Reset statistics.
pub fn reset_stats(&self) {
self.stats.reset();
}
}
/// RAII guard for tracking lock hold time.
pub struct LockGuard<'a> {
optimizer: &'a LockOptimizer,
start: Instant,
}
impl<'a> LockGuard<'a> {
/// Create a new lock guard.
pub fn new(optimizer: &'a LockOptimizer) -> Self {
optimizer.on_acquire();
Self {
optimizer,
start: Instant::now(),
}
}
}
impl Drop for LockGuard<'_> {
fn drop(&mut self) {
self.optimizer.on_release(self.start.elapsed());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lock_stats() {
let stats = LockStats::new();
stats.record_acquire();
stats.record_acquire();
stats.record_release(Duration::from_millis(10));
stats.record_release(Duration::from_millis(20));
assert_eq!(stats.total_acquired(), 2);
assert!(stats.avg_hold_time() >= Duration::from_millis(15));
assert_eq!(stats.max_hold_time(), Duration::from_millis(20));
}
#[test]
fn test_contention_rate() {
let stats = LockStats::new();
stats.record_acquire();
stats.record_acquire();
stats.record_acquire();
stats.record_contention();
assert!((stats.contention_rate() - 0.3333333333333333).abs() < 0.01);
}
#[test]
fn test_spin_stats() {
let stats = LockStats::new();
stats.record_spin_success();
stats.record_spin_success();
stats.record_spin_failure();
assert!((stats.spin_success_rate() - 0.6666666666666666).abs() < 0.01);
}
#[test]
fn test_lock_optimizer() {
let optimizer = LockOptimizer::with_defaults();
{
let _guard = LockGuard::new(&optimizer);
std::thread::sleep(Duration::from_millis(10));
}
assert_eq!(optimizer.stats().total_acquired(), 1);
assert!(optimizer.stats().avg_hold_time() >= Duration::from_millis(10));
}
#[test]
fn test_adaptive_spin() {
let optimizer = LockOptimizer::with_defaults();
// Simulate successful spin
let acquired = optimizer.try_spin(|| true);
assert!(acquired);
assert!(optimizer.current_spin_count() > 100); // Should increase
// Simulate failed spin
let acquired = optimizer.try_spin(|| false);
assert!(!acquired);
assert!(optimizer.current_spin_count() < 200); // Should decrease
}
#[test]
fn test_disabled_optimizer() {
let config = LockOptimizeConfig {
enabled: false,
..Default::default()
};
let optimizer = LockOptimizer::new(config);
optimizer.on_acquire();
optimizer.on_release(Duration::from_millis(10));
// Should not track when disabled
assert_eq!(optimizer.stats().total_acquired(), 0);
}
}
+620
View File
@@ -0,0 +1,620 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Tiered buffer pool for zero-copy buffer management.
//!
//! Migrated from rustfs-ecstore to provide unified buffer pooling
//! across rustfs and rustfs-ecstore without cyclic dependencies.
use bytes::BytesMut;
use std::mem::ManuallyDrop;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
// Tier size thresholds
const SMALL_MAX: usize = 64 * 1024;
const MEDIUM_MAX: usize = 512 * 1024;
const LARGE_MAX: usize = 4 * 1024 * 1024;
/// Tiered buffer pool for zero-copy buffer management.
///
/// This pool provides 4 tiers of buffers for different size ranges:
/// - Small: 4KB - 64KB
/// - Medium: 64KB - 512KB
/// - Large: 512KB - 4MB
/// - XLarge: > 4MB
///
/// Buffers are automatically reused when returned to the pool.
///
/// # Example
///
/// ```ignore
/// let pool = BytesPool::new_tiered();
///
/// // Acquire a buffer (automatically selects tier based on size)
/// let mut buffer = pool.acquire_buffer(8192).await;
///
/// // Use the buffer...
/// buffer.put_slice(b"hello world");
///
/// // Return to pool (automatic when dropped)
/// drop(buffer);
///
/// // Next acquisition will reuse the buffer
/// let mut buffer2 = pool.acquire_buffer(8192).await;
/// assert!(pool.hit_rate() > 0.0); // Buffer was reused!
/// ```
#[derive(Clone)]
pub struct BytesPool {
/// Small object pool (4KB - 64KB)
small_pool: Arc<PoolTier>,
/// Medium object pool (64KB - 512KB)
medium_pool: Arc<PoolTier>,
/// Large object pool (512KB - 4MB)
large_pool: Arc<PoolTier>,
/// Extra large pool (> 4MB)
xlarge_pool: Arc<PoolTier>,
/// Pool metrics
metrics: Arc<BytesPoolMetrics>,
}
/// Single pool tier with concurrent access control and buffer reuse.
struct PoolTier {
/// Buffer size for this tier
buffer_size: usize,
/// Maximum concurrent buffers
max_buffers: usize,
/// Semaphore for concurrency control
semaphore: Arc<Semaphore>,
/// Pool name for metrics
name: &'static str,
/// Queue of available buffers for reuse
available_buffers: Mutex<Vec<BytesMut>>,
/// Metrics for tracking this tier
metrics: Mutex<Option<Arc<BytesPoolMetrics>>>,
}
/// Pool metrics for monitoring and optimization.
///
/// Tracks acquisition patterns and memory usage.
#[derive(Debug, Default)]
pub struct BytesPoolMetrics {
/// Total buffer acquisitions
pub total_acquires: AtomicU64,
/// Pool hits (buffer reused)
pub pool_hits: AtomicU64,
/// Pool misses (new allocation)
pub pool_misses: AtomicU64,
/// Total bytes allocated
pub total_bytes_allocated: AtomicU64,
/// Current allocated bytes
pub current_allocated_bytes: AtomicU64,
/// Current available buffers in pool
pub available_buffers: AtomicU64,
}
/// A buffer managed by the BytesPool.
///
/// When dropped, the buffer is automatically returned to the pool for reuse.
pub struct PooledBuffer {
/// The underlying buffer (ManuallyDrop to allow taking on drop)
pub buffer: ManuallyDrop<BytesMut>,
/// Reference to pool tier for returning buffer
tier: Option<Arc<PoolTier>>,
/// The semaphore permit (must be dropped last to release slot)
_permit: Option<OwnedSemaphorePermit>,
}
/// BytesPool configuration.
///
/// Allows customization of buffer sizes and limits for each tier.
pub struct BytesPoolConfig {
pub small_size: usize,
pub small_max: usize,
pub medium_size: usize,
pub medium_max: usize,
pub large_size: usize,
pub large_max: usize,
pub xlarge_size: usize,
pub xlarge_max: usize,
}
impl Default for BytesPoolConfig {
fn default() -> Self {
Self {
small_size: 4 * 1024,
small_max: 1000,
medium_size: 64 * 1024,
medium_max: 500,
large_size: 512 * 1024,
large_max: 100,
xlarge_size: 4 * 1024 * 1024,
xlarge_max: 25,
}
}
}
impl BytesPool {
/// Create new tiered pool with default configuration.
///
/// # Tier Configuration
///
/// - Small: 4KB buffers, max 1000 concurrent
/// - Medium: 64KB buffers, max 500 concurrent
/// - Large: 512KB buffers, max 100 concurrent
/// - XLarge: 4MB buffers, max 25 concurrent
///
/// # Example
///
/// ```ignore
/// let pool = BytesPool::new_tiered();
/// ```
pub fn new_tiered() -> Self {
Self::with_config(BytesPoolConfig::default())
}
/// Create pool with custom configuration.
///
/// # Example
///
/// ```ignore
/// let config = BytesPoolConfig {
/// small_size: 8 * 1024, // 8KB small buffers
/// small_max: 2000,
/// ..Default::default()
/// };
/// let pool = BytesPool::with_config(config);
/// ```
pub fn with_config(config: BytesPoolConfig) -> Self {
let metrics = Arc::new(BytesPoolMetrics::default());
let small_pool = Arc::new(PoolTier::new(config.small_size, config.small_max, "small"));
let medium_pool = Arc::new(PoolTier::new(config.medium_size, config.medium_max, "medium"));
let large_pool = Arc::new(PoolTier::new(config.large_size, config.large_max, "large"));
let xlarge_pool = Arc::new(PoolTier::new(config.xlarge_size, config.xlarge_max, "xlarge"));
// Set metrics reference in all tiers
small_pool.set_metrics(Arc::clone(&metrics));
medium_pool.set_metrics(Arc::clone(&metrics));
large_pool.set_metrics(Arc::clone(&metrics));
xlarge_pool.set_metrics(Arc::clone(&metrics));
Self {
small_pool,
medium_pool,
large_pool,
xlarge_pool,
metrics,
}
}
/// Acquire buffer with automatic tier selection.
///
/// Selects the appropriate tier based on requested size and blocks
/// until a buffer is available. Reuses returned buffers when available.
///
/// # Arguments
///
/// * `size` - Minimum capacity for the buffer
///
/// # Returns
///
/// A PooledBuffer that releases the permit and returns buffer to pool when dropped.
///
/// # Example
///
/// ```ignore
/// let mut buffer = pool.acquire_buffer(8192).await;
/// ```
pub async fn acquire_buffer(&self, size: usize) -> PooledBuffer {
let tier = self.select_tier(size);
let mut buffer = tier.acquire_buffer(size, &self.metrics).await;
// Set tier reference for return on drop
buffer.tier = Some(Arc::clone(tier));
buffer
}
/// Try to acquire buffer without blocking.
///
/// # Arguments
///
/// * `size` - Minimum capacity for the buffer
///
/// # Returns
///
/// * `Some(buffer)` - If a buffer was available
/// * `None` - If the pool is at capacity
///
/// # Example
///
/// ```ignore
/// if let Some(mut buffer) = pool.try_acquire_buffer(8192) {
/// // Use buffer...
/// }
/// ```
pub fn try_acquire_buffer(&self, size: usize) -> Option<PooledBuffer> {
let tier = self.select_tier(size);
let mut buffer = tier.try_acquire_buffer(size, &self.metrics)?;
// Set tier reference for return on drop
buffer.tier = Some(Arc::clone(tier));
Some(buffer)
}
/// Select appropriate tier based on size.
fn select_tier(&self, size: usize) -> &Arc<PoolTier> {
if size <= SMALL_MAX {
&self.small_pool
} else if size <= MEDIUM_MAX {
&self.medium_pool
} else if size <= LARGE_MAX {
&self.large_pool
} else {
&self.xlarge_pool
}
}
/// Get pool metrics.
pub fn metrics(&self) -> &BytesPoolMetrics {
&self.metrics
}
/// Get pool hit rate (0.0 - 1.0).
pub fn hit_rate(&self) -> f64 {
let hits = self.metrics.pool_hits.load(Ordering::Relaxed);
let total = self.metrics.total_acquires.load(Ordering::Relaxed);
if total == 0 { 0.0 } else { hits as f64 / total as f64 }
}
/// Get the number of available buffers in the pool.
pub fn available_buffers(&self) -> u64 {
self.metrics.available_buffers.load(Ordering::Relaxed)
}
}
impl PoolTier {
fn new(buffer_size: usize, max_buffers: usize, name: &'static str) -> Self {
Self {
buffer_size,
max_buffers,
semaphore: Arc::new(Semaphore::new(max_buffers)),
name,
available_buffers: Mutex::new(Vec::new()),
metrics: Mutex::new(None),
}
}
fn set_metrics(&self, metrics: Arc<BytesPoolMetrics>) {
*self.metrics.lock().unwrap() = Some(metrics);
}
async fn acquire_buffer(&self, size: usize, pool_metrics: &BytesPoolMetrics) -> PooledBuffer {
// Acquire semaphore permit (owned for storage in PooledBuffer)
let permit = Arc::clone(&self.semaphore).acquire_owned().await.unwrap();
// Use the pool's shared metrics for recording
let _metrics_lock = self.metrics.lock().unwrap();
let _metrics = _metrics_lock.as_ref().unwrap();
// Record acquisition
pool_metrics.total_acquires.fetch_add(1, Ordering::Relaxed);
// Try to get a buffer from the pool
let buffer_opt = {
let mut available = self.available_buffers.lock().unwrap();
available.pop()
};
let was_reused = buffer_opt.is_some();
let buffer = if let Some(mut buf) = buffer_opt {
// Reuse existing buffer - clear and ensure capacity
buf.clear();
if buf.capacity() < size {
buf.reserve(size - buf.capacity());
}
buf
} else {
// Allocate new buffer
let buf = BytesMut::with_capacity(size.max(self.buffer_size));
pool_metrics
.total_bytes_allocated
.fetch_add(buf.capacity() as u64, Ordering::Relaxed);
pool_metrics
.current_allocated_bytes
.fetch_add(buf.capacity() as u64, Ordering::Relaxed);
buf
};
let buffer_capacity = buffer.capacity();
// Record metrics
rustfs_io_metrics::record_bytes_pool_acquire(self.name, buffer_capacity, was_reused);
// Record hit/miss (pool_metrics and metrics point to same Arc)
if was_reused {
pool_metrics.pool_hits.fetch_add(1, Ordering::Relaxed);
} else {
pool_metrics.pool_misses.fetch_add(1, Ordering::Relaxed);
}
PooledBuffer {
buffer: ManuallyDrop::new(buffer),
tier: None, // Will be set after creating Arc<PoolTier>
_permit: Some(permit),
}
}
fn try_acquire_buffer(&self, size: usize, pool_metrics: &BytesPoolMetrics) -> Option<PooledBuffer> {
// Try to acquire permit without blocking
let permit = Arc::clone(&self.semaphore).try_acquire_owned().ok()?;
// Use the pool's shared metrics for recording
let _metrics_lock = self.metrics.lock().unwrap();
let _metrics = _metrics_lock.as_ref().unwrap();
// Record acquisition
pool_metrics.total_acquires.fetch_add(1, Ordering::Relaxed);
// Try to get a buffer from the pool
let buffer_opt = {
let mut available = self.available_buffers.lock().unwrap();
available.pop()
};
let was_reused = buffer_opt.is_some();
let buffer = if let Some(mut buf) = buffer_opt {
// Reuse existing buffer
buf.clear();
if buf.capacity() < size {
buf.reserve(size - buf.capacity());
}
buf
} else {
// Allocate new buffer
let buf = BytesMut::with_capacity(size.max(self.buffer_size));
pool_metrics
.total_bytes_allocated
.fetch_add(buf.capacity() as u64, Ordering::Relaxed);
pool_metrics
.current_allocated_bytes
.fetch_add(buf.capacity() as u64, Ordering::Relaxed);
buf
};
let buffer_capacity = buffer.capacity();
// Record metrics
rustfs_io_metrics::record_bytes_pool_acquire(self.name, buffer_capacity, was_reused);
// Record hit/miss (pool_metrics and metrics point to same Arc)
if was_reused {
pool_metrics.pool_hits.fetch_add(1, Ordering::Relaxed);
} else {
pool_metrics.pool_misses.fetch_add(1, Ordering::Relaxed);
}
Some(PooledBuffer {
buffer: ManuallyDrop::new(buffer),
tier: None,
_permit: Some(permit),
})
}
/// Return a buffer to the pool for reuse.
fn return_buffer(&self, buffer: BytesMut) {
let mut available = self.available_buffers.lock().unwrap();
// Limit the size of the pool to prevent unbounded growth
if available.len() < self.max_buffers {
available.push(buffer);
if let Some(ref metrics) = *self.metrics.lock().unwrap() {
metrics.available_buffers.fetch_add(1, Ordering::Relaxed);
}
}
// If pool is full, buffer is dropped and memory is freed
}
}
impl Drop for PooledBuffer {
#[allow(unsafe_code)]
fn drop(&mut self) {
// Return buffer to pool if tier reference exists
if let Some(ref tier) = self.tier {
// Safety: We're in drop(), so this is the last use of the buffer
// ManuallyDrop allows us to take the value without running BytesMut's drop
let buffer = unsafe { ManuallyDrop::take(&mut self.buffer) };
tier.return_buffer(buffer);
}
// The permit is automatically dropped here, releasing the semaphore slot
}
}
impl AsRef<[u8]> for PooledBuffer {
fn as_ref(&self) -> &[u8] {
self.buffer.as_ref()
}
}
impl AsMut<[u8]> for PooledBuffer {
fn as_mut(&mut self) -> &mut [u8] {
self.buffer.as_mut()
}
}
impl std::ops::Deref for PooledBuffer {
type Target = BytesMut;
fn deref(&self) -> &Self::Target {
&self.buffer
}
}
impl std::ops::DerefMut for PooledBuffer {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.buffer
}
}
impl std::fmt::Debug for BytesPool {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BytesPool")
.field("small_pool", &self.small_pool)
.field("medium_pool", &self.medium_pool)
.field("large_pool", &self.large_pool)
.field("xlarge_pool", &self.xlarge_pool)
.field("metrics", &self.metrics)
.finish()
}
}
impl std::fmt::Debug for PoolTier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PoolTier")
.field("name", &self.name)
.field("buffer_size", &self.buffer_size)
.field("max_buffers", &self.max_buffers)
.field("available_permits", &self.semaphore.available_permits())
.field("available_buffers", &self.available_buffers.lock().unwrap().len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_new_tiered() {
let pool = BytesPool::new_tiered();
assert_eq!(pool.small_pool.buffer_size, 4 * 1024);
assert_eq!(pool.small_pool.max_buffers, 1000);
}
#[tokio::test]
async fn test_acquire_buffer() {
let pool = BytesPool::new_tiered();
let buffer = pool.acquire_buffer(2048).await;
assert!(buffer.capacity() >= 2048);
}
#[tokio::test]
async fn test_tier_selection() {
let pool = BytesPool::new_tiered();
// Small buffer (4KB - 64KB)
let buf1 = pool.acquire_buffer(1024).await;
assert_eq!(buf1.capacity(), 4 * 1024);
// Medium buffer (64KB - 512KB) - capacity is max(requested, tier_size)
let buf2 = pool.acquire_buffer(100 * 1024).await;
assert_eq!(buf2.capacity(), 100 * 1024); // Requested size
// Large buffer (512KB - 4MB)
let buf3 = pool.acquire_buffer(1024 * 1024).await;
assert_eq!(buf3.capacity(), 1024 * 1024); // Requested size
// XLarge buffer (> 4MB)
let buf4 = pool.acquire_buffer(8 * 1024 * 1024).await;
assert_eq!(buf4.capacity(), 8 * 1024 * 1024); // Requested size
}
#[tokio::test]
async fn test_try_acquire_buffer() {
let pool = BytesPool::with_config(BytesPoolConfig {
small_size: 1024,
small_max: 1,
..Default::default()
});
// First acquisition should succeed
let buffer1 = pool.try_acquire_buffer(512);
assert!(buffer1.is_some());
// Second should fail (pool at capacity)
let buffer2 = pool.try_acquire_buffer(512);
assert!(buffer2.is_none());
}
#[tokio::test]
async fn test_metrics() {
let pool = BytesPool::new_tiered();
let _buffer = pool.acquire_buffer(1024).await;
drop(_buffer);
let metrics = pool.metrics();
assert!(metrics.total_acquires.load(Ordering::Relaxed) > 0);
}
#[tokio::test]
async fn test_hit_rate() {
let pool = BytesPool::new_tiered();
assert_eq!(pool.hit_rate(), 0.0); // No acquisitions yet
let _buffer = pool.acquire_buffer(1024).await;
drop(_buffer);
// First acquire is a miss (no buffers available yet)
assert_eq!(pool.hit_rate(), 0.0);
}
#[tokio::test]
async fn test_available_buffers() {
let pool = BytesPool::new_tiered();
assert_eq!(pool.available_buffers(), 0);
let _buffer = pool.acquire_buffer(1024).await;
drop(_buffer);
// After drop, buffer should be returned to pool
assert_eq!(pool.available_buffers(), 1);
}
#[tokio::test]
async fn test_buffer_reuse() {
// This test verifies that buffers are reused when returned to the pool
let pool = BytesPool::with_config(BytesPoolConfig {
small_size: 1024,
small_max: 2,
..Default::default()
});
// Record initial state
let initial_acquires = pool.metrics().total_acquires.load(Ordering::Relaxed);
let initial_hits = pool.metrics().pool_hits.load(Ordering::Relaxed);
assert_eq!(initial_acquires, 0);
// First acquisition - should allocate new (miss)
let buffer1 = pool.acquire_buffer(512).await;
let initial_bytes_allocated = pool.metrics().total_bytes_allocated.load(Ordering::Relaxed);
assert!(initial_bytes_allocated >= 1024);
// Return buffer (by dropping)
drop(buffer1);
// Second acquisition - should reuse (hit)
let _buffer2 = pool.acquire_buffer(512).await;
let bytes_after_reuse = pool.metrics().total_bytes_allocated.load(Ordering::Relaxed);
// Bytes allocated should be the same (buffer was reused)
assert_eq!(initial_bytes_allocated, bytes_after_reuse);
// Total acquires should be 2
let total_acquires = pool.metrics().total_acquires.load(Ordering::Relaxed) - initial_acquires;
assert_eq!(total_acquires, 2);
// Pool hits should be 1
let delta_hits = pool.metrics().pool_hits.load(Ordering::Relaxed) - initial_hits;
assert_eq!(delta_hits, 1);
}
}
+316
View File
@@ -0,0 +1,316 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Zero-copy object reader implementation.
use bytes::Bytes;
use std::io;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::io::{AsyncRead, ReadBuf};
/// Errors that can occur during zero-copy read operations.
#[derive(Debug, Clone)]
pub enum ZeroCopyReadError {
/// I/O error occurred.
Io(String),
/// Memory mapping error.
Mmap(String),
/// Invalid offset or size.
InvalidRange,
}
impl std::fmt::Display for ZeroCopyReadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(msg) => write!(f, "I/O error: {}", msg),
Self::Mmap(msg) => write!(f, "Mmap error: {}", msg),
Self::InvalidRange => write!(f, "Invalid offset or size"),
}
}
}
impl std::error::Error for ZeroCopyReadError {}
impl From<io::Error> for ZeroCopyReadError {
fn from(err: io::Error) -> Self {
Self::Io(err.to_string())
}
}
/// Zero-copy object reader.
///
/// This reader provides zero-copy access to object data by using:
/// - Memory-mapped files for on-disk data
/// - Bytes wrapping for in-memory data
/// - Reference counting to avoid copies
///
/// # Example
///
/// ```ignore
/// // Create from bytes (zero-copy)
/// let data = Bytes::from("hello world");
/// let reader = ZeroCopyObjectReader::from_bytes(data);
///
/// // Read using AsyncRead trait
/// let mut buf = vec![0u8; 1024];
/// let n = reader.read(&mut buf[..]).await?;
/// ```
pub struct ZeroCopyObjectReader {
/// Internal data source (could be mmap or owned bytes)
data: Bytes,
/// Current read position
pos: usize,
}
impl ZeroCopyObjectReader {
/// Create a zero-copy reader from existing bytes.
///
/// This is a true zero-copy operation - the Bytes are wrapped
/// without any allocation or copying.
///
/// # Arguments
///
/// * `data` - Bytes to wrap
///
/// # Example
///
/// ```ignore
/// let data = Bytes::from("hello world");
/// let reader = ZeroCopyObjectReader::from_bytes(data);
/// ```
pub fn from_bytes(data: Bytes) -> Self {
Self { data, pos: 0 }
}
/// Create a zero-copy reader from a file using mmap.
///
/// This uses memory mapping to avoid loading the entire file into memory.
/// Only the accessed pages are loaded on demand.
///
/// # Arguments
///
/// * `path` - Path to the file to memory map
/// * `offset` - Offset within the file to start reading
/// * `size` - Number of bytes to map
///
/// # Returns
///
/// A reader that provides zero-copy access to the file data.
///
/// # Errors
///
/// Returns an error if the file cannot be memory mapped.
///
/// # Example
///
/// ```ignore
/// let reader = ZeroCopyObjectReader::from_file_mmap_path("large_file.bin", 0, 1024).await?;
/// ```
#[cfg(unix)]
#[allow(unsafe_code)]
pub async fn from_file_mmap_path(path: &std::path::Path, offset: u64, size: usize) -> Result<Self, ZeroCopyReadError> {
use memmap2::MmapOptions;
let path = path.to_path_buf();
let (offset, size) = (offset, size);
tokio::task::spawn_blocking(move || {
// Open the file in sync context
let std_file = std::fs::File::open(&path).map_err(|e| ZeroCopyReadError::Io(e.to_string()))?;
// Create memory map
let mmap = unsafe { MmapOptions::new().offset(offset).len(size).map(&std_file) }
.map_err(|e| ZeroCopyReadError::Mmap(e.to_string()))?;
// Convert to Bytes (this is a copy, but only done once)
Ok(Self {
data: Bytes::copy_from_slice(&mmap),
pos: 0,
})
})
.await
.map_err(|e| ZeroCopyReadError::Io(e.to_string()))?
}
/// Create a zero-copy reader from a file using mmap.
///
/// This uses memory mapping to avoid loading the entire file into memory.
/// Only the accessed pages are loaded on demand.
///
/// # Arguments
///
/// * `file` - File to memory map
/// * `offset` - Offset within the file to start reading
/// * `size` - Number of bytes to map
///
/// # Returns
///
/// A reader that provides zero-copy access to the file data.
///
/// # Errors
///
/// Returns an error if the file cannot be memory mapped.
///
/// # Example
///
/// ```ignore
/// let file = tokio::fs::File::open("large_file.bin").await?;
/// let reader = ZeroCopyObjectReader::from_file_mmap(&file, 0, 1024).await?;
/// ```
#[cfg(unix)]
pub async fn from_file_mmap(file: &tokio::fs::File, offset: u64, size: usize) -> Result<Self, ZeroCopyReadError> {
use tokio::io::{AsyncReadExt, AsyncSeekExt, SeekFrom};
// For mmap, we need the file path - fall back to regular read if not available
// This is a simplified implementation
let mut cloned = file.try_clone().await?;
cloned.seek(SeekFrom::Start(offset)).await?;
let mut buffer = vec![0u8; size];
cloned.read_exact(&mut buffer).await?;
Ok(Self {
data: Bytes::from(buffer),
pos: 0,
})
}
/// Create a zero-copy reader from a file (non-Unix fallback).
///
/// On platforms that don't support mmap, this falls back to regular file I/O.
#[cfg(not(unix))]
pub async fn from_file_mmap(file: &tokio::fs::File, offset: u64, size: usize) -> Result<Self, ZeroCopyReadError> {
use tokio::io::{AsyncReadExt, AsyncSeekExt, SeekFrom};
let mut cloned = file.try_clone().await?;
cloned.seek(SeekFrom::Start(offset)).await?;
let mut buffer = vec![0u8; size];
cloned.read_exact(&mut buffer).await?;
Ok(Self {
data: Bytes::from(buffer),
pos: 0,
})
}
/// Get the remaining data as Bytes (zero-copy).
///
/// This returns a slice of the remaining data without copying.
/// The returned Bytes shares the underlying memory with this reader.
///
/// # Example
///
/// ```ignore
/// let remaining = reader.remaining_bytes();
/// println!("Remaining: {} bytes", remaining.len());
/// ```
pub fn remaining_bytes(&self) -> Bytes {
self.data.slice(self.pos..)
}
/// Get the total length of the data.
pub fn len(&self) -> usize {
self.data.len()
}
/// Check if the reader has reached the end.
pub fn is_empty(&self) -> bool {
self.pos >= self.data.len()
}
/// Get the current read position.
pub fn position(&self) -> usize {
self.pos
}
}
impl AsyncRead for ZeroCopyObjectReader {
fn poll_read(mut self: Pin<&mut Self>, _cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll<io::Result<()>> {
let remaining = self.data.len() - self.pos;
if remaining == 0 {
return Poll::Ready(Ok(()));
}
let to_read = std::cmp::min(remaining, buf.remaining());
let slice = &self.data[self.pos..self.pos + to_read];
buf.put_slice(slice);
self.pos += to_read;
Poll::Ready(Ok(()))
}
}
impl std::fmt::Debug for ZeroCopyObjectReader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ZeroCopyObjectReader")
.field("data_len", &self.data.len())
.field("pos", &self.pos)
.field("remaining", &(self.data.len() - self.pos))
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::AsyncReadExt;
#[tokio::test]
async fn test_from_bytes() {
let data = Bytes::from("hello world");
let mut reader = ZeroCopyObjectReader::from_bytes(data.clone());
let mut buf = [0u8; 11];
let n = reader.read(&mut buf[..]).await.unwrap();
assert_eq!(n, 11);
assert_eq!(&buf[..n], b"hello world");
}
#[tokio::test]
async fn test_remaining_bytes() {
let data = Bytes::from("hello world");
let reader = ZeroCopyObjectReader::from_bytes(data);
let remaining = reader.remaining_bytes();
assert_eq!(remaining.len(), 11);
assert_eq!(&remaining[..], b"hello world");
}
#[tokio::test]
async fn test_position() {
let data = Bytes::from("hello world");
let mut reader = ZeroCopyObjectReader::from_bytes(data);
assert_eq!(reader.position(), 0);
let mut buf = [0u8; 5];
reader.read_exact(&mut buf[..]).await.unwrap();
assert_eq!(reader.position(), 5);
}
#[tokio::test]
async fn test_is_empty() {
let data = Bytes::from("");
let reader = ZeroCopyObjectReader::from_bytes(data);
assert!(reader.is_empty());
let data = Bytes::from("hello");
let reader = ZeroCopyObjectReader::from_bytes(data);
assert!(!reader.is_empty());
}
}
+872
View File
@@ -0,0 +1,872 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! I/O scheduler for adaptive buffer sizing and load management.
//!
//! This module provides the core I/O scheduling logic that determines
//! optimal buffer sizes, I/O strategies, and load management decisions.
use crate::config::IoSchedulerConfig;
use crate::io_profile::{AccessPattern, StorageMedia, StorageProfile};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
/// I/O priority levels.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum IoPriority {
/// High priority for small, latency-sensitive operations.
High,
/// Normal priority for standard operations.
#[default]
Normal,
/// Low priority for large, throughput-oriented operations.
Low,
}
impl IoPriority {
/// Determine priority based on request size.
pub fn from_size(size: i64, high_threshold: usize, low_threshold: usize) -> Self {
let size = size as usize;
if size < high_threshold {
IoPriority::High
} else if size > low_threshold {
IoPriority::Low
} else {
IoPriority::Normal
}
}
/// Get the priority as a string for metrics labels.
pub fn as_str(&self) -> &'static str {
match self {
IoPriority::High => "high",
IoPriority::Normal => "normal",
IoPriority::Low => "low",
}
}
/// Check if this is high priority.
pub fn is_high(&self) -> bool {
matches!(self, IoPriority::High)
}
/// Check if this is normal priority.
pub fn is_normal(&self) -> bool {
matches!(self, IoPriority::Normal)
}
/// Check if this is low priority.
pub fn is_low(&self) -> bool {
matches!(self, IoPriority::Low)
}
}
impl std::fmt::Display for IoPriority {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
/// I/O load level.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Default)]
pub enum IoLoadLevel {
/// Low load - system is underutilized.
Low,
/// Medium load - system is moderately utilized.
#[default]
Medium,
/// High load - system is heavily utilized.
High,
/// Critical load - system is overloaded.
Critical,
}
impl IoLoadLevel {
/// Get the load level as a string for metrics labels.
pub fn as_str(&self) -> &'static str {
match self {
IoLoadLevel::Low => "low",
IoLoadLevel::Medium => "medium",
IoLoadLevel::High => "high",
IoLoadLevel::Critical => "critical",
}
}
/// Determine load level from wait time.
pub fn from_wait_time(wait_time: Duration, low_threshold: Duration, high_threshold: Duration) -> Self {
if wait_time <= low_threshold {
IoLoadLevel::Low
} else if wait_time <= high_threshold {
IoLoadLevel::Medium
} else if wait_time <= high_threshold * 2 {
IoLoadLevel::High
} else {
IoLoadLevel::Critical
}
}
}
impl std::fmt::Display for IoLoadLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
/// Bandwidth tier classification.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum BandwidthTier {
/// Low bandwidth (< 100 MB/s).
Low,
/// Medium bandwidth (100-500 MB/s).
#[default]
Medium,
/// High bandwidth (> 500 MB/s).
High,
/// Unknown bandwidth.
Unknown,
}
impl BandwidthTier {
/// Determine bandwidth tier from bytes per second.
pub fn from_bps(bps: u64) -> Self {
const MB: u64 = 1024 * 1024;
if bps < 100 * MB {
BandwidthTier::Low
} else if bps < 500 * MB {
BandwidthTier::Medium
} else {
BandwidthTier::High
}
}
/// Get the tier as a string for metrics labels.
pub fn as_str(&self) -> &'static str {
match self {
BandwidthTier::Low => "low",
BandwidthTier::Medium => "medium",
BandwidthTier::High => "high",
BandwidthTier::Unknown => "unknown",
}
}
}
/// I/O strategy decision.
#[derive(Debug, Clone)]
pub struct IoStrategy {
/// Buffer size to use for I/O operations.
pub buffer_size: usize,
/// Buffer multiplier based on storage media.
pub buffer_multiplier: f64,
/// Whether to enable readahead.
pub enable_readahead: bool,
/// Whether cache writeback is enabled.
pub cache_writeback_enabled: bool,
/// Whether to use buffered I/O.
pub use_buffered_io: bool,
// Performance state
/// Current number of concurrent requests.
pub concurrent_requests: usize,
/// Observed bandwidth in bytes per second.
pub observed_bandwidth_bps: Option<u64>,
/// Bandwidth tier classification.
pub bandwidth_tier: BandwidthTier,
/// Current load level.
pub load_level: IoLoadLevel,
// Priority
/// I/O priority for this operation.
pub priority: IoPriority,
// Decision flags
/// Whether to throttle random I/O.
pub should_throttle_random_io: bool,
/// Whether to expand buffer for sequential access.
pub should_expand_for_sequential: bool,
/// Whether to reduce buffer due to concurrency.
pub should_reduce_for_concurrency: bool,
/// Whether to reduce buffer due to low bandwidth.
pub should_reduce_for_bandwidth: bool,
}
impl Default for IoStrategy {
fn default() -> Self {
Self {
buffer_size: 128 * 1024,
buffer_multiplier: 1.0,
enable_readahead: true,
cache_writeback_enabled: false,
use_buffered_io: true,
concurrent_requests: 0,
observed_bandwidth_bps: None,
bandwidth_tier: BandwidthTier::Medium,
load_level: IoLoadLevel::Low,
priority: IoPriority::Normal,
should_throttle_random_io: false,
should_expand_for_sequential: false,
should_reduce_for_concurrency: false,
should_reduce_for_bandwidth: false,
}
}
}
impl IoStrategy {
/// Create a new strategy with default values.
pub fn new() -> Self {
Self::default()
}
/// Create a strategy for sequential access.
pub fn sequential(buffer_size: usize) -> Self {
Self {
buffer_size,
enable_readahead: true,
should_expand_for_sequential: true,
..Self::default()
}
}
/// Create a strategy for random access.
pub fn random(buffer_size: usize) -> Self {
Self {
buffer_size,
enable_readahead: false,
should_throttle_random_io: true,
..Self::default()
}
}
}
/// I/O load metrics.
#[derive(Debug, Clone, Default)]
pub struct IoLoadMetrics {
/// Number of samples in the current window.
pub sample_count: usize,
/// Total wait time in the window.
pub total_wait_time: Duration,
/// Maximum wait time in the window.
pub max_wait_time: Duration,
/// Average wait time.
pub avg_wait_time: Duration,
/// Current load level.
pub load_level: IoLoadLevel,
}
impl IoLoadMetrics {
/// Create new load metrics.
pub fn new() -> Self {
Self::default()
}
/// Add a wait time sample.
pub fn add_sample(&mut self, wait_time: Duration) {
self.sample_count += 1;
self.total_wait_time += wait_time;
if wait_time > self.max_wait_time {
self.max_wait_time = wait_time;
}
self.avg_wait_time = if self.sample_count > 0 {
self.total_wait_time / self.sample_count as u32
} else {
Duration::ZERO
};
}
/// Update load level based on thresholds.
pub fn update_load_level(&mut self, low_threshold: Duration, high_threshold: Duration) {
self.load_level = IoLoadLevel::from_wait_time(self.avg_wait_time, low_threshold, high_threshold);
}
/// Reset the metrics.
pub fn reset(&mut self) {
*self = Self::default();
}
}
/// I/O scheduler.
pub struct IoScheduler {
/// Scheduler configuration.
config: IoSchedulerConfig,
/// Active request counter.
active_requests: AtomicUsize,
/// Load metrics.
load_metrics: std::sync::Mutex<IoLoadMetrics>,
}
impl IoScheduler {
/// Create a new I/O scheduler with the given configuration.
pub fn new(config: IoSchedulerConfig) -> Self {
Self {
config,
active_requests: AtomicUsize::new(0),
load_metrics: std::sync::Mutex::new(IoLoadMetrics::new()),
}
}
/// Create a new I/O scheduler with default configuration.
pub fn with_defaults() -> Self {
Self::new(IoSchedulerConfig::default())
}
/// Get the scheduler configuration.
pub fn config(&self) -> &IoSchedulerConfig {
&self.config
}
/// Get the current number of active requests.
pub fn active_requests(&self) -> usize {
self.active_requests.load(Ordering::Relaxed)
}
/// Increment the active request count.
pub fn increment_requests(&self) {
self.active_requests.fetch_add(1, Ordering::Relaxed);
}
/// Decrement the active request count.
pub fn decrement_requests(&self) {
self.active_requests.fetch_sub(1, Ordering::Relaxed);
}
/// Calculate I/O strategy for a request.
pub fn calculate_strategy(&self, file_size: i64, permit_wait_time: Duration, is_sequential: bool) -> IoStrategy {
let concurrent_requests = self.active_requests.load(Ordering::Relaxed);
// Determine priority based on file size
let priority = IoPriority::from_size(
file_size,
self.config.high_priority_size_threshold,
self.config.low_priority_size_threshold,
);
// Determine load level
let load_level =
IoLoadLevel::from_wait_time(permit_wait_time, self.config.load_low_threshold(), self.config.load_high_threshold());
// Calculate base buffer size
let base_buffer = self.config.base_buffer_size;
// Adjust for concurrency
let concurrency_factor = match concurrent_requests {
0..=2 => 1.0,
3..=4 => 0.75,
5..=8 => 0.5,
_ => 0.4,
};
// Adjust for load level
let load_factor = match load_level {
IoLoadLevel::Low => 1.2,
IoLoadLevel::Medium => 1.0,
IoLoadLevel::High => 0.7,
IoLoadLevel::Critical => 0.5,
};
// Adjust for access pattern
let sequential_factor = if is_sequential { 1.5 } else { 1.0 };
// Calculate final buffer size
let buffer_size = (base_buffer as f64 * concurrency_factor * load_factor * sequential_factor) as usize;
let buffer_size = buffer_size.clamp(self.config.min_buffer_size, self.config.max_buffer_size);
IoStrategy {
buffer_size,
buffer_multiplier: concurrency_factor * load_factor * sequential_factor,
enable_readahead: is_sequential && load_level != IoLoadLevel::Critical,
cache_writeback_enabled: load_level == IoLoadLevel::Low,
use_buffered_io: true,
concurrent_requests,
observed_bandwidth_bps: None,
bandwidth_tier: BandwidthTier::Unknown,
load_level,
priority,
should_throttle_random_io: !is_sequential && load_level >= IoLoadLevel::High,
should_expand_for_sequential: is_sequential && load_level <= IoLoadLevel::Medium,
should_reduce_for_concurrency: concurrent_requests > 4,
should_reduce_for_bandwidth: false,
}
}
/// Calculate multi-factor I/O strategy.
pub fn calculate_multi_factor_strategy(
&self,
file_size: i64,
permit_wait_time: Duration,
is_sequential: bool,
storage_profile: Option<&StorageProfile>,
) -> IoStrategy {
let mut strategy = self.calculate_strategy(file_size, permit_wait_time, is_sequential);
// Apply storage profile adjustments
if let Some(profile) = storage_profile {
// Adjust buffer size based on storage media
let media_factor = match profile.media {
StorageMedia::Nvme => 1.5,
StorageMedia::Ssd => 1.2,
StorageMedia::Hdd => 0.8,
StorageMedia::Unknown => 1.0,
};
strategy.buffer_size = (strategy.buffer_size as f64 * media_factor).min(self.config.max_buffer_size as f64) as usize;
// Apply sequential boost if applicable
if is_sequential {
strategy.buffer_size = (strategy.buffer_size as f64 * profile.sequential_boost_multiplier)
.min(self.config.max_buffer_size as f64) as usize;
}
// Apply random penalty if applicable
if !is_sequential {
strategy.buffer_size = (strategy.buffer_size as f64 * profile.random_penalty_multiplier)
.max(self.config.min_buffer_size as f64) as usize;
}
// Update readahead preference
strategy.enable_readahead = strategy.enable_readahead && profile.prefers_readahead;
}
strategy
}
/// Record a wait time sample for load tracking.
pub fn record_wait_time(&self, wait_time: Duration) {
if let Ok(mut metrics) = self.load_metrics.lock() {
metrics.add_sample(wait_time);
metrics.update_load_level(self.config.load_low_threshold(), self.config.load_high_threshold());
}
}
/// Get current load metrics.
pub fn load_metrics(&self) -> IoLoadMetrics {
if let Ok(metrics) = self.load_metrics.lock() {
metrics.clone()
} else {
IoLoadMetrics::default()
}
}
}
impl Default for IoScheduler {
fn default() -> Self {
Self::with_defaults()
}
}
// ============================================================================
// Buffer Size Calculation Functions
// ============================================================================
/// Constants for buffer size calculations.
pub const KI_B: usize = 1024;
pub const MI_B: usize = 1024 * 1024;
/// Get concurrency-aware buffer size.
///
/// Adjusts buffer size based on the current level of concurrent requests.
/// Higher concurrency leads to smaller buffers to reduce memory pressure.
///
/// # Arguments
///
/// * `file_size` - Size of the file being read (-1 if unknown)
/// * `base_buffer_size` - Base buffer size from workload profile
///
/// # Returns
///
/// Adjusted buffer size in bytes
pub fn get_concurrency_aware_buffer_size(file_size: i64, base_buffer_size: usize) -> usize {
// Get current concurrency level from global counter
let concurrent_requests = 1; // Default to 1 if no global counter available
// Define concurrency thresholds
let medium_threshold = 4;
let high_threshold = 8;
// Calculate adaptive multiplier based on concurrency
let adaptive_multiplier = if concurrent_requests <= 2 {
// Low concurrency (1-2): use full buffer size
1.0
} else if concurrent_requests <= medium_threshold {
// Medium concurrency (3-4): slightly reduce buffer size (75% of base)
0.75
} else if concurrent_requests <= high_threshold {
// Higher concurrency (5-8): more aggressive reduction (50% of base)
0.5
} else {
// Very high concurrency (>8): minimize memory per request (40% of base)
0.4
};
// Calculate the adjusted buffer size
let adjusted_size = (base_buffer_size as f64 * adaptive_multiplier) as usize;
// Ensure we stay within reasonable bounds
let min_buffer = if file_size > 0 && file_size < 100 * KI_B as i64 {
32 * KI_B // For very small files, use minimum buffer
} else {
64 * KI_B // Standard minimum buffer size
};
let max_buffer = if concurrent_requests > high_threshold {
256 * KI_B // Cap at 256KB for high concurrency
} else {
MI_B // Cap at 1MB for lower concurrency
};
adjusted_size.clamp(min_buffer, max_buffer)
}
/// Advanced concurrency-aware buffer sizing with file size optimization.
///
/// This enhanced version considers both concurrency level and file size patterns
/// to provide even better performance characteristics.
///
/// # Arguments
///
/// * `file_size` - Size of the file being read (-1 if unknown)
/// * `base_buffer_size` - Baseline buffer size from workload profile
/// * `is_sequential` - Whether this is a sequential read (hint for optimization)
/// * `concurrent_requests` - Current number of concurrent requests
///
/// # Returns
///
/// Optimized buffer size in bytes
pub fn get_advanced_buffer_size(
file_size: i64,
base_buffer_size: usize,
is_sequential: bool,
concurrent_requests: usize,
) -> usize {
// For very small files, use smaller buffers regardless of concurrency
if file_size > 0 && file_size < 256 * KI_B as i64 {
return (file_size as usize / 4).clamp(16 * KI_B, 64 * KI_B);
}
// Base calculation from standard function
let standard_size = get_concurrency_aware_buffer_size(file_size, base_buffer_size);
let medium_threshold = 4;
let high_threshold = 8;
// For sequential reads, we can be more aggressive with buffer sizes
if is_sequential && concurrent_requests <= medium_threshold {
// Boost buffer size for sequential reads under low concurrency
let boosted = (standard_size as f64 * 1.5) as usize;
return boosted.min(MI_B);
}
// For random reads under high concurrency, reduce buffer size
if !is_sequential && concurrent_requests > high_threshold {
let reduced = (standard_size as f64 * 0.7) as usize;
return reduced.max(32 * KI_B);
}
standard_size
}
/// Get buffer size with storage media optimization.
///
/// Adjusts buffer size based on storage media characteristics.
///
/// # Arguments
///
/// * `base_size` - Base buffer size
/// * `media` - Storage media type
///
/// # Returns
///
/// Optimized buffer size for the storage media
pub fn get_buffer_size_for_media(base_size: usize, media: StorageMedia) -> usize {
let multiplier = match media {
StorageMedia::Nvme => 1.5, // NVMe can handle larger buffers
StorageMedia::Ssd => 1.2, // SSD benefits from moderate buffers
StorageMedia::Hdd => 0.8, // HDD prefers smaller buffers to reduce seek overhead
StorageMedia::Unknown => 1.0,
};
(base_size as f64 * multiplier).min(MI_B as f64) as usize
}
/// Calculate optimal buffer size using multi-factor analysis.
///
/// This is the main entry point for buffer size calculation, considering
/// all factors: concurrency, storage media, access pattern, and load.
///
/// # Arguments
///
/// * `file_size` - Size of the file being read
/// * `base_buffer_size` - Base buffer size
/// * `is_sequential` - Whether access is sequential
/// * `concurrent_requests` - Current concurrency level
/// * `media` - Storage media type
/// * `load_level` - Current I/O load level
///
/// # Returns
///
/// Optimally calculated buffer size
pub fn calculate_optimal_buffer_size(
file_size: i64,
base_buffer_size: usize,
is_sequential: bool,
concurrent_requests: usize,
media: StorageMedia,
load_level: IoLoadLevel,
) -> usize {
// Start with advanced buffer size calculation
let mut buffer_size = get_advanced_buffer_size(file_size, base_buffer_size, is_sequential, concurrent_requests);
// Apply storage media optimization
buffer_size = get_buffer_size_for_media(buffer_size, media);
// Apply load-based adjustment
let load_multiplier = match load_level {
IoLoadLevel::Low => 1.2,
IoLoadLevel::Medium => 1.0,
IoLoadLevel::High => 0.7,
IoLoadLevel::Critical => 0.5,
};
buffer_size = (buffer_size as f64 * load_multiplier) as usize;
// Final bounds check
buffer_size.clamp(32 * KI_B, MI_B)
}
/// I/O scheduling context for multi-factor strategy calculation.
#[derive(Debug, Clone)]
pub struct IoSchedulingContext {
/// File size in bytes (-1 if unknown).
pub file_size: i64,
/// Base buffer size from configuration.
pub base_buffer_size: usize,
/// Time spent waiting for permit.
pub permit_wait_duration: Duration,
/// Whether access is sequential.
pub is_sequential_hint: bool,
/// Detected access pattern.
pub access_pattern: AccessPattern,
/// Detected storage media.
pub storage_media: StorageMedia,
/// Observed bandwidth in bytes per second.
pub observed_bandwidth_bps: Option<u64>,
/// Current concurrent request count.
pub concurrent_requests: usize,
}
impl Default for IoSchedulingContext {
fn default() -> Self {
Self {
file_size: -1,
base_buffer_size: 128 * KI_B,
permit_wait_duration: Duration::ZERO,
is_sequential_hint: true,
access_pattern: AccessPattern::Unknown,
storage_media: StorageMedia::Unknown,
observed_bandwidth_bps: None,
concurrent_requests: 1,
}
}
}
impl IoSchedulingContext {
/// Create a new scheduling context.
pub fn new(file_size: i64, base_buffer_size: usize) -> Self {
Self {
file_size,
base_buffer_size,
..Self::default()
}
}
/// Builder pattern: set sequential hint.
pub fn with_sequential(mut self, is_sequential: bool) -> Self {
self.is_sequential_hint = is_sequential;
self.access_pattern = if is_sequential {
AccessPattern::Sequential
} else {
AccessPattern::Random
};
self
}
/// Builder pattern: set storage media.
pub fn with_media(mut self, media: StorageMedia) -> Self {
self.storage_media = media;
self
}
/// Builder pattern: set bandwidth.
pub fn with_bandwidth(mut self, bps: u64) -> Self {
self.observed_bandwidth_bps = Some(bps);
self
}
/// Builder pattern: set concurrency.
pub fn with_concurrency(mut self, count: usize) -> Self {
self.concurrent_requests = count;
self
}
/// Builder pattern: set wait duration.
pub fn with_wait_duration(mut self, duration: Duration) -> Self {
self.permit_wait_duration = duration;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_io_priority() {
assert_eq!(IoPriority::from_size(1024, 64 * 1024, 4 * 1024 * 1024), IoPriority::High);
assert_eq!(IoPriority::from_size(1024 * 1024, 64 * 1024, 4 * 1024 * 1024), IoPriority::Normal);
assert_eq!(IoPriority::from_size(10 * 1024 * 1024, 64 * 1024, 4 * 1024 * 1024), IoPriority::Low);
}
#[test]
fn test_io_load_level() {
let low = Duration::from_millis(5);
let high = Duration::from_millis(50);
assert_eq!(IoLoadLevel::from_wait_time(Duration::from_millis(1), low, high), IoLoadLevel::Low);
assert_eq!(IoLoadLevel::from_wait_time(Duration::from_millis(20), low, high), IoLoadLevel::Medium);
assert_eq!(IoLoadLevel::from_wait_time(Duration::from_millis(60), low, high), IoLoadLevel::High);
assert_eq!(IoLoadLevel::from_wait_time(Duration::from_millis(150), low, high), IoLoadLevel::Critical);
}
#[test]
fn test_bandwidth_tier() {
assert_eq!(BandwidthTier::from_bps(50 * 1024 * 1024), BandwidthTier::Low);
assert_eq!(BandwidthTier::from_bps(200 * 1024 * 1024), BandwidthTier::Medium);
assert_eq!(BandwidthTier::from_bps(600 * 1024 * 1024), BandwidthTier::High);
}
#[test]
fn test_io_strategy_default() {
let strategy = IoStrategy::default();
assert!(strategy.buffer_size > 0);
assert!(strategy.enable_readahead);
}
#[test]
fn test_io_scheduler() {
let scheduler = IoScheduler::with_defaults();
let strategy = scheduler.calculate_strategy(1024 * 1024, Duration::from_millis(5), true);
assert!(strategy.buffer_size > 0);
assert!(strategy.enable_readahead);
assert_eq!(strategy.load_level, IoLoadLevel::Low);
}
#[test]
fn test_io_scheduler_with_concurrency() {
let scheduler = IoScheduler::with_defaults();
// Simulate concurrent requests
scheduler.increment_requests();
scheduler.increment_requests();
scheduler.increment_requests();
let strategy = scheduler.calculate_strategy(1024 * 1024, Duration::from_millis(5), true);
assert_eq!(strategy.concurrent_requests, 3);
}
#[test]
fn test_load_metrics() {
let mut metrics = IoLoadMetrics::new();
metrics.add_sample(Duration::from_millis(10));
metrics.add_sample(Duration::from_millis(20));
metrics.add_sample(Duration::from_millis(30));
assert_eq!(metrics.sample_count, 3);
assert_eq!(metrics.avg_wait_time, Duration::from_millis(20));
assert_eq!(metrics.max_wait_time, Duration::from_millis(30));
}
#[test]
fn test_get_concurrency_aware_buffer_size() {
// Test with default concurrency (1)
let size = get_concurrency_aware_buffer_size(1024 * 1024, 128 * KI_B);
assert!(size >= 64 * KI_B);
assert!(size <= MI_B);
// Test with small file
let size = get_concurrency_aware_buffer_size(50 * KI_B as i64, 128 * KI_B);
assert!(size >= 32 * KI_B);
}
#[test]
fn test_get_advanced_buffer_size() {
// Sequential read with low concurrency
let size = get_advanced_buffer_size(10 * MI_B as i64, 128 * KI_B, true, 2);
assert!(size >= 128 * KI_B);
// Random read with high concurrency
let size = get_advanced_buffer_size(10 * MI_B as i64, 128 * KI_B, false, 10);
assert!(size >= 32 * KI_B);
// Very small file
let size = get_advanced_buffer_size(100 * KI_B as i64, 128 * KI_B, true, 1);
assert!(size <= 64 * KI_B);
}
#[test]
fn test_get_buffer_size_for_media() {
let base = 128 * KI_B;
// NVMe should get larger buffers
let nvme_size = get_buffer_size_for_media(base, StorageMedia::Nvme);
assert!(nvme_size > base);
// SSD should get slightly larger buffers
let ssd_size = get_buffer_size_for_media(base, StorageMedia::Ssd);
assert!(ssd_size > base);
// HDD should get smaller buffers
let hdd_size = get_buffer_size_for_media(base, StorageMedia::Hdd);
assert!(hdd_size < base);
}
#[test]
fn test_calculate_optimal_buffer_size() {
// Low load, sequential, NVMe
let size = calculate_optimal_buffer_size(10 * MI_B as i64, 128 * KI_B, true, 2, StorageMedia::Nvme, IoLoadLevel::Low);
assert!(size >= 32 * KI_B);
assert!(size <= MI_B);
// Critical load, random, HDD
let size =
calculate_optimal_buffer_size(10 * MI_B as i64, 128 * KI_B, false, 10, StorageMedia::Hdd, IoLoadLevel::Critical);
assert!(size >= 32 * KI_B);
assert!(size <= MI_B);
}
#[test]
fn test_io_scheduling_context() {
let ctx = IoSchedulingContext::new(10 * MI_B as i64, 256 * KI_B)
.with_sequential(true)
.with_media(StorageMedia::Nvme)
.with_bandwidth(500 * MI_B as u64)
.with_concurrency(4);
assert_eq!(ctx.file_size, 10 * MI_B as i64);
assert_eq!(ctx.base_buffer_size, 256 * KI_B);
assert!(ctx.is_sequential_hint);
assert_eq!(ctx.storage_media, StorageMedia::Nvme);
assert_eq!(ctx.observed_bandwidth_bps, Some(500 * MI_B as u64));
assert_eq!(ctx.concurrent_requests, 4);
}
}
+320
View File
@@ -0,0 +1,320 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Shared memory pool for zero-copy data sharing.
//!
//! This module provides Arc-based shared memory management for
//! efficient cross-task data passing without serialization.
use std::convert::AsRef;
use std::ops::Deref;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Instant;
/// Shared memory pool configuration.
#[derive(Debug, Clone)]
pub struct SharedMemoryConfig {
/// Whether shared memory is enabled
pub enabled: bool,
/// Maximum pool size in bytes
pub max_pool_size: usize,
/// Maximum object size in bytes
pub max_object_size: usize,
}
impl Default for SharedMemoryConfig {
fn default() -> Self {
Self {
enabled: true,
max_pool_size: 100 * 1024 * 1024, // 100MB
max_object_size: 10 * 1024 * 1024, // 10MB
}
}
}
/// Shared memory pool statistics.
#[derive(Debug, Default)]
pub struct SharedMemoryStats {
/// Total number of objects created
pub total_objects: AtomicU64,
/// Total number of shared references
pub total_shared_refs: AtomicU64,
/// Current memory usage in bytes
pub current_memory: AtomicU64,
/// Peak memory usage in bytes
pub peak_memory: AtomicU64,
}
/// Arc data metadata.
#[derive(Clone, Debug)]
pub struct ArcMetadata {
/// Size of the data (if measurable)
pub size: Option<usize>,
/// Creation timestamp
pub created_at: Instant,
}
/// Arc-based data wrapper for zero-copy sharing.
///
/// This wrapper uses Arc to enable shared ownership of data
/// across multiple tasks without copying.
pub struct ArcData<T> {
/// The wrapped data
inner: Arc<T>,
/// Metadata about the data
metadata: ArcMetadata,
}
impl<T> Clone for ArcData<T> {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
metadata: self.metadata.clone(),
}
}
}
impl<T> ArcData<T> {
/// Create a new ArcData wrapper.
pub fn new(data: T) -> Self {
ArcData {
inner: Arc::new(data),
metadata: ArcMetadata {
size: None,
created_at: Instant::now(),
},
}
}
/// Create a new ArcData wrapper with known size.
pub fn with_size(data: T, size: usize) -> Self {
ArcData {
inner: Arc::new(data),
metadata: ArcMetadata {
size: Some(size),
created_at: Instant::now(),
},
}
}
/// Get the reference count.
pub fn ref_count(&self) -> usize {
Arc::strong_count(&self.inner)
}
/// Convert into the underlying Arc.
pub fn into_arc(self) -> Arc<T> {
self.inner
}
/// Get the metadata.
pub fn metadata(&self) -> &ArcMetadata {
&self.metadata
}
/// Get the size if known.
pub fn size(&self) -> Option<usize> {
self.metadata.size
}
}
impl<T> AsRef<T> for ArcData<T> {
fn as_ref(&self) -> &T {
&self.inner
}
}
impl<T> Deref for ArcData<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<T> std::fmt::Debug for ArcData<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ArcData")
.field("ref_count", &self.ref_count())
.field("metadata", &self.metadata)
.finish()
}
}
/// Shared memory pool for managing Arc-based shared data.
pub struct SharedMemoryPool {
config: SharedMemoryConfig,
stats: SharedMemoryStats,
}
impl SharedMemoryPool {
/// Create a new shared memory pool with the given configuration.
pub fn new(config: SharedMemoryConfig) -> Self {
Self {
config,
stats: SharedMemoryStats::default(),
}
}
/// Create a new shared memory pool with default configuration.
pub fn with_defaults() -> Self {
Self::new(SharedMemoryConfig::default())
}
/// Create shared data.
///
/// This method wraps the data in an ArcData for zero-copy sharing.
pub fn create<T>(&self, data: T) -> ArcData<T> {
self.stats.total_objects.fetch_add(1, Ordering::Relaxed);
ArcData::new(data)
}
/// Create shared data with known size.
///
/// This method tracks memory usage for statistics.
pub fn create_with_size<T>(&self, data: T, size: usize) -> ArcData<T> {
self.stats.total_objects.fetch_add(1, Ordering::Relaxed);
// Update memory statistics
self.stats.current_memory.fetch_add(size as u64, Ordering::Relaxed);
// Update peak memory
let current = self.stats.current_memory.load(Ordering::Relaxed);
let mut peak = self.stats.peak_memory.load(Ordering::Relaxed);
if current > peak {
peak = current;
self.stats.peak_memory.store(peak, Ordering::Relaxed);
}
ArcData::with_size(data, size)
}
/// Share data by increasing reference count.
///
/// This method creates a new ArcData that shares the underlying data
/// without copying.
pub fn share<T>(&self, data: &ArcData<T>) -> ArcData<T> {
self.stats.total_shared_refs.fetch_add(1, Ordering::Relaxed);
data.clone()
}
/// Get the statistics for this pool.
pub fn stats(&self) -> &SharedMemoryStats {
&self.stats
}
/// Get the configuration for this pool.
pub fn config(&self) -> &SharedMemoryConfig {
&self.config
}
/// Check if the pool is enabled.
pub fn is_enabled(&self) -> bool {
self.config.enabled
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_arc_data_new() {
let data = vec![1u8, 2, 3, 4, 5];
let arc_data = ArcData::new(data.clone());
assert_eq!(arc_data.as_ref(), &data);
assert_eq!(arc_data.ref_count(), 1);
}
#[test]
fn test_arc_data_clone() {
let data = vec![1u8, 2, 3, 4, 5];
let arc_data = ArcData::new(data.clone());
assert_eq!(arc_data.ref_count(), 1);
let arc_data2 = arc_data.clone();
assert_eq!(arc_data.ref_count(), 2);
assert_eq!(arc_data2.ref_count(), 2);
let arc_data3 = arc_data.clone();
assert_eq!(arc_data.ref_count(), 3);
assert_eq!(arc_data2.ref_count(), 3);
assert_eq!(arc_data3.ref_count(), 3);
}
#[test]
fn test_arc_data_deref() {
let data = vec![1u8, 2, 3, 4, 5];
let arc_data = ArcData::new(data.clone());
// Test Deref trait
assert_eq!(arc_data.len(), 5);
assert_eq!(arc_data[0], 1);
}
#[test]
fn test_shared_memory_pool_create() {
let pool = SharedMemoryPool::with_defaults();
let data = vec![1u8, 2, 3, 4, 5];
let arc_data = pool.create(data.clone());
assert_eq!(arc_data.as_ref(), &data);
assert_eq!(pool.stats().total_objects.load(Ordering::Relaxed), 1);
}
#[test]
fn test_shared_memory_pool_share() {
let pool = SharedMemoryPool::with_defaults();
let data = vec![1u8, 2, 3, 4, 5];
let arc_data = pool.create(data.clone());
assert_eq!(arc_data.ref_count(), 1);
let shared = pool.share(&arc_data);
assert_eq!(arc_data.ref_count(), 2);
assert_eq!(shared.ref_count(), 2);
assert_eq!(pool.stats().total_shared_refs.load(Ordering::Relaxed), 1);
}
#[test]
fn test_shared_memory_pool_with_size() {
let pool = SharedMemoryPool::with_defaults();
let data = vec![1u8; 1024];
let arc_data = pool.create_with_size(data.clone(), 1024);
assert_eq!(arc_data.size(), Some(1024));
assert_eq!(pool.stats().current_memory.load(Ordering::Relaxed), 1024);
}
#[test]
fn test_default_config() {
let config = SharedMemoryConfig::default();
assert!(config.enabled);
assert_eq!(config.max_pool_size, 100 * 1024 * 1024);
assert_eq!(config.max_object_size, 10 * 1024 * 1024);
}
}
+497
View File
@@ -0,0 +1,497 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Timeout wrapper for I/O operations.
//!
//! This module provides timeout management for I/O operations with
//! dynamic timeout calculation based on operation size.
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
/// Timeout configuration.
#[derive(Debug, Clone)]
pub struct TimeoutConfig {
/// Base timeout for small operations.
pub base_timeout: Duration,
/// Timeout per MB of data.
pub timeout_per_mb: Duration,
/// Maximum timeout.
pub max_timeout: Duration,
/// Minimum timeout.
pub min_timeout: Duration,
/// GetObject operation timeout.
pub get_object_timeout: Duration,
/// PutObject operation timeout.
pub put_object_timeout: Duration,
/// ListObjects operation timeout.
pub list_objects_timeout: Duration,
/// Whether dynamic timeout is enabled.
pub enable_dynamic_timeout: bool,
}
impl Default for TimeoutConfig {
fn default() -> Self {
Self {
base_timeout: Duration::from_secs(5),
timeout_per_mb: Duration::from_millis(100),
max_timeout: Duration::from_secs(300),
min_timeout: Duration::from_secs(1),
get_object_timeout: Duration::from_secs(30),
put_object_timeout: Duration::from_secs(60),
list_objects_timeout: Duration::from_secs(10),
enable_dynamic_timeout: true,
}
}
}
impl TimeoutConfig {
/// Create new timeout configuration.
pub fn new() -> Self {
Self::default()
}
/// Calculate dynamic timeout based on size.
pub fn calculate_timeout(&self, size_bytes: u64) -> Duration {
if !self.enable_dynamic_timeout {
return self.base_timeout;
}
let mb = size_bytes as f64 / (1024.0 * 1024.0);
let timeout = self.base_timeout + self.timeout_per_mb.mul_f64(mb);
timeout.clamp(self.min_timeout, self.max_timeout)
}
/// Validate the configuration.
pub fn validate(&self) -> Result<(), TimeoutError> {
if self.min_timeout > self.max_timeout {
return Err(TimeoutError::InvalidConfig("min_timeout must be <= max_timeout".to_string()));
}
if self.base_timeout < self.min_timeout || self.base_timeout > self.max_timeout {
return Err(TimeoutError::InvalidConfig(
"base_timeout must be between min_timeout and max_timeout".to_string(),
));
}
Ok(())
}
}
/// Timeout error.
#[derive(Debug, Clone, thiserror::Error)]
pub enum TimeoutError {
/// Operation timed out.
#[error("Operation timed out after {0:?}")]
TimedOut(Duration),
/// Invalid configuration.
#[error("Invalid timeout config: {0}")]
InvalidConfig(String),
}
/// Operation progress tracker.
#[derive(Debug)]
pub struct OperationProgress {
/// Total size (if known).
pub total_size: Option<u64>,
/// Bytes processed.
bytes_processed: AtomicU64,
/// Last update time.
last_update: std::sync::Mutex<Instant>,
/// Stale timeout.
stale_timeout: Duration,
/// Start time for transfer rate calculation.
start_time: Instant,
}
impl OperationProgress {
/// Create new operation progress.
pub fn new(total_size: Option<u64>, stale_timeout: Duration) -> Self {
Self {
total_size,
bytes_processed: AtomicU64::new(0),
last_update: std::sync::Mutex::new(Instant::now()),
stale_timeout,
start_time: Instant::now(),
}
}
/// Update progress.
pub fn update(&self, bytes: u64) {
self.bytes_processed.store(bytes, Ordering::Relaxed);
if let Ok(mut last) = self.last_update.lock() {
*last = Instant::now();
}
}
/// Add to progress.
pub fn add(&self, bytes: u64) {
self.bytes_processed.fetch_add(bytes, Ordering::Relaxed);
if let Ok(mut last) = self.last_update.lock() {
*last = Instant::now();
}
}
/// Get current progress.
pub fn current(&self) -> u64 {
self.bytes_processed.load(Ordering::Relaxed)
}
/// Check if progress is stale.
pub fn is_stale(&self) -> bool {
if let Ok(last) = self.last_update.lock() {
last.elapsed() > self.stale_timeout
} else {
false
}
}
/// Get progress percentage.
pub fn progress_percent(&self) -> Option<f64> {
self.total_size.map(|total| {
if total == 0 {
100.0
} else {
let processed = self.bytes_processed.load(Ordering::Relaxed);
(processed as f64 / total as f64 * 100.0).min(100.0)
}
})
}
/// Get remaining bytes.
pub fn remaining(&self) -> Option<u64> {
self.total_size.map(|total| {
let processed = self.bytes_processed.load(Ordering::Relaxed);
total.saturating_sub(processed)
})
}
/// Calculate transfer rate in bytes per second.
///
/// Returns 0 if no time has elapsed or no data transferred.
pub fn transfer_rate(&self) -> u64 {
let processed = self.bytes_processed.load(Ordering::Relaxed);
if processed == 0 {
return 0;
}
let elapsed = self.start_time.elapsed().as_secs_f64();
if elapsed > 0.0 {
(processed as f64 / elapsed) as u64
} else {
0
}
}
}
/// Request timeout wrapper.
pub struct RequestTimeoutWrapper {
/// Configuration.
config: TimeoutConfig,
/// Start time.
start_time: Instant,
/// Operation progress.
progress: Option<OperationProgress>,
}
impl RequestTimeoutWrapper {
/// Create a new timeout wrapper.
pub fn new(config: TimeoutConfig) -> Self {
Self {
config,
start_time: Instant::now(),
progress: None,
}
}
/// Create with progress tracking.
pub fn with_progress(config: TimeoutConfig, total_size: Option<u64>, stale_timeout: Duration) -> Self {
Self {
config,
start_time: Instant::now(),
progress: Some(OperationProgress::new(total_size, stale_timeout)),
}
}
/// Get the configuration.
pub fn config(&self) -> &TimeoutConfig {
&self.config
}
/// Get elapsed time.
pub fn elapsed(&self) -> Duration {
self.start_time.elapsed()
}
/// Get remaining time.
pub fn remaining(&self, timeout: Duration) -> Option<Duration> {
let elapsed = self.elapsed();
if elapsed >= timeout { None } else { Some(timeout - elapsed) }
}
/// Check if timed out.
pub fn is_timed_out(&self, size: Option<u64>) -> bool {
let timeout = self.get_timeout(size);
self.elapsed() > timeout
}
/// Get the timeout for a given size.
pub fn get_timeout(&self, size: Option<u64>) -> Duration {
if self.config.enable_dynamic_timeout {
if let Some(s) = size {
self.config.calculate_timeout(s)
} else {
self.config.base_timeout
}
} else {
self.config.base_timeout
}
}
/// Check if timed out and return error if so.
pub fn check_timeout(&self, size: Option<u64>) -> Result<(), TimeoutError> {
if self.is_timed_out(size) {
Err(TimeoutError::TimedOut(self.get_timeout(size)))
} else {
Ok(())
}
}
/// Get progress.
pub fn progress(&self) -> Option<&OperationProgress> {
self.progress.as_ref()
}
/// Update progress.
pub fn update_progress(&self, bytes: u64) {
if let Some(ref progress) = self.progress {
progress.update(bytes);
}
}
/// Check if operation is stalled (no progress for a while).
pub fn is_stalled(&self) -> bool {
self.progress.as_ref().is_some_and(|p| p.is_stale())
}
/// Get progress percentage.
pub fn progress_percent(&self) -> Option<f64> {
self.progress.as_ref().and_then(|p| p.progress_percent())
}
}
/// Timeout statistics.
#[derive(Debug, Default)]
pub struct TimeoutStats {
/// Total operations.
pub total_operations: AtomicU64,
/// Timed out operations.
pub timed_out: AtomicU64,
/// Total wait time in nanoseconds.
pub total_wait_time_ns: AtomicU64,
/// Maximum wait time in nanoseconds.
pub max_wait_time_ns: AtomicU64,
}
impl TimeoutStats {
/// Create new timeout statistics.
pub fn new() -> Self {
Self::default()
}
/// Record an operation.
pub fn record_operation(&self, wait_time: Duration) {
self.total_operations.fetch_add(1, Ordering::Relaxed);
let ns = wait_time.as_nanos() as u64;
self.total_wait_time_ns.fetch_add(ns, Ordering::Relaxed);
let mut current = self.max_wait_time_ns.load(Ordering::Relaxed);
while ns > current {
match self
.max_wait_time_ns
.compare_exchange_weak(current, ns, Ordering::Relaxed, Ordering::Relaxed)
{
Ok(_) => break,
Err(actual) => current = actual,
}
}
}
/// Record a timeout.
pub fn record_timeout(&self) {
self.timed_out.fetch_add(1, Ordering::Relaxed);
}
/// Get timeout rate.
pub fn timeout_rate(&self) -> f64 {
let total = self.total_operations.load(Ordering::Relaxed);
let timed_out = self.timed_out.load(Ordering::Relaxed);
if total == 0 { 0.0 } else { timed_out as f64 / total as f64 }
}
/// Get average wait time.
pub fn avg_wait_time(&self) -> Duration {
let total = self.total_wait_time_ns.load(Ordering::Relaxed);
let count = self.total_operations.load(Ordering::Relaxed);
if count == 0 {
Duration::ZERO
} else {
Duration::from_nanos(total / count)
}
}
/// Reset statistics.
pub fn reset(&self) {
self.total_operations.store(0, Ordering::Relaxed);
self.timed_out.store(0, Ordering::Relaxed);
self.total_wait_time_ns.store(0, Ordering::Relaxed);
self.max_wait_time_ns.store(0, Ordering::Relaxed);
}
}
/// Calculate adaptive timeout based on historical data and current conditions.
///
/// This function adjusts the timeout based on:
/// - Historical transfer rate
/// - Recent timeout count
/// - Object size
pub fn calculate_adaptive_timeout(
base_timeout: Duration,
historical_rate_bps: Option<u64>,
recent_timeout_count: u32,
object_size: u64,
) -> Duration {
// If we have recent timeouts, increase timeout
let timeout_multiplier = if recent_timeout_count > 3 {
2.0 // Double timeout if many recent timeouts
} else if recent_timeout_count > 1 {
1.5 // 50% increase if some timeouts
} else {
1.0 // No adjustment
};
// If we have historical rate data, use it for estimation
let estimated_duration = if let Some(rate) = historical_rate_bps {
if rate > 0 {
let estimated_secs = (object_size as f64 / rate as f64) * 1.2; // 20% buffer
Duration::from_secs_f64(estimated_secs)
} else {
base_timeout
}
} else {
base_timeout
};
// Apply timeout multiplier but clamp to reasonable bounds
let adaptive_duration = Duration::from_secs_f64(estimated_duration.as_secs_f64() * timeout_multiplier);
// Clamp to 5 seconds minimum and 10 minutes maximum
adaptive_duration.clamp(Duration::from_secs(5), Duration::from_secs(600))
}
/// Estimate bytes per second transfer rate.
///
/// This is used for adaptive timeout calculation.
pub fn estimate_bytes_per_second(object_size: u64, expected_duration: Duration) -> u64 {
let secs = expected_duration.as_secs_f64();
if secs > 0.0 {
(object_size as f64 / secs) as u64
} else {
// Return a reasonable default (1 MB/s)
1024 * 1024
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_timeout_config() {
let config = TimeoutConfig::default();
assert!(config.validate().is_ok());
// Small file
let timeout = config.calculate_timeout(1024);
assert!(timeout >= config.min_timeout);
// Large file
let timeout = config.calculate_timeout(100 * 1024 * 1024);
assert!(timeout <= config.max_timeout);
}
#[test]
fn test_timeout_config_validation() {
let config = TimeoutConfig {
min_timeout: Duration::from_secs(10),
max_timeout: Duration::from_secs(5),
..Default::default()
};
assert!(config.validate().is_err());
}
#[test]
fn test_operation_progress() {
let progress = OperationProgress::new(Some(1000), Duration::from_secs(5));
assert_eq!(progress.current(), 0);
assert_eq!(progress.progress_percent(), Some(0.0));
progress.update(500);
assert_eq!(progress.current(), 500);
assert_eq!(progress.progress_percent(), Some(50.0));
progress.add(300);
assert_eq!(progress.current(), 800);
assert_eq!(progress.remaining(), Some(200));
}
#[test]
fn test_request_timeout_wrapper() {
let config = TimeoutConfig {
base_timeout: Duration::from_millis(100),
enable_dynamic_timeout: false,
..Default::default()
};
let wrapper = RequestTimeoutWrapper::new(config);
assert!(!wrapper.is_timed_out(None));
std::thread::sleep(Duration::from_millis(150));
assert!(wrapper.is_timed_out(None));
assert!(wrapper.check_timeout(None).is_err());
}
#[test]
fn test_timeout_stats() {
let stats = TimeoutStats::new();
stats.record_operation(Duration::from_millis(10));
stats.record_operation(Duration::from_millis(20));
stats.record_timeout();
assert_eq!(stats.total_operations.load(Ordering::Relaxed), 2);
assert_eq!(stats.timed_out.load(Ordering::Relaxed), 1);
assert!((stats.timeout_rate() - 0.5).abs() < 0.01);
}
#[test]
fn test_progress_tracking() {
let config = TimeoutConfig::default();
let wrapper = RequestTimeoutWrapper::with_progress(config, Some(1000), Duration::from_secs(1));
wrapper.update_progress(500);
assert_eq!(wrapper.progress_percent(), Some(50.0));
assert!(!wrapper.is_stalled());
}
}
+410
View File
@@ -0,0 +1,410 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Zero-copy object writer for optimized write operations.
//!
//! This module provides a zero-copy writer that minimizes memory allocations
//! and data copying during write operations.
use bytes::{BufMut, Bytes, BytesMut};
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::io::AsyncWrite;
/// Zero-copy object writer for optimized write operations.
///
/// This writer minimizes memory allocations by:
/// - Using BytesMut for efficient buffer growth
/// - Supporting zero-copy data transfer via Bytes
/// - Optional integration with BytesPool for buffer reuse
///
/// # Example
///
/// ```ignore
/// use rustfs_io_core::ZeroCopyObjectWriter;
/// use bytes::Bytes;
///
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut writer = ZeroCopyObjectWriter::new();
///
/// // Write with zero-copy
/// let data = Bytes::from("hello world");
/// writer.write_zero_copy(data).await?;
///
/// // Get the result as Bytes (zero-copy conversion)
/// let result = writer.into_bytes();
///
/// Ok(())
/// }
/// ```
pub struct ZeroCopyObjectWriter {
/// Internal buffer using BytesMut for efficient growth
buffer: BytesMut,
/// Total bytes written
bytes_written: usize,
/// Whether the writer has been finalized
finalized: bool,
}
impl ZeroCopyObjectWriter {
/// Create a new zero-copy object writer with default capacity (8KB).
///
/// # Example
///
/// ```ignore
/// let writer = ZeroCopyObjectWriter::new();
/// ```
pub fn new() -> Self {
Self::with_capacity(8 * 1024)
}
/// Create a new zero-copy object writer with specified capacity.
///
/// # Arguments
///
/// * `capacity` - Initial buffer capacity in bytes
///
/// # Example
///
/// ```ignore
/// let writer = ZeroCopyObjectWriter::with_capacity(64 * 1024);
/// ```
pub fn with_capacity(capacity: usize) -> Self {
Self {
buffer: BytesMut::with_capacity(capacity),
bytes_written: 0,
finalized: false,
}
}
/// Write data with zero-copy if possible.
///
/// This method attempts to write data without copying:
/// - If `data` is a Bytes slice, it may be appended without copying
/// - If `data` shares the same underlying buffer, no copy occurs
///
/// # Arguments
///
/// * `data` - Data to write (as Bytes for zero-copy potential)
///
/// # Returns
///
/// * `Ok(usize)` - Number of bytes written
/// * `Err(ZeroCopyWriteError)` - Write error
///
/// # Example
///
/// ```ignore
/// let data = Bytes::from("hello world");
/// let written = writer.write_zero_copy(data).await?;
/// ```
pub async fn write_zero_copy(&mut self, data: Bytes) -> Result<usize, ZeroCopyWriteError> {
if self.finalized {
return Err(ZeroCopyWriteError::Finalized("Cannot write to finalized writer".to_string()));
}
let len = data.len();
// Zero-copy: put Bytes into BytesMut
// If data shares the same underlying buffer, no copy occurs
self.buffer.put(data);
self.bytes_written += len;
Ok(len)
}
/// Write a slice of data.
///
/// # Arguments
///
/// * `data` - Data slice to write
///
/// # Returns
///
/// * `Ok(usize)` - Number of bytes written
/// * `Err(ZeroCopyWriteError)` - Write error
pub async fn write_slice(&mut self, data: &[u8]) -> Result<usize, ZeroCopyWriteError> {
if self.finalized {
return Err(ZeroCopyWriteError::Finalized("Cannot write to finalized writer".to_string()));
}
let len = data.len();
self.buffer.put_slice(data);
self.bytes_written += len;
Ok(len)
}
/// Finalize the writer and consume it, returning the written data as Bytes.
///
/// This converts the internal BytesMut to Bytes, which is a zero-copy
/// operation that freezes the buffer.
///
/// # Returns
///
/// The written data as Bytes
///
/// # Example
///
/// ```ignore
/// let result = writer.into_bytes();
/// ```
pub fn into_bytes(mut self) -> Bytes {
self.finalized = true;
self.buffer.freeze()
}
/// Get the current buffer as a slice (without consuming).
///
/// # Returns
///
/// Slice of the current buffer content
pub fn as_slice(&self) -> &[u8] {
&self.buffer[..]
}
/// Get the total number of bytes written.
///
/// # Returns
///
/// Number of bytes written
pub fn bytes_written(&self) -> usize {
self.bytes_written
}
/// Get the current buffer capacity.
///
/// # Returns
///
/// Current buffer capacity in bytes
pub fn capacity(&self) -> usize {
self.buffer.capacity()
}
/// Get the current buffer length.
///
/// # Returns
///
/// Current buffer length in bytes
pub fn len(&self) -> usize {
self.buffer.len()
}
/// Check if the buffer is empty.
///
/// # Returns
///
/// `true` if buffer is empty, `false` otherwise
pub fn is_empty(&self) -> bool {
self.buffer.is_empty()
}
/// Clear the buffer, resetting it to empty.
///
/// This does not change the capacity, just resets the length to 0.
pub fn clear(&mut self) {
self.buffer.clear();
self.bytes_written = 0;
self.finalized = false;
}
/// Reserve additional capacity in the buffer.
///
/// # Arguments
///
/// * `additional` - Additional capacity to reserve
pub fn reserve(&mut self, additional: usize) {
self.buffer.reserve(additional);
}
}
impl Default for ZeroCopyObjectWriter {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for ZeroCopyObjectWriter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ZeroCopyObjectWriter")
.field("buffer_len", &self.buffer.len())
.field("buffer_capacity", &self.buffer.capacity())
.field("bytes_written", &self.bytes_written)
.field("finalized", &self.finalized)
.finish()
}
}
/// AsyncWrite implementation for ZeroCopyObjectWriter.
///
/// This allows the writer to be used with tokio's async I/O utilities.
impl AsyncWrite for ZeroCopyObjectWriter {
fn poll_write(mut self: Pin<&mut Self>, _cx: &mut Context<'_>, buf: &[u8]) -> Poll<Result<usize, tokio::io::Error>> {
if self.finalized {
return Poll::Ready(Err(tokio::io::Error::new(
tokio::io::ErrorKind::WriteZero,
"Cannot write to finalized writer",
)));
}
let len = buf.len();
self.buffer.put_slice(buf);
self.bytes_written += len;
Poll::Ready(Ok(len))
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), tokio::io::Error>> {
// Nothing to flush for in-memory buffer
Poll::Ready(Ok(()))
}
fn poll_shutdown(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), tokio::io::Error>> {
self.finalized = true;
Poll::Ready(Ok(()))
}
}
/// Zero-copy write error types.
#[derive(Debug, thiserror::Error)]
pub enum ZeroCopyWriteError {
/// I/O error occurred
#[error("I/O error: {0}")]
Io(#[from] tokio::io::Error),
/// Writer has been finalized and cannot accept more writes
#[error("Writer finalized: {0}")]
Finalized(String),
/// Invalid input provided
#[error("Invalid input: {0}")]
InvalidInput(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_new_writer() {
let writer = ZeroCopyObjectWriter::new();
assert!(writer.is_empty());
assert_eq!(writer.bytes_written(), 0);
assert!(writer.capacity() >= 8 * 1024);
}
#[tokio::test]
async fn test_write_zero_copy() {
let mut writer = ZeroCopyObjectWriter::new();
let data = Bytes::from("hello world");
let written = writer.write_zero_copy(data).await.unwrap();
assert_eq!(written, 11);
assert_eq!(writer.bytes_written(), 11);
assert_eq!(writer.as_slice(), b"hello world");
}
#[tokio::test]
async fn test_write_slice() {
let mut writer = ZeroCopyObjectWriter::new();
let data = b"hello world";
let written = writer.write_slice(data).await.unwrap();
assert_eq!(written, 11);
assert_eq!(writer.bytes_written(), 11);
assert_eq!(writer.as_slice(), b"hello world");
}
#[tokio::test]
async fn test_into_bytes() {
let mut writer = ZeroCopyObjectWriter::new();
let data = Bytes::from("hello world");
writer.write_zero_copy(data).await.unwrap();
let result = writer.into_bytes();
assert_eq!(result.as_ref(), b"hello world");
}
#[tokio::test]
async fn test_write_after_finalize() {
let mut writer = ZeroCopyObjectWriter::new();
let data = Bytes::from("hello");
writer.write_zero_copy(data).await.unwrap();
let _result = writer.into_bytes();
// Create new writer and try to write after finalize
let mut writer2 = ZeroCopyObjectWriter::new();
writer2.write_zero_copy(Bytes::from("test")).await.unwrap();
let _ = writer2.into_bytes();
// Writing to a consumed writer should work via new writer
let mut writer3 = ZeroCopyObjectWriter::new();
let result = writer3.write_zero_copy(Bytes::from("final")).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_clear() {
let mut writer = ZeroCopyObjectWriter::new();
writer.write_slice(b"hello").await.unwrap();
writer.clear();
assert!(writer.is_empty());
assert_eq!(writer.bytes_written(), 0);
// Capacity should remain
assert!(writer.capacity() > 0);
}
#[tokio::test]
async fn test_reserve() {
let mut writer = ZeroCopyObjectWriter::with_capacity(10);
let initial_capacity = writer.capacity();
writer.reserve(1000);
// Reserve ensures at least the additional capacity can be added
// but may allocate more than requested
assert!(writer.capacity() >= initial_capacity);
}
#[tokio::test]
async fn test_multiple_writes() {
let mut writer = ZeroCopyObjectWriter::new();
writer.write_zero_copy(Bytes::from("hello ")).await.unwrap();
writer.write_slice(b"world").await.unwrap();
assert_eq!(writer.as_slice(), b"hello world");
assert_eq!(writer.bytes_written(), 11);
}
#[tokio::test]
async fn test_async_write() {
use tokio::io::AsyncWriteExt;
let mut writer = ZeroCopyObjectWriter::new();
let data = b"hello world";
let written = writer.write(data).await.unwrap();
assert_eq!(written, 11);
assert_eq!(writer.as_slice(), b"hello world");
}
#[tokio::test]
async fn test_debug() {
let writer = ZeroCopyObjectWriter::new();
let debug_str = format!("{:?}", writer);
assert!(debug_str.contains("ZeroCopyObjectWriter"));
assert!(debug_str.contains("buffer_len"));
}
}