Compare commits

...

8 Commits

Author SHA1 Message Date
weisd 70e6bec2a4 feat:admin auth (#512)
* feat:admin auth

* fix:#509
2025-09-11 16:49:07 +08:00
guojidan cf863ba059 feat(lock): Add support for disabling lock manager (#511)
* feat(lock): Add support for disabling lock manager
Implement control of lock system activation and deactivation via environment variables
Add DisabledLockManager for lock-free operation scenarios
Introduce LockManager trait to uniformly manage different lock managers

Signed-off-by: junxiang Mu <1948535941@qq.com>

* refactor(lock): Optimize implementation of global lock manager and parsing of boolean environment variables
Refactor the implementation of the global lock manager: wrap FastObjectLockManager with Arc and add the as_fast_lock_manager method
Extract the boolean environment variable parsing logic into an independent function parse_bool_env_var

Signed-off-by: junxiang Mu <1948535941@qq.com>

---------

Signed-off-by: junxiang Mu <1948535941@qq.com>
2025-09-11 13:46:06 +08:00
guojidan d4beb1cc0b Fix lock (#510)
* Refactor: reimplement lock

Signed-off-by: junxiang Mu <1948535941@qq.com>

* Fix: fix test case failed

Signed-off-by: junxiang Mu <1948535941@qq.com>

* Improve: lock pref

Signed-off-by: junxiang Mu <1948535941@qq.com>

* fix(lock): Fix resource cleanup issue when batch lock acquisition fails
Ensure that the locks already acquired are properly released when batch lock acquisition fails to avoid memory leaks
Improve the lock protection mechanism to prevent double release issues
Add complete Apache license declarations to all files

Signed-off-by: junxiang Mu <1948535941@qq.com>

---------

Signed-off-by: junxiang Mu <1948535941@qq.com>
2025-09-11 12:10:35 +08:00
0xdx2 971e74281c fix:Fix some errors tested in mint (#507)
* refactor: replace new_object_layer_fn with get_validated_store for bucket validation

* feat: add validation for object tagging limits and uniqueness

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* feat: add EntityTooSmall error for multipart uploads and update error handling

* feat: validate max_parts input range for S3 multipart uploads

* Update rustfs/src/storage/ecfs.rs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: optimize tag key and value length validation checks

---------

Co-authored-by: damon <damonxue2@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-10 22:22:29 +08:00
Copilot ca9a2b6ab9 feat: Implement enhanced DNS resolver with hickory-resolver, TLS support, and layered fallback for Kubernetes environments (#505)
* Initial plan

* feat: Implement layered DNS resolver with caching and validation

Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>

* feat: Integrate DNS resolver into main application and fix formatting

Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>

* feat: Implement enhanced DNS resolver with Moka cache and layered fallback

Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>

* feat: Implement hickory-resolver with TLS support for enhanced DNS resolution

Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>

* upgrade

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2025-09-10 21:16:33 +08:00
houseme 4e00110bfe add bucket notification configuration (#502) 2025-09-10 00:56:27 +08:00
安正超 9c97524c3b feat: consolidate AI rules into unified AGENTS.md (#501)
- Merge all AI rules from .rules.md, .cursorrules, and CLAUDE.md into AGENTS.md
- Add competitor keyword prohibition rules (minio, ceph, swift, etc.)
- Simplify rules by removing overly detailed code examples
- Integrate new development principles as highest priority
- Remove old tool-specific rule files
- Fix clippy warnings for format string improvements
2025-09-09 21:36:34 +08:00
guojidan 14a8802ce7 Fix: fix collect usage data (#500)
Signed-off-by: junxiang Mu <1948535941@qq.com>
2025-09-09 18:39:51 +08:00
70 changed files with 7953 additions and 3462 deletions
-58
View File
@@ -1,58 +0,0 @@
# GitHub Copilot Rules for RustFS Project
## Core Rules Reference
This project follows the comprehensive AI coding rules defined in `.rules.md`. Please refer to that file for the complete set of development guidelines, coding standards, and best practices.
## Copilot-Specific Configuration
When using GitHub Copilot for this project, ensure you:
1. **Review the unified rules**: Always check `.rules.md` for the latest project guidelines
2. **Follow branch protection**: Never attempt to commit directly to main/master branch
3. **Use English**: All code comments, documentation, and variable names must be in English
4. **Clean code practices**: Only make modifications you're confident about
5. **Test thoroughly**: Ensure all changes pass formatting, linting, and testing requirements
## Quick Reference
### Critical Rules
- 🚫 **NEVER commit directly to main/master branch**
-**ALWAYS work on feature branches**
- 📝 **ALWAYS use English for code and documentation**
- 🧹 **ALWAYS clean up temporary files after use**
- 🎯 **ONLY make confident, necessary modifications**
### Pre-commit Checklist
```bash
# Before committing, always run:
cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings
cargo check --all-targets
cargo test
```
### Branch Workflow
```bash
git checkout main
git pull origin main
git checkout -b feat/your-feature-name
# Make your changes
git add .
git commit -m "feat: your feature description"
git push origin feat/your-feature-name
gh pr create
```
## Important Notes
- This file serves as an entry point for GitHub Copilot
- All detailed rules and guidelines are maintained in `.rules.md`
- Updates to coding standards should be made in `.rules.md` to ensure consistency across all AI tools
- When in doubt, always refer to `.rules.md` for authoritative guidance
## See Also
- [.rules.md](./.rules.md) - Complete AI coding rules and guidelines
- [CONTRIBUTING.md](./CONTRIBUTING.md) - Contribution guidelines
- [README.md](./README.md) - Project overview and setup instructions
-927
View File
@@ -1,927 +0,0 @@
# RustFS Project Cursor Rules
## 🚨🚨🚨 CRITICAL DEVELOPMENT RULES - ZERO TOLERANCE 🚨🚨🚨
### ⛔️ ABSOLUTE PROHIBITION: NEVER COMMIT DIRECTLY TO MASTER/MAIN BRANCH ⛔️
**🔥 THIS IS THE MOST CRITICAL RULE - VIOLATION WILL RESULT IN IMMEDIATE REVERSAL 🔥**
- **🚫 ZERO DIRECT COMMITS TO MAIN/MASTER BRANCH - ABSOLUTELY FORBIDDEN**
- **🚫 ANY DIRECT COMMIT TO MAIN BRANCH MUST BE IMMEDIATELY REVERTED**
- **🚫 NO EXCEPTIONS FOR HOTFIXES, EMERGENCIES, OR URGENT CHANGES**
- **🚫 NO EXCEPTIONS FOR SMALL CHANGES, TYPOS, OR DOCUMENTATION UPDATES**
- **🚫 NO EXCEPTIONS FOR ANYONE - MAINTAINERS, CONTRIBUTORS, OR ADMINS**
### 📋 MANDATORY WORKFLOW - STRICTLY ENFORCED
**EVERY SINGLE CHANGE MUST FOLLOW THIS WORKFLOW:**
1. **Check current branch**: `git branch` (MUST NOT be on main/master)
2. **Switch to main**: `git checkout main`
3. **Pull latest**: `git pull origin main`
4. **Create feature branch**: `git checkout -b feat/your-feature-name`
5. **Make changes ONLY on feature branch**
6. **Test thoroughly before committing**
7. **Commit and push to feature branch**: `git push origin feat/your-feature-name`
8. **Create Pull Request**: Use `gh pr create` (MANDATORY)
9. **Wait for PR approval**: NO self-merging allowed
10. **Merge through GitHub interface**: ONLY after approval
### 🔒 ENFORCEMENT MECHANISMS
- **Branch protection rules**: Main branch is protected
- **Pre-commit hooks**: Will block direct commits to main
- **CI/CD checks**: All PRs must pass before merging
- **Code review requirement**: At least one approval needed
- **Automated reversal**: Direct commits to main will be automatically reverted
## Project Overview
RustFS is a high-performance distributed object storage system written in Rust, compatible with S3 API. The project adopts a modular architecture, supporting erasure coding storage, multi-tenant management, observability, and other enterprise-level features.
## Core Architecture Principles
### 1. Modular Design
- Project uses Cargo workspace structure, containing multiple independent crates
- Core modules: `rustfs` (main service), `ecstore` (erasure coding storage), `common` (shared components)
- Functional modules: `iam` (identity management), `madmin` (management interface), `crypto` (encryption), etc.
- Tool modules: `cli` (command line tool), `crates/*` (utility libraries)
### 2. Asynchronous Programming Pattern
- Comprehensive use of `tokio` async runtime
- Prioritize `async/await` syntax
- Use `async-trait` for async methods in traits
- Avoid blocking operations, use `spawn_blocking` when necessary
### 3. Error Handling Strategy
- **Use modular, type-safe error handling with `thiserror`**
- Each module should define its own error type using `thiserror::Error` derive macro
- Support error chains and context information through `#[from]` and `#[source]` attributes
- Use `Result<T>` type aliases for consistency within each module
- Error conversion between modules should use explicit `From` implementations
- Follow the pattern: `pub type Result<T> = core::result::Result<T, Error>`
- Use `#[error("description")]` attributes for clear error messages
- Support error downcasting when needed through `other()` helper methods
- Implement `Clone` for errors when required by the domain logic
- **Current module error types:**
- `ecstore::error::StorageError` - Storage layer errors
- `ecstore::disk::error::DiskError` - Disk operation errors
- `iam::error::Error` - Identity and access management errors
- `policy::error::Error` - Policy-related errors
- `crypto::error::Error` - Cryptographic operation errors
- `filemeta::error::Error` - File metadata errors
- `rustfs::error::ApiError` - API layer errors
- Module-specific error types for specialized functionality
## Code Style Guidelines
### 1. Formatting Configuration
```toml
max_width = 130
fn_call_width = 90
single_line_let_else_max_width = 100
```
### 2. **🔧 MANDATORY Code Formatting Rules**
**CRITICAL**: All code must be properly formatted before committing. This project enforces strict formatting standards to maintain code consistency and readability.
#### Pre-commit Requirements (MANDATORY)
Before every commit, you **MUST**:
1. **Format your code**:
```bash
cargo fmt --all
```
2. **Verify formatting**:
```bash
cargo fmt --all --check
```
3. **Pass clippy checks**:
```bash
cargo clippy --all-targets --all-features -- -D warnings
```
4. **Ensure compilation**:
```bash
cargo check --all-targets
```
#### Quick Commands
Use these convenient Makefile targets for common tasks:
```bash
# Format all code
make fmt
# Check if code is properly formatted
make fmt-check
# Run clippy checks
make clippy
# Run compilation check
make check
# Run tests
make test
# Run all pre-commit checks (format + clippy + check + test)
make pre-commit
# Setup git hooks (one-time setup)
make setup-hooks
```
#### 🔒 Automated Pre-commit Hooks
This project includes a pre-commit hook that automatically runs before each commit to ensure:
- ✅ Code is properly formatted (`cargo fmt --all --check`)
- ✅ No clippy warnings (`cargo clippy --all-targets --all-features -- -D warnings`)
- ✅ Code compiles successfully (`cargo check --all-targets`)
**Setting Up Pre-commit Hooks** (MANDATORY for all developers):
Run this command once after cloning the repository:
```bash
make setup-hooks
```
Or manually:
```bash
chmod +x .git/hooks/pre-commit
```
#### 🚫 Commit Prevention
If your code doesn't meet the formatting requirements, the pre-commit hook will:
1. **Block the commit** and show clear error messages
2. **Provide exact commands** to fix the issues
3. **Guide you through** the resolution process
Example output when formatting fails:
```
❌ Code formatting check failed!
💡 Please run 'cargo fmt --all' to format your code before committing.
🔧 Quick fix:
cargo fmt --all
git add .
git commit
```
### 3. Naming Conventions
- Use `snake_case` for functions, variables, modules
- Use `PascalCase` for types, traits, enums
- Constants use `SCREAMING_SNAKE_CASE`
- Global variables prefix `GLOBAL_`, e.g., `GLOBAL_Endpoints`
- Use meaningful and descriptive names for variables, functions, and methods
- Avoid meaningless names like `temp`, `data`, `foo`, `bar`, `test123`
- Choose names that clearly express the purpose and intent
### 4. Type Declaration Guidelines
- **Prefer type inference over explicit type declarations** when the type is obvious from context
- Let the Rust compiler infer types whenever possible to reduce verbosity and improve maintainability
- Only specify types explicitly when:
- The type cannot be inferred by the compiler
- Explicit typing improves code clarity and readability
- Required for API boundaries (function signatures, public struct fields)
- Needed to resolve ambiguity between multiple possible types
**Good examples (prefer these):**
```rust
// Compiler can infer the type
let items = vec![1, 2, 3, 4];
let config = Config::default();
let result = process_data(&input);
// Iterator chains with clear context
let filtered: Vec<_> = items.iter().filter(|&&x| x > 2).collect();
```
**Avoid unnecessary explicit types:**
```rust
// Unnecessary - type is obvious
let items: Vec<i32> = vec![1, 2, 3, 4];
let config: Config = Config::default();
let result: ProcessResult = process_data(&input);
```
**When explicit types are beneficial:**
```rust
// API boundaries - always specify types
pub fn process_data(input: &[u8]) -> Result<ProcessResult, Error> { ... }
// Ambiguous cases - explicit type needed
let value: f64 = "3.14".parse().unwrap();
// Complex generic types - explicit for clarity
let cache: HashMap<String, Arc<Mutex<CacheEntry>>> = HashMap::new();
```
### 5. Documentation Comments
- Public APIs must have documentation comments
- Use `///` for documentation comments
- Complex functions add `# Examples` and `# Parameters` descriptions
- Error cases use `# Errors` descriptions
- Always use English for all comments and documentation
- Avoid meaningless comments like "debug 111" or placeholder text
### 6. Import Guidelines
- Standard library imports first
- Third-party crate imports in the middle
- Project internal imports last
- Group `use` statements with blank lines between groups
## Asynchronous Programming Guidelines
### 1. Trait Definition
```rust
#[async_trait::async_trait]
pub trait StorageAPI: Send + Sync {
async fn get_object(&self, bucket: &str, object: &str) -> Result<ObjectInfo>;
}
```
### 2. Error Handling
```rust
// Use ? operator to propagate errors
async fn example_function() -> Result<()> {
let data = read_file("path").await?;
process_data(data).await?;
Ok(())
}
```
### 3. Concurrency Control
- Use `Arc` and `Mutex`/`RwLock` for shared state management
- Prioritize async locks from `tokio::sync`
- Avoid holding locks for long periods
## Logging and Tracing Guidelines
### 1. Tracing Usage
```rust
#[tracing::instrument(skip(self, data))]
async fn process_data(&self, data: &[u8]) -> Result<()> {
info!("Processing {} bytes", data.len());
// Implementation logic
}
```
### 2. Log Levels
- `error!`: System errors requiring immediate attention
- `warn!`: Warning information that may affect functionality
- `info!`: Important business information
- `debug!`: Debug information for development use
- `trace!`: Detailed execution paths
### 3. Structured Logging
```rust
info!(
counter.rustfs_api_requests_total = 1_u64,
key_request_method = %request.method(),
key_request_uri_path = %request.uri().path(),
"API request processed"
);
```
## Error Handling Guidelines
### 1. Error Type Definition
```rust
// Use thiserror for module-specific error types
#[derive(thiserror::Error, Debug)]
pub enum MyError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Storage error: {0}")]
Storage(#[from] ecstore::error::StorageError),
#[error("Custom error: {message}")]
Custom { message: String },
#[error("File not found: {path}")]
FileNotFound { path: String },
#[error("Invalid configuration: {0}")]
InvalidConfig(String),
}
// Provide Result type alias for the module
pub type Result<T> = core::result::Result<T, MyError>;
```
### 2. Error Helper Methods
```rust
impl MyError {
/// Create error from any compatible error type
pub fn other<E>(error: E) -> Self
where
E: Into<Box<dyn std::error::Error + Send + Sync>>,
{
MyError::Io(std::io::Error::other(error))
}
}
```
### 3. Error Conversion Between Modules
```rust
// Convert between different module error types
impl From<ecstore::error::StorageError> for MyError {
fn from(e: ecstore::error::StorageError) -> Self {
match e {
ecstore::error::StorageError::FileNotFound => {
MyError::FileNotFound { path: "unknown".to_string() }
}
_ => MyError::Storage(e),
}
}
}
// Provide reverse conversion when needed
impl From<MyError> for ecstore::error::StorageError {
fn from(e: MyError) -> Self {
match e {
MyError::FileNotFound { .. } => ecstore::error::StorageError::FileNotFound,
MyError::Storage(e) => e,
_ => ecstore::error::StorageError::other(e),
}
}
}
```
### 4. Error Context and Propagation
```rust
// Use ? operator for clean error propagation
async fn example_function() -> Result<()> {
let data = read_file("path").await?;
process_data(data).await?;
Ok(())
}
// Add context to errors
fn process_with_context(path: &str) -> Result<()> {
std::fs::read(path)
.map_err(|e| MyError::Custom {
message: format!("Failed to read {}: {}", path, e)
})?;
Ok(())
}
```
### 5. API Error Conversion (S3 Example)
```rust
// Convert storage errors to API-specific errors
use s3s::{S3Error, S3ErrorCode};
#[derive(Debug)]
pub struct ApiError {
pub code: S3ErrorCode,
pub message: String,
pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
}
impl From<ecstore::error::StorageError> for ApiError {
fn from(err: ecstore::error::StorageError) -> Self {
let code = match &err {
ecstore::error::StorageError::BucketNotFound(_) => S3ErrorCode::NoSuchBucket,
ecstore::error::StorageError::ObjectNotFound(_, _) => S3ErrorCode::NoSuchKey,
ecstore::error::StorageError::BucketExists(_) => S3ErrorCode::BucketAlreadyExists,
ecstore::error::StorageError::InvalidArgument(_, _, _) => S3ErrorCode::InvalidArgument,
ecstore::error::StorageError::MethodNotAllowed => S3ErrorCode::MethodNotAllowed,
ecstore::error::StorageError::StorageFull => S3ErrorCode::ServiceUnavailable,
_ => S3ErrorCode::InternalError,
};
ApiError {
code,
message: err.to_string(),
source: Some(Box::new(err)),
}
}
}
impl From<ApiError> for S3Error {
fn from(err: ApiError) -> Self {
let mut s3e = S3Error::with_message(err.code, err.message);
if let Some(source) = err.source {
s3e.set_source(source);
}
s3e
}
}
```
### 6. Error Handling Best Practices
#### Pattern Matching and Error Classification
```rust
// Use pattern matching for specific error handling
async fn handle_storage_operation() -> Result<()> {
match storage.get_object("bucket", "key").await {
Ok(object) => process_object(object),
Err(ecstore::error::StorageError::ObjectNotFound(bucket, key)) => {
warn!("Object not found: {}/{}", bucket, key);
create_default_object(bucket, key).await
}
Err(ecstore::error::StorageError::BucketNotFound(bucket)) => {
error!("Bucket not found: {}", bucket);
Err(MyError::Custom {
message: format!("Bucket {} does not exist", bucket)
})
}
Err(e) => {
error!("Storage operation failed: {}", e);
Err(MyError::Storage(e))
}
}
}
```
#### Error Aggregation and Reporting
```rust
// Collect and report multiple errors
pub fn validate_configuration(config: &Config) -> Result<()> {
let mut errors = Vec::new();
if config.bucket_name.is_empty() {
errors.push("Bucket name cannot be empty");
}
if config.region.is_empty() {
errors.push("Region must be specified");
}
if !errors.is_empty() {
return Err(MyError::Custom {
message: format!("Configuration validation failed: {}", errors.join(", "))
});
}
Ok(())
}
```
#### Contextual Error Information
```rust
// Add operation context to errors
#[tracing::instrument(skip(self))]
async fn upload_file(&self, bucket: &str, key: &str, data: Vec<u8>) -> Result<()> {
self.storage
.put_object(bucket, key, data)
.await
.map_err(|e| MyError::Custom {
message: format!("Failed to upload {}/{}: {}", bucket, key, e)
})
}
```
## Performance Optimization Guidelines
### 1. Memory Management
- Use `Bytes` instead of `Vec<u8>` for zero-copy operations
- Avoid unnecessary cloning, use reference passing
- Use `Arc` for sharing large objects
### 2. Concurrency Optimization
```rust
// Use join_all for concurrent operations
let futures = disks.iter().map(|disk| disk.operation());
let results = join_all(futures).await;
```
### 3. Caching Strategy
- Use `LazyLock` for global caching
- Implement LRU cache to avoid memory leaks
## Testing Guidelines
### 1. Unit Tests
```rust
#[cfg(test)]
mod tests {
use super::*;
use test_case::test_case;
#[tokio::test]
async fn test_async_function() {
let result = async_function().await;
assert!(result.is_ok());
}
#[test_case("input1", "expected1")]
#[test_case("input2", "expected2")]
fn test_with_cases(input: &str, expected: &str) {
assert_eq!(function(input), expected);
}
#[test]
fn test_error_conversion() {
use ecstore::error::StorageError;
let storage_err = StorageError::BucketNotFound("test-bucket".to_string());
let api_err: ApiError = storage_err.into();
assert_eq!(api_err.code, S3ErrorCode::NoSuchBucket);
assert!(api_err.message.contains("test-bucket"));
assert!(api_err.source.is_some());
}
#[test]
fn test_error_types() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let my_err = MyError::Io(io_err);
// Test error matching
match my_err {
MyError::Io(_) => {}, // Expected
_ => panic!("Unexpected error type"),
}
}
#[test]
fn test_error_context() {
let result = process_with_context("nonexistent_file.txt");
assert!(result.is_err());
let err = result.unwrap_err();
match err {
MyError::Custom { message } => {
assert!(message.contains("Failed to read"));
assert!(message.contains("nonexistent_file.txt"));
}
_ => panic!("Expected Custom error"),
}
}
}
```
### 2. Integration Tests
- Use `e2e_test` module for end-to-end testing
- Simulate real storage environments
### 3. Test Quality Standards
- Write meaningful test cases that verify actual functionality
- Avoid placeholder or debug content like "debug 111", "test test", etc.
- Use descriptive test names that clearly indicate what is being tested
- Each test should have a clear purpose and verify specific behavior
- Test data should be realistic and representative of actual use cases
## Cross-Platform Compatibility Guidelines
### 1. CPU Architecture Compatibility
- **Always consider multi-platform and different CPU architecture compatibility** when writing code
- Support major architectures: x86_64, aarch64 (ARM64), and other target platforms
- Use conditional compilation for architecture-specific code:
```rust
#[cfg(target_arch = "x86_64")]
fn optimized_x86_64_function() { /* x86_64 specific implementation */ }
#[cfg(target_arch = "aarch64")]
fn optimized_aarch64_function() { /* ARM64 specific implementation */ }
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
fn generic_function() { /* Generic fallback implementation */ }
```
### 2. Platform-Specific Dependencies
- Use feature flags for platform-specific dependencies
- Provide fallback implementations for unsupported platforms
- Test on multiple architectures in CI/CD pipeline
### 3. Endianness Considerations
- Use explicit byte order conversion when dealing with binary data
- Prefer `to_le_bytes()`, `from_le_bytes()` for consistent little-endian format
- Use `byteorder` crate for complex binary format handling
### 4. SIMD and Performance Optimizations
- Use portable SIMD libraries like `wide` or `packed_simd`
- Provide fallback implementations for non-SIMD architectures
- Use runtime feature detection when appropriate
## Security Guidelines
### 1. Memory Safety
- Disable `unsafe` code (workspace.lints.rust.unsafe_code = "deny")
- Use `rustls` instead of `openssl`
### 2. Authentication and Authorization
```rust
// Use IAM system for permission checks
let identity = iam.authenticate(&access_key, &secret_key).await?;
iam.authorize(&identity, &action, &resource).await?;
```
## Configuration Management Guidelines
### 1. Environment Variables
- Use `RUSTFS_` prefix
- Support both configuration files and environment variables
- Provide reasonable default values
### 2. Configuration Structure
```rust
#[derive(Debug, Deserialize, Clone)]
pub struct Config {
pub address: String,
pub volumes: String,
#[serde(default)]
pub console_enable: bool,
}
```
## Dependency Management Guidelines
### 1. Workspace Dependencies
- Manage versions uniformly at workspace level
- Use `workspace = true` to inherit configuration
### 2. Feature Flags
```rust
[features]
default = ["file"]
gpu = ["dep:nvml-wrapper"]
kafka = ["dep:rdkafka"]
```
## Deployment and Operations Guidelines
### 1. Containerization
- Provide Dockerfile and docker-compose configuration
- Support multi-stage builds to optimize image size
### 2. Observability
- Integrate OpenTelemetry for distributed tracing
- Support Prometheus metrics collection
- Provide Grafana dashboards
### 3. Health Checks
```rust
// Implement health check endpoint
async fn health_check() -> Result<HealthStatus> {
// Check component status
}
```
## Code Review Checklist
### 1. **Code Formatting and Quality (MANDATORY)**
- [ ] **Code is properly formatted** (`cargo fmt --all --check` passes)
- [ ] **All clippy warnings are resolved** (`cargo clippy --all-targets --all-features -- -D warnings` passes)
- [ ] **Code compiles successfully** (`cargo check --all-targets` passes)
- [ ] **Pre-commit hooks are working** and all checks pass
- [ ] **No formatting-related changes** mixed with functional changes (separate commits)
### 2. Functionality
- [ ] Are all error cases properly handled?
- [ ] Is there appropriate logging?
- [ ] Is there necessary test coverage?
### 3. Performance
- [ ] Are unnecessary memory allocations avoided?
- [ ] Are async operations used correctly?
- [ ] Are there potential deadlock risks?
### 4. Security
- [ ] Are input parameters properly validated?
- [ ] Are there appropriate permission checks?
- [ ] Is information leakage avoided?
### 5. Cross-Platform Compatibility
- [ ] Does the code work on different CPU architectures (x86_64, aarch64)?
- [ ] Are platform-specific features properly gated with conditional compilation?
- [ ] Is byte order handling correct for binary data?
- [ ] Are there appropriate fallback implementations for unsupported platforms?
### 6. Code Commits and Documentation
- [ ] Does it comply with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)?
- [ ] Are commit messages concise and under 72 characters for the title line?
- [ ] Commit titles should be concise and in English, avoid Chinese
- [ ] Is PR description provided in copyable markdown format for easy copying?
## Common Patterns and Best Practices
### 1. Resource Management
```rust
// Use RAII pattern for resource management
pub struct ResourceGuard {
resource: Resource,
}
impl Drop for ResourceGuard {
fn drop(&mut self) {
// Clean up resources
}
}
```
### 2. Dependency Injection
```rust
// Use dependency injection pattern
pub struct Service {
config: Arc<Config>,
storage: Arc<dyn StorageAPI>,
}
```
### 3. Graceful Shutdown
```rust
// Implement graceful shutdown
async fn shutdown_gracefully(shutdown_rx: &mut Receiver<()>) {
tokio::select! {
_ = shutdown_rx.recv() => {
info!("Received shutdown signal");
// Perform cleanup operations
}
_ = tokio::time::sleep(SHUTDOWN_TIMEOUT) => {
warn!("Shutdown timeout reached");
}
}
}
```
## Domain-Specific Guidelines
### 1. Storage Operations
- All storage operations must support erasure coding
- Implement read/write quorum mechanisms
- Support data integrity verification
### 2. Network Communication
- Use gRPC for internal service communication
- HTTP/HTTPS support for S3-compatible API
- Implement connection pooling and retry mechanisms
### 3. Metadata Management
- Use FlatBuffers for serialization
- Support version control and migration
- Implement metadata caching
These rules should serve as guiding principles when developing the RustFS project, ensuring code quality, performance, and maintainability.
### 4. Code Operations
#### Branch Management
- **🚨 CRITICAL: NEVER modify code directly on main or master branch - THIS IS ABSOLUTELY FORBIDDEN 🚨**
- **⚠️ ANY DIRECT COMMITS TO MASTER/MAIN WILL BE REJECTED AND MUST BE REVERTED IMMEDIATELY ⚠️**
- **🔒 ALL CHANGES MUST GO THROUGH PULL REQUESTS - NO DIRECT COMMITS TO MAIN UNDER ANY CIRCUMSTANCES 🔒**
- **Always work on feature branches - NO EXCEPTIONS**
- Always check the .cursorrules file before starting to ensure you understand the project guidelines
- **MANDATORY workflow for ALL changes:**
1. `git checkout main` (switch to main branch)
2. `git pull` (get latest changes)
3. `git checkout -b feat/your-feature-name` (create and switch to feature branch)
4. Make your changes ONLY on the feature branch
5. Test thoroughly before committing
6. Commit and push to the feature branch
7. **Create a pull request for code review - THIS IS THE ONLY WAY TO MERGE TO MAIN**
8. **Wait for PR approval before merging - NEVER merge your own PRs without review**
- Use descriptive branch names following the pattern: `feat/feature-name`, `fix/issue-name`, `refactor/component-name`, etc.
- **Double-check current branch before ANY commit: `git branch` to ensure you're NOT on main/master**
- **Pull Request Requirements:**
- All changes must be submitted via PR regardless of size or urgency
- PRs must include comprehensive description and testing information
- PRs must pass all CI/CD checks before merging
- PRs require at least one approval from code reviewers
- Even hotfixes and emergency changes must go through PR process
- **Enforcement:**
- Main branch should be protected with branch protection rules
- Direct pushes to main should be blocked by repository settings
- Any accidental direct commits to main must be immediately reverted via PR
#### Development Workflow
## 🎯 **Core Development Principles**
- **🔴 Every change must be precise - don't modify unless you're confident**
- Carefully analyze code logic and ensure complete understanding before making changes
- When uncertain, prefer asking users or consulting documentation over blind modifications
- Use small iterative steps, modify only necessary parts at a time
- Evaluate impact scope before changes to ensure no new issues are introduced
- **🚀 GitHub PR creation prioritizes gh command usage**
- Prefer using `gh pr create` command to create Pull Requests
- Avoid having users manually create PRs through web interface
- Provide clear and professional PR titles and descriptions
- Using `gh` commands ensures better integration and automation
## 📝 **Code Quality Requirements**
- Use English for all code comments, documentation, and variable names
- Write meaningful and descriptive names for variables, functions, and methods
- Avoid meaningless test content like "debug 111" or placeholder values
- Before each change, carefully read the existing code to ensure you understand the code structure and implementation, do not break existing logic implementation, do not introduce new issues
- Ensure each change provides sufficient test cases to guarantee code correctness
- Do not arbitrarily modify numbers and constants in test cases, carefully analyze their meaning to ensure test case correctness
- When writing or modifying tests, check existing test cases to ensure they have scientific naming and rigorous logic testing, if not compliant, modify test cases to ensure scientific and rigorous testing
- **Before committing any changes, run `cargo clippy --all-targets --all-features -- -D warnings` to ensure all code passes Clippy checks**
- After each development completion, first git add . then git commit -m "feat: feature description" or "fix: issue description", ensure compliance with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
- **Keep commit messages concise and under 72 characters** for the title line, use body for detailed explanations if needed
- After each development completion, first git push to remote repository
- After each change completion, summarize the changes, do not create summary files, provide a brief change description, ensure compliance with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
- Provide change descriptions needed for PR in the conversation, ensure compliance with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
- **Always provide PR descriptions in English** after completing any changes, including:
- Clear and concise title following Conventional Commits format
- Detailed description of what was changed and why
- List of key changes and improvements
- Any breaking changes or migration notes if applicable
- Testing information and verification steps
- **Provide PR descriptions in copyable markdown format** enclosed in code blocks for easy one-click copying
## 🚫 AI 文档生成限制
### 禁止生成总结文档
- **严格禁止创建任何形式的AI生成总结文档**
- **不得创建包含大量表情符号、详细格式化表格和典型AI风格的文档**
- **不得在项目中生成以下类型的文档:**
- 基准测试总结文档(BENCHMARK*.md
- 实现对比分析文档(IMPLEMENTATION_COMPARISON*.md
- 性能分析报告文档
- 架构总结文档
- 功能对比文档
- 任何带有大量表情符号和格式化内容的文档
- **如果需要文档,请只在用户明确要求时创建,并保持简洁实用的风格**
- **文档应当专注于实际需要的信息,避免过度格式化和装饰性内容**
- **任何发现的AI生成总结文档都应该立即删除**
### 允许的文档类型
- README.md(项目介绍,保持简洁)
- 技术文档(仅在明确需要时创建)
- 用户手册(仅在明确需要时创建)
- API文档(从代码生成)
- 变更日志(CHANGELOG.md
+4 -6
View File
@@ -20,18 +20,16 @@
}
},
"env": {
"RUST_LOG": "rustfs=debug,ecstore=info,s3s=debug"
"RUST_LOG": "rustfs=debug,ecstore=info,s3s=debug,iam=info"
},
"args": [
"--access-key",
"AKEXAMPLERUSTFS",
"rustfsadmin",
"--secret-key",
"SKEXAMPLERUSTFS",
"rustfsadmin",
"--address",
"0.0.0.0:9010",
"--domain-name",
"127.0.0.1:9010",
"./target/volume/test{0...4}"
"./target/volume/test{1...4}"
],
"cwd": "${workspaceFolder}"
},
+218 -302
View File
@@ -1,4 +1,4 @@
# RustFS Project AI Coding Rules
# RustFS Project AI Agents Rules
## 🚨🚨🚨 CRITICAL DEVELOPMENT RULES - ZERO TOLERANCE 🚨🚨🚨
@@ -35,46 +35,194 @@
- **Code review requirement**: At least one approval needed
- **Automated reversal**: Direct commits to main will be automatically reverted
## 🎯 Core AI Development Principles
## 🎯 Core Development Principles (HIGHEST PRIORITY)
### Five Execution Steps
### Philosophy
#### 1. Task Analysis and Planning
- **Clear Objectives**: Deeply understand task requirements and expected results before starting coding
- **Plan Development**: List specific files, components, and functions that need modification, explaining the reasons for changes
- **Risk Assessment**: Evaluate the impact of changes on existing functionality, develop rollback plans
#### Core Beliefs
#### 2. Precise Code Location
- **File Identification**: Determine specific files and line numbers that need modification
- **Impact Analysis**: Avoid modifying irrelevant files, clearly state the reason for each file modification
- **Minimization Principle**: Unless explicitly required by the task, do not create new abstraction layers or refactor existing code
- **Incremental progress over big bangs** - Small changes that compile and pass tests
- **Learning from existing code** - Study and plan before implementing
- **Pragmatic over dogmatic** - Adapt to project reality
- **Clear intent over clever code** - Be boring and obvious
#### 3. Minimal Code Changes
- **Focus on Core**: Only write code directly required by the task
- **Avoid Redundancy**: Do not add unnecessary logs, comments, tests, or error handling
- **Isolation**: Ensure new code does not interfere with existing functionality, maintain code independence
#### Simplicity Means
#### 4. Strict Code Review
- **Correctness Check**: Verify the correctness and completeness of code logic
- **Style Consistency**: Ensure code conforms to established project coding style
- **Side Effect Assessment**: Evaluate the impact of changes on downstream systems
- Single responsibility per function/class
- Avoid premature abstractions
- No clever tricks - choose the boring solution
- If you need to explain it, it's too complex
#### 5. Clear Delivery Documentation
- **Change Summary**: Detailed explanation of all modifications and reasons
- **File List**: List all modified files and their specific changes
- **Risk Statement**: Mark any assumptions or potential risk points
### Process
### Core Principles
- **🎯 Precise Execution**: Strictly follow task requirements, no arbitrary innovation
- **⚡ Efficient Development**: Avoid over-design, only do necessary work
- **🛡️ Safe and Reliable**: Always follow development processes, ensure code quality and system stability
- **🔒 Cautious Modification**: Only modify when clearly knowing what needs to be changed and having confidence
#### 1. Planning & Staging
### Additional AI Behavior Rules
Break complex work into 3-5 stages. Document in `IMPLEMENTATION_PLAN.md`:
1. **Use English for all code comments and documentation** - All comments, variable names, function names, documentation, and user-facing text in code should be in English
2. **Clean up temporary scripts after use** - Any temporary scripts, test files, or helper files created during AI work should be removed after task completion
3. **Only make confident modifications** - Do not make speculative changes or "convenient" modifications outside the task scope. If uncertain about a change, ask for clarification rather than guessing
```markdown
## Stage N: [Name]
**Goal**: [Specific deliverable]
**Success Criteria**: [Testable outcomes]
**Tests**: [Specific test cases]
**Status**: [Not Started|In Progress|Complete]
```
- Update status as you progress
- Remove file when all stages are done
#### 2. Implementation Flow
1. **Understand** - Study existing patterns in codebase
2. **Test** - Write test first (red)
3. **Implement** - Minimal code to pass (green)
4. **Refactor** - Clean up with tests passing
5. **Commit** - With clear message linking to plan
#### 3. When Stuck (After 3 Attempts)
**CRITICAL**: Maximum 3 attempts per issue, then STOP.
1. **Document what failed**:
- What you tried
- Specific error messages
- Why you think it failed
2. **Research alternatives**:
- Find 2-3 similar implementations
- Note different approaches used
3. **Question fundamentals**:
- Is this the right abstraction level?
- Can this be split into smaller problems?
- Is there a simpler approach entirely?
4. **Try different angle**:
- Different library/framework feature?
- Different architectural pattern?
- Remove abstraction instead of adding?
### Technical Standards
#### Architecture Principles
- **Composition over inheritance** - Use dependency injection
- **Interfaces over singletons** - Enable testing and flexibility
- **Explicit over implicit** - Clear data flow and dependencies
- **Test-driven when possible** - Never disable tests, fix them
#### Code Quality
- **Every commit must**:
- Compile successfully
- Pass all existing tests
- Include tests for new functionality
- Follow project formatting/linting
- **Before committing**:
- Run formatters/linters
- Self-review changes
- Ensure commit message explains "why"
#### Error Handling
- Fail fast with descriptive messages
- Include context for debugging
- Handle errors at appropriate level
- Never silently swallow exceptions
### Decision Framework
When multiple valid approaches exist, choose based on:
1. **Testability** - Can I easily test this?
2. **Readability** - Will someone understand this in 6 months?
3. **Consistency** - Does this match project patterns?
4. **Simplicity** - Is this the simplest solution that works?
5. **Reversibility** - How hard to change later?
### Project Integration
#### Learning the Codebase
- Find 3 similar features/components
- Identify common patterns and conventions
- Use same libraries/utilities when possible
- Follow existing test patterns
#### Tooling
- Use project's existing build system
- Use project's test framework
- Use project's formatter/linter settings
- Don't introduce new tools without strong justification
### Quality Gates
#### Definition of Done
- [ ] Tests written and passing
- [ ] Code follows project conventions
- [ ] No linter/formatter warnings
- [ ] Commit messages are clear
- [ ] Implementation matches plan
- [ ] No TODOs without issue numbers
#### Test Guidelines
- Test behavior, not implementation
- One assertion per test when possible
- Clear test names describing scenario
- Use existing test utilities/helpers
- Tests should be deterministic
### Important Reminders
**NEVER**:
- Use `--no-verify` to bypass commit hooks
- Disable tests instead of fixing them
- Commit code that doesn't compile
- Make assumptions - verify with existing code
**ALWAYS**:
- Commit working code incrementally
- Update plan documentation as you go
- Learn from existing implementations
- Stop after 3 failed attempts and reassess
## 🚫 Competitor Keywords Prohibition
### Strictly Forbidden Keywords
**CRITICAL**: The following competitor keywords are absolutely forbidden in any code, documentation, comments, or project files:
- **minio** (and any variations like MinIO, MINIO)
- **aws-s3** (when referring to competing implementations)
- **ceph** (and any variations like Ceph, CEPH)
- **swift** (OpenStack Swift)
- **glusterfs** (and any variations like GlusterFS, Gluster)
- **seaweedfs** (and any variations like SeaweedFS, Seaweed)
- **garage** (and any variations like Garage)
- **zenko** (and any variations like Zenko)
- **scality** (and any variations like Scality)
### Enforcement
- **Code Review**: All PRs will be checked for competitor keywords
- **Automated Scanning**: CI/CD pipeline will scan for forbidden keywords
- **Immediate Rejection**: Any PR containing competitor keywords will be immediately rejected
- **Documentation**: All documentation must use generic terms like "S3-compatible storage" instead of specific competitor names
### Acceptable Alternatives
Instead of competitor names, use these generic terms:
- "S3-compatible storage system"
- "Object storage solution"
- "Distributed storage platform"
- "Cloud storage service"
- "Storage backend"
## Project Overview
@@ -127,21 +275,25 @@ single_line_let_else_max_width = 100
Before every commit, you **MUST**:
1. **Format your code**:
```bash
cargo fmt --all
```
2. **Verify formatting**:
```bash
cargo fmt --all --check
```
3. **Pass clippy checks**:
```bash
cargo clippy --all-targets --all-features -- -D warnings
```
4. **Ensure compilation**:
```bash
cargo check --all-targets
```
@@ -211,292 +363,94 @@ make setup-hooks
## Asynchronous Programming Guidelines
### 1. Trait Definition
```rust
#[async_trait::async_trait]
pub trait StorageAPI: Send + Sync {
async fn get_object(&self, bucket: &str, object: &str) -> Result<ObjectInfo>;
}
```
### 2. Error Handling
```rust
// Use ? operator to propagate errors
async fn example_function() -> Result<()> {
let data = read_file("path").await?;
process_data(data).await?;
Ok(())
}
```
### 3. Concurrency Control
- Comprehensive use of `tokio` async runtime
- Prioritize `async/await` syntax
- Use `async-trait` for async methods in traits
- Avoid blocking operations, use `spawn_blocking` when necessary
- Use `Arc` and `Mutex`/`RwLock` for shared state management
- Prioritize async locks from `tokio::sync`
- Avoid holding locks for long periods
## Logging and Tracing Guidelines
### 1. Tracing Usage
```rust
#[tracing::instrument(skip(self, data))]
async fn process_data(&self, data: &[u8]) -> Result<()> {
info!("Processing {} bytes", data.len());
// Implementation logic
}
```
### 2. Log Levels
- `error!`: System errors requiring immediate attention
- `warn!`: Warning information that may affect functionality
- `info!`: Important business information
- `debug!`: Debug information for development use
- `trace!`: Detailed execution paths
### 3. Structured Logging
```rust
info!(
counter.rustfs_api_requests_total = 1_u64,
key_request_method = %request.method(),
key_request_uri_path = %request.uri().path(),
"API request processed"
);
```
- Use `#[tracing::instrument(skip(self, data))]` for function tracing
- Log levels: `error!` (system errors), `warn!` (warnings), `info!` (business info), `debug!` (development), `trace!` (detailed paths)
- Use structured logging with key-value pairs for better observability
## Error Handling Guidelines
### 1. Error Type Definition
```rust
// Use thiserror for module-specific error types
#[derive(thiserror::Error, Debug)]
pub enum MyError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Storage error: {0}")]
Storage(#[from] ecstore::error::StorageError),
#[error("Custom error: {message}")]
Custom { message: String },
#[error("File not found: {path}")]
FileNotFound { path: String },
#[error("Invalid configuration: {0}")]
InvalidConfig(String),
}
// Provide Result type alias for the module
pub type Result<T> = core::result::Result<T, MyError>;
```
### 2. Error Helper Methods
```rust
impl MyError {
/// Create error from any compatible error type
pub fn other<E>(error: E) -> Self
where
E: Into<Box<dyn std::error::Error + Send + Sync>>,
{
MyError::Io(std::io::Error::other(error))
}
}
```
### 3. Error Context and Propagation
```rust
// Use ? operator for clean error propagation
async fn example_function() -> Result<()> {
let data = read_file("path").await?;
process_data(data).await?;
Ok(())
}
// Add context to errors
fn process_with_context(path: &str) -> Result<()> {
std::fs::read(path)
.map_err(|e| MyError::Custom {
message: format!("Failed to read {}: {}", path, e)
})?;
Ok(())
}
```
- Use `thiserror` for module-specific error types
- Support error chains and context information through `#[from]` and `#[source]` attributes
- Use `Result<T>` type aliases for consistency within each module
- Error conversion between modules should use explicit `From` implementations
- Follow the pattern: `pub type Result<T> = core::result::Result<T, Error>`
- Use `#[error("description")]` attributes for clear error messages
- Support error downcasting when needed through `other()` helper methods
- Implement `Clone` for errors when required by the domain logic
## Performance Optimization Guidelines
### 1. Memory Management
- Use `Bytes` instead of `Vec<u8>` for zero-copy operations
- Avoid unnecessary cloning, use reference passing
- Use `Arc` for sharing large objects
### 2. Concurrency Optimization
```rust
// Use join_all for concurrent operations
let futures = disks.iter().map(|disk| disk.operation());
let results = join_all(futures).await;
```
### 3. Caching Strategy
- Use `join_all` for concurrent operations
- Use `LazyLock` for global caching
- Implement LRU cache to avoid memory leaks
## Testing Guidelines
### 1. Unit Tests
```rust
#[cfg(test)]
mod tests {
use super::*;
use test_case::test_case;
#[tokio::test]
async fn test_async_function() {
let result = async_function().await;
assert!(result.is_ok());
}
#[test_case("input1", "expected1")]
#[test_case("input2", "expected2")]
fn test_with_cases(input: &str, expected: &str) {
assert_eq!(function(input), expected);
}
}
```
### 2. Integration Tests
- Use `e2e_test` module for end-to-end testing
- Simulate real storage environments
### 3. Test Quality Standards
- Write meaningful test cases that verify actual functionality
- Avoid placeholder or debug content like "debug 111", "test test", etc.
- Use descriptive test names that clearly indicate what is being tested
- Each test should have a clear purpose and verify specific behavior
- Test data should be realistic and representative of actual use cases
- Use `e2e_test` module for end-to-end testing
- Simulate real storage environments
## Cross-Platform Compatibility Guidelines
### 1. CPU Architecture Compatibility
- **Always consider multi-platform and different CPU architecture compatibility** when writing code
- Support major architectures: x86_64, aarch64 (ARM64), and other target platforms
- Use conditional compilation for architecture-specific code:
```rust
#[cfg(target_arch = "x86_64")]
fn optimized_x86_64_function() { /* x86_64 specific implementation */ }
#[cfg(target_arch = "aarch64")]
fn optimized_aarch64_function() { /* ARM64 specific implementation */ }
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
fn generic_function() { /* Generic fallback implementation */ }
```
### 2. Platform-Specific Dependencies
- Use conditional compilation for architecture-specific code
- Use feature flags for platform-specific dependencies
- Provide fallback implementations for unsupported platforms
- Test on multiple architectures in CI/CD pipeline
### 3. Endianness Considerations
- Use explicit byte order conversion when dealing with binary data
- Prefer `to_le_bytes()`, `from_le_bytes()` for consistent little-endian format
- Use `byteorder` crate for complex binary format handling
### 4. SIMD and Performance Optimizations
- Use portable SIMD libraries like `wide` or `packed_simd`
- Provide fallback implementations for non-SIMD architectures
- Use runtime feature detection when appropriate
## Security Guidelines
### 1. Memory Safety
- Disable `unsafe` code (workspace.lints.rust.unsafe_code = "deny")
- Use `rustls` instead of `openssl`
### 2. Authentication and Authorization
```rust
// Use IAM system for permission checks
let identity = iam.authenticate(&access_key, &secret_key).await?;
iam.authorize(&identity, &action, &resource).await?;
```
- Use IAM system for permission checks
- Validate input parameters properly
- Implement appropriate permission checks
- Avoid information leakage
## Configuration Management Guidelines
### 1. Environment Variables
- Use `RUSTFS_` prefix
- Use `RUSTFS_` prefix for environment variables
- Support both configuration files and environment variables
- Provide reasonable default values
### 2. Configuration Structure
```rust
#[derive(Debug, Deserialize, Clone)]
pub struct Config {
pub address: String,
pub volumes: String,
#[serde(default)]
pub console_enable: bool,
}
```
- Use `serde` for configuration serialization/deserialization
## Dependency Management Guidelines
### 1. Workspace Dependencies
- Manage versions uniformly at workspace level
- Use `workspace = true` to inherit configuration
### 2. Feature Flags
```rust
[features]
default = ["file"]
gpu = ["dep:nvml-wrapper"]
kafka = ["dep:rdkafka"]
```
- Use feature flags for optional dependencies
- Don't introduce new tools without strong justification
## Deployment and Operations Guidelines
### 1. Containerization
- Provide Dockerfile and docker-compose configuration
- Support multi-stage builds to optimize image size
### 2. Observability
- Integrate OpenTelemetry for distributed tracing
- Support Prometheus metrics collection
- Provide Grafana dashboards
### 3. Health Checks
```rust
// Implement health check endpoint
async fn health_check() -> Result<HealthStatus> {
// Check component status
}
```
- Implement health check endpoints
## Code Review Checklist
@@ -540,49 +494,11 @@ async fn health_check() -> Result<HealthStatus> {
- [ ] Commit titles should be concise and in English, avoid Chinese
- [ ] Is PR description provided in copyable markdown format for easy copying?
## Common Patterns and Best Practices
### 7. Competitor Keywords Check
### 1. Resource Management
```rust
// Use RAII pattern for resource management
pub struct ResourceGuard {
resource: Resource,
}
impl Drop for ResourceGuard {
fn drop(&mut self) {
// Clean up resources
}
}
```
### 2. Dependency Injection
```rust
// Use dependency injection pattern
pub struct Service {
config: Arc<Config>,
storage: Arc<dyn StorageAPI>,
}
```
### 3. Graceful Shutdown
```rust
// Implement graceful shutdown
async fn shutdown_gracefully(shutdown_rx: &mut Receiver<()>) {
tokio::select! {
_ = shutdown_rx.recv() => {
info!("Received shutdown signal");
// Perform cleanup operations
}
_ = tokio::time::sleep(SHUTDOWN_TIMEOUT) => {
warn!("Shutdown timeout reached");
}
}
}
```
- [ ] No competitor keywords found in code, comments, or documentation
- [ ] All references use generic terms like "S3-compatible storage"
- [ ] No specific competitor product names mentioned
## Domain-Specific Guidelines
@@ -612,7 +528,7 @@ async fn shutdown_gracefully(shutdown_rx: &mut Receiver<()>) {
- **⚠️ ANY DIRECT COMMITS TO MASTER/MAIN WILL BE REJECTED AND MUST BE REVERTED IMMEDIATELY ⚠️**
- **🔒 ALL CHANGES MUST GO THROUGH PULL REQUESTS - NO DIRECT COMMITS TO MAIN UNDER ANY CIRCUMSTANCES 🔒**
- **Always work on feature branches - NO EXCEPTIONS**
- Always check the .rules.md file before starting to ensure you understand the project guidelines
- Always check the AGENTS.md file before starting to ensure you understand the project guidelines
- **MANDATORY workflow for ALL changes:**
1. `git checkout main` (switch to main branch)
2. `git pull` (get latest changes)
@@ -699,4 +615,4 @@ async fn shutdown_gracefully(shutdown_rx: &mut Receiver<()>) {
- API documentation (generated from code)
- Changelog (CHANGELOG.md)
These rules should serve as guiding principles when developing the RustFS project, ensuring code quality, performance, and maintainability.
These rules should serve as guiding principles when developing the RustFS project, ensuring code quality, performance, and maintainability.
-68
View File
@@ -1,68 +0,0 @@
# Claude AI Rules for RustFS Project
## Core Rules Reference
This project follows the comprehensive AI coding rules defined in `.rules.md`. Please refer to that file for the complete set of development guidelines, coding standards, and best practices.
## Claude-Specific Configuration
When using Claude for this project, ensure you:
1. **Review the unified rules**: Always check `.rules.md` for the latest project guidelines
2. **Follow branch protection**: Never attempt to commit directly to main/master branch
3. **Use English**: All code comments, documentation, and variable names must be in English
4. **Clean code practices**: Only make modifications you're confident about
5. **Test thoroughly**: Ensure all changes pass formatting, linting, and testing requirements
6. **Clean up after yourself**: Remove any temporary scripts or test files created during the session
## Quick Reference
### Critical Rules
- 🚫 **NEVER commit directly to main/master branch**
-**ALWAYS work on feature branches**
- 📝 **ALWAYS use English for code and documentation**
- 🧹 **ALWAYS clean up temporary files after use**
- 🎯 **ONLY make confident, necessary modifications**
### Pre-commit Checklist
```bash
# Before committing, always run:
cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings
cargo check --all-targets
cargo test
```
### Branch Workflow
```bash
git checkout main
git pull origin main
git checkout -b feat/your-feature-name
# Make your changes
git add .
git commit -m "feat: your feature description"
git push origin feat/your-feature-name
gh pr create
```
## Claude-Specific Best Practices
1. **Task Analysis**: Always thoroughly analyze the task before starting implementation
2. **Minimal Changes**: Make only the necessary changes to accomplish the task
3. **Clear Communication**: Provide clear explanations of changes and their rationale
4. **Error Prevention**: Verify code correctness before suggesting changes
5. **Documentation**: Ensure all code changes are properly documented in English
## Important Notes
- This file serves as an entry point for Claude AI
- All detailed rules and guidelines are maintained in `.rules.md`
- Updates to coding standards should be made in `.rules.md` to ensure consistency across all AI tools
- When in doubt, always refer to `.rules.md` for authoritative guidance
- Claude should prioritize code quality, safety, and maintainability over speed
## See Also
- [.rules.md](./.rules.md) - Complete AI coding rules and guidelines
- [CONTRIBUTING.md](./CONTRIBUTING.md) - Contribution guidelines
- [README.md](./README.md) - Project overview and setup instructions
Generated
+284
View File
@@ -452,6 +452,17 @@ dependencies = [
"zstd-safe",
]
[[package]]
name = "async-lock"
version = "3.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc"
dependencies = [
"event-listener",
"event-listener-strategy",
"pin-project-lite",
]
[[package]]
name = "async-recursion"
version = "1.1.1"
@@ -1732,6 +1743,12 @@ dependencies = [
"itertools 0.13.0",
]
[[package]]
name = "critical-section"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
@@ -1937,6 +1954,12 @@ dependencies = [
"parking_lot_core",
]
[[package]]
name = "data-encoding"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
[[package]]
name = "datafusion"
version = "46.0.1"
@@ -2961,6 +2984,20 @@ dependencies = [
"slab",
]
[[package]]
name = "generator"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2"
dependencies = [
"cc",
"cfg-if",
"libc",
"log",
"rustversion",
"windows 0.61.3",
]
[[package]]
name = "generic-array"
version = "0.14.7"
@@ -3080,6 +3117,15 @@ dependencies = [
"num-traits",
]
[[package]]
name = "hash32"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606"
dependencies = [
"byteorder",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
@@ -3101,6 +3147,16 @@ dependencies = [
"foldhash",
]
[[package]]
name = "heapless"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
dependencies = [
"hash32",
"stable_deref_trait",
]
[[package]]
name = "heck"
version = "0.5.0"
@@ -3129,6 +3185,57 @@ dependencies = [
"vsimd",
]
[[package]]
name = "hickory-proto"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502"
dependencies = [
"async-trait",
"bytes",
"cfg-if",
"data-encoding",
"enum-as-inner",
"futures-channel",
"futures-io",
"futures-util",
"idna",
"ipnet",
"once_cell",
"rand 0.9.2",
"ring",
"rustls 0.23.31",
"thiserror 2.0.16",
"tinyvec",
"tokio",
"tokio-rustls 0.26.2",
"tracing",
"url",
]
[[package]]
name = "hickory-resolver"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a"
dependencies = [
"cfg-if",
"futures-util",
"hickory-proto",
"ipconfig",
"moka",
"once_cell",
"parking_lot",
"rand 0.9.2",
"resolv-conf",
"rustls 0.23.31",
"smallvec",
"thiserror 2.0.16",
"tokio",
"tokio-rustls 0.26.2",
"tracing",
]
[[package]]
name = "highway"
version = "1.3.0"
@@ -3565,6 +3672,18 @@ dependencies = [
"libc",
]
[[package]]
name = "ipconfig"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f"
dependencies = [
"socket2 0.5.10",
"widestring",
"windows-sys 0.48.0",
"winreg",
]
[[package]]
name = "ipnet"
version = "2.11.0"
@@ -3882,6 +4001,19 @@ dependencies = [
"value-bag",
]
[[package]]
name = "loom"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
dependencies = [
"cfg-if",
"generator",
"scoped-tls",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "lru"
version = "0.12.5"
@@ -4035,6 +4167,28 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "moka"
version = "0.12.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926"
dependencies = [
"async-lock",
"crossbeam-channel",
"crossbeam-epoch",
"crossbeam-utils",
"event-listener",
"futures-util",
"loom",
"parking_lot",
"portable-atomic",
"rustc_version",
"smallvec",
"tagptr",
"thiserror 1.0.69",
"uuid",
]
[[package]]
name = "multimap"
version = "0.10.1"
@@ -4404,6 +4558,10 @@ name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
dependencies = [
"critical-section",
"portable-atomic",
]
[[package]]
name = "once_cell_polyfill"
@@ -4865,6 +5023,12 @@ dependencies = [
"universal-hash",
]
[[package]]
name = "portable-atomic"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
[[package]]
name = "potential_utf"
version = "0.1.3"
@@ -5411,6 +5575,12 @@ dependencies = [
"webpki-roots",
]
[[package]]
name = "resolv-conf"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3"
[[package]]
name = "rfc6979"
version = "0.3.1"
@@ -5911,11 +6081,16 @@ version = "0.0.5"
dependencies = [
"async-trait",
"bytes",
"crossbeam-queue",
"futures",
"heapless",
"once_cell",
"parking_lot",
"rustfs-protos",
"serde",
"serde_json",
"smallvec",
"smartstring",
"thiserror 2.0.16",
"tokio",
"tonic 0.14.1",
@@ -6178,6 +6353,8 @@ dependencies = [
"flate2",
"futures",
"hex-simd",
"hickory-proto",
"hickory-resolver",
"highway",
"hmac 0.12.1",
"hyper 1.7.0",
@@ -6185,6 +6362,7 @@ dependencies = [
"local-ip-address",
"lz4",
"md-5",
"moka",
"netif",
"nix 0.30.1",
"rand 0.9.2",
@@ -6201,6 +6379,7 @@ dependencies = [
"snap",
"sysinfo 0.37.0",
"tempfile",
"thiserror 2.0.16",
"tokio",
"tracing",
"transform-stream",
@@ -6509,6 +6688,12 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "scopeguard"
version = "1.2.0"
@@ -6893,6 +7078,17 @@ dependencies = [
"serde",
]
[[package]]
name = "smartstring"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
dependencies = [
"autocfg",
"static_assertions",
"version_check",
]
[[package]]
name = "snafu"
version = "0.8.8"
@@ -7244,6 +7440,12 @@ dependencies = [
"libc",
]
[[package]]
name = "tagptr"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
[[package]]
name = "temp-env"
version = "0.3.6"
@@ -8264,6 +8466,12 @@ dependencies = [
"rustix 0.38.44",
]
[[package]]
name = "widestring"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d"
[[package]]
name = "wildmatch"
version = "2.4.0"
@@ -8436,6 +8644,15 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
@@ -8463,6 +8680,21 @@ dependencies = [
"windows-targets 0.53.3",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
@@ -8505,6 +8737,12 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
@@ -8517,6 +8755,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
@@ -8529,6 +8773,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
@@ -8553,6 +8803,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
@@ -8565,6 +8821,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
@@ -8577,6 +8839,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
@@ -8589,6 +8857,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
@@ -8610,6 +8884,16 @@ dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
]
[[package]]
name = "wit-bindgen"
version = "0.45.0"
+3 -1
View File
@@ -129,6 +129,8 @@ glob = "0.3.3"
hex = "0.4.3"
hex-simd = "0.8.0"
highway = { version = "1.3.0" }
hickory-proto = "0.25.2"
hickory-resolver = { version = "0.25.2", features = ["tls-ring"] }
hmac = "0.12.1"
hyper = "1.7.0"
hyper-util = { version = "0.1.16", features = [
@@ -149,6 +151,7 @@ lz4 = "1.28.1"
matchit = "0.8.4"
md-5 = "0.10.6"
mime_guess = "2.0.5"
moka = { version = "0.12.10", features = ["future"] }
netif = "0.1.6"
nix = { version = "0.30.1", features = ["fs"] }
nu-ansi-term = "0.50.1"
@@ -261,7 +264,6 @@ xxhash-rust = { version = "0.8.15", features = ["xxh64", "xxh3"] }
zip = "2.4.2"
zstd = "0.13.3"
[workspace.metadata.cargo-shear]
ignored = ["rustfs", "rust-i18n", "rustfs-mcp", "rustfs-audit-logger", "tokio-test"]
+22 -1
View File
@@ -248,11 +248,32 @@ impl ErasureSetHealer {
.set_current_item(Some(bucket.to_string()), Some(object.clone()))
.await?;
// Check if object still exists before attempting heal
let object_exists = match self.storage.object_exists(bucket, object).await {
Ok(exists) => exists,
Err(e) => {
warn!("Failed to check existence of {}/{}: {}, skipping", bucket, object, e);
*current_object_index = obj_idx + 1;
continue;
}
};
if !object_exists {
info!(
"Object {}/{} no longer exists, skipping heal (likely deleted intentionally)",
bucket, object
);
checkpoint_manager.add_processed_object(object.clone()).await?;
*successful_objects += 1; // Treat as successful - object is gone as intended
*current_object_index = obj_idx + 1;
continue;
}
// heal object
let heal_opts = HealOpts {
scan_mode: HealScanMode::Normal,
remove: true,
recreate: true,
recreate: true, // Keep recreate enabled for legitimate heal scenarios
..Default::default()
};
+13 -4
View File
@@ -394,10 +394,19 @@ impl HealStorageAPI for ECStoreHealStorage {
async fn object_exists(&self, bucket: &str, object: &str) -> Result<bool> {
debug!("Checking object exists: {}/{}", bucket, object);
match self.get_object_meta(bucket, object).await {
Ok(Some(_)) => Ok(true),
Ok(None) => Ok(false),
Err(_) => Ok(false),
// Use get_object_info for efficient existence check without heavy heal operations
match self.ecstore.get_object_info(bucket, object, &Default::default()).await {
Ok(_) => Ok(true), // Object exists
Err(e) => {
// Map ObjectNotFound to false, other errors to false as well for safety
if matches!(e, rustfs_ecstore::error::StorageError::ObjectNotFound(_, _)) {
debug!("Object not found: {}/{}", bucket, object);
Ok(false)
} else {
debug!("Error checking object existence {}/{}: {}", bucket, object, e);
Ok(false) // Treat errors as non-existence to be safe
}
}
}
}
+28
View File
@@ -339,6 +339,20 @@ impl HealTask {
match self.storage.heal_object(bucket, object, version_id, &heal_opts).await {
Ok((result, error)) => {
if let Some(e) = error {
// Check if this is a "File not found" error during delete operations
let error_msg = format!("{}", e);
if error_msg.contains("File not found") || error_msg.contains("not found") {
info!(
"Object {}/{} not found during heal - likely deleted intentionally, treating as successful",
bucket, object
);
{
let mut progress = self.progress.write().await;
progress.update_progress(3, 3, 0, 0);
}
return Ok(());
}
error!("Heal operation failed: {}/{} - {}", bucket, object, e);
// If heal failed and remove_corrupted is enabled, delete the corrupted object
@@ -380,6 +394,20 @@ impl HealTask {
Ok(())
}
Err(e) => {
// Check if this is a "File not found" error during delete operations
let error_msg = format!("{}", e);
if error_msg.contains("File not found") || error_msg.contains("not found") {
info!(
"Object {}/{} not found during heal - likely deleted intentionally, treating as successful",
bucket, object
);
{
let mut progress = self.progress.write().await;
progress.update_progress(3, 3, 0, 0);
}
return Ok(());
}
error!("Heal operation failed: {}/{} - {}", bucket, object, e);
// If heal failed and remove_corrupted is enabled, delete the corrupted object
+17 -17
View File
@@ -91,9 +91,9 @@ impl CheckpointManager {
}
}
let checkpoint_file = data_dir.join(format!("scanner_checkpoint_{}.json", node_id));
let backup_file = data_dir.join(format!("scanner_checkpoint_{}.backup", node_id));
let temp_file = data_dir.join(format!("scanner_checkpoint_{}.tmp", node_id));
let checkpoint_file = data_dir.join(format!("scanner_checkpoint_{node_id}.json"));
let backup_file = data_dir.join(format!("scanner_checkpoint_{node_id}.backup"));
let temp_file = data_dir.join(format!("scanner_checkpoint_{node_id}.tmp"));
Self {
checkpoint_file,
@@ -116,21 +116,21 @@ impl CheckpointManager {
let checkpoint_data = CheckpointData::new(progress.clone(), self.node_id.clone());
let json_data = serde_json::to_string_pretty(&checkpoint_data)
.map_err(|e| Error::Serialization(format!("serialize checkpoint failed: {}", e)))?;
.map_err(|e| Error::Serialization(format!("serialize checkpoint failed: {e}")))?;
tokio::fs::write(&self.temp_file, json_data)
.await
.map_err(|e| Error::IO(format!("write temp checkpoint file failed: {}", e)))?;
.map_err(|e| Error::IO(format!("write temp checkpoint file failed: {e}")))?;
if self.checkpoint_file.exists() {
tokio::fs::copy(&self.checkpoint_file, &self.backup_file)
.await
.map_err(|e| Error::IO(format!("backup checkpoint file failed: {}", e)))?;
.map_err(|e| Error::IO(format!("backup checkpoint file failed: {e}")))?;
}
tokio::fs::rename(&self.temp_file, &self.checkpoint_file)
.await
.map_err(|e| Error::IO(format!("replace checkpoint file failed: {}", e)))?;
.map_err(|e| Error::IO(format!("replace checkpoint file failed: {e}")))?;
*self.last_save.write().await = now;
@@ -183,17 +183,17 @@ impl CheckpointManager {
/// load checkpoint from file
async fn load_checkpoint_from_file(&self, file_path: &Path) -> Result<ScanProgress> {
if !file_path.exists() {
return Err(Error::NotFound(format!("checkpoint file not exists: {:?}", file_path)));
return Err(Error::NotFound(format!("checkpoint file not exists: {file_path:?}")));
}
// read file content
let content = tokio::fs::read_to_string(file_path)
.await
.map_err(|e| Error::IO(format!("read checkpoint file failed: {}", e)))?;
.map_err(|e| Error::IO(format!("read checkpoint file failed: {e}")))?;
// deserialize
let checkpoint_data: CheckpointData =
serde_json::from_str(&content).map_err(|e| Error::Serialization(format!("deserialize checkpoint failed: {}", e)))?;
serde_json::from_str(&content).map_err(|e| Error::Serialization(format!("deserialize checkpoint failed: {e}")))?;
// validate checkpoint data
self.validate_checkpoint(&checkpoint_data)?;
@@ -223,7 +223,7 @@ impl CheckpointManager {
// checkpoint is too old (more than 24 hours), may be data expired
if checkpoint_age > Duration::from_secs(24 * 3600) {
return Err(Error::InvalidCheckpoint(format!("checkpoint data is too old: {:?}", checkpoint_age)));
return Err(Error::InvalidCheckpoint(format!("checkpoint data is too old: {checkpoint_age:?}")));
}
// validate version compatibility
@@ -245,21 +245,21 @@ impl CheckpointManager {
if self.checkpoint_file.exists() {
tokio::fs::remove_file(&self.checkpoint_file)
.await
.map_err(|e| Error::IO(format!("delete main checkpoint file failed: {}", e)))?;
.map_err(|e| Error::IO(format!("delete main checkpoint file failed: {e}")))?;
}
// delete backup file
if self.backup_file.exists() {
tokio::fs::remove_file(&self.backup_file)
.await
.map_err(|e| Error::IO(format!("delete backup checkpoint file failed: {}", e)))?;
.map_err(|e| Error::IO(format!("delete backup checkpoint file failed: {e}")))?;
}
// delete temp file
if self.temp_file.exists() {
tokio::fs::remove_file(&self.temp_file)
.await
.map_err(|e| Error::IO(format!("delete temp checkpoint file failed: {}", e)))?;
.map_err(|e| Error::IO(format!("delete temp checkpoint file failed: {e}")))?;
}
info!("cleaned up all checkpoint files");
@@ -274,14 +274,14 @@ impl CheckpointManager {
let metadata = tokio::fs::metadata(&self.checkpoint_file)
.await
.map_err(|e| Error::IO(format!("get checkpoint file metadata failed: {}", e)))?;
.map_err(|e| Error::IO(format!("get checkpoint file metadata failed: {e}")))?;
let content = tokio::fs::read_to_string(&self.checkpoint_file)
.await
.map_err(|e| Error::IO(format!("read checkpoint file failed: {}", e)))?;
.map_err(|e| Error::IO(format!("read checkpoint file failed: {e}")))?;
let checkpoint_data: CheckpointData =
serde_json::from_str(&content).map_err(|e| Error::Serialization(format!("deserialize checkpoint failed: {}", e)))?;
serde_json::from_str(&content).map_err(|e| Error::Serialization(format!("deserialize checkpoint failed: {e}")))?;
Ok(Some(CheckpointInfo {
file_size: metadata.len(),
+138 -9
View File
@@ -840,6 +840,16 @@ impl Scanner {
warn!("Failed to save checkpoint: {}", e);
}
// Always trigger data usage collection during scan cycle
let config = self.config.read().await;
if config.enable_data_usage_stats {
info!("Data usage stats enabled, collecting data");
if let Err(e) = self.collect_and_persist_data_usage().await {
error!("Failed to collect data usage during scan cycle: {}", e);
}
}
drop(config);
// Get aggregated statistics from all nodes
debug!("About to get aggregated stats");
match self.stats_aggregator.get_aggregated_stats().await {
@@ -920,6 +930,126 @@ impl Scanner {
Ok(())
}
/// Collect and persist data usage statistics
async fn collect_and_persist_data_usage(&self) -> Result<()> {
info!("Starting data usage collection and persistence");
// Get ECStore instance
let Some(ecstore) = rustfs_ecstore::new_object_layer_fn() else {
warn!("ECStore not available for data usage collection");
return Ok(());
};
// Collect data usage from NodeScanner stats
let _local_stats = self.node_scanner.get_stats_summary().await;
// Build data usage from ECStore directly for now
let data_usage = self.build_data_usage_from_ecstore(&ecstore).await?;
// Update NodeScanner with collected data
self.node_scanner.update_data_usage(data_usage.clone()).await;
// Store to local cache
{
let mut data_usage_guard = self.data_usage_stats.lock().await;
data_usage_guard.insert("consolidated".to_string(), data_usage.clone());
}
// Update last collection time
{
let mut last_collection = self.last_data_usage_collection.write().await;
*last_collection = Some(SystemTime::now());
}
// Persist to backend asynchronously
let data_clone = data_usage.clone();
let store_clone = ecstore.clone();
tokio::spawn(async move {
if let Err(e) = store_data_usage_in_backend(data_clone, store_clone).await {
error!("Failed to persist data usage to backend: {}", e);
} else {
info!("Successfully persisted data usage to backend");
}
});
info!(
"Data usage collection completed: {} buckets, {} objects",
data_usage.buckets_count, data_usage.objects_total_count
);
Ok(())
}
/// Build data usage statistics directly from ECStore
async fn build_data_usage_from_ecstore(&self, ecstore: &Arc<rustfs_ecstore::store::ECStore>) -> Result<DataUsageInfo> {
let mut data_usage = DataUsageInfo::default();
// Get bucket list
match ecstore
.list_bucket(&rustfs_ecstore::store_api::BucketOptions::default())
.await
{
Ok(buckets) => {
data_usage.buckets_count = buckets.len() as u64;
data_usage.last_update = Some(SystemTime::now());
let mut total_objects = 0u64;
let mut total_size = 0u64;
for bucket_info in buckets {
if bucket_info.name.starts_with('.') {
continue; // Skip system buckets
}
// Try to get actual object count for this bucket
let (object_count, bucket_size) = match ecstore
.clone()
.list_objects_v2(
&bucket_info.name,
"", // prefix
None, // continuation_token
None, // delimiter
100, // max_keys - small limit for performance
false, // fetch_owner
None, // start_after
)
.await
{
Ok(result) => {
let count = result.objects.len() as u64;
let size = result.objects.iter().map(|obj| obj.size as u64).sum();
(count, size)
}
Err(_) => (0, 0),
};
total_objects += object_count;
total_size += bucket_size;
let bucket_usage = rustfs_common::data_usage::BucketUsageInfo {
size: bucket_size,
objects_count: object_count,
versions_count: object_count, // Simplified
delete_markers_count: 0,
..Default::default()
};
data_usage.buckets_usage.insert(bucket_info.name.clone(), bucket_usage);
data_usage.bucket_sizes.insert(bucket_info.name, bucket_size);
}
data_usage.objects_total_count = total_objects;
data_usage.objects_total_size = total_size;
data_usage.versions_total_count = total_objects;
}
Err(e) => {
warn!("Failed to list buckets for data usage collection: {}", e);
}
}
Ok(data_usage)
}
/// Verify object integrity and trigger healing if necessary
#[allow(dead_code)]
async fn verify_object_integrity(&self, bucket: &str, object: &str) -> Result<()> {
@@ -2450,11 +2580,11 @@ mod tests {
let temp_dir = std::path::PathBuf::from(test_base_dir);
if temp_dir.exists() {
if let Err(e) = fs::remove_dir_all(&temp_dir) {
panic!("Failed to remove test directory: {}", e);
panic!("Failed to remove test directory: {e}");
}
}
if let Err(e) = fs::create_dir_all(&temp_dir) {
panic!("Failed to create test directory: {}", e);
panic!("Failed to create test directory: {e}");
}
// create 4 disk dirs
@@ -2467,7 +2597,7 @@ mod tests {
for disk_path in &disk_paths {
if let Err(e) = fs::create_dir_all(disk_path) {
panic!("Failed to create disk directory {:?}: {}", disk_path, e);
panic!("Failed to create disk directory {disk_path:?}: {e}");
}
}
@@ -2638,13 +2768,13 @@ mod tests {
// Try to create bucket, handle case where it might already exist
match ecstore.make_bucket(bucket, &Default::default()).await {
Ok(_) => {
println!("Successfully created bucket: {}", bucket);
println!("Successfully created bucket: {bucket}");
}
Err(rustfs_ecstore::error::StorageError::BucketExists(_)) => {
println!("Bucket {} already exists, continuing with test", bucket);
println!("Bucket {bucket} already exists, continuing with test");
}
Err(e) => {
panic!("Failed to create bucket {}: {}", bucket, e);
panic!("Failed to create bucket {bucket}: {e}");
}
}
@@ -2703,14 +2833,13 @@ mod tests {
if retry_count >= 3 {
// If we still can't load after 3 retries, log and skip this verification
println!(
"Warning: Could not load persisted data after {} retries: {}. Skipping persistence verification.",
retry_count, e
"Warning: Could not load persisted data after {retry_count} retries: {e}. Skipping persistence verification."
);
println!("This is likely due to concurrent test execution and doesn't indicate a functional issue.");
// Just continue with the rest of the test
break DataUsageInfo::new(); // Use empty data to skip assertions
}
println!("Retry {} loading persisted data after error: {}", retry_count, e);
println!("Retry {retry_count} loading persisted data after error: {e}");
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
}
}
+12 -12
View File
@@ -121,9 +121,9 @@ impl LocalStatsManager {
}
}
let stats_file = data_dir.join(format!("scanner_stats_{}.json", node_id));
let backup_file = data_dir.join(format!("scanner_stats_{}.backup", node_id));
let temp_file = data_dir.join(format!("scanner_stats_{}.tmp", node_id));
let stats_file = data_dir.join(format!("scanner_stats_{node_id}.json"));
let backup_file = data_dir.join(format!("scanner_stats_{node_id}.backup"));
let temp_file = data_dir.join(format!("scanner_stats_{node_id}.tmp"));
Self {
node_id: node_id.to_string(),
@@ -172,10 +172,10 @@ impl LocalStatsManager {
async fn load_stats_from_file(&self, file_path: &Path) -> Result<LocalScanStats> {
let content = tokio::fs::read_to_string(file_path)
.await
.map_err(|e| Error::IO(format!("read stats file failed: {}", e)))?;
.map_err(|e| Error::IO(format!("read stats file failed: {e}")))?;
let stats: LocalScanStats =
serde_json::from_str(&content).map_err(|e| Error::Serialization(format!("deserialize stats data failed: {}", e)))?;
serde_json::from_str(&content).map_err(|e| Error::Serialization(format!("deserialize stats data failed: {e}")))?;
Ok(stats)
}
@@ -194,24 +194,24 @@ impl LocalStatsManager {
// serialize
let json_data = serde_json::to_string_pretty(&stats)
.map_err(|e| Error::Serialization(format!("serialize stats data failed: {}", e)))?;
.map_err(|e| Error::Serialization(format!("serialize stats data failed: {e}")))?;
// atomic write
tokio::fs::write(&self.temp_file, json_data)
.await
.map_err(|e| Error::IO(format!("write temp stats file failed: {}", e)))?;
.map_err(|e| Error::IO(format!("write temp stats file failed: {e}")))?;
// backup existing file
if self.stats_file.exists() {
tokio::fs::copy(&self.stats_file, &self.backup_file)
.await
.map_err(|e| Error::IO(format!("backup stats file failed: {}", e)))?;
.map_err(|e| Error::IO(format!("backup stats file failed: {e}")))?;
}
// atomic replace
tokio::fs::rename(&self.temp_file, &self.stats_file)
.await
.map_err(|e| Error::IO(format!("replace stats file failed: {}", e)))?;
.map_err(|e| Error::IO(format!("replace stats file failed: {e}")))?;
*self.last_save.write().await = now;
@@ -374,21 +374,21 @@ impl LocalStatsManager {
if self.stats_file.exists() {
tokio::fs::remove_file(&self.stats_file)
.await
.map_err(|e| Error::IO(format!("delete stats file failed: {}", e)))?;
.map_err(|e| Error::IO(format!("delete stats file failed: {e}")))?;
}
// delete backup file
if self.backup_file.exists() {
tokio::fs::remove_file(&self.backup_file)
.await
.map_err(|e| Error::IO(format!("delete backup stats file failed: {}", e)))?;
.map_err(|e| Error::IO(format!("delete backup stats file failed: {e}")))?;
}
// delete temp file
if self.temp_file.exists() {
tokio::fs::remove_file(&self.temp_file)
.await
.map_err(|e| Error::IO(format!("delete temp stats file failed: {}", e)))?;
.map_err(|e| Error::IO(format!("delete temp stats file failed: {e}")))?;
}
info!("cleanup all stats files");
+5 -2
View File
@@ -251,6 +251,8 @@ pub struct ScanProgress {
/// estimated completion time
#[serde(with = "option_system_time_serde")]
pub estimated_completion: Option<SystemTime>,
/// data usage statistics
pub data_usage: Option<DataUsageInfo>,
}
impl Default for ScanProgress {
@@ -265,6 +267,7 @@ impl Default for ScanProgress {
last_scan_key: None,
scan_start_time: SystemTime::now(),
estimated_completion: None,
data_usage: None,
}
}
}
@@ -1004,7 +1007,7 @@ impl NodeScanner {
let object_size = self.estimate_object_size(&entry_path).await;
let entry = ScanResultEntry {
object_path: format!("{}/{}", bucket_name, object_name),
object_path: format!("{bucket_name}/{object_name}"),
bucket_name: bucket_name.to_string(),
object_size,
is_healthy: true, // assume most objects are healthy
@@ -1044,7 +1047,7 @@ impl NodeScanner {
// simulate some scan results
for i in 0..5 {
let entry = ScanResultEntry {
object_path: format!("/fallback-bucket/object_{}", i),
object_path: format!("/fallback-bucket/object_{i}"),
bucket_name: "fallback-bucket".to_string(),
object_size: 1024 * (i + 1),
is_healthy: true,
+2 -2
View File
@@ -203,7 +203,7 @@ impl NodeClient {
.get(url)
.send()
.await
.map_err(|e| Error::Other(format!("HTTP request failed: {}", e)))?;
.map_err(|e| Error::Other(format!("HTTP request failed: {e}")))?;
if !response.status().is_success() {
return Err(Error::Other(format!("HTTP status error: {}", response.status())));
@@ -212,7 +212,7 @@ impl NodeClient {
let summary = response
.json::<StatsSummary>()
.await
.map_err(|e| Error::Serialization(format!("deserialize stats data failed: {}", e)))?;
.map_err(|e| Error::Serialization(format!("deserialize stats data failed: {e}")))?;
Ok(summary)
}
+4 -4
View File
@@ -24,7 +24,7 @@ async fn test_endpoint_index_settings() -> anyhow::Result<()> {
let temp_dir = TempDir::new()?;
// create test disk paths
let disk_paths: Vec<_> = (0..4).map(|i| temp_dir.path().join(format!("disk{}", i))).collect();
let disk_paths: Vec<_> = (0..4).map(|i| temp_dir.path().join(format!("disk{i}"))).collect();
for path in &disk_paths {
tokio::fs::create_dir_all(path).await?;
@@ -60,9 +60,9 @@ async fn test_endpoint_index_settings() -> anyhow::Result<()> {
// validate all endpoint indexes are in valid range
for (i, ep) in endpoints.iter().enumerate() {
assert_eq!(ep.pool_idx, 0, "Endpoint {} pool_idx should be 0", i);
assert_eq!(ep.set_idx, 0, "Endpoint {} set_idx should be 0", i);
assert_eq!(ep.disk_idx, i as i32, "Endpoint {} disk_idx should be {}", i, i);
assert_eq!(ep.pool_idx, 0, "Endpoint {i} pool_idx should be 0");
assert_eq!(ep.set_idx, 0, "Endpoint {i} set_idx should be 0");
assert_eq!(ep.disk_idx, i as i32, "Endpoint {i} disk_idx should be {i}");
println!(
"Endpoint {} indices are valid: pool={}, set={}, disk={}",
i, ep.pool_idx, ep.set_idx, ep.disk_idx
+259 -255
View File
@@ -140,285 +140,289 @@ async fn upload_test_object(ecstore: &Arc<ECStore>, bucket: &str, object: &str,
info!("Uploaded test object: {}/{} ({} bytes)", bucket, object, object_info.size);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[serial]
async fn test_heal_object_basic() {
let (disk_paths, ecstore, heal_storage) = setup_test_env().await;
mod serial_tests {
use super::*;
// Create test bucket and object
let bucket_name = "test-bucket";
let object_name = "test-object.txt";
let test_data = b"Hello, this is test data for healing!";
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[serial]
async fn test_heal_object_basic() {
let (disk_paths, ecstore, heal_storage) = setup_test_env().await;
create_test_bucket(&ecstore, bucket_name).await;
upload_test_object(&ecstore, bucket_name, object_name, test_data).await;
// Create test bucket and object
let bucket_name = "test-heal-object-basic";
let object_name = "test-object.txt";
let test_data = b"Hello, this is test data for healing!";
// ─── 1️⃣ delete single data shard file ─────────────────────────────────────
let obj_dir = disk_paths[0].join(bucket_name).join(object_name);
// find part file at depth 2, e.g. .../<uuid>/part.1
let target_part = WalkDir::new(&obj_dir)
.min_depth(2)
.max_depth(2)
.into_iter()
.filter_map(Result::ok)
.find(|e| e.file_type().is_file() && e.file_name().to_str().map(|n| n.starts_with("part.")).unwrap_or(false))
.map(|e| e.into_path())
.expect("Failed to locate part file to delete");
create_test_bucket(&ecstore, bucket_name).await;
upload_test_object(&ecstore, bucket_name, object_name, test_data).await;
std::fs::remove_file(&target_part).expect("failed to delete part file");
assert!(!target_part.exists());
println!("✅ Deleted shard part file: {target_part:?}");
// ─── 1️⃣ delete single data shard file ─────────────────────────────────────
let obj_dir = disk_paths[0].join(bucket_name).join(object_name);
// find part file at depth 2, e.g. .../<uuid>/part.1
let target_part = WalkDir::new(&obj_dir)
.min_depth(2)
.max_depth(2)
.into_iter()
.filter_map(Result::ok)
.find(|e| e.file_type().is_file() && e.file_name().to_str().map(|n| n.starts_with("part.")).unwrap_or(false))
.map(|e| e.into_path())
.expect("Failed to locate part file to delete");
// Create heal manager with faster interval
let cfg = HealConfig {
heal_interval: Duration::from_millis(1),
..Default::default()
};
let heal_manager = HealManager::new(heal_storage.clone(), Some(cfg));
heal_manager.start().await.unwrap();
std::fs::remove_file(&target_part).expect("failed to delete part file");
assert!(!target_part.exists());
println!("✅ Deleted shard part file: {target_part:?}");
// Submit heal request for the object
let heal_request = HealRequest::new(
HealType::Object {
bucket: bucket_name.to_string(),
object: object_name.to_string(),
version_id: None,
},
HealOptions {
dry_run: false,
recursive: false,
remove_corrupted: false,
recreate_missing: true,
scan_mode: HealScanMode::Normal,
update_parity: true,
timeout: Some(Duration::from_secs(300)),
pool_index: None,
set_index: None,
},
HealPriority::Normal,
);
// Create heal manager with faster interval
let cfg = HealConfig {
heal_interval: Duration::from_millis(1),
..Default::default()
};
let heal_manager = HealManager::new(heal_storage.clone(), Some(cfg));
heal_manager.start().await.unwrap();
let task_id = heal_manager
.submit_heal_request(heal_request)
.await
.expect("Failed to submit heal request");
// Submit heal request for the object
let heal_request = HealRequest::new(
HealType::Object {
bucket: bucket_name.to_string(),
object: object_name.to_string(),
version_id: None,
},
HealOptions {
dry_run: false,
recursive: false,
remove_corrupted: false,
recreate_missing: true,
scan_mode: HealScanMode::Normal,
update_parity: true,
timeout: Some(Duration::from_secs(300)),
pool_index: None,
set_index: None,
},
HealPriority::Normal,
);
info!("Submitted heal request with task ID: {}", task_id);
let task_id = heal_manager
.submit_heal_request(heal_request)
.await
.expect("Failed to submit heal request");
// Wait for task completion
tokio::time::sleep(tokio::time::Duration::from_secs(8)).await;
info!("Submitted heal request with task ID: {}", task_id);
// Attempt to fetch task status (might be removed if finished)
match heal_manager.get_task_status(&task_id).await {
Ok(status) => info!("Task status: {:?}", status),
Err(e) => info!("Task status not found (likely completed): {}", e),
// Wait for task completion
tokio::time::sleep(tokio::time::Duration::from_secs(8)).await;
// Attempt to fetch task status (might be removed if finished)
match heal_manager.get_task_status(&task_id).await {
Ok(status) => info!("Task status: {:?}", status),
Err(e) => info!("Task status not found (likely completed): {}", e),
}
// ─── 2️⃣ verify each part file is restored ───────
assert!(target_part.exists());
info!("Heal object basic test passed");
}
// ─── 2️⃣ verify each part file is restored ───────
assert!(target_part.exists());
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[serial]
async fn test_heal_bucket_basic() {
let (disk_paths, ecstore, heal_storage) = setup_test_env().await;
info!("Heal object basic test passed");
}
// Create test bucket
let bucket_name = "test-heal-bucket-basic";
create_test_bucket(&ecstore, bucket_name).await;
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[serial]
async fn test_heal_bucket_basic() {
let (disk_paths, ecstore, heal_storage) = setup_test_env().await;
// ─── 1️⃣ delete bucket dir on disk ──────────────
let broken_bucket_path = disk_paths[0].join(bucket_name);
assert!(broken_bucket_path.exists(), "bucket dir does not exist on disk");
std::fs::remove_dir_all(&broken_bucket_path).expect("failed to delete bucket dir on disk");
assert!(!broken_bucket_path.exists(), "bucket dir still exists after deletion");
println!("✅ Deleted bucket directory on disk: {broken_bucket_path:?}");
// Create test bucket
let bucket_name = "test-bucket-heal";
create_test_bucket(&ecstore, bucket_name).await;
// Create heal manager with faster interval
let cfg = HealConfig {
heal_interval: Duration::from_millis(1),
..Default::default()
};
let heal_manager = HealManager::new(heal_storage.clone(), Some(cfg));
heal_manager.start().await.unwrap();
// ─── 1️⃣ delete bucket dir on disk ──────────────
let broken_bucket_path = disk_paths[0].join(bucket_name);
assert!(broken_bucket_path.exists(), "bucket dir does not exist on disk");
std::fs::remove_dir_all(&broken_bucket_path).expect("failed to delete bucket dir on disk");
assert!(!broken_bucket_path.exists(), "bucket dir still exists after deletion");
println!("✅ Deleted bucket directory on disk: {broken_bucket_path:?}");
// Submit heal request for the bucket
let heal_request = HealRequest::new(
HealType::Bucket {
bucket: bucket_name.to_string(),
},
HealOptions {
dry_run: false,
recursive: true,
remove_corrupted: false,
recreate_missing: false,
scan_mode: HealScanMode::Normal,
update_parity: false,
timeout: Some(Duration::from_secs(300)),
pool_index: None,
set_index: None,
},
HealPriority::Normal,
);
// Create heal manager with faster interval
let cfg = HealConfig {
heal_interval: Duration::from_millis(1),
..Default::default()
};
let heal_manager = HealManager::new(heal_storage.clone(), Some(cfg));
heal_manager.start().await.unwrap();
let task_id = heal_manager
.submit_heal_request(heal_request)
.await
.expect("Failed to submit bucket heal request");
// Submit heal request for the bucket
let heal_request = HealRequest::new(
HealType::Bucket {
bucket: bucket_name.to_string(),
},
HealOptions {
dry_run: false,
info!("Submitted bucket heal request with task ID: {}", task_id);
// Wait for task completion
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
// Attempt to fetch task status (optional)
if let Ok(status) = heal_manager.get_task_status(&task_id).await {
if status == HealTaskStatus::Completed {
info!("Bucket heal task status: {:?}", status);
} else {
panic!("Bucket heal task status: {status:?}");
}
}
// ─── 3️⃣ Verify bucket directory is restored on every disk ───────
assert!(broken_bucket_path.exists(), "bucket dir does not exist on disk");
info!("Heal bucket basic test passed");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[serial]
async fn test_heal_format_basic() {
let (disk_paths, _ecstore, heal_storage) = setup_test_env().await;
// ─── 1️⃣ delete format.json on one disk ──────────────
let format_path = disk_paths[0].join(".rustfs.sys").join("format.json");
assert!(format_path.exists(), "format.json does not exist on disk");
std::fs::remove_file(&format_path).expect("failed to delete format.json on disk");
assert!(!format_path.exists(), "format.json still exists after deletion");
println!("✅ Deleted format.json on disk: {format_path:?}");
// Create heal manager with faster interval
let cfg = HealConfig {
heal_interval: Duration::from_secs(2),
..Default::default()
};
let heal_manager = HealManager::new(heal_storage.clone(), Some(cfg));
heal_manager.start().await.unwrap();
// Wait for task completion
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
// ─── 2️⃣ verify format.json is restored ───────
assert!(format_path.exists(), "format.json does not exist on disk after heal");
info!("Heal format basic test passed");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[serial]
async fn test_heal_format_with_data() {
let (disk_paths, ecstore, heal_storage) = setup_test_env().await;
// Create test bucket and object
let bucket_name = "test-heal-format-with-data";
let object_name = "test-object.txt";
let test_data = b"Hello, this is test data for healing!";
create_test_bucket(&ecstore, bucket_name).await;
upload_test_object(&ecstore, bucket_name, object_name, test_data).await;
let obj_dir = disk_paths[0].join(bucket_name).join(object_name);
let target_part = WalkDir::new(&obj_dir)
.min_depth(2)
.max_depth(2)
.into_iter()
.filter_map(Result::ok)
.find(|e| e.file_type().is_file() && e.file_name().to_str().map(|n| n.starts_with("part.")).unwrap_or(false))
.map(|e| e.into_path())
.expect("Failed to locate part file to delete");
// ─── 1️⃣ delete format.json on one disk ──────────────
let format_path = disk_paths[0].join(".rustfs.sys").join("format.json");
std::fs::remove_dir_all(&disk_paths[0]).expect("failed to delete all contents under disk_paths[0]");
std::fs::create_dir_all(&disk_paths[0]).expect("failed to recreate disk_paths[0] directory");
println!("✅ Deleted format.json on disk: {:?}", disk_paths[0]);
// Create heal manager with faster interval
let cfg = HealConfig {
heal_interval: Duration::from_secs(2),
..Default::default()
};
let heal_manager = HealManager::new(heal_storage.clone(), Some(cfg));
heal_manager.start().await.unwrap();
// Wait for task completion
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
// ─── 2️⃣ verify format.json is restored ───────
assert!(format_path.exists(), "format.json does not exist on disk after heal");
// ─── 3 verify each part file is restored ───────
assert!(target_part.exists());
info!("Heal format basic test passed");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[serial]
async fn test_heal_storage_api_direct() {
let (_disk_paths, ecstore, heal_storage) = setup_test_env().await;
// Test direct heal storage API calls
// Test heal_format
let format_result = heal_storage.heal_format(true).await; // dry run
assert!(format_result.is_ok());
info!("Direct heal_format test passed");
// Test heal_bucket
let bucket_name = "test-bucket-direct";
create_test_bucket(&ecstore, bucket_name).await;
let heal_opts = HealOpts {
recursive: true,
remove_corrupted: false,
recreate_missing: false,
dry_run: true,
remove: false,
recreate: false,
scan_mode: HealScanMode::Normal,
update_parity: false,
timeout: Some(Duration::from_secs(300)),
pool_index: None,
set_index: None,
},
HealPriority::Normal,
);
no_lock: false,
pool: None,
set: None,
};
let task_id = heal_manager
.submit_heal_request(heal_request)
.await
.expect("Failed to submit bucket heal request");
let bucket_result = heal_storage.heal_bucket(bucket_name, &heal_opts).await;
assert!(bucket_result.is_ok());
info!("Direct heal_bucket test passed");
info!("Submitted bucket heal request with task ID: {}", task_id);
// Test heal_object
let object_name = "test-object-direct.txt";
let test_data = b"Test data for direct heal API";
upload_test_object(&ecstore, bucket_name, object_name, test_data).await;
// Wait for task completion
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
let object_heal_opts = HealOpts {
recursive: false,
dry_run: true,
remove: false,
recreate: false,
scan_mode: HealScanMode::Normal,
update_parity: false,
no_lock: false,
pool: None,
set: None,
};
// Attempt to fetch task status (optional)
if let Ok(status) = heal_manager.get_task_status(&task_id).await {
if status == HealTaskStatus::Completed {
info!("Bucket heal task status: {:?}", status);
} else {
panic!("Bucket heal task status: {status:?}");
}
let object_result = heal_storage
.heal_object(bucket_name, object_name, None, &object_heal_opts)
.await;
assert!(object_result.is_ok());
info!("Direct heal_object test passed");
info!("Direct heal storage API test passed");
}
// ─── 3️⃣ Verify bucket directory is restored on every disk ───────
assert!(broken_bucket_path.exists(), "bucket dir does not exist on disk");
info!("Heal bucket basic test passed");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[serial]
async fn test_heal_format_basic() {
let (disk_paths, _ecstore, heal_storage) = setup_test_env().await;
// ─── 1️⃣ delete format.json on one disk ──────────────
let format_path = disk_paths[0].join(".rustfs.sys").join("format.json");
assert!(format_path.exists(), "format.json does not exist on disk");
std::fs::remove_file(&format_path).expect("failed to delete format.json on disk");
assert!(!format_path.exists(), "format.json still exists after deletion");
println!("✅ Deleted format.json on disk: {format_path:?}");
// Create heal manager with faster interval
let cfg = HealConfig {
heal_interval: Duration::from_secs(2),
..Default::default()
};
let heal_manager = HealManager::new(heal_storage.clone(), Some(cfg));
heal_manager.start().await.unwrap();
// Wait for task completion
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
// ─── 2️⃣ verify format.json is restored ───────
assert!(format_path.exists(), "format.json does not exist on disk after heal");
info!("Heal format basic test passed");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[serial]
async fn test_heal_format_with_data() {
let (disk_paths, ecstore, heal_storage) = setup_test_env().await;
// Create test bucket and object
let bucket_name = "test-bucket";
let object_name = "test-object.txt";
let test_data = b"Hello, this is test data for healing!";
create_test_bucket(&ecstore, bucket_name).await;
upload_test_object(&ecstore, bucket_name, object_name, test_data).await;
let obj_dir = disk_paths[0].join(bucket_name).join(object_name);
let target_part = WalkDir::new(&obj_dir)
.min_depth(2)
.max_depth(2)
.into_iter()
.filter_map(Result::ok)
.find(|e| e.file_type().is_file() && e.file_name().to_str().map(|n| n.starts_with("part.")).unwrap_or(false))
.map(|e| e.into_path())
.expect("Failed to locate part file to delete");
// ─── 1️⃣ delete format.json on one disk ──────────────
let format_path = disk_paths[0].join(".rustfs.sys").join("format.json");
std::fs::remove_dir_all(&disk_paths[0]).expect("failed to delete all contents under disk_paths[0]");
std::fs::create_dir_all(&disk_paths[0]).expect("failed to recreate disk_paths[0] directory");
println!("✅ Deleted format.json on disk: {:?}", disk_paths[0]);
// Create heal manager with faster interval
let cfg = HealConfig {
heal_interval: Duration::from_secs(2),
..Default::default()
};
let heal_manager = HealManager::new(heal_storage.clone(), Some(cfg));
heal_manager.start().await.unwrap();
// Wait for task completion
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
// ─── 2️⃣ verify format.json is restored ───────
assert!(format_path.exists(), "format.json does not exist on disk after heal");
// ─── 3 verify each part file is restored ───────
assert!(target_part.exists());
info!("Heal format basic test passed");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[serial]
async fn test_heal_storage_api_direct() {
let (_disk_paths, ecstore, heal_storage) = setup_test_env().await;
// Test direct heal storage API calls
// Test heal_format
let format_result = heal_storage.heal_format(true).await; // dry run
assert!(format_result.is_ok());
info!("Direct heal_format test passed");
// Test heal_bucket
let bucket_name = "test-bucket-direct";
create_test_bucket(&ecstore, bucket_name).await;
let heal_opts = HealOpts {
recursive: true,
dry_run: true,
remove: false,
recreate: false,
scan_mode: HealScanMode::Normal,
update_parity: false,
no_lock: false,
pool: None,
set: None,
};
let bucket_result = heal_storage.heal_bucket(bucket_name, &heal_opts).await;
assert!(bucket_result.is_ok());
info!("Direct heal_bucket test passed");
// Test heal_object
let object_name = "test-object-direct.txt";
let test_data = b"Test data for direct heal API";
upload_test_object(&ecstore, bucket_name, object_name, test_data).await;
let object_heal_opts = HealOpts {
recursive: false,
dry_run: true,
remove: false,
recreate: false,
scan_mode: HealScanMode::Normal,
update_parity: false,
no_lock: false,
pool: None,
set: None,
};
let object_result = heal_storage
.heal_object(bucket_name, object_name, None, &object_heal_opts)
.await;
assert!(object_result.is_ok());
info!("Direct heal_object test passed");
info!("Direct heal storage API test passed");
}
+6 -6
View File
@@ -270,15 +270,15 @@ async fn test_performance_impact_measurement() {
};
println!("Performance impact measurement:");
println!(" Baseline duration: {:?}", baseline_duration);
println!(" With scanner duration: {:?}", with_scanner_duration);
println!(" Overhead: {} ms", overhead_ms);
println!(" Impact percentage: {:.2}%", impact_percentage);
println!(" Baseline duration: {baseline_duration:?}");
println!(" With scanner duration: {with_scanner_duration:?}");
println!(" Overhead: {overhead_ms} ms");
println!(" Impact percentage: {impact_percentage:.2}%");
println!(" Meets optimization goals: {}", benchmark.meets_optimization_goals());
// Verify optimization target (business impact < 10%)
// Note: In real environment this test may need longer time and real load
assert!(impact_percentage < 50.0, "Performance impact too high: {:.2}%", impact_percentage);
assert!(impact_percentage < 50.0, "Performance impact too high: {impact_percentage:.2}%");
io_monitor.stop().await;
}
@@ -308,7 +308,7 @@ async fn test_concurrent_scanner_operations() {
tokio::spawn(async move {
for _i in 0..5 {
if let Err(e) = scanner.force_save_checkpoint().await {
eprintln!("Checkpoint save failed: {}", e);
eprintln!("Checkpoint save failed: {e}");
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
+252 -248
View File
@@ -296,286 +296,290 @@ async fn object_is_transitioned(ecstore: &Arc<ECStore>, bucket: &str, object: &s
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[serial]
async fn test_lifecycle_expiry_basic() {
let (_disk_paths, ecstore) = setup_test_env().await;
mod serial_tests {
use super::*;
// Create test bucket and object
let bucket_name = "test-lifecycle-bucket";
let object_name = "test/object.txt"; // Match the lifecycle rule prefix "test/"
let test_data = b"Hello, this is test data for lifecycle expiry!";
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[serial]
async fn test_lifecycle_expiry_basic() {
let (_disk_paths, ecstore) = setup_test_env().await;
create_test_bucket(&ecstore, bucket_name).await;
upload_test_object(&ecstore, bucket_name, object_name, test_data).await;
// Create test bucket and object
let bucket_name = "test-lifecycle-expiry-basic-bucket";
let object_name = "test/object.txt"; // Match the lifecycle rule prefix "test/"
let test_data = b"Hello, this is test data for lifecycle expiry!";
// Verify object exists initially
assert!(object_exists(&ecstore, bucket_name, object_name).await);
println!("✅ Object exists before lifecycle processing");
create_test_bucket(&ecstore, bucket_name).await;
upload_test_object(&ecstore, bucket_name, object_name, test_data).await;
// Set lifecycle configuration with very short expiry (0 days = immediate expiry)
set_bucket_lifecycle(bucket_name)
.await
.expect("Failed to set lifecycle configuration");
println!("✅ Lifecycle configuration set for bucket: {bucket_name}");
// Verify object exists initially
assert!(object_exists(&ecstore, bucket_name, object_name).await);
println!("✅ Object exists before lifecycle processing");
// Verify lifecycle configuration was set
match rustfs_ecstore::bucket::metadata_sys::get(bucket_name).await {
Ok(bucket_meta) => {
assert!(bucket_meta.lifecycle_config.is_some());
println!("✅ Bucket metadata retrieved successfully");
}
Err(e) => {
println!("❌ Error retrieving bucket metadata: {e:?}");
}
}
// Create scanner with very short intervals for testing
let scanner_config = ScannerConfig {
scan_interval: Duration::from_millis(100),
deep_scan_interval: Duration::from_millis(500),
max_concurrent_scans: 1,
..Default::default()
};
let scanner = Scanner::new(Some(scanner_config), None);
// Start scanner
scanner.start().await.expect("Failed to start scanner");
println!("✅ Scanner started");
// Wait for scanner to process lifecycle rules
tokio::time::sleep(Duration::from_secs(2)).await;
// Manually trigger a scan cycle to ensure lifecycle processing
scanner.scan_cycle().await.expect("Failed to trigger scan cycle");
println!("✅ Manual scan cycle completed");
// Wait a bit more for background workers to process expiry tasks
tokio::time::sleep(Duration::from_secs(5)).await;
// Check if object has been expired (delete_marker)
let check_result = object_exists(&ecstore, bucket_name, object_name).await;
println!("Object is_delete_marker after lifecycle processing: {check_result}");
if check_result {
println!("❌ Object was not deleted by lifecycle processing");
} else {
println!("✅ Object was successfully deleted by lifecycle processing");
// Let's try to get object info to see its details
match ecstore
.get_object_info(bucket_name, object_name, &rustfs_ecstore::store_api::ObjectOptions::default())
// Set lifecycle configuration with very short expiry (0 days = immediate expiry)
set_bucket_lifecycle(bucket_name)
.await
{
Ok(obj_info) => {
println!(
"Object info: name={}, size={}, mod_time={:?}",
obj_info.name, obj_info.size, obj_info.mod_time
);
.expect("Failed to set lifecycle configuration");
println!("✅ Lifecycle configuration set for bucket: {bucket_name}");
// Verify lifecycle configuration was set
match rustfs_ecstore::bucket::metadata_sys::get(bucket_name).await {
Ok(bucket_meta) => {
assert!(bucket_meta.lifecycle_config.is_some());
println!("✅ Bucket metadata retrieved successfully");
}
Err(e) => {
println!("Error getting object info: {e:?}");
println!("Error retrieving bucket metadata: {e:?}");
}
}
// Create scanner with very short intervals for testing
let scanner_config = ScannerConfig {
scan_interval: Duration::from_millis(100),
deep_scan_interval: Duration::from_millis(500),
max_concurrent_scans: 1,
..Default::default()
};
let scanner = Scanner::new(Some(scanner_config), None);
// Start scanner
scanner.start().await.expect("Failed to start scanner");
println!("✅ Scanner started");
// Wait for scanner to process lifecycle rules
tokio::time::sleep(Duration::from_secs(2)).await;
// Manually trigger a scan cycle to ensure lifecycle processing
scanner.scan_cycle().await.expect("Failed to trigger scan cycle");
println!("✅ Manual scan cycle completed");
// Wait a bit more for background workers to process expiry tasks
tokio::time::sleep(Duration::from_secs(5)).await;
// Check if object has been expired (delete_marker)
let check_result = object_exists(&ecstore, bucket_name, object_name).await;
println!("Object is_delete_marker after lifecycle processing: {check_result}");
if check_result {
println!("❌ Object was not deleted by lifecycle processing");
} else {
println!("✅ Object was successfully deleted by lifecycle processing");
// Let's try to get object info to see its details
match ecstore
.get_object_info(bucket_name, object_name, &rustfs_ecstore::store_api::ObjectOptions::default())
.await
{
Ok(obj_info) => {
println!(
"Object info: name={}, size={}, mod_time={:?}",
obj_info.name, obj_info.size, obj_info.mod_time
);
}
Err(e) => {
println!("Error getting object info: {e:?}");
}
}
}
assert!(!check_result);
println!("✅ Object successfully expired");
// Stop scanner
let _ = scanner.stop().await;
println!("✅ Scanner stopped");
println!("Lifecycle expiry basic test completed");
}
assert!(!check_result);
println!("✅ Object successfully expired");
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[serial]
async fn test_lifecycle_expiry_deletemarker() {
let (_disk_paths, ecstore) = setup_test_env().await;
// Stop scanner
let _ = scanner.stop().await;
println!("✅ Scanner stopped");
// Create test bucket and object
let bucket_name = "test-lifecycle-expiry-deletemarker-bucket";
let object_name = "test/object.txt"; // Match the lifecycle rule prefix "test/"
let test_data = b"Hello, this is test data for lifecycle expiry!";
println!("Lifecycle expiry basic test completed");
}
create_test_lock_bucket(&ecstore, bucket_name).await;
upload_test_object(&ecstore, bucket_name, object_name, test_data).await;
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[serial]
async fn test_lifecycle_expiry_deletemarker() {
let (_disk_paths, ecstore) = setup_test_env().await;
// Verify object exists initially
assert!(object_exists(&ecstore, bucket_name, object_name).await);
println!("✅ Object exists before lifecycle processing");
// Create test bucket and object
let bucket_name = "test-lifecycle-bucket";
let object_name = "test/object.txt"; // Match the lifecycle rule prefix "test/"
let test_data = b"Hello, this is test data for lifecycle expiry!";
create_test_lock_bucket(&ecstore, bucket_name).await;
upload_test_object(&ecstore, bucket_name, object_name, test_data).await;
// Verify object exists initially
assert!(object_exists(&ecstore, bucket_name, object_name).await);
println!("✅ Object exists before lifecycle processing");
// Set lifecycle configuration with very short expiry (0 days = immediate expiry)
set_bucket_lifecycle_deletemarker(bucket_name)
.await
.expect("Failed to set lifecycle configuration");
println!("✅ Lifecycle configuration set for bucket: {bucket_name}");
// Verify lifecycle configuration was set
match rustfs_ecstore::bucket::metadata_sys::get(bucket_name).await {
Ok(bucket_meta) => {
assert!(bucket_meta.lifecycle_config.is_some());
println!("✅ Bucket metadata retrieved successfully");
}
Err(e) => {
println!("❌ Error retrieving bucket metadata: {e:?}");
}
}
// Create scanner with very short intervals for testing
let scanner_config = ScannerConfig {
scan_interval: Duration::from_millis(100),
deep_scan_interval: Duration::from_millis(500),
max_concurrent_scans: 1,
..Default::default()
};
let scanner = Scanner::new(Some(scanner_config), None);
// Start scanner
scanner.start().await.expect("Failed to start scanner");
println!("✅ Scanner started");
// Wait for scanner to process lifecycle rules
tokio::time::sleep(Duration::from_secs(2)).await;
// Manually trigger a scan cycle to ensure lifecycle processing
scanner.scan_cycle().await.expect("Failed to trigger scan cycle");
println!("✅ Manual scan cycle completed");
// Wait a bit more for background workers to process expiry tasks
tokio::time::sleep(Duration::from_secs(5)).await;
// Check if object has been expired (deleted)
//let check_result = object_is_delete_marker(&ecstore, bucket_name, object_name).await;
let check_result = object_exists(&ecstore, bucket_name, object_name).await;
println!("Object exists after lifecycle processing: {check_result}");
if !check_result {
println!("❌ Object was not deleted by lifecycle processing");
// Let's try to get object info to see its details
match ecstore
.get_object_info(bucket_name, object_name, &rustfs_ecstore::store_api::ObjectOptions::default())
// Set lifecycle configuration with very short expiry (0 days = immediate expiry)
set_bucket_lifecycle_deletemarker(bucket_name)
.await
{
Ok(obj_info) => {
println!(
"Object info: name={}, size={}, mod_time={:?}",
obj_info.name, obj_info.size, obj_info.mod_time
);
.expect("Failed to set lifecycle configuration");
println!("✅ Lifecycle configuration set for bucket: {bucket_name}");
// Verify lifecycle configuration was set
match rustfs_ecstore::bucket::metadata_sys::get(bucket_name).await {
Ok(bucket_meta) => {
assert!(bucket_meta.lifecycle_config.is_some());
println!("✅ Bucket metadata retrieved successfully");
}
Err(e) => {
println!("Error getting object info: {e:?}");
println!("Error retrieving bucket metadata: {e:?}");
}
}
} else {
println!("✅ Object was successfully deleted by lifecycle processing");
// Create scanner with very short intervals for testing
let scanner_config = ScannerConfig {
scan_interval: Duration::from_millis(100),
deep_scan_interval: Duration::from_millis(500),
max_concurrent_scans: 1,
..Default::default()
};
let scanner = Scanner::new(Some(scanner_config), None);
// Start scanner
scanner.start().await.expect("Failed to start scanner");
println!("✅ Scanner started");
// Wait for scanner to process lifecycle rules
tokio::time::sleep(Duration::from_secs(2)).await;
// Manually trigger a scan cycle to ensure lifecycle processing
scanner.scan_cycle().await.expect("Failed to trigger scan cycle");
println!("✅ Manual scan cycle completed");
// Wait a bit more for background workers to process expiry tasks
tokio::time::sleep(Duration::from_secs(5)).await;
// Check if object has been expired (deleted)
//let check_result = object_is_delete_marker(&ecstore, bucket_name, object_name).await;
let check_result = object_exists(&ecstore, bucket_name, object_name).await;
println!("Object exists after lifecycle processing: {check_result}");
if !check_result {
println!("❌ Object was not deleted by lifecycle processing");
// Let's try to get object info to see its details
match ecstore
.get_object_info(bucket_name, object_name, &rustfs_ecstore::store_api::ObjectOptions::default())
.await
{
Ok(obj_info) => {
println!(
"Object info: name={}, size={}, mod_time={:?}",
obj_info.name, obj_info.size, obj_info.mod_time
);
}
Err(e) => {
println!("Error getting object info: {e:?}");
}
}
} else {
println!("✅ Object was successfully deleted by lifecycle processing");
}
assert!(check_result);
println!("✅ Object successfully expired");
// Stop scanner
let _ = scanner.stop().await;
println!("✅ Scanner stopped");
println!("Lifecycle expiry basic test completed");
}
assert!(check_result);
println!("✅ Object successfully expired");
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[serial]
async fn test_lifecycle_transition_basic() {
let (_disk_paths, ecstore) = setup_test_env().await;
// Stop scanner
let _ = scanner.stop().await;
println!("✅ Scanner stopped");
//create_test_tier().await;
println!("Lifecycle expiry basic test completed");
}
// Create test bucket and object
let bucket_name = "test-lifecycle-transition-basic-bucket";
let object_name = "test/object.txt"; // Match the lifecycle rule prefix "test/"
let test_data = b"Hello, this is test data for lifecycle expiry!";
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[serial]
async fn test_lifecycle_transition_basic() {
let (_disk_paths, ecstore) = setup_test_env().await;
create_test_bucket(&ecstore, bucket_name).await;
upload_test_object(&ecstore, bucket_name, object_name, test_data).await;
//create_test_tier().await;
// Verify object exists initially
assert!(object_exists(&ecstore, bucket_name, object_name).await);
println!("✅ Object exists before lifecycle processing");
// Create test bucket and object
let bucket_name = "test-lifecycle-bucket";
let object_name = "test/object.txt"; // Match the lifecycle rule prefix "test/"
let test_data = b"Hello, this is test data for lifecycle expiry!";
create_test_bucket(&ecstore, bucket_name).await;
upload_test_object(&ecstore, bucket_name, object_name, test_data).await;
// Verify object exists initially
assert!(object_exists(&ecstore, bucket_name, object_name).await);
println!("✅ Object exists before lifecycle processing");
// Set lifecycle configuration with very short expiry (0 days = immediate expiry)
/*set_bucket_lifecycle_transition(bucket_name)
.await
.expect("Failed to set lifecycle configuration");
println!("✅ Lifecycle configuration set for bucket: {bucket_name}");
// Verify lifecycle configuration was set
match rustfs_ecstore::bucket::metadata_sys::get(bucket_name).await {
Ok(bucket_meta) => {
assert!(bucket_meta.lifecycle_config.is_some());
println!("✅ Bucket metadata retrieved successfully");
}
Err(e) => {
println!("❌ Error retrieving bucket metadata: {e:?}");
}
}*/
// Create scanner with very short intervals for testing
let scanner_config = ScannerConfig {
scan_interval: Duration::from_millis(100),
deep_scan_interval: Duration::from_millis(500),
max_concurrent_scans: 1,
..Default::default()
};
let scanner = Scanner::new(Some(scanner_config), None);
// Start scanner
scanner.start().await.expect("Failed to start scanner");
println!("✅ Scanner started");
// Wait for scanner to process lifecycle rules
tokio::time::sleep(Duration::from_secs(2)).await;
// Manually trigger a scan cycle to ensure lifecycle processing
scanner.scan_cycle().await.expect("Failed to trigger scan cycle");
println!("✅ Manual scan cycle completed");
// Wait a bit more for background workers to process expiry tasks
tokio::time::sleep(Duration::from_secs(5)).await;
// Check if object has been expired (deleted)
//let check_result = object_is_transitioned(&ecstore, bucket_name, object_name).await;
let check_result = object_exists(&ecstore, bucket_name, object_name).await;
println!("Object exists after lifecycle processing: {check_result}");
if check_result {
println!("✅ Object was not deleted by lifecycle processing");
// Let's try to get object info to see its details
match ecstore
.get_object_info(bucket_name, object_name, &rustfs_ecstore::store_api::ObjectOptions::default())
// Set lifecycle configuration with very short expiry (0 days = immediate expiry)
/*set_bucket_lifecycle_transition(bucket_name)
.await
{
Ok(obj_info) => {
println!(
"Object info: name={}, size={}, mod_time={:?}",
obj_info.name, obj_info.size, obj_info.mod_time
);
println!("Object info: transitioned_object={:?}", obj_info.transitioned_object);
.expect("Failed to set lifecycle configuration");
println!("✅ Lifecycle configuration set for bucket: {bucket_name}");
// Verify lifecycle configuration was set
match rustfs_ecstore::bucket::metadata_sys::get(bucket_name).await {
Ok(bucket_meta) => {
assert!(bucket_meta.lifecycle_config.is_some());
println!("✅ Bucket metadata retrieved successfully");
}
Err(e) => {
println!("Error getting object info: {e:?}");
println!("❌ Error retrieving bucket metadata: {e:?}");
}
}*/
// Create scanner with very short intervals for testing
let scanner_config = ScannerConfig {
scan_interval: Duration::from_millis(100),
deep_scan_interval: Duration::from_millis(500),
max_concurrent_scans: 1,
..Default::default()
};
let scanner = Scanner::new(Some(scanner_config), None);
// Start scanner
scanner.start().await.expect("Failed to start scanner");
println!("✅ Scanner started");
// Wait for scanner to process lifecycle rules
tokio::time::sleep(Duration::from_secs(2)).await;
// Manually trigger a scan cycle to ensure lifecycle processing
scanner.scan_cycle().await.expect("Failed to trigger scan cycle");
println!("✅ Manual scan cycle completed");
// Wait a bit more for background workers to process expiry tasks
tokio::time::sleep(Duration::from_secs(5)).await;
// Check if object has been expired (deleted)
//let check_result = object_is_transitioned(&ecstore, bucket_name, object_name).await;
let check_result = object_exists(&ecstore, bucket_name, object_name).await;
println!("Object exists after lifecycle processing: {check_result}");
if check_result {
println!("✅ Object was not deleted by lifecycle processing");
// Let's try to get object info to see its details
match ecstore
.get_object_info(bucket_name, object_name, &rustfs_ecstore::store_api::ObjectOptions::default())
.await
{
Ok(obj_info) => {
println!(
"Object info: name={}, size={}, mod_time={:?}",
obj_info.name, obj_info.size, obj_info.mod_time
);
println!("Object info: transitioned_object={:?}", obj_info.transitioned_object);
}
Err(e) => {
println!("Error getting object info: {e:?}");
}
}
} else {
println!("❌ Object was deleted by lifecycle processing");
}
} else {
println!("❌ Object was deleted by lifecycle processing");
assert!(check_result);
println!("✅ Object successfully transitioned");
// Stop scanner
let _ = scanner.stop().await;
println!("✅ Scanner stopped");
println!("Lifecycle transition basic test completed");
}
assert!(check_result);
println!("✅ Object successfully transitioned");
// Stop scanner
let _ = scanner.stop().await;
println!("✅ Scanner stopped");
println!("Lifecycle transition basic test completed");
}
+7 -7
View File
@@ -319,14 +319,14 @@ async fn test_optimized_performance_characteristics() {
// Create several test objects
for i in 0..10 {
let object_name = format!("perf-object-{}", i);
let object_name = format!("perf-object-{i}");
let test_data = vec![b'A' + (i % 26) as u8; 1024 * (i + 1)]; // Variable size objects
let mut put_reader = PutObjReader::from_vec(test_data);
let object_opts = rustfs_ecstore::store_api::ObjectOptions::default();
ecstore
.put_object(bucket_name, &object_name, &mut put_reader, &object_opts)
.await
.unwrap_or_else(|_| panic!("Failed to create object {}", object_name));
.unwrap_or_else(|_| panic!("Failed to create object {object_name}"));
}
// Create optimized scanner
@@ -340,7 +340,7 @@ async fn test_optimized_performance_characteristics() {
let scan_result = scanner.scan_cycle().await;
let scan_duration = start_time.elapsed();
println!("Optimized scan completed in: {:?}", scan_duration);
println!("Optimized scan completed in: {scan_duration:?}");
assert!(scan_result.is_ok(), "Performance scan should succeed");
// Verify the scan was reasonably fast (should be faster than old concurrent scanner)
@@ -359,11 +359,11 @@ async fn test_optimized_performance_characteristics() {
let _scan_result2 = scanner.scan_cycle().await;
let scan_duration2 = start_time2.elapsed();
println!("Second optimized scan completed in: {:?}", scan_duration2);
println!("Second optimized scan completed in: {scan_duration2:?}");
// Second scan should be similar or faster due to caching
let performance_ratio = scan_duration2.as_millis() as f64 / scan_duration.as_millis() as f64;
println!("Performance ratio (second/first): {:.2}", performance_ratio);
println!("Performance ratio (second/first): {performance_ratio:.2}");
// Clean up
let _ = std::fs::remove_dir_all(std::path::Path::new(TEST_DIR_PERF));
@@ -404,7 +404,7 @@ async fn test_optimized_load_balancing_and_throttling() {
];
for (expected_level, latency, qps, error_rate, connections) in load_scenarios {
println!("Testing load scenario: {:?}", expected_level);
println!("Testing load scenario: {expected_level:?}");
// Update business metrics to simulate load
node_scanner
@@ -416,7 +416,7 @@ async fn test_optimized_load_balancing_and_throttling() {
// Get current load level
let current_level = io_monitor.get_business_load_level().await;
println!("Detected load level: {:?}", current_level);
println!("Detected load level: {current_level:?}");
// Get throttling decision
let _current_metrics = io_monitor.get_current_metrics().await;
@@ -298,7 +298,7 @@ async fn test_scanner_performance_impact() {
let throttle_stats = throttler.get_throttle_stats().await;
println!("Performance test results:");
println!(" Load level: {:?}", load_level);
println!(" Load level: {load_level:?}");
println!(" Throttle decisions: {}", throttle_stats.total_decisions);
println!(" Average delay: {:?}", throttle_stats.average_delay);
+88 -6
View File
@@ -14,10 +14,10 @@
use std::{collections::HashMap, sync::Arc};
use crate::{bucket::metadata_sys::get_replication_config, config::com::read_config, store::ECStore};
use crate::{bucket::metadata_sys::get_replication_config, config::com::read_config, store::ECStore, store_api::StorageAPI};
use rustfs_common::data_usage::{BucketTargetUsageInfo, DataUsageCache, DataUsageEntry, DataUsageInfo, SizeSummary};
use rustfs_utils::path::SLASH_SEPARATOR;
use tracing::{error, warn};
use tracing::{error, info, warn};
use crate::error::Error;
@@ -61,12 +61,13 @@ pub async fn store_data_usage_in_backend(data_usage_info: DataUsageInfo, store:
/// Load data usage info from backend storage
pub async fn load_data_usage_from_backend(store: Arc<ECStore>) -> Result<DataUsageInfo, Error> {
let buf: Vec<u8> = match read_config(store, &DATA_USAGE_OBJ_NAME_PATH).await {
let buf: Vec<u8> = match read_config(store.clone(), &DATA_USAGE_OBJ_NAME_PATH).await {
Ok(data) => data,
Err(e) => {
error!("Failed to read data usage info from backend: {}", e);
if e == crate::error::Error::ConfigNotFound {
return Ok(DataUsageInfo::default());
warn!("Data usage config not found, building basic statistics");
return build_basic_data_usage_info(store).await;
}
return Err(Error::other(e));
}
@@ -75,9 +76,22 @@ pub async fn load_data_usage_from_backend(store: Arc<ECStore>) -> Result<DataUsa
let mut data_usage_info: DataUsageInfo =
serde_json::from_slice(&buf).map_err(|e| Error::other(format!("Failed to deserialize data usage info: {e}")))?;
warn!("Loaded data usage info from backend {:?}", &data_usage_info);
info!("Loaded data usage info from backend with {} buckets", data_usage_info.buckets_count);
// Handle backward compatibility like original code
// Validate data and supplement if empty
if data_usage_info.buckets_count == 0 || data_usage_info.buckets_usage.is_empty() {
warn!("Loaded data is empty, supplementing with basic statistics");
if let Ok(basic_info) = build_basic_data_usage_info(store.clone()).await {
data_usage_info.buckets_count = basic_info.buckets_count;
data_usage_info.buckets_usage = basic_info.buckets_usage;
data_usage_info.bucket_sizes = basic_info.bucket_sizes;
data_usage_info.objects_total_count = basic_info.objects_total_count;
data_usage_info.objects_total_size = basic_info.objects_total_size;
data_usage_info.last_update = basic_info.last_update;
}
}
// Handle backward compatibility
if data_usage_info.buckets_usage.is_empty() {
data_usage_info.buckets_usage = data_usage_info
.bucket_sizes
@@ -102,6 +116,7 @@ pub async fn load_data_usage_from_backend(store: Arc<ECStore>) -> Result<DataUsa
.collect();
}
// Handle replication info
for (bucket, bui) in &data_usage_info.buckets_usage {
if bui.replicated_size_v1 > 0
|| bui.replication_failed_count_v1 > 0
@@ -129,6 +144,73 @@ pub async fn load_data_usage_from_backend(store: Arc<ECStore>) -> Result<DataUsa
Ok(data_usage_info)
}
/// Build basic data usage info with real object counts
async fn build_basic_data_usage_info(store: Arc<ECStore>) -> Result<DataUsageInfo, Error> {
let mut data_usage_info = DataUsageInfo::default();
// Get bucket list
match store.list_bucket(&crate::store_api::BucketOptions::default()).await {
Ok(buckets) => {
data_usage_info.buckets_count = buckets.len() as u64;
data_usage_info.last_update = Some(std::time::SystemTime::now());
let mut total_objects = 0u64;
let mut total_size = 0u64;
for bucket_info in buckets {
if bucket_info.name.starts_with('.') {
continue; // Skip system buckets
}
// Try to get actual object count for this bucket
let (object_count, bucket_size) = match store
.clone()
.list_objects_v2(
&bucket_info.name,
"", // prefix
None, // continuation_token
None, // delimiter
100, // max_keys - small limit for performance
false, // fetch_owner
None, // start_after
)
.await
{
Ok(result) => {
let count = result.objects.len() as u64;
let size = result.objects.iter().map(|obj| obj.size as u64).sum();
(count, size)
}
Err(_) => (0, 0),
};
total_objects += object_count;
total_size += bucket_size;
let bucket_usage = rustfs_common::data_usage::BucketUsageInfo {
size: bucket_size,
objects_count: object_count,
versions_count: object_count, // Simplified
delete_markers_count: 0,
..Default::default()
};
data_usage_info.buckets_usage.insert(bucket_info.name.clone(), bucket_usage);
data_usage_info.bucket_sizes.insert(bucket_info.name, bucket_size);
}
data_usage_info.objects_total_count = total_objects;
data_usage_info.objects_total_size = total_size;
data_usage_info.versions_total_count = total_objects;
}
Err(e) => {
warn!("Failed to list buckets for basic data usage info: {}", e);
}
}
Ok(data_usage_info)
}
/// Create a data usage cache entry from size summary
pub fn create_cache_entry_from_summary(summary: &SizeSummary) -> DataUsageEntry {
let mut entry = DataUsageEntry::default();
+16 -5
View File
@@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use rustfs_utils::{XHost, check_local_server_addr, get_host_ip, is_local_host};
use tracing::{instrument, warn};
use rustfs_utils::{XHost, check_local_server_addr, get_host_ip, get_host_ip_async, is_local_host};
use tracing::{error, instrument, warn};
use crate::{
disk::endpoint::{Endpoint, EndpointType},
@@ -241,9 +241,20 @@ impl PoolEndpointList {
}
let host = ep.url.host().unwrap();
let host_ip_set = host_ip_cache.entry(host.clone()).or_insert({
get_host_ip(host.clone()).map_err(|e| Error::other(format!("host '{host}' cannot resolve: {e}")))?
});
let host_ip_set = if let Some(set) = host_ip_cache.get(&host) {
set
} else {
let ips = match get_host_ip(host.clone()) {
Ok(ips) => ips,
Err(e) => {
error!("host {} not found, error:{}", host, e);
get_host_ip_async(host.clone())
.map_err(|e| Error::other(format!("host '{host}' cannot resolve: {e}")))?
}
};
host_ip_cache.insert(host.clone(), ips);
host_ip_cache.get(&host).unwrap()
};
let path = ep.get_file_path();
match path_ip_map.entry(path) {
+14 -3
View File
@@ -52,8 +52,14 @@ impl super::Erasure {
for _ in start_block..end_block {
let (mut shards, errs) = reader.read().await;
if errs.iter().filter(|e| e.is_none()).count() < self.data_shards {
return Err(Error::other(format!("can not reconstruct data: not enough data shards {errs:?}")));
// Check if we have enough shards to reconstruct data
// We need at least data_shards available shards (data + parity combined)
let available_shards = errs.iter().filter(|e| e.is_none()).count();
if available_shards < self.data_shards {
return Err(Error::other(format!(
"can not reconstruct data: not enough available shards (need {}, have {}) {errs:?}",
self.data_shards, available_shards
)));
}
if self.parity_shards > 0 {
@@ -65,7 +71,12 @@ impl super::Erasure {
.map(|s| Bytes::from(s.unwrap_or_default()))
.collect::<Vec<_>>();
let mut writers = MultiWriter::new(writers, self.data_shards);
// Calculate proper write quorum for heal operation
// For heal, we only write to disks that need healing, so write quorum should be
// the number of available writers (disks that need healing)
let available_writers = writers.iter().filter(|w| w.is_some()).count();
let write_quorum = available_writers.max(1); // At least 1 writer must succeed
let mut writers = MultiWriter::new(writers, write_quorum);
writers.write(shards).await?;
}
+5
View File
@@ -148,6 +148,9 @@ pub enum StorageError {
#[error("Specified part could not be found. PartNumber {0}, Expected {1}, got {2}")]
InvalidPart(usize, String, String),
#[error("Your proposed upload is smaller than the minimum allowed size. Part {0} size {1} is less than minimum {2}")]
EntityTooSmall(usize, i64, i64),
#[error("Invalid version id: {0}/{1}-{2}")]
InvalidVersionID(String, String, String),
#[error("invalid data movement operation, source and destination pool are the same for : {0}/{1}-{2}")]
@@ -408,6 +411,7 @@ impl Clone for StorageError {
// StorageError::InsufficientWriteQuorum => StorageError::InsufficientWriteQuorum,
StorageError::DecommissionNotStarted => StorageError::DecommissionNotStarted,
StorageError::InvalidPart(a, b, c) => StorageError::InvalidPart(*a, b.clone(), c.clone()),
StorageError::EntityTooSmall(a, b, c) => StorageError::EntityTooSmall(*a, *b, *c),
StorageError::DoneForNow => StorageError::DoneForNow,
StorageError::DecommissionAlreadyRunning => StorageError::DecommissionAlreadyRunning,
StorageError::ErasureReadQuorum => StorageError::ErasureReadQuorum,
@@ -486,6 +490,7 @@ impl StorageError {
StorageError::InsufficientReadQuorum(_, _) => 0x39,
StorageError::InsufficientWriteQuorum(_, _) => 0x3A,
StorageError::PreconditionFailed => 0x3B,
StorageError::EntityTooSmall(_, _, _) => 0x3C,
}
}
+191 -119
View File
@@ -110,7 +110,7 @@ pub const MAX_PARTS_COUNT: usize = 10000;
#[derive(Clone, Debug)]
pub struct SetDisks {
pub namespace_lock: Arc<rustfs_lock::NamespaceLock>,
pub fast_lock_manager: Arc<rustfs_lock::FastObjectLockManager>,
pub locker_owner: String,
pub disks: Arc<RwLock<Vec<Option<DiskStore>>>>,
pub set_endpoints: Vec<Endpoint>,
@@ -124,7 +124,7 @@ pub struct SetDisks {
impl SetDisks {
#[allow(clippy::too_many_arguments)]
pub async fn new(
namespace_lock: Arc<rustfs_lock::NamespaceLock>,
fast_lock_manager: Arc<rustfs_lock::FastObjectLockManager>,
locker_owner: String,
disks: Arc<RwLock<Vec<Option<DiskStore>>>>,
set_drive_count: usize,
@@ -135,7 +135,7 @@ impl SetDisks {
format: FormatV3,
) -> Arc<Self> {
Arc::new(SetDisks {
namespace_lock,
fast_lock_manager,
locker_owner,
disks,
set_drive_count,
@@ -2326,7 +2326,10 @@ impl SetDisks {
version_id: &str,
opts: &HealOpts,
) -> disk::error::Result<(HealResultItem, Option<DiskError>)> {
info!("SetDisks heal_object");
info!(
"SetDisks heal_object: bucket={}, object={}, version_id={}, opts={:?}",
bucket, object, version_id, opts
);
let mut result = HealResultItem {
heal_item_type: HealItemType::Object.to_string(),
bucket: bucket.to_string(),
@@ -2336,9 +2339,34 @@ impl SetDisks {
..Default::default()
};
if !opts.no_lock {
// TODO: locker
}
let _write_lock_guard = if !opts.no_lock {
info!("Acquiring write lock for object: {}, owner: {}", object, self.locker_owner);
// Check if lock is already held
let key = rustfs_lock::fast_lock::types::ObjectKey::new(bucket, object);
if let Some(lock_info) = self.fast_lock_manager.get_lock_info(&key) {
warn!("Lock already exists for object {}: {:?}", object, lock_info);
} else {
info!("No existing lock found for object {}", object);
}
let start_time = std::time::Instant::now();
let lock_result = self
.fast_lock_manager
.acquire_write_lock(bucket, object, self.locker_owner.as_str())
.await
.map_err(|e| {
let elapsed = start_time.elapsed();
error!("Failed to acquire write lock for heal operation after {:?}: {:?}", elapsed, e);
DiskError::other(format!("Failed to acquire write lock for heal operation: {:?}", e))
})?;
let elapsed = start_time.elapsed();
info!("Successfully acquired write lock for object: {} in {:?}", object, elapsed);
Some(lock_result)
} else {
info!("Skipping lock acquisition (no_lock=true)");
None
};
let version_id_op = {
if version_id.is_empty() {
@@ -2351,6 +2379,7 @@ impl SetDisks {
let disks = { self.disks.read().await.clone() };
let (mut parts_metadata, errs) = Self::read_all_fileinfo(&disks, "", bucket, object, version_id, true, true).await?;
info!("Read file info: parts_metadata.len()={}, errs={:?}", parts_metadata.len(), errs);
if DiskError::is_all_not_found(&errs) {
warn!(
"heal_object failed, all obj part not found, bucket: {}, obj: {}, version_id: {}",
@@ -2369,6 +2398,7 @@ impl SetDisks {
));
}
info!("About to call object_quorum_from_meta with parts_metadata.len()={}", parts_metadata.len());
match Self::object_quorum_from_meta(&parts_metadata, &errs, self.default_parity_count) {
Ok((read_quorum, _)) => {
result.parity_blocks = result.disk_count - read_quorum as usize;
@@ -2476,13 +2506,20 @@ impl SetDisks {
}
if disks_to_heal_count == 0 {
info!("No disks to heal, returning early");
return Ok((result, None));
}
if opts.dry_run {
info!("Dry run mode, returning early");
return Ok((result, None));
}
info!(
"Proceeding with heal: disks_to_heal_count={}, dry_run={}",
disks_to_heal_count, opts.dry_run
);
if !latest_meta.deleted && disks_to_heal_count > latest_meta.erasure.parity_blocks {
error!(
"file({} : {}) part corrupt too much, can not to fix, disks_to_heal_count: {}, parity_blocks: {}",
@@ -2608,6 +2645,11 @@ impl SetDisks {
let src_data_dir = latest_meta.data_dir.unwrap().to_string();
let dst_data_dir = latest_meta.data_dir.unwrap();
info!(
"Checking heal conditions: deleted={}, is_remote={}",
latest_meta.deleted,
latest_meta.is_remote()
);
if !latest_meta.deleted && !latest_meta.is_remote() {
let erasure_info = latest_meta.erasure;
for part in latest_meta.parts.iter() {
@@ -2660,19 +2702,30 @@ impl SetDisks {
false
}
};
// write to all disks
for disk in self.disks.read().await.iter() {
let writer = create_bitrot_writer(
is_inline_buffer,
disk.as_ref(),
RUSTFS_META_TMP_BUCKET,
&format!("{}/{}/part.{}", tmp_id, dst_data_dir, part.number),
erasure.shard_file_size(part.size as i64),
erasure.shard_size(),
HashAlgorithm::HighwayHash256,
)
.await?;
writers.push(Some(writer));
// create writers for all disk positions, but only for outdated disks
info!(
"Creating writers: latest_disks len={}, out_dated_disks len={}",
latest_disks.len(),
out_dated_disks.len()
);
for (index, disk) in latest_disks.iter().enumerate() {
if let Some(outdated_disk) = &out_dated_disks[index] {
info!("Creating writer for index {} (outdated disk)", index);
let writer = create_bitrot_writer(
is_inline_buffer,
Some(outdated_disk),
RUSTFS_META_TMP_BUCKET,
&format!("{}/{}/part.{}", tmp_id, dst_data_dir, part.number),
erasure.shard_file_size(part.size as i64),
erasure.shard_size(),
HashAlgorithm::HighwayHash256,
)
.await?;
writers.push(Some(writer));
} else {
info!("Skipping writer for index {} (not outdated)", index);
writers.push(None);
}
// if let Some(disk) = disk {
// // let filewriter = {
@@ -2775,8 +2828,8 @@ impl SetDisks {
}
}
// Rename from tmp location to the actual location.
for (index, disk) in out_dated_disks.iter().enumerate() {
if let Some(disk) = disk {
for (index, outdated_disk) in out_dated_disks.iter().enumerate() {
if let Some(disk) = outdated_disk {
// record the index of the updated disks
parts_metadata[index].erasure.index = index + 1;
// Attempt a rename now from healed data to final location.
@@ -2916,6 +2969,12 @@ impl SetDisks {
dry_run: bool,
remove: bool,
) -> Result<(HealResultItem, Option<DiskError>)> {
let _write_lock_guard = self
.fast_lock_manager
.acquire_write_lock("", object, self.locker_owner.as_str())
.await
.map_err(|e| DiskError::other(format!("Failed to acquire write lock for heal directory operation: {:?}", e)))?;
let disks = {
let disks = self.disks.read().await;
disks.clone()
@@ -3271,18 +3330,16 @@ impl ObjectIO for SetDisks {
opts: &ObjectOptions,
) -> Result<GetObjectReader> {
// Acquire a shared read-lock early to protect read consistency
// let mut _read_lock_guard: Option<rustfs_lock::LockGuard> = None;
// if !opts.no_lock {
// let guard_opt = self
// .namespace_lock
// .rlock_guard(object, &self.locker_owner, Duration::from_secs(5), Duration::from_secs(10))
// .await?;
// if guard_opt.is_none() {
// return Err(Error::other("can not get lock. please retry".to_string()));
// }
// _read_lock_guard = guard_opt;
// }
let _read_lock_guard = if !opts.no_lock {
Some(
self.fast_lock_manager
.acquire_read_lock("", object, self.locker_owner.as_str())
.await
.map_err(|_| Error::other("can not get lock. please retry".to_string()))?,
)
} else {
None
};
let (fi, files, disks) = self
.get_object_fileinfo(bucket, object, opts, true)
@@ -3361,18 +3418,16 @@ impl ObjectIO for SetDisks {
let disks = self.disks.read().await;
// Acquire per-object exclusive lock via RAII guard. It auto-releases asynchronously on drop.
// let mut _object_lock_guard: Option<rustfs_lock::LockGuard> = None;
// if !opts.no_lock {
// let guard_opt = self
// .namespace_lock
// .lock_guard(object, &self.locker_owner, Duration::from_secs(5), Duration::from_secs(10))
// .await?;
// if guard_opt.is_none() {
// return Err(Error::other("can not get lock. please retry".to_string()));
// }
// _object_lock_guard = guard_opt;
// }
let _object_lock_guard = if !opts.no_lock {
Some(
self.fast_lock_manager
.acquire_write_lock("", object, self.locker_owner.as_str())
.await
.map_err(|_| Error::other("can not get lock. please retry".to_string()))?,
)
} else {
None
};
if let Some(http_preconditions) = opts.http_preconditions.clone() {
if let Some(err) = self.check_write_precondition(bucket, object, opts).await {
@@ -3660,17 +3715,11 @@ impl StorageAPI for SetDisks {
}
// Guard lock for source object metadata update
let mut _lock_guard: Option<rustfs_lock::LockGuard> = None;
{
let guard_opt = self
.namespace_lock
.lock_guard(src_object, &self.locker_owner, Duration::from_secs(5), Duration::from_secs(10))
.await?;
if guard_opt.is_none() {
return Err(Error::other("can not get lock. please retry".to_string()));
}
_lock_guard = guard_opt;
}
let _lock_guard = self
.fast_lock_manager
.acquire_write_lock("", src_object, self.locker_owner.as_str())
.await
.map_err(|_| Error::other("can not get lock. please retry".to_string()))?;
let disks = self.get_disks_internal().await;
@@ -3766,17 +3815,11 @@ impl StorageAPI for SetDisks {
#[tracing::instrument(skip(self))]
async fn delete_object_version(&self, bucket: &str, object: &str, fi: &FileInfo, force_del_marker: bool) -> Result<()> {
// Guard lock for single object delete-version
let mut _lock_guard: Option<rustfs_lock::LockGuard> = None;
{
let guard_opt = self
.namespace_lock
.lock_guard(object, &self.locker_owner, Duration::from_secs(5), Duration::from_secs(10))
.await?;
if guard_opt.is_none() {
return Err(Error::other("can not get lock. please retry".to_string()));
}
_lock_guard = guard_opt;
}
let _lock_guard = self
.fast_lock_manager
.acquire_write_lock("", object, self.locker_owner.as_str())
.await
.map_err(|_| Error::other("can not get lock. please retry".to_string()))?;
let disks = self.get_disks(0, 0).await?;
let write_quorum = disks.len() / 2 + 1;
@@ -3833,21 +3876,31 @@ impl StorageAPI for SetDisks {
del_errs.push(None)
}
// Per-object guards to keep until function end
let mut _guards: HashMap<String, rustfs_lock::LockGuard> = HashMap::new();
// Acquire locks for all objects first; mark errors for failures
for (i, dobj) in objects.iter().enumerate() {
if !_guards.contains_key(&dobj.object_name) {
match self
.namespace_lock
.lock_guard(&dobj.object_name, &self.locker_owner, Duration::from_secs(5), Duration::from_secs(10))
.await?
{
Some(g) => {
_guards.insert(dobj.object_name.clone(), g);
}
None => {
del_errs[i] = Some(Error::other("can not get lock. please retry"));
// Use fast batch locking to acquire all locks atomically
let mut _guards: HashMap<String, rustfs_lock::FastLockGuard> = HashMap::new();
let mut unique_objects: std::collections::HashSet<String> = std::collections::HashSet::new();
// Collect unique object names
for dobj in &objects {
unique_objects.insert(dobj.object_name.clone());
}
// Acquire all locks in batch to prevent deadlocks
for object_name in unique_objects {
match self
.fast_lock_manager
.acquire_write_lock("", object_name.as_str(), self.locker_owner.as_str())
.await
{
Ok(guard) => {
_guards.insert(object_name, guard);
}
Err(_) => {
// Mark all operations on this object as failed
for (i, dobj) in objects.iter().enumerate() {
if dobj.object_name == object_name {
del_errs[i] = Some(Error::other("can not get lock. please retry"));
}
}
}
}
@@ -3967,17 +4020,16 @@ impl StorageAPI for SetDisks {
#[tracing::instrument(skip(self))]
async fn delete_object(&self, bucket: &str, object: &str, opts: ObjectOptions) -> Result<ObjectInfo> {
// Guard lock for single object delete
let mut _lock_guard: Option<rustfs_lock::LockGuard> = None;
if !opts.delete_prefix {
let guard_opt = self
.namespace_lock
.lock_guard(object, &self.locker_owner, Duration::from_secs(5), Duration::from_secs(10))
.await?;
if guard_opt.is_none() {
return Err(Error::other("can not get lock. please retry".to_string()));
}
_lock_guard = guard_opt;
}
let _lock_guard = if !opts.delete_prefix {
Some(
self.fast_lock_manager
.acquire_write_lock("", object, self.locker_owner.as_str())
.await
.map_err(|_| Error::other("can not get lock. please retry".to_string()))?,
)
} else {
None
};
if opts.delete_prefix {
self.delete_prefix(bucket, object)
.await
@@ -4156,17 +4208,16 @@ impl StorageAPI for SetDisks {
#[tracing::instrument(skip(self))]
async fn get_object_info(&self, bucket: &str, object: &str, opts: &ObjectOptions) -> Result<ObjectInfo> {
// Acquire a shared read-lock to protect consistency during info fetch
// let mut _read_lock_guard: Option<rustfs_lock::LockGuard> = None;
// if !opts.no_lock {
// let guard_opt = self
// .namespace_lock
// .rlock_guard(object, &self.locker_owner, Duration::from_secs(5), Duration::from_secs(10))
// .await?;
// if guard_opt.is_none() {
// return Err(Error::other("can not get lock. please retry".to_string()));
// }
// _read_lock_guard = guard_opt;
// }
let _read_lock_guard = if !opts.no_lock {
Some(
self.fast_lock_manager
.acquire_read_lock("", object, self.locker_owner.as_str())
.await
.map_err(|_| Error::other("can not get lock. please retry".to_string()))?,
)
} else {
None
};
let (fi, _, _) = self
.get_object_fileinfo(bucket, object, opts, false)
@@ -4199,17 +4250,16 @@ impl StorageAPI for SetDisks {
// TODO: nslock
// Guard lock for metadata update
// let mut _lock_guard: Option<rustfs_lock::LockGuard> = None;
// if !opts.no_lock {
// let guard_opt = self
// .namespace_lock
// .lock_guard(object, &self.locker_owner, Duration::from_secs(5), Duration::from_secs(10))
// .await?;
// if guard_opt.is_none() {
// return Err(Error::other("can not get lock. please retry".to_string()));
// }
// _lock_guard = guard_opt;
// }
let _lock_guard = if !opts.no_lock {
Some(
self.fast_lock_manager
.acquire_write_lock("", object, self.locker_owner.as_str())
.await
.map_err(|_| Error::other("can not get lock. please retry".to_string()))?,
)
} else {
None
};
let disks = self.get_disks_internal().await;
@@ -5268,10 +5318,16 @@ impl StorageAPI for SetDisks {
if (i < uploaded_parts.len() - 1) && !is_min_allowed_part_size(ext_part.actual_size) {
error!(
"complete_multipart_upload is_min_allowed_part_size err {:?}, part_id={}, bucket={}, object={}",
ext_part.actual_size, p.part_num, bucket, object
"complete_multipart_upload part size too small: part {} size {} is less than minimum {}",
p.part_num,
ext_part.actual_size,
GLOBAL_MIN_PART_SIZE.as_u64()
);
return Err(Error::InvalidPart(p.part_num, ext_part.etag.clone(), p.etag.clone().unwrap_or_default()));
return Err(Error::EntityTooSmall(
p.part_num,
ext_part.actual_size,
GLOBAL_MIN_PART_SIZE.as_u64() as i64,
));
}
object_size += ext_part.size;
@@ -5461,6 +5517,17 @@ impl StorageAPI for SetDisks {
version_id: &str,
opts: &HealOpts,
) -> Result<(HealResultItem, Option<Error>)> {
let _write_lock_guard = if !opts.no_lock {
Some(
self.fast_lock_manager
.acquire_write_lock("", object, self.locker_owner.as_str())
.await
.map_err(|e| Error::other(format!("Failed to acquire write lock for heal operation: {:?}", e)))?,
)
} else {
None
};
if has_suffix(object, SLASH_SEPARATOR) {
let (result, err) = self.heal_object_dir(bucket, object, opts.dry_run, opts.remove).await?;
return Ok((result, err.map(|e| e.into())));
@@ -5678,6 +5745,11 @@ async fn disks_with_all_parts(
object: &str,
scan_mode: HealScanMode,
) -> disk::error::Result<(Vec<Option<DiskStore>>, HashMap<usize, Vec<usize>>, HashMap<usize, Vec<usize>>)> {
info!(
"disks_with_all_parts: starting with online_disks.len()={}, scan_mode={:?}",
online_disks.len(),
scan_mode
);
let mut available_disks = vec![None; online_disks.len()];
let mut data_errs_by_disk: HashMap<usize, Vec<usize>> = HashMap::new();
for i in 0..online_disks.len() {
+6 -9
View File
@@ -163,18 +163,15 @@ impl Sets {
}
}
let lock_clients = create_unique_clients(&set_endpoints).await?;
let _lock_clients = create_unique_clients(&set_endpoints).await?;
// Bind lock quorum to EC write quorum for this set: data_shards (+1 if equal to parity) per default_write_quorum()
let mut write_quorum = set_drive_count - parity_count;
if write_quorum == parity_count {
write_quorum += 1;
}
let namespace_lock =
rustfs_lock::NamespaceLock::with_clients_and_quorum(format!("set-{i}"), lock_clients, write_quorum);
// Note: write_quorum was used for the old lock system, no longer needed with FastLock
let _write_quorum = set_drive_count - parity_count;
// Create fast lock manager for high performance
let fast_lock_manager = Arc::new(rustfs_lock::FastObjectLockManager::new());
let set_disks = SetDisks::new(
Arc::new(namespace_lock),
fast_lock_manager,
GLOBAL_Local_Node_Name.read().await.to_string(),
Arc::new(RwLock::new(set_drive)),
set_drive_count,
+5
View File
@@ -42,3 +42,8 @@ url.workspace = true
uuid.workspace = true
thiserror.workspace = true
once_cell.workspace = true
parking_lot = "0.12"
smallvec = "1.11"
smartstring = "1.0"
crossbeam-queue = "0.3"
heapless = "0.8"
@@ -0,0 +1,43 @@
// 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 environment variable control of lock system
use rustfs_lock::{LockManager, get_global_lock_manager};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let manager = get_global_lock_manager();
println!("Lock system status: {}", if manager.is_disabled() { "DISABLED" } else { "ENABLED" });
match std::env::var("RUSTFS_ENABLE_LOCKS") {
Ok(value) => println!("RUSTFS_ENABLE_LOCKS set to: {}", value),
Err(_) => println!("RUSTFS_ENABLE_LOCKS not set (defaults to enabled)"),
}
// Test acquiring a lock
let result = manager.acquire_read_lock("test-bucket", "test-object", "test-owner").await;
match result {
Ok(guard) => {
println!("Lock acquired successfully! Disabled: {}", guard.is_disabled());
}
Err(e) => {
println!("Failed to acquire lock: {:?}", e);
}
}
println!("Environment control example completed");
Ok(())
}
+122 -105
View File
@@ -12,30 +12,35 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use crate::{
GlobalLockManager,
client::LockClient,
error::Result,
local::LocalLockMap,
fast_lock::{FastLockGuard, LockManager},
types::{LockId, LockInfo, LockMetadata, LockPriority, LockRequest, LockResponse, LockStats, LockType},
};
/// Local lock client
///
/// Uses global singleton LocalLockMap to ensure all clients access the same lock instance
/// Local lock client using FastLock
#[derive(Debug, Clone)]
pub struct LocalClient;
pub struct LocalClient {
guard_storage: Arc<RwLock<HashMap<LockId, FastLockGuard>>>,
}
impl LocalClient {
/// Create new local client
pub fn new() -> Self {
Self
Self {
guard_storage: Arc::new(RwLock::new(HashMap::new())),
}
}
/// Get global lock map instance
pub fn get_lock_map(&self) -> Arc<LocalLockMap> {
crate::get_global_lock_map()
/// Get the global lock manager
pub fn get_lock_manager(&self) -> Arc<GlobalLockManager> {
crate::get_global_lock_manager()
}
}
@@ -48,71 +53,102 @@ impl Default for LocalClient {
#[async_trait::async_trait]
impl LockClient for LocalClient {
async fn acquire_exclusive(&self, request: &LockRequest) -> Result<LockResponse> {
let lock_map = self.get_lock_map();
let success = lock_map
.lock_with_ttl_id(request)
.await
.map_err(|e| crate::error::LockError::internal(format!("Lock acquisition failed: {e}")))?;
if success {
let lock_info = LockInfo {
id: crate::types::LockId::new_deterministic(&request.resource),
resource: request.resource.clone(),
lock_type: LockType::Exclusive,
status: crate::types::LockStatus::Acquired,
owner: request.owner.clone(),
acquired_at: std::time::SystemTime::now(),
expires_at: std::time::SystemTime::now() + request.ttl,
last_refreshed: std::time::SystemTime::now(),
metadata: request.metadata.clone(),
priority: request.priority,
wait_start_time: None,
};
Ok(LockResponse::success(lock_info, std::time::Duration::ZERO))
} else {
Ok(LockResponse::failure("Lock acquisition failed".to_string(), std::time::Duration::ZERO))
let lock_manager = self.get_lock_manager();
let lock_request = crate::fast_lock::ObjectLockRequest::new_write("", request.resource.clone(), request.owner.clone())
.with_acquire_timeout(request.acquire_timeout);
match lock_manager.acquire_lock(lock_request).await {
Ok(guard) => {
let lock_id = crate::types::LockId::new_deterministic(&request.resource);
// Store guard for later release
let mut guards = self.guard_storage.write().await;
guards.insert(lock_id.clone(), guard);
let lock_info = LockInfo {
id: lock_id,
resource: request.resource.clone(),
lock_type: LockType::Exclusive,
status: crate::types::LockStatus::Acquired,
owner: request.owner.clone(),
acquired_at: std::time::SystemTime::now(),
expires_at: std::time::SystemTime::now() + request.ttl,
last_refreshed: std::time::SystemTime::now(),
metadata: request.metadata.clone(),
priority: request.priority,
wait_start_time: None,
};
Ok(LockResponse::success(lock_info, std::time::Duration::ZERO))
}
Err(crate::fast_lock::LockResult::Timeout) => {
Ok(LockResponse::failure("Lock acquisition timeout", request.acquire_timeout))
}
Err(crate::fast_lock::LockResult::Conflict {
current_owner,
current_mode,
}) => Ok(LockResponse::failure(
format!("Lock conflict: resource held by {} in {:?} mode", current_owner, current_mode),
std::time::Duration::ZERO,
)),
Err(crate::fast_lock::LockResult::Acquired) => {
unreachable!("Acquired should not be an error")
}
}
}
async fn acquire_shared(&self, request: &LockRequest) -> Result<LockResponse> {
let lock_map = self.get_lock_map();
let success = lock_map
.rlock_with_ttl_id(request)
.await
.map_err(|e| crate::error::LockError::internal(format!("Shared lock acquisition failed: {e}")))?;
if success {
let lock_info = LockInfo {
id: crate::types::LockId::new_deterministic(&request.resource),
resource: request.resource.clone(),
lock_type: LockType::Shared,
status: crate::types::LockStatus::Acquired,
owner: request.owner.clone(),
acquired_at: std::time::SystemTime::now(),
expires_at: std::time::SystemTime::now() + request.ttl,
last_refreshed: std::time::SystemTime::now(),
metadata: request.metadata.clone(),
priority: request.priority,
wait_start_time: None,
};
Ok(LockResponse::success(lock_info, std::time::Duration::ZERO))
} else {
Ok(LockResponse::failure("Lock acquisition failed".to_string(), std::time::Duration::ZERO))
let lock_manager = self.get_lock_manager();
let lock_request = crate::fast_lock::ObjectLockRequest::new_read("", request.resource.clone(), request.owner.clone())
.with_acquire_timeout(request.acquire_timeout);
match lock_manager.acquire_lock(lock_request).await {
Ok(guard) => {
let lock_id = crate::types::LockId::new_deterministic(&request.resource);
// Store guard for later release
let mut guards = self.guard_storage.write().await;
guards.insert(lock_id.clone(), guard);
let lock_info = LockInfo {
id: lock_id,
resource: request.resource.clone(),
lock_type: LockType::Shared,
status: crate::types::LockStatus::Acquired,
owner: request.owner.clone(),
acquired_at: std::time::SystemTime::now(),
expires_at: std::time::SystemTime::now() + request.ttl,
last_refreshed: std::time::SystemTime::now(),
metadata: request.metadata.clone(),
priority: request.priority,
wait_start_time: None,
};
Ok(LockResponse::success(lock_info, std::time::Duration::ZERO))
}
Err(crate::fast_lock::LockResult::Timeout) => {
Ok(LockResponse::failure("Lock acquisition timeout", request.acquire_timeout))
}
Err(crate::fast_lock::LockResult::Conflict {
current_owner,
current_mode,
}) => Ok(LockResponse::failure(
format!("Lock conflict: resource held by {} in {:?} mode", current_owner, current_mode),
std::time::Duration::ZERO,
)),
Err(crate::fast_lock::LockResult::Acquired) => {
unreachable!("Acquired should not be an error")
}
}
}
async fn release(&self, lock_id: &LockId) -> Result<bool> {
let lock_map = self.get_lock_map();
// Try to release the lock directly by ID
match lock_map.unlock_by_id(lock_id).await {
Ok(()) => Ok(true),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
// Try as read lock if exclusive unlock failed
match lock_map.runlock_by_id(lock_id).await {
Ok(()) => Ok(true),
Err(_) => Err(crate::error::LockError::internal("Lock ID not found".to_string())),
}
}
Err(e) => Err(crate::error::LockError::internal(format!("Release lock failed: {e}"))),
let mut guards = self.guard_storage.write().await;
if let Some(guard) = guards.remove(lock_id) {
// Guard automatically releases the lock when dropped
drop(guard);
Ok(true)
} else {
// Lock not found or already released
Ok(false)
}
}
@@ -126,45 +162,26 @@ impl LockClient for LocalClient {
}
async fn check_status(&self, lock_id: &LockId) -> Result<Option<LockInfo>> {
let lock_map = self.get_lock_map();
// Check if the lock exists in our locks map
let locks_guard = lock_map.locks.read().await;
if let Some(entry) = locks_guard.get(lock_id) {
let entry_guard = entry.read().await;
// Determine lock type and owner based on the entry
if let Some(owner) = &entry_guard.writer {
Ok(Some(LockInfo {
id: lock_id.clone(),
resource: lock_id.resource.clone(),
lock_type: crate::types::LockType::Exclusive,
status: crate::types::LockStatus::Acquired,
owner: owner.clone(),
acquired_at: std::time::SystemTime::now(),
expires_at: std::time::SystemTime::now() + std::time::Duration::from_secs(30),
last_refreshed: std::time::SystemTime::now(),
metadata: LockMetadata::default(),
priority: LockPriority::Normal,
wait_start_time: None,
}))
} else if !entry_guard.readers.is_empty() {
Ok(Some(LockInfo {
id: lock_id.clone(),
resource: lock_id.resource.clone(),
lock_type: crate::types::LockType::Shared,
status: crate::types::LockStatus::Acquired,
owner: entry_guard.readers.iter().next().map(|(k, _)| k.clone()).unwrap_or_default(),
acquired_at: std::time::SystemTime::now(),
expires_at: std::time::SystemTime::now() + std::time::Duration::from_secs(30),
last_refreshed: std::time::SystemTime::now(),
metadata: LockMetadata::default(),
priority: LockPriority::Normal,
wait_start_time: None,
}))
} else {
Ok(None)
}
let guards = self.guard_storage.read().await;
if let Some(guard) = guards.get(lock_id) {
// We have an active guard for this lock
let lock_type = match guard.mode() {
crate::fast_lock::types::LockMode::Shared => crate::types::LockType::Shared,
crate::fast_lock::types::LockMode::Exclusive => crate::types::LockType::Exclusive,
};
Ok(Some(LockInfo {
id: lock_id.clone(),
resource: lock_id.resource.clone(),
lock_type,
status: crate::types::LockStatus::Acquired,
owner: guard.owner().to_string(),
acquired_at: std::time::SystemTime::now(),
expires_at: std::time::SystemTime::now() + std::time::Duration::from_secs(30),
last_refreshed: std::time::SystemTime::now(),
metadata: LockMetadata::default(),
priority: LockPriority::Normal,
wait_start_time: None,
}))
} else {
Ok(None)
}
+325
View File
@@ -0,0 +1,325 @@
// 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.
// Benchmarks comparing fast lock vs old lock performance
#[cfg(test)]
#[allow(dead_code)] // Temporarily disable benchmark tests
mod benchmarks {
use super::super::*;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::task;
/// Benchmark single-threaded lock operations
#[tokio::test]
async fn bench_single_threaded_fast_locks() {
let manager = Arc::new(FastObjectLockManager::new());
let iterations = 10000;
// Warm up
for i in 0..100 {
let _guard = manager
.acquire_write_lock("bucket", &format!("warm_{}", i), "owner")
.await
.unwrap();
}
// Benchmark write locks
let start = Instant::now();
for i in 0..iterations {
let _guard = manager
.acquire_write_lock("bucket", &format!("object_{}", i), "owner")
.await
.unwrap();
}
let duration = start.elapsed();
println!("Fast locks: {} write locks in {:?}", iterations, duration);
println!("Average: {:?} per lock", duration / iterations);
let metrics = manager.get_metrics();
println!("Fast path rate: {:.2}%", metrics.shard_metrics.fast_path_rate() * 100.0);
// Should be much faster than old implementation
assert!(duration.as_millis() < 1000, "Should complete 10k locks in <1s");
assert!(metrics.shard_metrics.fast_path_rate() > 0.95, "Should have >95% fast path rate");
}
/// Benchmark concurrent lock operations
#[tokio::test]
async fn bench_concurrent_fast_locks() {
let manager = Arc::new(FastObjectLockManager::new());
let concurrent_tasks = 100;
let iterations_per_task = 100;
let start = Instant::now();
let mut handles = Vec::new();
for task_id in 0..concurrent_tasks {
let manager_clone = manager.clone();
let handle = task::spawn(async move {
for i in 0..iterations_per_task {
let object_name = format!("obj_{}_{}", task_id, i);
let _guard = manager_clone
.acquire_write_lock("bucket", &object_name, &format!("owner_{}", task_id))
.await
.unwrap();
// Simulate some work
tokio::task::yield_now().await;
}
});
handles.push(handle);
}
// Wait for all tasks
for handle in handles {
handle.await.unwrap();
}
let duration = start.elapsed();
let total_ops = concurrent_tasks * iterations_per_task;
println!("Concurrent fast locks: {} operations across {} tasks in {:?}",
total_ops, concurrent_tasks, duration);
println!("Throughput: {:.2} ops/sec", total_ops as f64 / duration.as_secs_f64());
let metrics = manager.get_metrics();
println!("Fast path rate: {:.2}%", metrics.shard_metrics.fast_path_rate() * 100.0);
println!("Contention events: {}", metrics.shard_metrics.contention_events);
// Should maintain high throughput even with concurrency
assert!(duration.as_millis() < 5000, "Should complete concurrent ops in <5s");
}
/// Benchmark contended lock operations
#[tokio::test]
async fn bench_contended_locks() {
let manager = Arc::new(FastObjectLockManager::new());
let concurrent_tasks = 50;
let shared_objects = 10; // High contention on few objects
let iterations_per_task = 50;
let start = Instant::now();
let mut handles = Vec::new();
for task_id in 0..concurrent_tasks {
let manager_clone = manager.clone();
let handle = task::spawn(async move {
for i in 0..iterations_per_task {
let object_name = format!("shared_{}", i % shared_objects);
// Mix of read and write operations
if i % 3 == 0 {
// Write operation
if let Ok(_guard) = manager_clone
.acquire_write_lock("bucket", &object_name, &format!("owner_{}", task_id))
.await
{
tokio::task::yield_now().await;
}
} else {
// Read operation
if let Ok(_guard) = manager_clone
.acquire_read_lock("bucket", &object_name, &format!("owner_{}", task_id))
.await
{
tokio::task::yield_now().await;
}
}
}
});
handles.push(handle);
}
// Wait for all tasks
for handle in handles {
handle.await.unwrap();
}
let duration = start.elapsed();
println!("Contended locks: {} tasks on {} objects in {:?}",
concurrent_tasks, shared_objects, duration);
let metrics = manager.get_metrics();
println!("Total acquisitions: {}", metrics.shard_metrics.total_acquisitions());
println!("Fast path rate: {:.2}%", metrics.shard_metrics.fast_path_rate() * 100.0);
println!("Average wait time: {:?}", metrics.shard_metrics.avg_wait_time());
println!("Timeout rate: {:.2}%", metrics.shard_metrics.timeout_rate() * 100.0);
// Even with contention, should maintain reasonable performance
assert!(metrics.shard_metrics.timeout_rate() < 0.1, "Should have <10% timeout rate");
assert!(metrics.shard_metrics.avg_wait_time() < Duration::from_millis(100), "Avg wait should be <100ms");
}
/// Benchmark batch operations
#[tokio::test]
async fn bench_batch_operations() {
let manager = FastObjectLockManager::new();
let batch_sizes = vec![10, 50, 100, 500];
for batch_size in batch_sizes {
// Create batch request
let mut batch = BatchLockRequest::new("batch_owner");
for i in 0..batch_size {
batch = batch.add_write_lock("bucket", &format!("batch_obj_{}", i));
}
let start = Instant::now();
let result = manager.acquire_locks_batch(batch).await;
let duration = start.elapsed();
assert!(result.all_acquired, "Batch should succeed");
println!("Batch size {}: {:?} ({:.2} μs per lock)",
batch_size,
duration,
duration.as_micros() as f64 / batch_size as f64);
// Batch should be much faster than individual acquisitions
assert!(duration.as_millis() < batch_size as u128 / 10,
"Batch should be 10x+ faster than individual locks");
}
}
/// Benchmark version-specific locks
#[tokio::test]
async fn bench_versioned_locks() {
let manager = Arc::new(FastObjectLockManager::new());
let objects = 100;
let versions_per_object = 10;
let start = Instant::now();
let mut handles = Vec::new();
for obj_id in 0..objects {
let manager_clone = manager.clone();
let handle = task::spawn(async move {
for version in 0..versions_per_object {
let _guard = manager_clone
.acquire_write_lock_versioned(
"bucket",
&format!("obj_{}", obj_id),
&format!("v{}", version),
"version_owner"
)
.await
.unwrap();
}
});
handles.push(handle);
}
for handle in handles {
handle.await.unwrap();
}
let duration = start.elapsed();
let total_ops = objects * versions_per_object;
println!("Versioned locks: {} version locks in {:?}", total_ops, duration);
println!("Throughput: {:.2} locks/sec", total_ops as f64 / duration.as_secs_f64());
let metrics = manager.get_metrics();
println!("Fast path rate: {:.2}%", metrics.shard_metrics.fast_path_rate() * 100.0);
// Versioned locks should not interfere with each other
assert!(metrics.shard_metrics.fast_path_rate() > 0.9, "Should maintain high fast path rate");
}
/// Compare with theoretical maximum performance
#[tokio::test]
async fn bench_theoretical_maximum() {
let manager = Arc::new(FastObjectLockManager::new());
let iterations = 100000;
// Measure pure fast path performance (no contention)
let start = Instant::now();
for i in 0..iterations {
let _guard = manager
.acquire_write_lock("bucket", &format!("unique_{}", i), "owner")
.await
.unwrap();
}
let duration = start.elapsed();
println!("Theoretical maximum: {} unique locks in {:?}", iterations, duration);
println!("Rate: {:.2} locks/sec", iterations as f64 / duration.as_secs_f64());
println!("Latency: {:?} per lock", duration / iterations);
let metrics = manager.get_metrics();
println!("Fast path rate: {:.2}%", metrics.shard_metrics.fast_path_rate() * 100.0);
// Should achieve very high performance with no contention
assert!(metrics.shard_metrics.fast_path_rate() > 0.99, "Should be nearly 100% fast path");
assert!(duration.as_secs_f64() / (iterations as f64) < 0.0001, "Should be <100μs per lock");
}
/// Performance regression test
#[tokio::test]
async fn performance_regression_test() {
let manager = Arc::new(FastObjectLockManager::new());
// This test ensures we maintain performance targets
let test_cases = vec![
("single_thread", 1, 10000),
("low_contention", 10, 1000),
("high_contention", 100, 100),
];
for (test_name, threads, ops_per_thread) in test_cases {
let start = Instant::now();
let mut handles = Vec::new();
for thread_id in 0..threads {
let manager_clone = manager.clone();
let handle = task::spawn(async move {
for op_id in 0..ops_per_thread {
let object = if threads == 1 {
format!("obj_{}_{}", thread_id, op_id)
} else {
format!("obj_{}", op_id % 100) // Create contention
};
let owner = format!("owner_{}", thread_id);
let _guard = manager_clone
.acquire_write_lock("bucket", object, owner)
.await
.unwrap();
}
});
handles.push(handle);
}
for handle in handles {
handle.await.unwrap();
}
let duration = start.elapsed();
let total_ops = threads * ops_per_thread;
let ops_per_sec = total_ops as f64 / duration.as_secs_f64();
println!("{}: {:.2} ops/sec", test_name, ops_per_sec);
// Performance targets (adjust based on requirements)
match test_name {
"single_thread" => assert!(ops_per_sec > 50000.0, "Single thread should exceed 50k ops/sec"),
"low_contention" => assert!(ops_per_sec > 20000.0, "Low contention should exceed 20k ops/sec"),
"high_contention" => assert!(ops_per_sec > 5000.0, "High contention should exceed 5k ops/sec"),
_ => {}
}
}
}
}
@@ -0,0 +1,291 @@
// 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.
//! Disabled lock manager that bypasses all locking operations
//! Used when RUSTFS_ENABLE_LOCKS environment variable is set to false
use std::sync::Arc;
use crate::fast_lock::{
guard::FastLockGuard,
manager_trait::LockManager,
metrics::AggregatedMetrics,
types::{BatchLockRequest, BatchLockResult, LockConfig, LockResult, ObjectKey, ObjectLockInfo, ObjectLockRequest},
};
/// Disabled lock manager that always returns success without actual locking
///
/// This manager is used when locks are disabled via environment variables.
/// All lock operations immediately return success, effectively bypassing
/// the locking mechanism entirely.
#[derive(Debug)]
pub struct DisabledLockManager {
_config: LockConfig,
}
impl DisabledLockManager {
/// Create new disabled lock manager
pub fn new() -> Self {
Self::with_config(LockConfig::default())
}
/// Create new disabled lock manager with custom config
pub fn with_config(config: LockConfig) -> Self {
Self { _config: config }
}
/// Always succeeds - returns a no-op guard
pub async fn acquire_lock(&self, request: ObjectLockRequest) -> Result<FastLockGuard, LockResult> {
Ok(FastLockGuard::new_disabled(request.key, request.mode, request.owner))
}
/// Always succeeds - returns a no-op guard
pub async fn acquire_read_lock(
&self,
bucket: impl Into<Arc<str>>,
object: impl Into<Arc<str>>,
owner: impl Into<Arc<str>>,
) -> Result<FastLockGuard, LockResult> {
let request = ObjectLockRequest::new_read(bucket, object, owner);
self.acquire_lock(request).await
}
/// Always succeeds - returns a no-op guard
pub async fn acquire_read_lock_versioned(
&self,
bucket: impl Into<Arc<str>>,
object: impl Into<Arc<str>>,
version: impl Into<Arc<str>>,
owner: impl Into<Arc<str>>,
) -> Result<FastLockGuard, LockResult> {
let request = ObjectLockRequest::new_read(bucket, object, owner).with_version(version);
self.acquire_lock(request).await
}
/// Always succeeds - returns a no-op guard
pub async fn acquire_write_lock(
&self,
bucket: impl Into<Arc<str>>,
object: impl Into<Arc<str>>,
owner: impl Into<Arc<str>>,
) -> Result<FastLockGuard, LockResult> {
let request = ObjectLockRequest::new_write(bucket, object, owner);
self.acquire_lock(request).await
}
/// Always succeeds - returns a no-op guard
pub async fn acquire_write_lock_versioned(
&self,
bucket: impl Into<Arc<str>>,
object: impl Into<Arc<str>>,
version: impl Into<Arc<str>>,
owner: impl Into<Arc<str>>,
) -> Result<FastLockGuard, LockResult> {
let request = ObjectLockRequest::new_write(bucket, object, owner).with_version(version);
self.acquire_lock(request).await
}
/// Always succeeds - all locks acquired
pub async fn acquire_locks_batch(&self, batch_request: BatchLockRequest) -> BatchLockResult {
let successful_locks: Vec<ObjectKey> = batch_request.requests.into_iter().map(|req| req.key).collect();
BatchLockResult {
successful_locks,
failed_locks: Vec::new(),
all_acquired: true,
}
}
/// Always returns None - no locks to query
pub fn get_lock_info(&self, _key: &ObjectKey) -> Option<ObjectLockInfo> {
None
}
/// Returns empty metrics
pub fn get_metrics(&self) -> AggregatedMetrics {
AggregatedMetrics::empty()
}
/// Always returns 0 - no locks exist
pub fn total_lock_count(&self) -> usize {
0
}
/// Returns empty pool stats
pub fn get_pool_stats(&self) -> Vec<(u64, u64, u64, usize)> {
Vec::new()
}
/// No-op cleanup - nothing to clean
pub async fn cleanup_expired(&self) -> usize {
0
}
/// No-op cleanup - nothing to clean
pub async fn cleanup_expired_traditional(&self) -> usize {
0
}
/// No-op shutdown
pub async fn shutdown(&self) {
// Nothing to shutdown
}
}
impl Default for DisabledLockManager {
fn default() -> Self {
Self::new()
}
}
#[async_trait::async_trait]
impl LockManager for DisabledLockManager {
async fn acquire_lock(&self, request: ObjectLockRequest) -> Result<FastLockGuard, LockResult> {
self.acquire_lock(request).await
}
async fn acquire_read_lock(
&self,
bucket: impl Into<Arc<str>> + Send,
object: impl Into<Arc<str>> + Send,
owner: impl Into<Arc<str>> + Send,
) -> Result<FastLockGuard, LockResult> {
self.acquire_read_lock(bucket, object, owner).await
}
async fn acquire_read_lock_versioned(
&self,
bucket: impl Into<Arc<str>> + Send,
object: impl Into<Arc<str>> + Send,
version: impl Into<Arc<str>> + Send,
owner: impl Into<Arc<str>> + Send,
) -> Result<FastLockGuard, LockResult> {
self.acquire_read_lock_versioned(bucket, object, version, owner).await
}
async fn acquire_write_lock(
&self,
bucket: impl Into<Arc<str>> + Send,
object: impl Into<Arc<str>> + Send,
owner: impl Into<Arc<str>> + Send,
) -> Result<FastLockGuard, LockResult> {
self.acquire_write_lock(bucket, object, owner).await
}
async fn acquire_write_lock_versioned(
&self,
bucket: impl Into<Arc<str>> + Send,
object: impl Into<Arc<str>> + Send,
version: impl Into<Arc<str>> + Send,
owner: impl Into<Arc<str>> + Send,
) -> Result<FastLockGuard, LockResult> {
self.acquire_write_lock_versioned(bucket, object, version, owner).await
}
async fn acquire_locks_batch(&self, batch_request: BatchLockRequest) -> BatchLockResult {
self.acquire_locks_batch(batch_request).await
}
fn get_lock_info(&self, key: &ObjectKey) -> Option<ObjectLockInfo> {
self.get_lock_info(key)
}
fn get_metrics(&self) -> AggregatedMetrics {
self.get_metrics()
}
fn total_lock_count(&self) -> usize {
self.total_lock_count()
}
fn get_pool_stats(&self) -> Vec<(u64, u64, u64, usize)> {
self.get_pool_stats()
}
async fn cleanup_expired(&self) -> usize {
self.cleanup_expired().await
}
async fn cleanup_expired_traditional(&self) -> usize {
self.cleanup_expired_traditional().await
}
async fn shutdown(&self) {
self.shutdown().await
}
fn is_disabled(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_disabled_manager_basic_operations() {
let manager = DisabledLockManager::new();
// All operations should succeed immediately
let read_guard = manager
.acquire_read_lock("bucket", "object", "owner1")
.await
.expect("Disabled manager should always succeed");
let write_guard = manager
.acquire_write_lock("bucket", "object", "owner2")
.await
.expect("Disabled manager should always succeed");
// Guards should indicate they are disabled
assert!(read_guard.is_disabled());
assert!(write_guard.is_disabled());
}
#[tokio::test]
async fn test_disabled_manager_batch_operations() {
let manager = DisabledLockManager::new();
let batch = BatchLockRequest::new("owner")
.add_read_lock("bucket", "obj1")
.add_write_lock("bucket", "obj2")
.with_all_or_nothing(true);
let result = manager.acquire_locks_batch(batch).await;
assert!(result.all_acquired);
assert_eq!(result.successful_locks.len(), 2);
assert!(result.failed_locks.is_empty());
}
#[tokio::test]
async fn test_disabled_manager_metrics() {
let manager = DisabledLockManager::new();
// Metrics should indicate empty/disabled state
let metrics = manager.get_metrics();
assert!(metrics.is_empty());
assert_eq!(manager.total_lock_count(), 0);
assert!(manager.get_pool_stats().is_empty());
}
#[tokio::test]
async fn test_disabled_manager_cleanup() {
let manager = DisabledLockManager::new();
// Cleanup should be no-op
assert_eq!(manager.cleanup_expired().await, 0);
assert_eq!(manager.cleanup_expired_traditional().await, 0);
}
}
+512
View File
@@ -0,0 +1,512 @@
// 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.
use crate::fast_lock::{
shard::LockShard,
types::{LockMode, ObjectKey},
};
use std::sync::Arc;
/// RAII guard for fast object locks
///
/// Automatically releases the lock when dropped, ensuring no lock leakage
/// even in panic scenarios.
pub struct FastLockGuard {
key: ObjectKey,
mode: LockMode,
owner: Arc<str>,
shard: Option<Arc<LockShard>>, // None when locks are disabled
released: bool,
disabled: bool, // True when locks are disabled globally
}
impl FastLockGuard {
pub(crate) fn new(key: ObjectKey, mode: LockMode, owner: Arc<str>, shard: Arc<LockShard>) -> Self {
Self {
key,
mode,
owner,
shard: Some(shard),
released: false,
disabled: false,
}
}
/// Create a disabled guard (when locks are globally disabled)
pub(crate) fn new_disabled(key: ObjectKey, mode: LockMode, owner: Arc<str>) -> Self {
Self {
key,
mode,
owner,
shard: None,
released: false,
disabled: true,
}
}
/// Get the object key this guard protects
pub fn key(&self) -> &ObjectKey {
&self.key
}
/// Get the lock mode (Shared or Exclusive)
pub fn mode(&self) -> LockMode {
self.mode
}
/// Get the lock owner
pub fn owner(&self) -> &Arc<str> {
&self.owner
}
/// Manually release the lock early
///
/// Returns true if the lock was successfully released, false if it was
/// already released or the release failed.
pub fn release(&mut self) -> bool {
if self.released {
return false;
}
if self.disabled {
// For disabled locks, always succeed
self.released = true;
return true;
}
if let Some(shard) = &self.shard {
let success = shard.release_lock(&self.key, &self.owner, self.mode);
if success {
self.released = true;
}
success
} else {
// Should not happen, but handle gracefully
self.released = true;
false
}
}
/// Check if the lock has been released
pub fn is_released(&self) -> bool {
self.released
}
/// Check if this guard represents a disabled lock
pub fn is_disabled(&self) -> bool {
self.disabled
}
/// Get lock information for monitoring
pub fn lock_info(&self) -> Option<crate::fast_lock::types::ObjectLockInfo> {
if self.released || self.disabled {
None
} else if let Some(shard) = &self.shard {
shard.get_lock_info(&self.key)
} else {
None
}
}
}
impl Drop for FastLockGuard {
fn drop(&mut self) {
if !self.released && !self.disabled {
if let Some(shard) = &self.shard {
let success = shard.release_lock(&self.key, &self.owner, self.mode);
if !success {
tracing::warn!(
"Failed to release lock during drop: key={}, owner={}, mode={:?}",
self.key,
self.owner,
self.mode
);
}
}
}
}
}
impl std::fmt::Debug for FastLockGuard {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FastLockGuard")
.field("key", &self.key)
.field("mode", &self.mode)
.field("owner", &self.owner)
.field("released", &self.released)
.field("disabled", &self.disabled)
.finish()
}
}
/// Multiple lock guards that can be released atomically
///
/// Useful for batch operations where you want to ensure all locks
/// are held until a critical section is complete.
#[derive(Debug)]
pub struct MultipleLockGuards {
guards: Vec<FastLockGuard>,
}
impl MultipleLockGuards {
/// Create new multiple guards container
pub fn new() -> Self {
Self { guards: Vec::new() }
}
/// Add a guard to the collection
pub fn add(&mut self, guard: FastLockGuard) {
self.guards.push(guard);
}
/// Get number of guards
pub fn len(&self) -> usize {
self.guards.len()
}
/// Check if empty
pub fn is_empty(&self) -> bool {
self.guards.is_empty()
}
/// Get iterator over guards
pub fn iter(&self) -> std::slice::Iter<'_, FastLockGuard> {
self.guards.iter()
}
/// Get mutable iterator over guards
pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, FastLockGuard> {
self.guards.iter_mut()
}
/// Release all locks manually
///
/// Returns the number of locks successfully released.
pub fn release_all(&mut self) -> usize {
let mut released_count = 0;
for guard in &mut self.guards {
if guard.release() {
released_count += 1;
}
}
released_count
}
/// Check how many locks are still held
pub fn active_count(&self) -> usize {
self.guards.iter().filter(|guard| !guard.is_released()).count()
}
/// Get all object keys
pub fn keys(&self) -> Vec<&ObjectKey> {
self.guards.iter().map(|guard| guard.key()).collect()
}
/// Split guards by lock mode (consumes the original guards)
pub fn split_by_mode(mut self) -> (Vec<FastLockGuard>, Vec<FastLockGuard>) {
let mut shared_guards = Vec::new();
let mut exclusive_guards = Vec::new();
for guard in self.guards.drain(..) {
match guard.mode() {
LockMode::Shared => shared_guards.push(guard),
LockMode::Exclusive => exclusive_guards.push(guard),
}
}
(shared_guards, exclusive_guards)
}
/// Split guards by lock mode without consuming (returns references)
pub fn split_by_mode_ref(&self) -> (Vec<&FastLockGuard>, Vec<&FastLockGuard>) {
let mut shared_guards = Vec::new();
let mut exclusive_guards = Vec::new();
for guard in &self.guards {
match guard.mode() {
LockMode::Shared => shared_guards.push(guard),
LockMode::Exclusive => exclusive_guards.push(guard),
}
}
(shared_guards, exclusive_guards)
}
/// Merge multiple guard collections into this one
pub fn merge(&mut self, mut other: MultipleLockGuards) {
self.guards.append(&mut other.guards);
}
/// Merge multiple individual guards into this collection
pub fn merge_guards(&mut self, guards: Vec<FastLockGuard>) {
self.guards.extend(guards);
}
/// Filter guards by predicate (non-consuming)
pub fn filter<F>(&self, predicate: F) -> Vec<&FastLockGuard>
where
F: Fn(&FastLockGuard) -> bool,
{
self.guards.iter().filter(|guard| predicate(guard)).collect()
}
/// Filter guards by predicate (consuming)
pub fn filter_owned<F>(self, predicate: F) -> Vec<FastLockGuard>
where
F: Fn(&FastLockGuard) -> bool,
{
// Use a safe approach that avoids Drop interaction issues
self.into_iter().filter(|guard| predicate(guard)).collect()
}
/// Get guards for specific bucket
pub fn guards_for_bucket(&self, bucket: &str) -> Vec<&FastLockGuard> {
self.filter(|guard| guard.key().bucket.as_ref() == bucket)
}
/// Get guards for specific owner
pub fn guards_for_owner(&self, owner: &str) -> Vec<&FastLockGuard> {
self.filter(|guard| guard.owner().as_ref() == owner)
}
}
impl Default for MultipleLockGuards {
fn default() -> Self {
Self::new()
}
}
impl From<Vec<FastLockGuard>> for MultipleLockGuards {
fn from(guards: Vec<FastLockGuard>) -> Self {
Self { guards }
}
}
impl From<FastLockGuard> for MultipleLockGuards {
fn from(guard: FastLockGuard) -> Self {
Self { guards: vec![guard] }
}
}
impl IntoIterator for MultipleLockGuards {
type Item = FastLockGuard;
type IntoIter = std::vec::IntoIter<FastLockGuard>;
fn into_iter(mut self) -> Self::IntoIter {
// Use mem::replace to avoid Drop interaction issues
// This approach is safer than mem::take as it prevents the Drop from seeing empty state
let guards = std::mem::take(&mut self.guards);
std::mem::forget(self); // Prevent Drop from running on emptied state
guards.into_iter()
}
}
impl<'a> IntoIterator for &'a MultipleLockGuards {
type Item = &'a FastLockGuard;
type IntoIter = std::slice::Iter<'a, FastLockGuard>;
fn into_iter(self) -> Self::IntoIter {
self.guards.iter()
}
}
impl<'a> IntoIterator for &'a mut MultipleLockGuards {
type Item = &'a mut FastLockGuard;
type IntoIter = std::slice::IterMut<'a, FastLockGuard>;
fn into_iter(self) -> Self::IntoIter {
self.guards.iter_mut()
}
}
impl Drop for MultipleLockGuards {
fn drop(&mut self) {
// Guards will be dropped individually, each releasing their lock
let active_count = self.active_count();
if active_count > 0 {
tracing::debug!("Dropping MultipleLockGuards with {} active locks", active_count);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fast_lock::{manager::FastObjectLockManager, types::ObjectKey};
#[tokio::test]
async fn test_guard_basic_operations() {
let manager = FastObjectLockManager::new();
let mut guard = manager
.acquire_write_lock("bucket", "object", "owner")
.await
.expect("Failed to acquire lock");
assert!(!guard.is_released());
assert_eq!(guard.mode(), LockMode::Exclusive);
assert_eq!(guard.key().bucket.as_ref(), "bucket");
assert_eq!(guard.key().object.as_ref(), "object");
// Manual release
assert!(guard.release());
assert!(guard.is_released());
// Second release should fail
assert!(!guard.release());
}
#[tokio::test]
async fn test_guard_auto_release() {
let manager = FastObjectLockManager::new();
let key = ObjectKey::new("bucket", "object");
// Acquire lock in a scope
{
let _guard = manager
.acquire_write_lock("bucket", "object", "owner")
.await
.expect("Failed to acquire lock");
// Lock should be held here
assert!(manager.get_lock_info(&key).is_some());
} // Guard dropped here, lock should be released
// Give a moment for cleanup
tokio::task::yield_now().await;
// Should be able to acquire the lock again immediately
let _guard2 = manager
.acquire_write_lock("bucket", "object", "owner2")
.await
.expect("Failed to re-acquire lock after auto-release");
}
#[tokio::test]
async fn test_multiple_guards() {
let manager = FastObjectLockManager::new();
let mut multiple = MultipleLockGuards::new();
// Acquire multiple locks
let guard1 = manager.acquire_read_lock("bucket", "obj1", "owner").await.unwrap();
let guard2 = manager.acquire_read_lock("bucket", "obj2", "owner").await.unwrap();
let guard3 = manager.acquire_write_lock("bucket", "obj3", "owner").await.unwrap();
multiple.add(guard1);
multiple.add(guard2);
multiple.add(guard3);
assert_eq!(multiple.len(), 3);
assert_eq!(multiple.active_count(), 3);
// Test split by mode without consuming
let (shared_refs, exclusive_refs) = multiple.split_by_mode_ref();
assert_eq!(shared_refs.len(), 2);
assert_eq!(exclusive_refs.len(), 1);
// Original should still have all guards
assert_eq!(multiple.len(), 3);
// Split by mode (consuming)
let (shared, exclusive) = multiple.split_by_mode();
assert_eq!(shared.len(), 2);
assert_eq!(exclusive.len(), 1);
// Test merge functionality
let mut new_multiple = MultipleLockGuards::new();
new_multiple.merge_guards(shared);
new_multiple.merge_guards(exclusive);
assert_eq!(new_multiple.len(), 3);
}
#[tokio::test]
async fn test_guard_iteration_improvements() {
let manager = FastObjectLockManager::new();
let mut multiple = MultipleLockGuards::new();
// Acquire locks for different buckets and owners
let guard1 = manager.acquire_read_lock("bucket1", "obj1", "owner1").await.unwrap();
let guard2 = manager.acquire_read_lock("bucket2", "obj2", "owner1").await.unwrap();
let guard3 = manager.acquire_write_lock("bucket1", "obj3", "owner2").await.unwrap();
multiple.add(guard1);
multiple.add(guard2);
multiple.add(guard3);
// Test filtering by bucket
let bucket1_guards = multiple.guards_for_bucket("bucket1");
assert_eq!(bucket1_guards.len(), 2);
// Test filtering by owner
let owner1_guards = multiple.guards_for_owner("owner1");
assert_eq!(owner1_guards.len(), 2);
// Test custom filter
let write_guards = multiple.filter(|guard| guard.mode() == LockMode::Exclusive);
assert_eq!(write_guards.len(), 1);
// Test that original is not consumed
assert_eq!(multiple.len(), 3);
}
#[tokio::test]
async fn test_into_iter_safety() {
let manager = FastObjectLockManager::new();
let mut multiple = MultipleLockGuards::new();
// Acquire some locks
let guard1 = manager.acquire_read_lock("bucket", "obj1", "owner").await.unwrap();
let guard2 = manager.acquire_read_lock("bucket", "obj2", "owner").await.unwrap();
multiple.add(guard1);
multiple.add(guard2);
assert_eq!(multiple.len(), 2);
// Test into_iter consumption
let guards: Vec<_> = multiple.into_iter().collect();
assert_eq!(guards.len(), 2);
// multiple is consumed here, so we can't access it anymore
// This ensures Drop is handled correctly without double-drop issues
}
#[tokio::test]
async fn test_guard_panic_safety() {
let manager = Arc::new(FastObjectLockManager::new());
let _key = ObjectKey::new("bucket", "object");
// Test that locks are released even if task panics
let manager_clone = manager.clone();
let handle = tokio::spawn(async move {
let _guard = manager_clone
.acquire_write_lock("bucket", "object", "owner")
.await
.expect("Failed to acquire lock");
// Simulate panic
panic!("Simulated panic");
});
// Wait for panic
let _ = handle.await;
// Should be able to acquire lock again
let _guard = manager
.acquire_write_lock("bucket", "object", "owner2")
.await
.expect("Failed to acquire lock after panic");
}
}
@@ -0,0 +1,255 @@
// 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 integration of FastObjectLockManager in set_disk.rs
// This shows how to replace the current slow lock system
use crate::fast_lock::{BatchLockRequest, FastObjectLockManager, ObjectLockRequest};
use std::sync::Arc;
use std::time::Duration;
/// Example integration into SetDisks structure
pub struct SetDisksWithFastLock {
/// Replace the old namespace_lock with fast lock manager
pub fast_lock_manager: Arc<FastObjectLockManager>,
pub locker_owner: String,
// ... other fields remain the same
}
impl SetDisksWithFastLock {
/// Example: Replace get_object_reader with fast locking
pub async fn get_object_reader_fast(
&self,
bucket: &str,
object: &str,
version: Option<&str>,
// ... other parameters
) -> Result<(), Box<dyn std::error::Error>> {
// Fast path: Try to acquire read lock immediately
let _read_guard = if let Some(v) = version {
// Version-specific lock
self.fast_lock_manager
.acquire_read_lock_versioned(bucket, object, v, self.locker_owner.as_str())
.await
.map_err(|_| "Lock acquisition failed")?
} else {
// Latest version lock
self.fast_lock_manager
.acquire_read_lock(bucket, object, self.locker_owner.as_str())
.await
.map_err(|_| "Lock acquisition failed")?
};
// Critical section: Read object
// The lock is automatically released when _read_guard goes out of scope
// ... actual read operation logic
Ok(())
}
/// Example: Replace put_object with fast locking
pub async fn put_object_fast(
&self,
bucket: &str,
object: &str,
version: Option<&str>,
// ... other parameters
) -> Result<(), Box<dyn std::error::Error>> {
// Acquire exclusive write lock with timeout
let request = ObjectLockRequest::new_write(bucket, object, self.locker_owner.as_str())
.with_acquire_timeout(Duration::from_secs(5))
.with_lock_timeout(Duration::from_secs(30));
let request = if let Some(v) = version {
request.with_version(v)
} else {
request
};
let _write_guard = self
.fast_lock_manager
.acquire_lock(request)
.await
.map_err(|_| "Lock acquisition failed")?;
// Critical section: Write object
// ... actual write operation logic
Ok(())
// Lock automatically released when _write_guard drops
}
/// Example: Replace delete_objects with batch fast locking
pub async fn delete_objects_fast(
&self,
bucket: &str,
objects: Vec<(&str, Option<&str>)>, // (object_name, version)
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
// Create batch request for atomic locking
let mut batch = BatchLockRequest::new(self.locker_owner.as_str()).with_all_or_nothing(true); // Either lock all or fail
// Add all objects to batch (sorted internally to prevent deadlocks)
for (object, version) in &objects {
let mut request = ObjectLockRequest::new_write(bucket, *object, self.locker_owner.as_str());
if let Some(v) = version {
request = request.with_version(*v);
}
batch.requests.push(request);
}
// Acquire all locks atomically
let batch_result = self.fast_lock_manager.acquire_locks_batch(batch).await;
if !batch_result.all_acquired {
return Err("Failed to acquire all locks for batch delete".into());
}
// Critical section: Delete all objects
let mut deleted = Vec::new();
for (object, _version) in objects {
// ... actual delete operation logic
deleted.push(object.to_string());
}
// All locks automatically released when guards go out of scope
Ok(deleted)
}
/// Example: Health check integration
pub fn get_lock_health(&self) -> crate::fast_lock::metrics::AggregatedMetrics {
self.fast_lock_manager.get_metrics()
}
/// Example: Cleanup integration
pub async fn cleanup_expired_locks(&self) -> usize {
self.fast_lock_manager.cleanup_expired().await
}
}
/// Performance comparison demonstration
pub mod performance_comparison {
use super::*;
use std::time::Instant;
pub async fn benchmark_fast_vs_old() {
let fast_manager = Arc::new(FastObjectLockManager::new());
let owner = "benchmark_owner";
// Benchmark fast lock acquisition
let start = Instant::now();
let mut guards = Vec::new();
for i in 0..1000 {
let guard = fast_manager
.acquire_write_lock("bucket", format!("object_{}", i), owner)
.await
.expect("Failed to acquire fast lock");
guards.push(guard);
}
let fast_duration = start.elapsed();
println!("Fast lock: 1000 acquisitions in {:?}", fast_duration);
// Release all
drop(guards);
// Compare with metrics
let metrics = fast_manager.get_metrics();
println!("Fast path rate: {:.2}%", metrics.shard_metrics.fast_path_rate() * 100.0);
println!("Average wait time: {:?}", metrics.shard_metrics.avg_wait_time());
println!("Total operations/sec: {:.2}", metrics.ops_per_second());
}
}
/// Migration guide from old to new system
pub mod migration_guide {
/*
Step-by-step migration from old lock system:
1. Replace namespace_lock field:
OLD: pub namespace_lock: Arc<rustfs_lock::NamespaceLock>
NEW: pub fast_lock_manager: Arc<FastObjectLockManager>
2. Replace lock acquisition:
OLD: self.namespace_lock.lock_guard(object, &self.locker_owner, timeout, ttl).await?
NEW: self.fast_lock_manager.acquire_write_lock(bucket, object, &self.locker_owner).await?
3. Replace read lock acquisition:
OLD: self.namespace_lock.rlock_guard(object, &self.locker_owner, timeout, ttl).await?
NEW: self.fast_lock_manager.acquire_read_lock(bucket, object, &self.locker_owner).await?
4. Add version support where needed:
NEW: self.fast_lock_manager.acquire_write_lock_versioned(bucket, object, version, owner).await?
5. Replace batch operations:
OLD: Multiple individual lock_guard calls in loop
NEW: Single BatchLockRequest with all objects
6. Remove manual lock release (RAII handles it automatically)
OLD: guard.disarm() or explicit release
NEW: Just let guard go out of scope
Expected performance improvements:
- 10-50x faster lock acquisition
- 90%+ fast path success rate
- Sub-millisecond lock operations
- No deadlock issues with batch operations
- Automatic cleanup and monitoring
*/
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_integration_example() {
let fast_manager = Arc::new(FastObjectLockManager::new());
let set_disks = SetDisksWithFastLock {
fast_lock_manager: fast_manager,
locker_owner: "test_owner".to_string(),
};
// Test read operation
assert!(set_disks.get_object_reader_fast("bucket", "object", None).await.is_ok());
// Test write operation
assert!(set_disks.put_object_fast("bucket", "object", Some("v1")).await.is_ok());
// Test batch delete
let objects = vec![("obj1", None), ("obj2", Some("v1"))];
let result = set_disks.delete_objects_fast("bucket", objects).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_version_locking() {
let fast_manager = Arc::new(FastObjectLockManager::new());
// Should be able to lock different versions simultaneously
let guard_v1 = fast_manager
.acquire_write_lock_versioned("bucket", "object", "v1", "owner1")
.await
.expect("Failed to lock v1");
let guard_v2 = fast_manager
.acquire_write_lock_versioned("bucket", "object", "v2", "owner2")
.await
.expect("Failed to lock v2");
// Both locks should coexist
assert!(!guard_v1.is_released());
assert!(!guard_v2.is_released());
}
}
@@ -0,0 +1,169 @@
// 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.
//! Integration tests for performance optimizations
#[cfg(test)]
mod tests {
use crate::fast_lock::FastObjectLockManager;
use tokio::time::Duration;
#[tokio::test]
async fn test_object_pool_integration() {
let manager = FastObjectLockManager::new();
// Create many locks to test pool efficiency
let mut guards = Vec::new();
for i in 0..100 {
let bucket = format!("test-bucket-{}", i % 10); // Reuse some bucket names
let object = format!("test-object-{}", i);
let guard = manager
.acquire_write_lock(bucket.as_str(), object.as_str(), "test-owner")
.await
.expect("Failed to acquire lock");
guards.push(guard);
}
// Drop all guards to return objects to pool
drop(guards);
// Wait a moment for cleanup
tokio::time::sleep(Duration::from_millis(100)).await;
// Get pool statistics from all shards
let pool_stats = manager.get_pool_stats();
let (hits, misses, releases, pool_size) = pool_stats.iter().fold((0, 0, 0, 0), |acc, stats| {
(acc.0 + stats.0, acc.1 + stats.1, acc.2 + stats.2, acc.3 + stats.3)
});
let hit_rate = if hits + misses > 0 {
hits as f64 / (hits + misses) as f64
} else {
0.0
};
println!(
"Pool stats - Hits: {}, Misses: {}, Releases: {}, Pool size: {}",
hits, misses, releases, pool_size
);
println!("Hit rate: {:.2}%", hit_rate * 100.0);
// We should see some pool activity
assert!(hits + misses > 0, "Pool should have been used");
}
#[tokio::test]
async fn test_optimized_notification_system() {
let manager = FastObjectLockManager::new();
// Test that notifications work by measuring timing
let start = std::time::Instant::now();
// Acquire two read locks on different objects (should be fast)
let guard1 = manager
.acquire_read_lock("bucket", "object1", "reader1")
.await
.expect("Failed to acquire first read lock");
let guard2 = manager
.acquire_read_lock("bucket", "object2", "reader2")
.await
.expect("Failed to acquire second read lock");
let duration = start.elapsed();
println!("Two read locks on different objects took: {:?}", duration);
// Should be very fast since no contention
assert!(duration < Duration::from_millis(10), "Read locks should be fast with no contention");
drop(guard1);
drop(guard2);
// Test same object contention
let start = std::time::Instant::now();
let guard1 = manager
.acquire_read_lock("bucket", "same-object", "reader1")
.await
.expect("Failed to acquire first read lock on same object");
let guard2 = manager
.acquire_read_lock("bucket", "same-object", "reader2")
.await
.expect("Failed to acquire second read lock on same object");
let duration = start.elapsed();
println!("Two read locks on same object took: {:?}", duration);
// Should still be fast since read locks are compatible
assert!(duration < Duration::from_millis(10), "Compatible read locks should be fast");
drop(guard1);
drop(guard2);
}
#[tokio::test]
async fn test_fast_path_optimization() {
let manager = FastObjectLockManager::new();
// First acquisition should be fast path
let start = std::time::Instant::now();
let guard1 = manager
.acquire_read_lock("bucket", "object", "reader1")
.await
.expect("Failed to acquire first read lock");
let first_duration = start.elapsed();
// Second read lock should also be fast path
let start = std::time::Instant::now();
let guard2 = manager
.acquire_read_lock("bucket", "object", "reader2")
.await
.expect("Failed to acquire second read lock");
let second_duration = start.elapsed();
println!("First lock: {:?}, Second lock: {:?}", first_duration, second_duration);
// Both should be very fast (sub-millisecond typically)
assert!(first_duration < Duration::from_millis(10));
assert!(second_duration < Duration::from_millis(10));
drop(guard1);
drop(guard2);
}
#[tokio::test]
async fn test_batch_operations_optimization() {
let manager = FastObjectLockManager::new();
// Test batch operation with sorted keys
let batch = crate::fast_lock::BatchLockRequest::new("batch-owner")
.add_read_lock("bucket", "obj1")
.add_read_lock("bucket", "obj2")
.add_write_lock("bucket", "obj3")
.with_all_or_nothing(false);
let start = std::time::Instant::now();
let result = manager.acquire_locks_batch(batch).await;
let duration = start.elapsed();
println!("Batch operation took: {:?}", duration);
assert!(result.all_acquired, "All locks should be acquired");
assert_eq!(result.successful_locks.len(), 3);
assert!(result.failed_locks.is_empty());
// Batch should be reasonably fast
assert!(duration < Duration::from_millis(100));
}
}
+587
View File
@@ -0,0 +1,587 @@
// 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.
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio::time::{Instant, interval};
use crate::fast_lock::{
guard::FastLockGuard,
manager_trait::LockManager,
metrics::{AggregatedMetrics, GlobalMetrics},
shard::LockShard,
types::{BatchLockRequest, BatchLockResult, LockConfig, LockResult, ObjectKey, ObjectLockInfo, ObjectLockRequest},
};
/// High-performance object lock manager
#[derive(Debug)]
pub struct FastObjectLockManager {
shards: Vec<Arc<LockShard>>,
shard_mask: usize,
config: LockConfig,
metrics: Arc<GlobalMetrics>,
cleanup_handle: RwLock<Option<tokio::task::JoinHandle<()>>>,
}
impl FastObjectLockManager {
/// Create new lock manager with default config
pub fn new() -> Self {
Self::with_config(LockConfig::default())
}
/// Create new lock manager with custom config
pub fn with_config(config: LockConfig) -> Self {
let shard_count = config.shard_count;
assert!(shard_count.is_power_of_two(), "Shard count must be power of 2");
let shards: Vec<Arc<LockShard>> = (0..shard_count).map(|i| Arc::new(LockShard::new(i))).collect();
let metrics = Arc::new(GlobalMetrics::new(shard_count));
let manager = Self {
shards,
shard_mask: shard_count - 1,
config,
metrics,
cleanup_handle: RwLock::new(None),
};
// Start background cleanup task
manager.start_cleanup_task();
manager
}
/// Acquire object lock
pub async fn acquire_lock(&self, request: ObjectLockRequest) -> Result<FastLockGuard, LockResult> {
let shard = self.get_shard(&request.key);
match shard.acquire_lock(&request).await {
Ok(()) => Ok(FastLockGuard::new(request.key, request.mode, request.owner, shard.clone())),
Err(err) => Err(err),
}
}
/// Acquire shared (read) lock
pub async fn acquire_read_lock(
&self,
bucket: impl Into<Arc<str>>,
object: impl Into<Arc<str>>,
owner: impl Into<Arc<str>>,
) -> Result<FastLockGuard, LockResult> {
let request = ObjectLockRequest::new_read(bucket, object, owner);
self.acquire_lock(request).await
}
/// Acquire shared (read) lock for specific version
pub async fn acquire_read_lock_versioned(
&self,
bucket: impl Into<Arc<str>>,
object: impl Into<Arc<str>>,
version: impl Into<Arc<str>>,
owner: impl Into<Arc<str>>,
) -> Result<FastLockGuard, LockResult> {
let request = ObjectLockRequest::new_read(bucket, object, owner).with_version(version);
self.acquire_lock(request).await
}
/// Acquire exclusive (write) lock
pub async fn acquire_write_lock(
&self,
bucket: impl Into<Arc<str>>,
object: impl Into<Arc<str>>,
owner: impl Into<Arc<str>>,
) -> Result<FastLockGuard, LockResult> {
let request = ObjectLockRequest::new_write(bucket, object, owner);
self.acquire_lock(request).await
}
/// Acquire exclusive (write) lock for specific version
pub async fn acquire_write_lock_versioned(
&self,
bucket: impl Into<Arc<str>>,
object: impl Into<Arc<str>>,
version: impl Into<Arc<str>>,
owner: impl Into<Arc<str>>,
) -> Result<FastLockGuard, LockResult> {
let request = ObjectLockRequest::new_write(bucket, object, owner).with_version(version);
self.acquire_lock(request).await
}
/// Acquire multiple locks atomically - optimized version
pub async fn acquire_locks_batch(&self, batch_request: BatchLockRequest) -> BatchLockResult {
// Pre-sort requests by (shard_id, key) to avoid deadlocks
let mut sorted_requests = batch_request.requests;
sorted_requests.sort_unstable_by(|a, b| {
let shard_a = a.key.shard_index(self.shard_mask);
let shard_b = b.key.shard_index(self.shard_mask);
shard_a.cmp(&shard_b).then_with(|| a.key.cmp(&b.key))
});
// Try to use stack-allocated vectors for small batches, fallback to heap if needed
let shard_groups = self.group_requests_by_shard(sorted_requests);
// Choose strategy based on request type
if batch_request.all_or_nothing {
self.acquire_locks_two_phase_commit(&shard_groups).await
} else {
self.acquire_locks_best_effort(&shard_groups).await
}
}
/// Group requests by shard with proper fallback handling
fn group_requests_by_shard(
&self,
requests: Vec<ObjectLockRequest>,
) -> std::collections::HashMap<usize, Vec<ObjectLockRequest>> {
let mut shard_groups = std::collections::HashMap::new();
for request in requests {
let shard_id = request.key.shard_index(self.shard_mask);
shard_groups.entry(shard_id).or_insert_with(Vec::new).push(request);
}
shard_groups
}
/// Best effort acquisition (allows partial success)
async fn acquire_locks_best_effort(
&self,
shard_groups: &std::collections::HashMap<usize, Vec<ObjectLockRequest>>,
) -> BatchLockResult {
let mut all_successful = Vec::new();
let mut all_failed = Vec::new();
for (&shard_id, requests) in shard_groups {
let shard = &self.shards[shard_id];
// Try fast path first for each request
for request in requests {
if shard.try_fast_path_only(request) {
all_successful.push(request.key.clone());
} else {
// Fallback to slow path
match shard.acquire_lock(request).await {
Ok(()) => all_successful.push(request.key.clone()),
Err(err) => all_failed.push((request.key.clone(), err)),
}
}
}
}
let all_acquired = all_failed.is_empty();
BatchLockResult {
successful_locks: all_successful,
failed_locks: all_failed,
all_acquired,
}
}
/// Two-phase commit for atomic acquisition
async fn acquire_locks_two_phase_commit(
&self,
shard_groups: &std::collections::HashMap<usize, Vec<ObjectLockRequest>>,
) -> BatchLockResult {
// Phase 1: Try to acquire all locks
let mut acquired_locks = Vec::new();
let mut failed_locks = Vec::new();
'outer: for (&shard_id, requests) in shard_groups {
let shard = &self.shards[shard_id];
for request in requests {
match shard.acquire_lock(request).await {
Ok(()) => {
acquired_locks.push((request.key.clone(), request.mode, request.owner.clone()));
}
Err(err) => {
failed_locks.push((request.key.clone(), err));
break 'outer; // Stop on first failure
}
}
}
}
// Phase 2: If any failed, release all acquired locks with error tracking
if !failed_locks.is_empty() {
let mut cleanup_failures = 0;
for (key, mode, owner) in acquired_locks {
let shard = self.get_shard(&key);
if !shard.release_lock(&key, &owner, mode) {
cleanup_failures += 1;
tracing::warn!(
"Failed to release lock during batch cleanup: bucket={}, object={}",
key.bucket,
key.object
);
}
}
if cleanup_failures > 0 {
tracing::error!("Batch lock cleanup had {} failures", cleanup_failures);
}
return BatchLockResult {
successful_locks: Vec::new(),
failed_locks,
all_acquired: false,
};
}
// All successful
BatchLockResult {
successful_locks: acquired_locks.into_iter().map(|(key, _, _)| key).collect(),
failed_locks: Vec::new(),
all_acquired: true,
}
}
/// Get lock information for monitoring
pub fn get_lock_info(&self, key: &crate::fast_lock::types::ObjectKey) -> Option<crate::fast_lock::types::ObjectLockInfo> {
let shard = self.get_shard(key);
shard.get_lock_info(key)
}
/// Get aggregated metrics
pub fn get_metrics(&self) -> crate::fast_lock::metrics::AggregatedMetrics {
let shard_metrics: Vec<_> = self.shards.iter().map(|shard| shard.metrics().snapshot()).collect();
self.metrics.aggregate_shard_metrics(&shard_metrics)
}
/// Get total number of active locks across all shards
pub fn total_lock_count(&self) -> usize {
self.shards.iter().map(|shard| shard.lock_count()).sum()
}
/// Get pool statistics from all shards
pub fn get_pool_stats(&self) -> Vec<(u64, u64, u64, usize)> {
self.shards.iter().map(|shard| shard.pool_stats()).collect()
}
/// Force cleanup of expired locks using adaptive strategy
pub async fn cleanup_expired(&self) -> usize {
let mut total_cleaned = 0;
for shard in &self.shards {
total_cleaned += shard.adaptive_cleanup();
}
self.metrics.record_cleanup_run(total_cleaned);
total_cleaned
}
/// Force cleanup with traditional strategy (for compatibility)
pub async fn cleanup_expired_traditional(&self) -> usize {
let max_idle_millis = self.config.max_idle_time.as_millis() as u64;
let mut total_cleaned = 0;
for shard in &self.shards {
total_cleaned += shard.cleanup_expired_millis(max_idle_millis);
}
self.metrics.record_cleanup_run(total_cleaned);
total_cleaned
}
/// Shutdown the lock manager and cleanup resources
pub async fn shutdown(&self) {
if let Some(handle) = self.cleanup_handle.write().await.take() {
handle.abort();
}
// Final cleanup
self.cleanup_expired().await;
}
/// Get shard for object key
fn get_shard(&self, key: &crate::fast_lock::types::ObjectKey) -> &Arc<LockShard> {
let index = key.shard_index(self.shard_mask);
&self.shards[index]
}
/// Start background cleanup task
fn start_cleanup_task(&self) {
let shards = self.shards.clone();
let metrics = self.metrics.clone();
let cleanup_interval = self.config.cleanup_interval;
let _max_idle_time = self.config.max_idle_time;
let handle = tokio::spawn(async move {
let mut interval = interval(cleanup_interval);
loop {
interval.tick().await;
let start = Instant::now();
let mut total_cleaned = 0;
// Use adaptive cleanup for better performance
for shard in &shards {
total_cleaned += shard.adaptive_cleanup();
}
if total_cleaned > 0 {
metrics.record_cleanup_run(total_cleaned);
tracing::debug!("Cleanup completed: {} objects cleaned in {:?}", total_cleaned, start.elapsed());
}
}
});
// Store handle for shutdown
if let Ok(mut cleanup_handle) = self.cleanup_handle.try_write() {
*cleanup_handle = Some(handle);
}
}
}
impl Default for FastObjectLockManager {
fn default() -> Self {
Self::new()
}
}
// Implement Drop to ensure cleanup
impl Drop for FastObjectLockManager {
fn drop(&mut self) {
// Note: We can't use async in Drop, so we just abort the cleanup task
if let Ok(handle_guard) = self.cleanup_handle.try_read() {
if let Some(handle) = handle_guard.as_ref() {
handle.abort();
}
}
}
}
#[async_trait::async_trait]
impl LockManager for FastObjectLockManager {
async fn acquire_lock(&self, request: ObjectLockRequest) -> Result<FastLockGuard, LockResult> {
self.acquire_lock(request).await
}
async fn acquire_read_lock(
&self,
bucket: impl Into<Arc<str>> + Send,
object: impl Into<Arc<str>> + Send,
owner: impl Into<Arc<str>> + Send,
) -> Result<FastLockGuard, LockResult> {
self.acquire_read_lock(bucket, object, owner).await
}
async fn acquire_read_lock_versioned(
&self,
bucket: impl Into<Arc<str>> + Send,
object: impl Into<Arc<str>> + Send,
version: impl Into<Arc<str>> + Send,
owner: impl Into<Arc<str>> + Send,
) -> Result<FastLockGuard, LockResult> {
self.acquire_read_lock_versioned(bucket, object, version, owner).await
}
async fn acquire_write_lock(
&self,
bucket: impl Into<Arc<str>> + Send,
object: impl Into<Arc<str>> + Send,
owner: impl Into<Arc<str>> + Send,
) -> Result<FastLockGuard, LockResult> {
self.acquire_write_lock(bucket, object, owner).await
}
async fn acquire_write_lock_versioned(
&self,
bucket: impl Into<Arc<str>> + Send,
object: impl Into<Arc<str>> + Send,
version: impl Into<Arc<str>> + Send,
owner: impl Into<Arc<str>> + Send,
) -> Result<FastLockGuard, LockResult> {
self.acquire_write_lock_versioned(bucket, object, version, owner).await
}
async fn acquire_locks_batch(&self, batch_request: BatchLockRequest) -> BatchLockResult {
self.acquire_locks_batch(batch_request).await
}
fn get_lock_info(&self, key: &ObjectKey) -> Option<ObjectLockInfo> {
self.get_lock_info(key)
}
fn get_metrics(&self) -> AggregatedMetrics {
self.get_metrics()
}
fn total_lock_count(&self) -> usize {
self.total_lock_count()
}
fn get_pool_stats(&self) -> Vec<(u64, u64, u64, usize)> {
self.get_pool_stats()
}
async fn cleanup_expired(&self) -> usize {
self.cleanup_expired().await
}
async fn cleanup_expired_traditional(&self) -> usize {
self.cleanup_expired_traditional().await
}
async fn shutdown(&self) {
self.shutdown().await
}
fn is_disabled(&self) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::time::Duration;
#[tokio::test]
async fn test_manager_basic_operations() {
let manager = FastObjectLockManager::new();
// Test read lock
let read_guard = manager
.acquire_read_lock("bucket", "object", "owner1")
.await
.expect("Failed to acquire read lock");
// Should be able to acquire another read lock
let read_guard2 = manager
.acquire_read_lock("bucket", "object", "owner2")
.await
.expect("Failed to acquire second read lock");
drop(read_guard);
drop(read_guard2);
// Test write lock
let write_guard = manager
.acquire_write_lock("bucket", "object", "owner1")
.await
.expect("Failed to acquire write lock");
drop(write_guard);
}
#[tokio::test]
async fn test_manager_contention() {
let manager = Arc::new(FastObjectLockManager::new());
// Acquire write lock
let write_guard = manager
.acquire_write_lock("bucket", "object", "owner1")
.await
.expect("Failed to acquire write lock");
// Try to acquire read lock (should timeout)
let manager_clone = manager.clone();
let read_result =
tokio::time::timeout(Duration::from_millis(100), manager_clone.acquire_read_lock("bucket", "object", "owner2")).await;
assert!(read_result.is_err()); // Should timeout
drop(write_guard);
// Now read lock should succeed
let read_guard = manager
.acquire_read_lock("bucket", "object", "owner2")
.await
.expect("Failed to acquire read lock after write lock released");
drop(read_guard);
}
#[tokio::test]
async fn test_versioned_locks() {
let manager = FastObjectLockManager::new();
// Acquire lock on version v1
let v1_guard = manager
.acquire_write_lock_versioned("bucket", "object", "v1", "owner1")
.await
.expect("Failed to acquire v1 lock");
// Should be able to acquire lock on version v2 simultaneously
let v2_guard = manager
.acquire_write_lock_versioned("bucket", "object", "v2", "owner2")
.await
.expect("Failed to acquire v2 lock");
drop(v1_guard);
drop(v2_guard);
}
#[tokio::test]
async fn test_batch_operations() {
let manager = FastObjectLockManager::new();
let batch = BatchLockRequest::new("owner")
.add_read_lock("bucket", "obj1")
.add_write_lock("bucket", "obj2")
.with_all_or_nothing(true);
let result = manager.acquire_locks_batch(batch).await;
assert!(result.all_acquired);
assert_eq!(result.successful_locks.len(), 2);
assert!(result.failed_locks.is_empty());
}
#[tokio::test]
async fn test_metrics() {
let manager = FastObjectLockManager::new();
// Perform some operations
let _guard1 = manager.acquire_read_lock("bucket", "obj1", "owner").await.unwrap();
let _guard2 = manager.acquire_write_lock("bucket", "obj2", "owner").await.unwrap();
let metrics = manager.get_metrics();
assert!(metrics.shard_metrics.total_acquisitions() > 0);
assert!(metrics.shard_metrics.fast_path_rate() > 0.0);
}
#[tokio::test]
async fn test_cleanup() {
let config = LockConfig {
max_idle_time: Duration::from_secs(1), // Use 1 second for easier testing
..Default::default()
};
let manager = FastObjectLockManager::with_config(config);
// Acquire and release some locks
{
let _guard = manager.acquire_read_lock("bucket", "obj1", "owner1").await.unwrap();
let _guard2 = manager.acquire_read_lock("bucket", "obj2", "owner2").await.unwrap();
} // Locks are released here
// Check lock count before cleanup
let count_before = manager.total_lock_count();
assert!(count_before >= 2, "Should have at least 2 locks before cleanup");
// Wait for idle timeout
tokio::time::sleep(Duration::from_secs(2)).await;
// Force cleanup with traditional method to ensure cleanup for testing
let cleaned = manager.cleanup_expired_traditional().await;
let count_after = manager.total_lock_count();
// The test should pass if cleanup works at all
assert!(
cleaned > 0 || count_after < count_before,
"Cleanup should either clean locks or they should be cleaned by other means"
);
}
}
@@ -0,0 +1,93 @@
// 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.
//! Unified trait for lock managers (enabled and disabled)
use crate::fast_lock::{
guard::FastLockGuard,
metrics::AggregatedMetrics,
types::{BatchLockRequest, BatchLockResult, LockResult, ObjectKey, ObjectLockInfo, ObjectLockRequest},
};
use std::sync::Arc;
/// Unified trait for lock managers
///
/// This trait allows transparent switching between enabled and disabled lock managers
/// based on environment variables.
#[async_trait::async_trait]
pub trait LockManager: Send + Sync {
/// Acquire object lock
async fn acquire_lock(&self, request: ObjectLockRequest) -> Result<FastLockGuard, LockResult>;
/// Acquire shared (read) lock
async fn acquire_read_lock(
&self,
bucket: impl Into<Arc<str>> + Send,
object: impl Into<Arc<str>> + Send,
owner: impl Into<Arc<str>> + Send,
) -> Result<FastLockGuard, LockResult>;
/// Acquire shared (read) lock for specific version
async fn acquire_read_lock_versioned(
&self,
bucket: impl Into<Arc<str>> + Send,
object: impl Into<Arc<str>> + Send,
version: impl Into<Arc<str>> + Send,
owner: impl Into<Arc<str>> + Send,
) -> Result<FastLockGuard, LockResult>;
/// Acquire exclusive (write) lock
async fn acquire_write_lock(
&self,
bucket: impl Into<Arc<str>> + Send,
object: impl Into<Arc<str>> + Send,
owner: impl Into<Arc<str>> + Send,
) -> Result<FastLockGuard, LockResult>;
/// Acquire exclusive (write) lock for specific version
async fn acquire_write_lock_versioned(
&self,
bucket: impl Into<Arc<str>> + Send,
object: impl Into<Arc<str>> + Send,
version: impl Into<Arc<str>> + Send,
owner: impl Into<Arc<str>> + Send,
) -> Result<FastLockGuard, LockResult>;
/// Acquire multiple locks atomically
async fn acquire_locks_batch(&self, batch_request: BatchLockRequest) -> BatchLockResult;
/// Get lock information for monitoring
fn get_lock_info(&self, key: &ObjectKey) -> Option<ObjectLockInfo>;
/// Get aggregated metrics
fn get_metrics(&self) -> AggregatedMetrics;
/// Get total number of active locks across all shards
fn total_lock_count(&self) -> usize;
/// Get pool statistics from all shards
fn get_pool_stats(&self) -> Vec<(u64, u64, u64, usize)>;
/// Force cleanup of expired locks
async fn cleanup_expired(&self) -> usize;
/// Force cleanup with traditional strategy
async fn cleanup_expired_traditional(&self) -> usize;
/// Shutdown the lock manager and cleanup resources
async fn shutdown(&self);
/// Check if this manager is disabled
fn is_disabled(&self) -> bool;
}
+354
View File
@@ -0,0 +1,354 @@
// 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.
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
/// Atomic metrics for lock operations
#[derive(Debug)]
pub struct ShardMetrics {
pub fast_path_success: AtomicU64,
pub slow_path_success: AtomicU64,
pub timeouts: AtomicU64,
pub releases: AtomicU64,
pub cleanups: AtomicU64,
pub contention_events: AtomicU64,
pub total_wait_time_ns: AtomicU64,
pub max_wait_time_ns: AtomicU64,
}
impl Default for ShardMetrics {
fn default() -> Self {
Self::new()
}
}
impl ShardMetrics {
pub fn new() -> Self {
Self {
fast_path_success: AtomicU64::new(0),
slow_path_success: AtomicU64::new(0),
timeouts: AtomicU64::new(0),
releases: AtomicU64::new(0),
cleanups: AtomicU64::new(0),
contention_events: AtomicU64::new(0),
total_wait_time_ns: AtomicU64::new(0),
max_wait_time_ns: AtomicU64::new(0),
}
}
pub fn record_fast_path_success(&self) {
self.fast_path_success.fetch_add(1, Ordering::Relaxed);
}
pub fn record_slow_path_success(&self) {
self.slow_path_success.fetch_add(1, Ordering::Relaxed);
self.contention_events.fetch_add(1, Ordering::Relaxed);
}
pub fn record_timeout(&self) {
self.timeouts.fetch_add(1, Ordering::Relaxed);
}
pub fn record_release(&self) {
self.releases.fetch_add(1, Ordering::Relaxed);
}
pub fn record_cleanup(&self, count: usize) {
self.cleanups.fetch_add(count as u64, Ordering::Relaxed);
}
pub fn record_wait_time(&self, wait_time: Duration) {
let wait_ns = wait_time.as_nanos() as u64;
self.total_wait_time_ns.fetch_add(wait_ns, Ordering::Relaxed);
// Update max wait time
let mut current_max = self.max_wait_time_ns.load(Ordering::Relaxed);
while wait_ns > current_max {
match self
.max_wait_time_ns
.compare_exchange_weak(current_max, wait_ns, Ordering::Relaxed, Ordering::Relaxed)
{
Ok(_) => break,
Err(x) => current_max = x,
}
}
}
/// Get total successful acquisitions
pub fn total_acquisitions(&self) -> u64 {
self.fast_path_success.load(Ordering::Relaxed) + self.slow_path_success.load(Ordering::Relaxed)
}
/// Get fast path hit rate (0.0 to 1.0)
pub fn fast_path_rate(&self) -> f64 {
let total = self.total_acquisitions();
if total == 0 {
0.0
} else {
self.fast_path_success.load(Ordering::Relaxed) as f64 / total as f64
}
}
/// Get average wait time in nanoseconds
pub fn avg_wait_time_ns(&self) -> f64 {
let total_wait = self.total_wait_time_ns.load(Ordering::Relaxed);
let slow_path = self.slow_path_success.load(Ordering::Relaxed);
if slow_path == 0 {
0.0
} else {
total_wait as f64 / slow_path as f64
}
}
/// Get snapshot of current metrics
pub fn snapshot(&self) -> MetricsSnapshot {
MetricsSnapshot {
fast_path_success: self.fast_path_success.load(Ordering::Relaxed),
slow_path_success: self.slow_path_success.load(Ordering::Relaxed),
timeouts: self.timeouts.load(Ordering::Relaxed),
releases: self.releases.load(Ordering::Relaxed),
cleanups: self.cleanups.load(Ordering::Relaxed),
contention_events: self.contention_events.load(Ordering::Relaxed),
total_wait_time_ns: self.total_wait_time_ns.load(Ordering::Relaxed),
max_wait_time_ns: self.max_wait_time_ns.load(Ordering::Relaxed),
}
}
}
/// Snapshot of metrics at a point in time
#[derive(Debug, Clone)]
pub struct MetricsSnapshot {
pub fast_path_success: u64,
pub slow_path_success: u64,
pub timeouts: u64,
pub releases: u64,
pub cleanups: u64,
pub contention_events: u64,
pub total_wait_time_ns: u64,
pub max_wait_time_ns: u64,
}
impl MetricsSnapshot {
/// Create empty snapshot (for disabled lock manager)
pub fn empty() -> Self {
Self {
fast_path_success: 0,
slow_path_success: 0,
timeouts: 0,
releases: 0,
cleanups: 0,
contention_events: 0,
total_wait_time_ns: 0,
max_wait_time_ns: 0,
}
}
pub fn total_acquisitions(&self) -> u64 {
self.fast_path_success + self.slow_path_success
}
pub fn fast_path_rate(&self) -> f64 {
let total = self.total_acquisitions();
if total == 0 {
0.0
} else {
self.fast_path_success as f64 / total as f64
}
}
pub fn avg_wait_time(&self) -> Duration {
if self.slow_path_success == 0 {
Duration::ZERO
} else {
Duration::from_nanos(self.total_wait_time_ns / self.slow_path_success)
}
}
pub fn max_wait_time(&self) -> Duration {
Duration::from_nanos(self.max_wait_time_ns)
}
pub fn timeout_rate(&self) -> f64 {
let total_attempts = self.total_acquisitions() + self.timeouts;
if total_attempts == 0 {
0.0
} else {
self.timeouts as f64 / total_attempts as f64
}
}
}
/// Global metrics aggregator
#[derive(Debug)]
pub struct GlobalMetrics {
shard_count: usize,
start_time: Instant,
cleanup_runs: AtomicU64,
total_objects_cleaned: AtomicU64,
}
impl GlobalMetrics {
pub fn new(shard_count: usize) -> Self {
Self {
shard_count,
start_time: Instant::now(),
cleanup_runs: AtomicU64::new(0),
total_objects_cleaned: AtomicU64::new(0),
}
}
pub fn record_cleanup_run(&self, objects_cleaned: usize) {
self.cleanup_runs.fetch_add(1, Ordering::Relaxed);
self.total_objects_cleaned
.fetch_add(objects_cleaned as u64, Ordering::Relaxed);
}
pub fn uptime(&self) -> Duration {
self.start_time.elapsed()
}
/// Aggregate metrics from all shards
pub fn aggregate_shard_metrics(&self, shard_metrics: &[MetricsSnapshot]) -> AggregatedMetrics {
let mut total = MetricsSnapshot {
fast_path_success: 0,
slow_path_success: 0,
timeouts: 0,
releases: 0,
cleanups: 0,
contention_events: 0,
total_wait_time_ns: 0,
max_wait_time_ns: 0,
};
for snapshot in shard_metrics {
total.fast_path_success += snapshot.fast_path_success;
total.slow_path_success += snapshot.slow_path_success;
total.timeouts += snapshot.timeouts;
total.releases += snapshot.releases;
total.cleanups += snapshot.cleanups;
total.contention_events += snapshot.contention_events;
total.total_wait_time_ns += snapshot.total_wait_time_ns;
total.max_wait_time_ns = total.max_wait_time_ns.max(snapshot.max_wait_time_ns);
}
AggregatedMetrics {
shard_metrics: total,
shard_count: self.shard_count,
uptime: self.uptime(),
cleanup_runs: self.cleanup_runs.load(Ordering::Relaxed),
total_objects_cleaned: self.total_objects_cleaned.load(Ordering::Relaxed),
}
}
}
/// Aggregated metrics from all shards
#[derive(Debug, Clone)]
pub struct AggregatedMetrics {
pub shard_metrics: MetricsSnapshot,
pub shard_count: usize,
pub uptime: Duration,
pub cleanup_runs: u64,
pub total_objects_cleaned: u64,
}
impl AggregatedMetrics {
/// Create empty metrics (for disabled lock manager)
pub fn empty() -> Self {
Self {
shard_metrics: MetricsSnapshot::empty(),
shard_count: 0,
uptime: Duration::ZERO,
cleanup_runs: 0,
total_objects_cleaned: 0,
}
}
/// Check if metrics are empty (indicates disabled or no activity)
pub fn is_empty(&self) -> bool {
self.shard_count == 0 && self.shard_metrics.total_acquisitions() == 0 && self.shard_metrics.releases == 0
}
/// Get operations per second
pub fn ops_per_second(&self) -> f64 {
let total_ops = self.shard_metrics.total_acquisitions() + self.shard_metrics.releases;
let uptime_secs = self.uptime.as_secs_f64();
if uptime_secs > 0.0 {
total_ops as f64 / uptime_secs
} else {
0.0
}
}
/// Get average locks per shard
pub fn avg_locks_per_shard(&self) -> f64 {
if self.shard_count > 0 {
self.shard_metrics.total_acquisitions() as f64 / self.shard_count as f64
} else {
0.0
}
}
/// Check if performance is healthy
pub fn is_healthy(&self) -> bool {
let fast_path_rate = self.shard_metrics.fast_path_rate();
let timeout_rate = self.shard_metrics.timeout_rate();
let avg_wait = self.shard_metrics.avg_wait_time();
// Healthy if:
// - Fast path rate > 80%
// - Timeout rate < 5%
// - Average wait time < 10ms
fast_path_rate > 0.8 && timeout_rate < 0.05 && avg_wait < Duration::from_millis(10)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_shard_metrics() {
let metrics = ShardMetrics::new();
metrics.record_fast_path_success();
metrics.record_fast_path_success();
metrics.record_slow_path_success();
metrics.record_timeout();
assert_eq!(metrics.total_acquisitions(), 3);
assert_eq!(metrics.fast_path_rate(), 2.0 / 3.0);
let snapshot = metrics.snapshot();
assert_eq!(snapshot.fast_path_success, 2);
assert_eq!(snapshot.slow_path_success, 1);
assert_eq!(snapshot.timeouts, 1);
}
#[test]
fn test_global_metrics() {
let global = GlobalMetrics::new(4);
let shard_metrics = [ShardMetrics::new(), ShardMetrics::new()];
shard_metrics[0].record_fast_path_success();
shard_metrics[1].record_slow_path_success();
let snapshots: Vec<MetricsSnapshot> = shard_metrics.iter().map(|m| m.snapshot()).collect();
let aggregated = global.aggregate_shard_metrics(&snapshots);
assert_eq!(aggregated.shard_metrics.total_acquisitions(), 2);
assert_eq!(aggregated.shard_count, 4);
}
}
+60
View File
@@ -0,0 +1,60 @@
// 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.
//! Fast Object Lock System
//!
//! High-performance versioned object locking system optimized for object storage scenarios
//!
//! ## Core Features
//!
//! 1. **Sharded Architecture** - Hash-based object key sharding to avoid global lock contention
//! 2. **Version Awareness** - Support for multi-version object locking with fine-grained control
//! 3. **Fast Path** - Lock-free fast paths for common operations
//! 4. **Async Optimized** - True async locks that avoid thread blocking
//! 5. **Auto Cleanup** - Access-time based automatic lock reclamation
pub mod disabled_manager;
pub mod guard;
pub mod integration_example;
pub mod integration_test;
pub mod manager;
pub mod manager_trait;
pub mod metrics;
pub mod object_pool;
pub mod optimized_notify;
pub mod shard;
pub mod state;
pub mod types;
// #[cfg(test)]
// pub mod benchmarks; // Temporarily disabled due to compilation issues
// Re-export main types
pub use disabled_manager::DisabledLockManager;
pub use guard::FastLockGuard;
pub use manager::FastObjectLockManager;
pub use manager_trait::LockManager;
pub use types::*;
/// Default shard count (must be power of 2)
pub const DEFAULT_SHARD_COUNT: usize = 1024;
/// Default lock timeout
pub const DEFAULT_LOCK_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
/// Default acquire timeout
pub const DEFAULT_ACQUIRE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
/// Lock cleanup interval
pub const CLEANUP_INTERVAL: std::time::Duration = std::time::Duration::from_secs(60);
+155
View File
@@ -0,0 +1,155 @@
// 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.
use crate::fast_lock::state::ObjectLockState;
use crossbeam_queue::SegQueue;
use std::sync::atomic::{AtomicU64, Ordering};
/// Simple object pool for ObjectLockState to reduce allocation overhead
#[derive(Debug)]
pub struct ObjectStatePool {
pool: SegQueue<Box<ObjectLockState>>,
stats: PoolStats,
}
#[derive(Debug)]
struct PoolStats {
hits: AtomicU64,
misses: AtomicU64,
releases: AtomicU64,
}
impl ObjectStatePool {
pub fn new() -> Self {
Self {
pool: SegQueue::new(),
stats: PoolStats {
hits: AtomicU64::new(0),
misses: AtomicU64::new(0),
releases: AtomicU64::new(0),
},
}
}
/// Get an ObjectLockState from the pool or create a new one
pub fn acquire(&self) -> Box<ObjectLockState> {
if let Some(mut obj) = self.pool.pop() {
self.stats.hits.fetch_add(1, Ordering::Relaxed);
obj.reset_for_reuse();
obj
} else {
self.stats.misses.fetch_add(1, Ordering::Relaxed);
Box::new(ObjectLockState::new())
}
}
/// Return an ObjectLockState to the pool
pub fn release(&self, obj: Box<ObjectLockState>) {
// Only keep the pool at reasonable size to avoid memory bloat
if self.pool.len() < 1000 {
self.stats.releases.fetch_add(1, Ordering::Relaxed);
self.pool.push(obj);
}
// Otherwise let it drop naturally
}
/// Get pool statistics
pub fn stats(&self) -> (u64, u64, u64, usize) {
let hits = self.stats.hits.load(Ordering::Relaxed);
let misses = self.stats.misses.load(Ordering::Relaxed);
let releases = self.stats.releases.load(Ordering::Relaxed);
let pool_size = self.pool.len();
(hits, misses, releases, pool_size)
}
/// Get hit rate (0.0 to 1.0)
pub fn hit_rate(&self) -> f64 {
let hits = self.stats.hits.load(Ordering::Relaxed);
let misses = self.stats.misses.load(Ordering::Relaxed);
let total = hits + misses;
if total == 0 { 0.0 } else { hits as f64 / total as f64 }
}
}
impl Default for ObjectStatePool {
fn default() -> Self {
Self::new()
}
}
impl ObjectLockState {
/// Reset state for reuse from pool
pub fn reset_for_reuse(&mut self) {
// Reset atomic state
self.atomic_state = crate::fast_lock::state::AtomicLockState::new();
// Clear owners
*self.current_owner.write() = None;
self.shared_owners.write().clear();
// Reset priority
*self.priority.write() = crate::fast_lock::types::LockPriority::Normal;
// Note: We don't reset notifications as they should be handled by drop/recreation
// The optimized_notify will be reset automatically on next use
self.optimized_notify = crate::fast_lock::optimized_notify::OptimizedNotify::new();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_object_pool() {
let pool = ObjectStatePool::new();
// First acquisition should be a miss
let obj1 = pool.acquire();
let (hits, misses, _, _) = pool.stats();
assert_eq!(hits, 0);
assert_eq!(misses, 1);
// Return to pool
pool.release(obj1);
let (_, _, releases, pool_size) = pool.stats();
assert_eq!(releases, 1);
assert_eq!(pool_size, 1);
// Second acquisition should be a hit
let _obj2 = pool.acquire();
let (hits, misses, _, _) = pool.stats();
assert_eq!(hits, 1);
assert_eq!(misses, 1);
assert_eq!(pool.hit_rate(), 0.5);
}
#[test]
fn test_state_reset() {
let mut state = ObjectLockState::new();
// Modify state
*state.current_owner.write() = Some("test_owner".into());
state.shared_owners.write().push("shared_owner".into());
// Reset
state.reset_for_reuse();
// Verify reset
assert!(state.current_owner.read().is_none());
assert!(state.shared_owners.read().is_empty());
}
}
@@ -0,0 +1,134 @@
// 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.
use once_cell::sync::Lazy;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
use tokio::sync::Notify;
/// Optimized notification pool to reduce memory overhead and thundering herd effects
static NOTIFY_POOL: Lazy<Vec<Arc<Notify>>> = Lazy::new(|| (0..64).map(|_| Arc::new(Notify::new())).collect());
/// Optimized notification system for object locks
#[derive(Debug)]
pub struct OptimizedNotify {
/// Number of readers waiting
pub reader_waiters: AtomicU32,
/// Number of writers waiting
pub writer_waiters: AtomicU32,
/// Index into the global notify pool
pub notify_pool_index: AtomicUsize,
}
impl OptimizedNotify {
pub fn new() -> Self {
// Use random pool index to distribute load
use std::time::{SystemTime, UNIX_EPOCH};
let seed = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
let pool_index = (seed as usize) % NOTIFY_POOL.len();
Self {
reader_waiters: AtomicU32::new(0),
writer_waiters: AtomicU32::new(0),
notify_pool_index: AtomicUsize::new(pool_index),
}
}
/// Notify waiting readers
pub fn notify_readers(&self) {
if self.reader_waiters.load(Ordering::Acquire) > 0 {
let pool_index = self.notify_pool_index.load(Ordering::Relaxed) % NOTIFY_POOL.len();
NOTIFY_POOL[pool_index].notify_waiters();
}
}
/// Notify one waiting writer
pub fn notify_writer(&self) {
if self.writer_waiters.load(Ordering::Acquire) > 0 {
let pool_index = self.notify_pool_index.load(Ordering::Relaxed) % NOTIFY_POOL.len();
NOTIFY_POOL[pool_index].notify_one();
}
}
/// Wait for reader notification
pub async fn wait_for_read(&self) {
self.reader_waiters.fetch_add(1, Ordering::AcqRel);
let pool_index = self.notify_pool_index.load(Ordering::Relaxed) % NOTIFY_POOL.len();
NOTIFY_POOL[pool_index].notified().await;
self.reader_waiters.fetch_sub(1, Ordering::AcqRel);
}
/// Wait for writer notification
pub async fn wait_for_write(&self) {
self.writer_waiters.fetch_add(1, Ordering::AcqRel);
let pool_index = self.notify_pool_index.load(Ordering::Relaxed) % NOTIFY_POOL.len();
NOTIFY_POOL[pool_index].notified().await;
self.writer_waiters.fetch_sub(1, Ordering::AcqRel);
}
/// Check if anyone is waiting
pub fn has_waiters(&self) -> bool {
self.reader_waiters.load(Ordering::Acquire) > 0 || self.writer_waiters.load(Ordering::Acquire) > 0
}
}
impl Default for OptimizedNotify {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::time::{Duration, timeout};
#[tokio::test]
async fn test_optimized_notify() {
let notify = OptimizedNotify::new();
// Test that notification works
let notify_clone = Arc::new(notify);
let notify_for_task = notify_clone.clone();
let handle = tokio::spawn(async move {
notify_for_task.wait_for_read().await;
});
// Give some time for the task to start waiting
tokio::time::sleep(Duration::from_millis(10)).await;
notify_clone.notify_readers();
// Should complete quickly
assert!(timeout(Duration::from_millis(100), handle).await.is_ok());
}
#[tokio::test]
async fn test_writer_notification() {
let notify = Arc::new(OptimizedNotify::new());
let notify_for_task = notify.clone();
let handle = tokio::spawn(async move {
notify_for_task.wait_for_write().await;
});
tokio::time::sleep(Duration::from_millis(10)).await;
notify.notify_writer();
assert!(timeout(Duration::from_millis(100), handle).await.is_ok());
}
}
+575
View File
@@ -0,0 +1,575 @@
// 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.
use parking_lot::RwLock;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime};
use tokio::time::timeout;
use crate::fast_lock::{
metrics::ShardMetrics,
object_pool::ObjectStatePool,
state::ObjectLockState,
types::{LockMode, LockResult, ObjectKey, ObjectLockRequest},
};
/// Lock shard to reduce global contention
#[derive(Debug)]
pub struct LockShard {
/// Object lock states - using parking_lot for better performance
objects: RwLock<HashMap<ObjectKey, Arc<ObjectLockState>>>,
/// Object state pool for memory optimization
object_pool: ObjectStatePool,
/// Shard-level metrics
metrics: ShardMetrics,
/// Shard ID for debugging
_shard_id: usize,
}
impl LockShard {
pub fn new(shard_id: usize) -> Self {
Self {
objects: RwLock::new(HashMap::new()),
object_pool: ObjectStatePool::new(),
metrics: ShardMetrics::new(),
_shard_id: shard_id,
}
}
/// Acquire lock with fast path optimization
pub async fn acquire_lock(&self, request: &ObjectLockRequest) -> Result<(), LockResult> {
let start_time = Instant::now();
// Try fast path first
if let Some(_state) = self.try_fast_path(request) {
self.metrics.record_fast_path_success();
return Ok(());
}
// Slow path with waiting
self.acquire_lock_slow_path(request, start_time).await
}
/// Try fast path only (without fallback to slow path)
pub fn try_fast_path_only(&self, request: &ObjectLockRequest) -> bool {
// Early check to avoid unnecessary lock contention
if let Some(state) = self.objects.read().get(&request.key) {
if !state.atomic_state.is_fast_path_available(request.mode) {
return false;
}
}
self.try_fast_path(request).is_some()
}
/// Try fast path lock acquisition (lock-free when possible)
fn try_fast_path(&self, request: &ObjectLockRequest) -> Option<Arc<ObjectLockState>> {
// First try to get existing state without write lock
{
let objects = self.objects.read();
if let Some(state) = objects.get(&request.key) {
let state = state.clone();
drop(objects);
// Try atomic acquisition
let success = match request.mode {
LockMode::Shared => state.try_acquire_shared_fast(&request.owner),
LockMode::Exclusive => state.try_acquire_exclusive_fast(&request.owner),
};
if success {
return Some(state);
}
}
}
// If object doesn't exist and we're requesting exclusive lock,
// try to create and acquire atomically
if request.mode == LockMode::Exclusive {
let mut objects = self.objects.write();
// Double-check after acquiring write lock
if let Some(state) = objects.get(&request.key) {
let state = state.clone();
drop(objects);
if state.try_acquire_exclusive_fast(&request.owner) {
return Some(state);
}
} else {
// Create new state from pool and acquire immediately
let state_box = self.object_pool.acquire();
let state = Arc::new(*state_box);
if state.try_acquire_exclusive_fast(&request.owner) {
objects.insert(request.key.clone(), state.clone());
return Some(state);
}
}
}
None
}
/// Slow path with async waiting
async fn acquire_lock_slow_path(&self, request: &ObjectLockRequest, start_time: Instant) -> Result<(), LockResult> {
let deadline = start_time + request.acquire_timeout;
loop {
// Get or create object state
let state = {
let mut objects = self.objects.write();
match objects.get(&request.key) {
Some(state) => state.clone(),
None => {
let state_box = self.object_pool.acquire();
let state = Arc::new(*state_box);
objects.insert(request.key.clone(), state.clone());
state
}
}
};
// Try acquisition again
let success = match request.mode {
LockMode::Shared => state.try_acquire_shared_fast(&request.owner),
LockMode::Exclusive => state.try_acquire_exclusive_fast(&request.owner),
};
if success {
self.metrics.record_slow_path_success();
return Ok(());
}
// Check timeout
if Instant::now() >= deadline {
self.metrics.record_timeout();
return Err(LockResult::Timeout);
}
// Wait for notification using optimized notify system
let remaining = deadline - Instant::now();
let wait_result = match request.mode {
LockMode::Shared => {
state.atomic_state.inc_readers_waiting();
let result = timeout(remaining, state.optimized_notify.wait_for_read()).await;
state.atomic_state.dec_readers_waiting();
result
}
LockMode::Exclusive => {
state.atomic_state.inc_writers_waiting();
let result = timeout(remaining, state.optimized_notify.wait_for_write()).await;
state.atomic_state.dec_writers_waiting();
result
}
};
if wait_result.is_err() {
self.metrics.record_timeout();
return Err(LockResult::Timeout);
}
// Continue the loop to try acquisition again
}
}
/// Release lock
pub fn release_lock(&self, key: &ObjectKey, owner: &Arc<str>, mode: LockMode) -> bool {
let should_cleanup;
let result;
{
let objects = self.objects.read();
if let Some(state) = objects.get(key) {
result = match mode {
LockMode::Shared => state.release_shared(owner),
LockMode::Exclusive => state.release_exclusive(owner),
};
if result {
self.metrics.record_release();
// Check if cleanup is needed
should_cleanup = !state.is_locked() && !state.atomic_state.has_waiters();
} else {
should_cleanup = false;
}
} else {
result = false;
should_cleanup = false;
}
}
// Perform cleanup outside of the read lock
if should_cleanup {
self.schedule_cleanup(key.clone());
}
result
}
/// Batch acquire locks with ordering to prevent deadlocks
pub async fn acquire_locks_batch(
&self,
mut requests: Vec<ObjectLockRequest>,
all_or_nothing: bool,
) -> Result<Vec<ObjectKey>, Vec<(ObjectKey, LockResult)>> {
// Sort requests by key to prevent deadlocks
requests.sort_by(|a, b| a.key.cmp(&b.key));
let mut acquired = Vec::new();
let mut failed = Vec::new();
for request in requests {
match self.acquire_lock(&request).await {
Ok(()) => acquired.push((request.key.clone(), request.mode, request.owner.clone())),
Err(err) => {
failed.push((request.key, err));
if all_or_nothing {
// Release all acquired locks using their correct owner and mode
let mut cleanup_failures = 0;
for (key, mode, owner) in &acquired {
if !self.release_lock(key, owner, *mode) {
cleanup_failures += 1;
tracing::warn!(
"Failed to release lock during batch cleanup in shard: bucket={}, object={}",
key.bucket,
key.object
);
}
}
if cleanup_failures > 0 {
tracing::error!("Shard batch lock cleanup had {} failures", cleanup_failures);
}
return Err(failed);
}
}
}
}
if failed.is_empty() {
Ok(acquired.into_iter().map(|(key, _, _)| key).collect())
} else {
Err(failed)
}
}
/// Get lock information for monitoring
pub fn get_lock_info(&self, key: &ObjectKey) -> Option<crate::fast_lock::types::ObjectLockInfo> {
let objects = self.objects.read();
if let Some(state) = objects.get(key) {
if let Some(mode) = state.current_mode() {
let owner = match mode {
LockMode::Exclusive => {
let current_owner = state.current_owner.read();
current_owner.clone()?
}
LockMode::Shared => {
let shared_owners = state.shared_owners.read();
shared_owners.first()?.clone()
}
};
let priority = *state.priority.read();
// Estimate acquisition time (approximate)
let acquired_at = SystemTime::now() - Duration::from_secs(60);
let expires_at = acquired_at + Duration::from_secs(300);
return Some(crate::fast_lock::types::ObjectLockInfo {
key: key.clone(),
mode,
owner,
acquired_at,
expires_at,
priority,
});
}
}
None
}
/// Get current load factor of the shard
pub fn current_load_factor(&self) -> f64 {
let objects = self.objects.read();
let total_locks = objects.len();
if total_locks == 0 {
return 0.0;
}
let active_locks = objects.values().filter(|state| state.is_locked()).count();
active_locks as f64 / total_locks as f64
}
/// Get count of active locks
pub fn active_lock_count(&self) -> usize {
let objects = self.objects.read();
objects.values().filter(|state| state.is_locked()).count()
}
/// Adaptive cleanup based on current load
pub fn adaptive_cleanup(&self) -> usize {
let current_load = self.current_load_factor();
let lock_count = self.lock_count();
// Dynamically adjust cleanup strategy based on load
let cleanup_batch_size = match current_load {
load if load > 0.9 => lock_count / 20, // High load: small batch cleanup
load if load > 0.7 => lock_count / 10, // Medium load: moderate cleanup
_ => lock_count / 5, // Low load: aggressive cleanup
};
// Use longer timeout for high load scenarios
let cleanup_threshold_millis = match current_load {
load if load > 0.8 => 300_000, // 5 minutes for high load
load if load > 0.5 => 180_000, // 3 minutes for medium load
_ => 60_000, // 1 minute for low load
};
self.cleanup_expired_batch(cleanup_batch_size.max(10), cleanup_threshold_millis)
}
/// Cleanup expired and unused locks
pub fn cleanup_expired(&self, max_idle_secs: u64) -> usize {
let max_idle_millis = max_idle_secs * 1000;
self.cleanup_expired_millis(max_idle_millis)
}
/// Cleanup expired and unused locks with millisecond precision
pub fn cleanup_expired_millis(&self, max_idle_millis: u64) -> usize {
let mut cleaned = 0;
let now_millis = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_millis() as u64;
let mut objects = self.objects.write();
objects.retain(|_key, state| {
if !state.is_locked() && !state.atomic_state.has_waiters() {
let last_access_secs = state.atomic_state.last_accessed();
let last_access_millis = last_access_secs * 1000; // Convert to millis
let idle_time = now_millis.saturating_sub(last_access_millis);
if idle_time > max_idle_millis {
cleaned += 1;
false // Remove this entry
} else {
true // Keep this entry
}
} else {
true // Keep locked or waited entries
}
});
self.metrics.record_cleanup(cleaned);
cleaned
}
/// Batch cleanup with limited processing to avoid blocking
fn cleanup_expired_batch(&self, max_batch_size: usize, cleanup_threshold_millis: u64) -> usize {
let mut cleaned = 0;
let now_millis = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_millis() as u64;
let mut objects = self.objects.write();
let mut processed = 0;
// Process in batches to avoid long-held locks
let mut to_recycle = Vec::new();
objects.retain(|_key, state| {
if processed >= max_batch_size {
return true; // Stop processing after batch limit
}
processed += 1;
if !state.is_locked() && !state.atomic_state.has_waiters() {
let last_access_millis = state.atomic_state.last_accessed() * 1000;
let idle_time = now_millis.saturating_sub(last_access_millis);
if idle_time > cleanup_threshold_millis {
// Try to recycle the state back to pool if possible
if let Ok(state_box) = Arc::try_unwrap(state.clone()) {
to_recycle.push(state_box);
}
cleaned += 1;
false // Remove
} else {
true // Keep
}
} else {
true // Keep active locks
}
});
// Return recycled objects to pool
for state_box in to_recycle {
let boxed_state = Box::new(state_box);
self.object_pool.release(boxed_state);
}
self.metrics.record_cleanup(cleaned);
cleaned
}
/// Get shard metrics
pub fn metrics(&self) -> &ShardMetrics {
&self.metrics
}
/// Get current lock count
pub fn lock_count(&self) -> usize {
self.objects.read().len()
}
/// Schedule background cleanup for a key
fn schedule_cleanup(&self, key: ObjectKey) {
// Don't immediately cleanup - let cleanup_expired handle it
// This allows the cleanup test to work properly
let _ = key; // Suppress unused variable warning
}
/// Get object pool statistics
pub fn pool_stats(&self) -> (u64, u64, u64, usize) {
self.object_pool.stats()
}
/// Get object pool hit rate
pub fn pool_hit_rate(&self) -> f64 {
self.object_pool.hit_rate()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fast_lock::types::{LockPriority, ObjectKey};
#[tokio::test]
async fn test_shard_fast_path() {
let shard = LockShard::new(0);
let key = ObjectKey::new("bucket", "object");
let owner: Arc<str> = Arc::from("owner");
let request = ObjectLockRequest {
key: key.clone(),
mode: LockMode::Exclusive,
owner: owner.clone(),
acquire_timeout: Duration::from_secs(1),
lock_timeout: Duration::from_secs(30),
priority: LockPriority::Normal,
};
// Should succeed via fast path
assert!(shard.acquire_lock(&request).await.is_ok());
assert!(shard.release_lock(&key, &owner, LockMode::Exclusive));
}
#[tokio::test]
async fn test_shard_contention() {
let shard = Arc::new(LockShard::new(0));
let key = ObjectKey::new("bucket", "object");
let owner1: Arc<str> = Arc::from("owner1");
let owner2: Arc<str> = Arc::from("owner2");
let request1 = ObjectLockRequest {
key: key.clone(),
mode: LockMode::Exclusive,
owner: owner1.clone(),
acquire_timeout: Duration::from_secs(1),
lock_timeout: Duration::from_secs(30),
priority: LockPriority::Normal,
};
let request2 = ObjectLockRequest {
key: key.clone(),
mode: LockMode::Exclusive,
owner: owner2.clone(),
acquire_timeout: Duration::from_millis(100),
lock_timeout: Duration::from_secs(30),
priority: LockPriority::Normal,
};
// First lock should succeed
assert!(shard.acquire_lock(&request1).await.is_ok());
// Second lock should timeout
assert!(matches!(shard.acquire_lock(&request2).await, Err(LockResult::Timeout)));
// Release first lock
assert!(shard.release_lock(&key, &owner1, LockMode::Exclusive));
}
#[tokio::test]
async fn test_batch_operations() {
let shard = LockShard::new(0);
let owner: Arc<str> = Arc::from("owner");
let requests = vec![
ObjectLockRequest {
key: ObjectKey::new("bucket", "obj1"),
mode: LockMode::Exclusive,
owner: owner.clone(),
acquire_timeout: Duration::from_secs(1),
lock_timeout: Duration::from_secs(30),
priority: LockPriority::Normal,
},
ObjectLockRequest {
key: ObjectKey::new("bucket", "obj2"),
mode: LockMode::Shared,
owner: owner.clone(),
acquire_timeout: Duration::from_secs(1),
lock_timeout: Duration::from_secs(30),
priority: LockPriority::Normal,
},
];
let result = shard.acquire_locks_batch(requests, true).await;
assert!(result.is_ok());
let acquired = result.unwrap();
assert_eq!(acquired.len(), 2);
}
#[tokio::test]
async fn test_batch_lock_cleanup_safety() {
let shard = LockShard::new(0);
// First acquire a lock that will block the batch operation
let blocking_request = ObjectLockRequest::new_write("bucket", "obj1", "blocking_owner");
shard.acquire_lock(&blocking_request).await.unwrap();
// Now try a batch operation that should fail and clean up properly
let requests = vec![
ObjectLockRequest::new_read("bucket", "obj2", "batch_owner"), // This should succeed
ObjectLockRequest::new_write("bucket", "obj1", "batch_owner"), // This should fail due to existing lock
];
let result = shard.acquire_locks_batch(requests, true).await;
assert!(result.is_err()); // Should fail due to obj1 being locked
// Verify that obj2 lock was properly cleaned up (no resource leak)
let obj2_key = ObjectKey::new("bucket", "obj2");
assert!(shard.get_lock_info(&obj2_key).is_none(), "obj2 should not be locked after cleanup");
// Verify obj1 is still locked by the original owner
let obj1_key = ObjectKey::new("bucket", "obj1");
let lock_info = shard.get_lock_info(&obj1_key);
assert!(lock_info.is_some(), "obj1 should still be locked by blocking_owner");
}
}
+474
View File
@@ -0,0 +1,474 @@
// 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.
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, SystemTime};
use tokio::sync::Notify;
use crate::fast_lock::optimized_notify::OptimizedNotify;
use crate::fast_lock::types::{LockMode, LockPriority};
/// Optimized atomic lock state encoding in u64
/// Bits: [63:48] reserved | [47:32] writers_waiting | [31:16] readers_waiting | [15:8] readers_count | [7:1] flags | [0] writer_flag
const WRITER_FLAG_MASK: u64 = 0x1;
const READERS_SHIFT: u8 = 8;
const READERS_MASK: u64 = 0xFF << READERS_SHIFT; // Support up to 255 concurrent readers
const READERS_WAITING_SHIFT: u8 = 16;
const READERS_WAITING_MASK: u64 = 0xFFFF << READERS_WAITING_SHIFT;
const WRITERS_WAITING_SHIFT: u8 = 32;
const WRITERS_WAITING_MASK: u64 = 0xFFFF << WRITERS_WAITING_SHIFT;
// Fast path check masks
const NO_WRITER_AND_NO_WAITING_WRITERS: u64 = WRITER_FLAG_MASK | WRITERS_WAITING_MASK;
const COMPLETELY_UNLOCKED: u64 = 0;
/// Fast atomic lock state for single version
#[derive(Debug)]
pub struct AtomicLockState {
state: AtomicU64,
last_accessed: AtomicU64,
}
impl Default for AtomicLockState {
fn default() -> Self {
Self::new()
}
}
impl AtomicLockState {
pub fn new() -> Self {
Self {
state: AtomicU64::new(0),
last_accessed: AtomicU64::new(
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_secs(),
),
}
}
/// Check if fast path is available for given lock mode
#[inline(always)]
pub fn is_fast_path_available(&self, mode: LockMode) -> bool {
let state = self.state.load(Ordering::Relaxed); // Use Relaxed for better performance
match mode {
LockMode::Shared => {
// No writer and no waiting writers
(state & NO_WRITER_AND_NO_WAITING_WRITERS) == 0
}
LockMode::Exclusive => {
// Completely unlocked
state == COMPLETELY_UNLOCKED
}
}
}
/// Try to acquire shared lock (fast path)
pub fn try_acquire_shared(&self) -> bool {
self.update_access_time();
loop {
let current = self.state.load(Ordering::Acquire);
// Fast path check - cannot acquire if there's a writer or writers waiting
if (current & NO_WRITER_AND_NO_WAITING_WRITERS) != 0 {
return false;
}
let readers = self.readers_count(current);
if readers == 0xFF {
// Updated limit to 255
return false; // Too many readers
}
let new_state = current + (1 << READERS_SHIFT);
if self
.state
.compare_exchange_weak(current, new_state, Ordering::AcqRel, Ordering::Relaxed)
.is_ok()
{
return true;
}
}
}
/// Try to acquire exclusive lock (fast path)
pub fn try_acquire_exclusive(&self) -> bool {
self.update_access_time();
// Must be completely unlocked to acquire exclusive
let expected = 0;
let new_state = WRITER_FLAG_MASK;
self.state
.compare_exchange(expected, new_state, Ordering::AcqRel, Ordering::Relaxed)
.is_ok()
}
/// Release shared lock
pub fn release_shared(&self) -> bool {
loop {
let current = self.state.load(Ordering::Acquire);
let readers = self.readers_count(current);
if readers == 0 {
return false; // No shared lock to release
}
let new_state = current - (1 << READERS_SHIFT);
if self
.state
.compare_exchange_weak(current, new_state, Ordering::AcqRel, Ordering::Relaxed)
.is_ok()
{
self.update_access_time();
return true;
}
}
}
/// Release exclusive lock
pub fn release_exclusive(&self) -> bool {
loop {
let current = self.state.load(Ordering::Acquire);
if (current & WRITER_FLAG_MASK) == 0 {
return false; // No exclusive lock to release
}
let new_state = current & !WRITER_FLAG_MASK;
if self
.state
.compare_exchange_weak(current, new_state, Ordering::AcqRel, Ordering::Relaxed)
.is_ok()
{
self.update_access_time();
return true;
}
}
}
/// Increment waiting readers count
pub fn inc_readers_waiting(&self) {
loop {
let current = self.state.load(Ordering::Acquire);
let waiting = self.readers_waiting(current);
if waiting == 0xFFFF {
break; // Max waiting readers
}
let new_state = current + (1 << READERS_WAITING_SHIFT);
if self
.state
.compare_exchange_weak(current, new_state, Ordering::AcqRel, Ordering::Relaxed)
.is_ok()
{
break;
}
}
}
/// Decrement waiting readers count
pub fn dec_readers_waiting(&self) {
loop {
let current = self.state.load(Ordering::Acquire);
let waiting = self.readers_waiting(current);
if waiting == 0 {
break; // No waiting readers
}
let new_state = current - (1 << READERS_WAITING_SHIFT);
if self
.state
.compare_exchange_weak(current, new_state, Ordering::AcqRel, Ordering::Relaxed)
.is_ok()
{
break;
}
}
}
/// Increment waiting writers count
pub fn inc_writers_waiting(&self) {
loop {
let current = self.state.load(Ordering::Acquire);
let waiting = self.writers_waiting(current);
if waiting == 0xFFFF {
break; // Max waiting writers
}
let new_state = current + (1 << WRITERS_WAITING_SHIFT);
if self
.state
.compare_exchange_weak(current, new_state, Ordering::AcqRel, Ordering::Relaxed)
.is_ok()
{
break;
}
}
}
/// Decrement waiting writers count
pub fn dec_writers_waiting(&self) {
loop {
let current = self.state.load(Ordering::Acquire);
let waiting = self.writers_waiting(current);
if waiting == 0 {
break; // No waiting writers
}
let new_state = current - (1 << WRITERS_WAITING_SHIFT);
if self
.state
.compare_exchange_weak(current, new_state, Ordering::AcqRel, Ordering::Relaxed)
.is_ok()
{
break;
}
}
}
/// Check if lock is completely free
pub fn is_free(&self) -> bool {
let state = self.state.load(Ordering::Acquire);
state == 0
}
/// Check if anyone is waiting
pub fn has_waiters(&self) -> bool {
let state = self.state.load(Ordering::Acquire);
self.readers_waiting(state) > 0 || self.writers_waiting(state) > 0
}
/// Get last access time
pub fn last_accessed(&self) -> u64 {
self.last_accessed.load(Ordering::Relaxed)
}
pub fn update_access_time(&self) {
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_secs();
self.last_accessed.store(now, Ordering::Relaxed);
}
fn readers_count(&self, state: u64) -> u8 {
((state & READERS_MASK) >> READERS_SHIFT) as u8
}
fn readers_waiting(&self, state: u64) -> u16 {
((state & READERS_WAITING_MASK) >> READERS_WAITING_SHIFT) as u16
}
fn writers_waiting(&self, state: u64) -> u16 {
((state & WRITERS_WAITING_MASK) >> WRITERS_WAITING_SHIFT) as u16
}
}
/// Object lock state with version support - optimized memory layout
#[derive(Debug)]
#[repr(align(64))] // Align to cache line boundary
pub struct ObjectLockState {
// First cache line: Most frequently accessed data
/// Atomic state for fast operations
pub atomic_state: AtomicLockState,
// Second cache line: Notification mechanisms
/// Notification for readers (traditional)
pub read_notify: Notify,
/// Notification for writers (traditional)
pub write_notify: Notify,
/// Optimized notification system (optional)
pub optimized_notify: OptimizedNotify,
// Third cache line: Less frequently accessed data
/// Current owner of exclusive lock (if any)
pub current_owner: parking_lot::RwLock<Option<Arc<str>>>,
/// Shared owners - optimized for small number of readers
pub shared_owners: parking_lot::RwLock<smallvec::SmallVec<[Arc<str>; 4]>>,
/// Lock priority for conflict resolution
pub priority: parking_lot::RwLock<LockPriority>,
}
impl Default for ObjectLockState {
fn default() -> Self {
Self::new()
}
}
impl ObjectLockState {
pub fn new() -> Self {
Self {
atomic_state: AtomicLockState::new(),
read_notify: Notify::new(),
write_notify: Notify::new(),
optimized_notify: OptimizedNotify::new(),
current_owner: parking_lot::RwLock::new(None),
shared_owners: parking_lot::RwLock::new(smallvec::SmallVec::new()),
priority: parking_lot::RwLock::new(LockPriority::Normal),
}
}
/// Try fast path shared lock acquisition
pub fn try_acquire_shared_fast(&self, owner: &Arc<str>) -> bool {
if self.atomic_state.try_acquire_shared() {
self.atomic_state.update_access_time();
let mut shared = self.shared_owners.write();
if !shared.contains(owner) {
shared.push(owner.clone());
}
true
} else {
false
}
}
/// Try fast path exclusive lock acquisition
pub fn try_acquire_exclusive_fast(&self, owner: &Arc<str>) -> bool {
if self.atomic_state.try_acquire_exclusive() {
self.atomic_state.update_access_time();
let mut current = self.current_owner.write();
*current = Some(owner.clone());
true
} else {
false
}
}
/// Release shared lock
pub fn release_shared(&self, owner: &Arc<str>) -> bool {
let mut shared = self.shared_owners.write();
if let Some(pos) = shared.iter().position(|x| x.as_ref() == owner.as_ref()) {
shared.remove(pos);
if self.atomic_state.release_shared() {
// Notify waiting writers if no more readers
if shared.is_empty() {
drop(shared);
self.optimized_notify.notify_writer();
}
true
} else {
// Inconsistency - re-add owner
shared.push(owner.clone());
false
}
} else {
false
}
}
/// Release exclusive lock
pub fn release_exclusive(&self, owner: &Arc<str>) -> bool {
let mut current = self.current_owner.write();
if current.as_ref() == Some(owner) {
if self.atomic_state.release_exclusive() {
*current = None;
drop(current);
// Notify waiters using optimized system - prefer writers over readers
if self
.atomic_state
.writers_waiting(self.atomic_state.state.load(Ordering::Acquire))
> 0
{
self.optimized_notify.notify_writer();
} else {
self.optimized_notify.notify_readers();
}
true
} else {
false
}
} else {
false
}
}
/// Check if object is locked
pub fn is_locked(&self) -> bool {
!self.atomic_state.is_free()
}
/// Get current lock mode
pub fn current_mode(&self) -> Option<LockMode> {
let state = self.atomic_state.state.load(Ordering::Acquire);
if (state & WRITER_FLAG_MASK) != 0 {
Some(LockMode::Exclusive)
} else if self.atomic_state.readers_count(state) > 0 {
Some(LockMode::Shared)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_atomic_lock_state() {
let state = AtomicLockState::new();
// Test shared lock
assert!(state.try_acquire_shared());
assert!(state.try_acquire_shared());
assert!(!state.try_acquire_exclusive());
assert!(state.release_shared());
assert!(state.release_shared());
assert!(!state.release_shared());
// Test exclusive lock
assert!(state.try_acquire_exclusive());
assert!(!state.try_acquire_shared());
assert!(!state.try_acquire_exclusive());
assert!(state.release_exclusive());
assert!(!state.release_exclusive());
}
#[test]
fn test_object_lock_state() {
let state = ObjectLockState::new();
let owner1 = Arc::from("owner1");
let owner2 = Arc::from("owner2");
// Test shared locks
assert!(state.try_acquire_shared_fast(&owner1));
assert!(state.try_acquire_shared_fast(&owner2));
assert!(!state.try_acquire_exclusive_fast(&owner1));
assert!(state.release_shared(&owner1));
assert!(state.release_shared(&owner2));
// Test exclusive lock
assert!(state.try_acquire_exclusive_fast(&owner1));
assert!(!state.try_acquire_shared_fast(&owner2));
assert!(state.release_exclusive(&owner1));
}
}
+386
View File
@@ -0,0 +1,386 @@
// 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.
use once_cell::unsync::OnceCell;
use serde::{Deserialize, Serialize};
use smartstring::SmartString;
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use std::time::{Duration, SystemTime};
/// Object key for version-aware locking
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ObjectKey {
pub bucket: Arc<str>,
pub object: Arc<str>,
pub version: Option<Arc<str>>, // None means latest version
}
impl ObjectKey {
pub fn new(bucket: impl Into<Arc<str>>, object: impl Into<Arc<str>>) -> Self {
Self {
bucket: bucket.into(),
object: object.into(),
version: None,
}
}
pub fn with_version(bucket: impl Into<Arc<str>>, object: impl Into<Arc<str>>, version: impl Into<Arc<str>>) -> Self {
Self {
bucket: bucket.into(),
object: object.into(),
version: Some(version.into()),
}
}
pub fn as_latest(&self) -> Self {
Self {
bucket: self.bucket.clone(),
object: self.object.clone(),
version: None,
}
}
/// Get shard index from object key hash
pub fn shard_index(&self, shard_mask: usize) -> usize {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
self.hash(&mut hasher);
hasher.finish() as usize & shard_mask
}
}
/// Optimized object key using smart strings for better performance
#[derive(Debug, Clone)]
pub struct OptimizedObjectKey {
/// Bucket name - uses inline storage for small strings
pub bucket: SmartString<smartstring::LazyCompact>,
/// Object name - uses inline storage for small strings
pub object: SmartString<smartstring::LazyCompact>,
/// Version - optional for latest version semantics
pub version: Option<SmartString<smartstring::LazyCompact>>,
/// Cached hash to avoid recomputation
hash_cache: OnceCell<u64>,
}
// Manual implementations to handle OnceCell properly
impl PartialEq for OptimizedObjectKey {
fn eq(&self, other: &Self) -> bool {
self.bucket == other.bucket && self.object == other.object && self.version == other.version
}
}
impl Eq for OptimizedObjectKey {}
impl Hash for OptimizedObjectKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.bucket.hash(state);
self.object.hash(state);
self.version.hash(state);
}
}
impl PartialOrd for OptimizedObjectKey {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for OptimizedObjectKey {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.bucket
.cmp(&other.bucket)
.then_with(|| self.object.cmp(&other.object))
.then_with(|| self.version.cmp(&other.version))
}
}
impl OptimizedObjectKey {
pub fn new(
bucket: impl Into<SmartString<smartstring::LazyCompact>>,
object: impl Into<SmartString<smartstring::LazyCompact>>,
) -> Self {
Self {
bucket: bucket.into(),
object: object.into(),
version: None,
hash_cache: OnceCell::new(),
}
}
pub fn with_version(
bucket: impl Into<SmartString<smartstring::LazyCompact>>,
object: impl Into<SmartString<smartstring::LazyCompact>>,
version: impl Into<SmartString<smartstring::LazyCompact>>,
) -> Self {
Self {
bucket: bucket.into(),
object: object.into(),
version: Some(version.into()),
hash_cache: OnceCell::new(),
}
}
/// Get shard index with cached hash for better performance
pub fn shard_index(&self, shard_mask: usize) -> usize {
let hash = *self.hash_cache.get_or_init(|| {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
self.hash(&mut hasher);
hasher.finish()
});
(hash as usize) & shard_mask
}
/// Reset hash cache if key is modified
pub fn invalidate_cache(&mut self) {
self.hash_cache = OnceCell::new();
}
/// Convert from regular ObjectKey
pub fn from_object_key(key: &ObjectKey) -> Self {
Self {
bucket: SmartString::from(key.bucket.as_ref()),
object: SmartString::from(key.object.as_ref()),
version: key.version.as_ref().map(|v| SmartString::from(v.as_ref())),
hash_cache: OnceCell::new(),
}
}
/// Convert to regular ObjectKey
pub fn to_object_key(&self) -> ObjectKey {
ObjectKey {
bucket: Arc::from(self.bucket.as_str()),
object: Arc::from(self.object.as_str()),
version: self.version.as_ref().map(|v| Arc::from(v.as_str())),
}
}
}
impl std::fmt::Display for ObjectKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(version) = &self.version {
write!(f, "{}/{}@{}", self.bucket, self.object, version)
} else {
write!(f, "{}/{}@latest", self.bucket, self.object)
}
}
}
/// Lock type for object operations
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum LockMode {
/// Shared lock for read operations
Shared,
/// Exclusive lock for write operations
Exclusive,
}
/// Lock request for object
#[derive(Debug, Clone)]
pub struct ObjectLockRequest {
pub key: ObjectKey,
pub mode: LockMode,
pub owner: Arc<str>,
pub acquire_timeout: Duration,
pub lock_timeout: Duration,
pub priority: LockPriority,
}
impl ObjectLockRequest {
pub fn new_read(bucket: impl Into<Arc<str>>, object: impl Into<Arc<str>>, owner: impl Into<Arc<str>>) -> Self {
Self {
key: ObjectKey::new(bucket, object),
mode: LockMode::Shared,
owner: owner.into(),
acquire_timeout: crate::fast_lock::DEFAULT_ACQUIRE_TIMEOUT,
lock_timeout: crate::fast_lock::DEFAULT_LOCK_TIMEOUT,
priority: LockPriority::Normal,
}
}
pub fn new_write(bucket: impl Into<Arc<str>>, object: impl Into<Arc<str>>, owner: impl Into<Arc<str>>) -> Self {
Self {
key: ObjectKey::new(bucket, object),
mode: LockMode::Exclusive,
owner: owner.into(),
acquire_timeout: crate::fast_lock::DEFAULT_ACQUIRE_TIMEOUT,
lock_timeout: crate::fast_lock::DEFAULT_LOCK_TIMEOUT,
priority: LockPriority::Normal,
}
}
pub fn with_version(mut self, version: impl Into<Arc<str>>) -> Self {
self.key.version = Some(version.into());
self
}
pub fn with_acquire_timeout(mut self, timeout: Duration) -> Self {
self.acquire_timeout = timeout;
self
}
pub fn with_lock_timeout(mut self, timeout: Duration) -> Self {
self.lock_timeout = timeout;
self
}
pub fn with_priority(mut self, priority: LockPriority) -> Self {
self.priority = priority;
self
}
}
/// Lock priority for conflict resolution
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
pub enum LockPriority {
Low = 1,
#[default]
Normal = 2,
High = 3,
Critical = 4,
}
/// Lock acquisition result
#[derive(Debug)]
pub enum LockResult {
/// Lock acquired successfully
Acquired,
/// Lock acquisition failed due to timeout
Timeout,
/// Lock acquisition failed due to conflict
Conflict {
current_owner: Arc<str>,
current_mode: LockMode,
},
}
/// Configuration for the lock manager
#[derive(Debug, Clone)]
pub struct LockConfig {
pub shard_count: usize,
pub default_lock_timeout: Duration,
pub default_acquire_timeout: Duration,
pub cleanup_interval: Duration,
pub max_idle_time: Duration,
pub enable_metrics: bool,
}
impl Default for LockConfig {
fn default() -> Self {
Self {
shard_count: crate::fast_lock::DEFAULT_SHARD_COUNT,
default_lock_timeout: crate::fast_lock::DEFAULT_LOCK_TIMEOUT,
default_acquire_timeout: crate::fast_lock::DEFAULT_ACQUIRE_TIMEOUT,
cleanup_interval: crate::fast_lock::CLEANUP_INTERVAL,
max_idle_time: Duration::from_secs(300), // 5 minutes
enable_metrics: true,
}
}
}
/// Lock information for monitoring
#[derive(Debug, Clone)]
pub struct ObjectLockInfo {
pub key: ObjectKey,
pub mode: LockMode,
pub owner: Arc<str>,
pub acquired_at: SystemTime,
pub expires_at: SystemTime,
pub priority: LockPriority,
}
/// Batch lock operation request
#[derive(Debug)]
pub struct BatchLockRequest {
pub requests: Vec<ObjectLockRequest>,
pub owner: Arc<str>,
pub all_or_nothing: bool, // If true, either all locks are acquired or none
}
impl BatchLockRequest {
pub fn new(owner: impl Into<Arc<str>>) -> Self {
Self {
requests: Vec::new(),
owner: owner.into(),
all_or_nothing: true,
}
}
pub fn add_read_lock(mut self, bucket: impl Into<Arc<str>>, object: impl Into<Arc<str>>) -> Self {
self.requests
.push(ObjectLockRequest::new_read(bucket, object, self.owner.clone()));
self
}
pub fn add_write_lock(mut self, bucket: impl Into<Arc<str>>, object: impl Into<Arc<str>>) -> Self {
self.requests
.push(ObjectLockRequest::new_write(bucket, object, self.owner.clone()));
self
}
pub fn with_all_or_nothing(mut self, enable: bool) -> Self {
self.all_or_nothing = enable;
self
}
}
/// Batch lock operation result
#[derive(Debug)]
pub struct BatchLockResult {
pub successful_locks: Vec<ObjectKey>,
pub failed_locks: Vec<(ObjectKey, LockResult)>,
pub all_acquired: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_object_key() {
let key1 = ObjectKey::new("bucket1", "object1");
let key2 = ObjectKey::with_version("bucket1", "object1", "v1");
assert_eq!(key1.bucket.as_ref(), "bucket1");
assert_eq!(key1.object.as_ref(), "object1");
assert_eq!(key1.version, None);
assert_eq!(key2.version.as_ref().unwrap().as_ref(), "v1");
// Test display
assert_eq!(key1.to_string(), "bucket1/object1@latest");
assert_eq!(key2.to_string(), "bucket1/object1@v1");
}
#[test]
fn test_lock_request() {
let req = ObjectLockRequest::new_read("bucket", "object", "owner")
.with_version("v1")
.with_priority(LockPriority::High);
assert_eq!(req.mode, LockMode::Shared);
assert_eq!(req.priority, LockPriority::High);
assert_eq!(req.key.version.as_ref().unwrap().as_ref(), "v1");
}
#[test]
fn test_batch_request() {
let batch = BatchLockRequest::new("owner")
.add_read_lock("bucket", "obj1")
.add_write_lock("bucket", "obj2");
assert_eq!(batch.requests.len(), 2);
assert_eq!(batch.requests[0].mode, LockMode::Shared);
assert_eq!(batch.requests[1].mode, LockMode::Exclusive);
}
}
+284 -9
View File
@@ -22,8 +22,8 @@ pub mod namespace;
// Abstraction Layer Modules
pub mod client;
// Local Layer Modules
pub mod local;
// Fast Lock System (New High-Performance Implementation)
pub mod fast_lock;
// Core Modules
pub mod error;
@@ -40,8 +40,12 @@ pub use crate::{
client::{LockClient, local::LocalClient, remote::RemoteClient},
// Error types
error::{LockError, Result},
// Fast Lock System exports
fast_lock::{
BatchLockRequest, BatchLockResult, DisabledLockManager, FastLockGuard, FastObjectLockManager, LockManager, LockMode,
LockResult, ObjectKey, ObjectLockInfo, ObjectLockRequest, metrics::AggregatedMetrics,
},
guard::LockGuard,
local::LocalLockMap,
// Main components
namespace::{NamespaceLock, NamespaceLockManager},
// Core types
@@ -65,18 +69,205 @@ pub const BUILD_TIMESTAMP: &str = "unknown";
pub const MAX_DELETE_LIST: usize = 1000;
// ============================================================================
// Global Lock Map
// Global FastLock Manager
// ============================================================================
// Global singleton lock map shared across all lock implementations
// Global singleton FastLock manager shared across all lock implementations
use once_cell::sync::OnceCell;
use std::sync::Arc;
static GLOBAL_LOCK_MAP: OnceCell<Arc<local::LocalLockMap>> = OnceCell::new();
/// Enum wrapper for different lock manager implementations
pub enum GlobalLockManager {
Enabled(Arc<fast_lock::FastObjectLockManager>),
Disabled(fast_lock::DisabledLockManager),
}
/// Get the global shared lock map instance
pub fn get_global_lock_map() -> Arc<local::LocalLockMap> {
GLOBAL_LOCK_MAP.get_or_init(|| Arc::new(local::LocalLockMap::new())).clone()
impl Default for GlobalLockManager {
fn default() -> Self {
Self::new()
}
}
impl GlobalLockManager {
/// Create a lock manager based on environment variable configuration
pub fn new() -> Self {
// Check RUSTFS_ENABLE_LOCKS environment variable
let locks_enabled = std::env::var("RUSTFS_ENABLE_LOCKS")
.unwrap_or_else(|_| "true".to_string())
.to_lowercase();
match locks_enabled.as_str() {
"false" | "0" | "no" | "off" | "disabled" => {
tracing::info!("Lock system disabled via RUSTFS_ENABLE_LOCKS environment variable");
Self::Disabled(fast_lock::DisabledLockManager::new())
}
_ => {
tracing::info!("Lock system enabled");
Self::Enabled(Arc::new(fast_lock::FastObjectLockManager::new()))
}
}
}
/// Check if the lock manager is disabled
pub fn is_disabled(&self) -> bool {
matches!(self, Self::Disabled(_))
}
/// Get the FastObjectLockManager if enabled, otherwise returns None
pub fn as_fast_lock_manager(&self) -> Option<Arc<fast_lock::FastObjectLockManager>> {
match self {
Self::Enabled(manager) => Some(manager.clone()),
Self::Disabled(_) => None,
}
}
}
#[async_trait::async_trait]
impl fast_lock::LockManager for GlobalLockManager {
async fn acquire_lock(
&self,
request: fast_lock::ObjectLockRequest,
) -> std::result::Result<fast_lock::FastLockGuard, fast_lock::LockResult> {
match self {
Self::Enabled(manager) => manager.acquire_lock(request).await,
Self::Disabled(manager) => manager.acquire_lock(request).await,
}
}
async fn acquire_read_lock(
&self,
bucket: impl Into<Arc<str>> + Send,
object: impl Into<Arc<str>> + Send,
owner: impl Into<Arc<str>> + Send,
) -> std::result::Result<fast_lock::FastLockGuard, fast_lock::LockResult> {
match self {
Self::Enabled(manager) => manager.acquire_read_lock(bucket, object, owner).await,
Self::Disabled(manager) => manager.acquire_read_lock(bucket, object, owner).await,
}
}
async fn acquire_read_lock_versioned(
&self,
bucket: impl Into<Arc<str>> + Send,
object: impl Into<Arc<str>> + Send,
version: impl Into<Arc<str>> + Send,
owner: impl Into<Arc<str>> + Send,
) -> std::result::Result<fast_lock::FastLockGuard, fast_lock::LockResult> {
match self {
Self::Enabled(manager) => manager.acquire_read_lock_versioned(bucket, object, version, owner).await,
Self::Disabled(manager) => manager.acquire_read_lock_versioned(bucket, object, version, owner).await,
}
}
async fn acquire_write_lock(
&self,
bucket: impl Into<Arc<str>> + Send,
object: impl Into<Arc<str>> + Send,
owner: impl Into<Arc<str>> + Send,
) -> std::result::Result<fast_lock::FastLockGuard, fast_lock::LockResult> {
match self {
Self::Enabled(manager) => manager.acquire_write_lock(bucket, object, owner).await,
Self::Disabled(manager) => manager.acquire_write_lock(bucket, object, owner).await,
}
}
async fn acquire_write_lock_versioned(
&self,
bucket: impl Into<Arc<str>> + Send,
object: impl Into<Arc<str>> + Send,
version: impl Into<Arc<str>> + Send,
owner: impl Into<Arc<str>> + Send,
) -> std::result::Result<fast_lock::FastLockGuard, fast_lock::LockResult> {
match self {
Self::Enabled(manager) => manager.acquire_write_lock_versioned(bucket, object, version, owner).await,
Self::Disabled(manager) => manager.acquire_write_lock_versioned(bucket, object, version, owner).await,
}
}
async fn acquire_locks_batch(&self, batch_request: fast_lock::BatchLockRequest) -> fast_lock::BatchLockResult {
match self {
Self::Enabled(manager) => manager.acquire_locks_batch(batch_request).await,
Self::Disabled(manager) => manager.acquire_locks_batch(batch_request).await,
}
}
fn get_lock_info(&self, key: &fast_lock::ObjectKey) -> Option<fast_lock::ObjectLockInfo> {
match self {
Self::Enabled(manager) => manager.get_lock_info(key),
Self::Disabled(manager) => manager.get_lock_info(key),
}
}
fn get_metrics(&self) -> AggregatedMetrics {
match self {
Self::Enabled(manager) => manager.get_metrics(),
Self::Disabled(manager) => manager.get_metrics(),
}
}
fn total_lock_count(&self) -> usize {
match self {
Self::Enabled(manager) => manager.total_lock_count(),
Self::Disabled(manager) => manager.total_lock_count(),
}
}
fn get_pool_stats(&self) -> Vec<(u64, u64, u64, usize)> {
match self {
Self::Enabled(manager) => manager.get_pool_stats(),
Self::Disabled(manager) => manager.get_pool_stats(),
}
}
async fn cleanup_expired(&self) -> usize {
match self {
Self::Enabled(manager) => manager.cleanup_expired().await,
Self::Disabled(manager) => manager.cleanup_expired().await,
}
}
async fn cleanup_expired_traditional(&self) -> usize {
match self {
Self::Enabled(manager) => manager.cleanup_expired_traditional().await,
Self::Disabled(manager) => manager.cleanup_expired_traditional().await,
}
}
async fn shutdown(&self) {
match self {
Self::Enabled(manager) => manager.shutdown().await,
Self::Disabled(manager) => manager.shutdown().await,
}
}
fn is_disabled(&self) -> bool {
match self {
Self::Enabled(manager) => manager.is_disabled(),
Self::Disabled(manager) => manager.is_disabled(),
}
}
}
static GLOBAL_LOCK_MANAGER: OnceCell<Arc<GlobalLockManager>> = OnceCell::new();
/// Get the global shared lock manager instance
///
/// Returns either FastObjectLockManager or DisabledLockManager based on
/// the RUSTFS_ENABLE_LOCKS environment variable.
pub fn get_global_lock_manager() -> Arc<GlobalLockManager> {
GLOBAL_LOCK_MANAGER.get_or_init(|| Arc::new(GlobalLockManager::new())).clone()
}
/// Get the global shared FastLock manager instance (legacy)
///
/// This function is deprecated. Use get_global_lock_manager() instead.
/// Returns FastObjectLockManager when locks are enabled, or panics when disabled.
#[deprecated(note = "Use get_global_lock_manager() instead")]
pub fn get_global_fast_lock_manager() -> Arc<fast_lock::FastObjectLockManager> {
let manager = get_global_lock_manager();
manager.as_fast_lock_manager().unwrap_or_else(|| {
panic!("Cannot get FastObjectLockManager when locks are disabled. Use get_global_lock_manager() instead.");
})
}
// ============================================================================
@@ -89,3 +280,87 @@ pub fn create_namespace_lock(namespace: String, _distributed: bool) -> Namespace
// This function just creates an empty NamespaceLock
NamespaceLock::new(namespace)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_global_lock_manager_basic() {
let manager = get_global_lock_manager();
// Should be able to acquire locks
let guard = manager.acquire_read_lock("bucket", "object", "owner").await;
assert!(guard.is_ok());
// Test metrics
let _metrics = manager.get_metrics();
// Even if locks are disabled, metrics should be available (empty or real)
// shard_count is usize so always >= 0
}
#[tokio::test]
async fn test_disabled_manager_direct() {
let manager = fast_lock::DisabledLockManager::new();
// All operations should succeed immediately
let guard = manager.acquire_read_lock("bucket", "object", "owner").await;
assert!(guard.is_ok());
assert!(guard.unwrap().is_disabled());
// Metrics should be empty
let metrics = manager.get_metrics();
assert!(metrics.is_empty());
assert_eq!(manager.total_lock_count(), 0);
}
#[tokio::test]
async fn test_enabled_manager_direct() {
let manager = fast_lock::FastObjectLockManager::new();
// Operations should work normally
let guard = manager.acquire_read_lock("bucket", "object", "owner").await;
assert!(guard.is_ok());
assert!(!guard.unwrap().is_disabled());
// Should have real metrics
let _metrics = manager.get_metrics();
// Note: total_lock_count might be > 0 due to previous lock acquisition
}
#[tokio::test]
async fn test_global_manager_enum_wrapper() {
// Test the GlobalLockManager enum directly
let enabled_manager = GlobalLockManager::Enabled(Arc::new(fast_lock::FastObjectLockManager::new()));
let disabled_manager = GlobalLockManager::Disabled(fast_lock::DisabledLockManager::new());
assert!(!enabled_manager.is_disabled());
assert!(disabled_manager.is_disabled());
// Test trait methods work for both
let enabled_guard = enabled_manager.acquire_read_lock("bucket", "obj", "owner").await;
let disabled_guard = disabled_manager.acquire_read_lock("bucket", "obj", "owner").await;
assert!(enabled_guard.is_ok());
assert!(disabled_guard.is_ok());
assert!(!enabled_guard.unwrap().is_disabled());
assert!(disabled_guard.unwrap().is_disabled());
}
#[tokio::test]
async fn test_batch_operations_work() {
let manager = get_global_lock_manager();
let batch = fast_lock::BatchLockRequest::new("owner")
.add_read_lock("bucket", "obj1")
.add_write_lock("bucket", "obj2");
let result = manager.acquire_locks_batch(batch).await;
// Should succeed regardless of whether locks are enabled or disabled
assert!(result.all_acquired);
assert_eq!(result.successful_locks.len(), 2);
assert!(result.failed_locks.is_empty());
}
}
File diff suppressed because it is too large Load Diff
+6 -3
View File
@@ -532,7 +532,10 @@ pub type Timestamp = u64;
/// Get current timestamp
pub fn current_timestamp() -> Timestamp {
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs()
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_secs()
}
/// Convert timestamp to system time
@@ -542,7 +545,7 @@ pub fn timestamp_to_system_time(timestamp: Timestamp) -> SystemTime {
/// Convert system time to timestamp
pub fn system_time_to_timestamp(time: SystemTime) -> Timestamp {
time.duration_since(UNIX_EPOCH).unwrap().as_secs()
time.duration_since(UNIX_EPOCH).unwrap_or(Duration::ZERO).as_secs()
}
/// Deadlock detection result structure
@@ -685,7 +688,7 @@ mod tests {
let converted = timestamp_to_system_time(timestamp);
// Allow for small time differences
let diff = now.duration_since(converted).unwrap();
let diff = now.duration_since(converted).unwrap_or(Duration::ZERO);
assert!(diff < Duration::from_secs(1));
}
+1 -1
View File
@@ -69,7 +69,7 @@ impl EventNotifier {
target_list_guard
.keys()
.iter()
.map(|target_id| target_id.to_arn(region).to_arn_string())
.map(|target_id| target_id.to_arn(region).to_string())
.collect()
}
+2 -2
View File
@@ -101,8 +101,8 @@ impl BucketNotificationConfig {
for target_id in target_id_set {
// Construct the ARN string for this target_id and self.region
let arn_to_check = target_id.to_arn(&self.region); // Assuming TargetID has to_arn
if !arn_list.contains(&arn_to_check.to_arn_string()) {
return Err(BucketNotificationConfigError::ArnNotFound(arn_to_check.to_arn_string()));
if !arn_list.contains(&arn_to_check.to_string()) {
return Err(BucketNotificationConfigError::ArnNotFound(arn_to_check.to_string()));
}
}
}
+4 -4
View File
@@ -168,7 +168,7 @@ impl QueueConfig {
// Validate ARN (similar to Go's Queue.Validate)
// The Go code checks targetList.Exists(q.ARN.TargetID)
// Here we check against a provided arn_list
let _config_arn_str = self.arn.to_arn_string();
let _config_arn_str = self.arn.to_string();
if !self.arn.region.is_empty() && self.arn.region != region {
return Err(ParseConfigError::UnknownRegion(self.arn.region.clone()));
}
@@ -187,8 +187,8 @@ impl QueueConfig {
partition: self.arn.partition.clone(), // or default "rustfs"
};
if !arn_list.contains(&effective_arn.to_arn_string()) {
return Err(ParseConfigError::ArnNotFound(effective_arn.to_arn_string()));
if !arn_list.contains(&effective_arn.to_string()) {
return Err(ParseConfigError::ArnNotFound(effective_arn.to_string()));
}
Ok(())
}
@@ -266,7 +266,7 @@ impl NotificationConfiguration {
queue_config.validate(current_region, arn_list)?;
let queue_key = (
queue_config.id.clone(),
queue_config.arn.to_arn_string(), // Assuming that the ARN structure implements Display or ToString
queue_config.arn.to_string(), // Assuming that the ARN structure implements Display or ToString
);
if !unique_queues.insert(queue_key.clone()) {
return Err(ParseConfigError::DuplicateQueueConfiguration(queue_key.0, queue_key.1));
+13 -5
View File
@@ -29,8 +29,9 @@ fn main() -> Result<(), AnyError> {
let need_compile = match version.compare_ext(&VERSION_PROTOBUF) {
Ok(cmp::Ordering::Greater) => true,
Ok(_) => {
let version_err = Version::build_error_message(&version, &VERSION_PROTOBUF).unwrap();
println!("cargo:warning=Tool `protoc` {version_err}, skip compiling.");
if let Some(version_err) = Version::build_error_message(&version, &VERSION_PROTOBUF) {
println!("cargo:warning=Tool `protoc` {version_err}, skip compiling.");
}
false
}
Err(version_err) => {
@@ -144,8 +145,9 @@ fn compile_flatbuffers_models<P: AsRef<Path>, S: AsRef<str>>(
let need_compile = match version.compare_ext(&VERSION_FLATBUFFERS) {
Ok(cmp::Ordering::Greater) => true,
Ok(_) => {
let version_err = Version::build_error_message(&version, &VERSION_FLATBUFFERS).unwrap();
println!("cargo:warning=Tool `{flatc_path}` {version_err}, skip compiling.");
if let Some(version_err) = Version::build_error_message(&version, &VERSION_FLATBUFFERS) {
println!("cargo:warning=Tool `{flatc_path}` {version_err}, skip compiling.");
}
false
}
Err(version_err) => {
@@ -253,7 +255,13 @@ impl Version {
} else {
match self.compare_major_version(expected_version) {
cmp::Ordering::Greater => Ok(cmp::Ordering::Greater),
_ => Err(Self::build_error_message(self, expected_version).unwrap()),
_ => {
if let Some(error_msg) = Self::build_error_message(self, expected_version) {
Err(error_msg)
} else {
Err("Unknown version comparison error".to_string())
}
}
}
}
}
+1 -1
View File
@@ -212,7 +212,7 @@ impl Serialize for ARN {
where
S: Serializer,
{
serializer.serialize_str(&self.to_arn_string())
serializer.serialize_str(&self.to_string())
}
}
+26 -21
View File
@@ -27,44 +27,49 @@ categories = ["web-programming", "development-tools", "cryptography"]
[dependencies]
base64-simd = { workspace = true, optional = true }
blake3 = { workspace = true, optional = true }
crc32fast.workspace = true
brotli = { workspace = true, optional = true }
bytes = { workspace = true, optional = true }
crc32fast = { workspace = true }
flate2 = { workspace = true, optional = true }
futures = { workspace = true, optional = true }
hex-simd = { workspace = true, optional = true }
highway = { workspace = true, optional = true }
hickory-resolver = { workspace = true, optional = true }
hickory-proto = { workspace = true, optional = true }
hmac = { workspace = true, optional = true }
hyper = { workspace = true, optional = true }
hyper-util = { workspace = true, optional = true }
local-ip-address = { workspace = true, optional = true }
lz4 = { workspace = true, optional = true }
md-5 = { workspace = true, optional = true }
moka = { workspace = true, optional = true, features = ["future"] }
netif = { workspace = true, optional = true }
nix = { workspace = true, optional = true }
rand = { workspace = true, optional = true }
regex = { workspace = true, optional = true }
rustfs-config = { workspace = true, features = ["constants"] }
rustls = { workspace = true, optional = true }
rustls-pemfile = { workspace = true, optional = true }
rustls-pki-types = { workspace = true, optional = true }
s3s = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
siphasher = { workspace = true, optional = true }
tempfile = { workspace = true, optional = true }
tokio = { workspace = true, optional = true, features = ["io-util", "macros"] }
tracing = { workspace = true }
url = { workspace = true, optional = true }
flate2 = { workspace = true, optional = true }
brotli = { workspace = true, optional = true }
zstd = { workspace = true, optional = true }
snap = { workspace = true, optional = true }
lz4 = { workspace = true, optional = true }
rand = { workspace = true, optional = true }
futures = { workspace = true, optional = true }
transform-stream = { workspace = true, optional = true }
bytes = { workspace = true, optional = true }
sysinfo = { workspace = true, optional = true }
hyper-util = { workspace = true, optional = true }
sha1 = { workspace = true, optional = true }
sha2 = { workspace = true, optional = true }
hmac = { workspace = true, optional = true }
s3s = { workspace = true, optional = true }
hyper = { workspace = true, optional = true }
siphasher = { workspace = true, optional = true }
snap = { workspace = true, optional = true }
sysinfo = { workspace = true, optional = true }
tempfile = { workspace = true, optional = true }
thiserror = { workspace = true, optional = true }
tokio = { workspace = true, optional = true, features = ["io-util", "macros"] }
tracing = { workspace = true }
transform-stream = { workspace = true, optional = true }
url = { workspace = true, optional = true }
zstd = { workspace = true, optional = true }
[dev-dependencies]
tempfile = { workspace = true }
rand = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
[target.'cfg(windows)'.dependencies]
winapi = { workspace = true, optional = true, features = ["std", "fileapi", "minwindef", "ntdef", "winnt"] }
@@ -76,7 +81,7 @@ workspace = true
default = ["ip"] # features that are enabled by default
ip = ["dep:local-ip-address"] # ip characteristics and their dependencies
tls = ["dep:rustls", "dep:rustls-pemfile", "dep:rustls-pki-types"] # tls characteristics and their dependencies
net = ["ip", "dep:url", "dep:netif", "dep:futures", "dep:transform-stream", "dep:bytes", "dep:s3s", "dep:hyper", "dep:hyper-util"] # empty network features
net = ["ip", "dep:url", "dep:netif", "dep:futures", "dep:transform-stream", "dep:bytes", "dep:s3s", "dep:hyper", "dep:hyper-util", "dep:hickory-resolver", "dep:hickory-proto", "dep:moka", "dep:thiserror"] # network features with DNS resolver
io = ["dep:tokio"]
path = []
notify = ["dep:hyper", "dep:s3s"] # file system notification features
+473
View File
@@ -0,0 +1,473 @@
// 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.
//! Layered DNS resolution utility for Kubernetes environments
//!
//! This module provides robust DNS resolution with multiple fallback layers:
//! 1. Local cache (Moka) for previously resolved results
//! 2. System DNS resolver (container/host adaptive) using hickory-resolver
//! 3. Public DNS servers as final fallback (8.8.8.8, 1.1.1.1) using hickory-resolver with TLS
//!
//! The resolver is designed to handle 5-level or deeper domain names that may fail
//! in Kubernetes environments due to CoreDNS configuration, DNS recursion limits,
//! or network-related issues. Uses hickory-resolver for actual DNS queries with TLS support.
use hickory_resolver::Resolver;
use hickory_resolver::config::ResolverConfig;
use hickory_resolver::name_server::TokioConnectionProvider;
use moka::future::Cache;
use std::net::IpAddr;
use std::sync::OnceLock;
use std::time::Duration;
use tracing::{debug, error, info, instrument, warn};
/// Maximum FQDN length according to RFC standards
const MAX_FQDN_LENGTH: usize = 253;
/// Maximum DNS label length according to RFC standards
const MAX_LABEL_LENGTH: usize = 63;
/// Cache entry TTL in seconds
const CACHE_TTL_SECONDS: u64 = 300; // 5 minutes
/// Maximum cache size (number of entries)
const MAX_CACHE_SIZE: u64 = 10000;
/// DNS resolution error types with detailed context and tracing information
#[derive(Debug, thiserror::Error)]
pub enum DnsError {
#[error("Invalid domain format: {reason}")]
InvalidFormat { reason: String },
#[error("Local cache miss for domain: {domain}")]
CacheMiss { domain: String },
#[error("System DNS resolution failed for domain: {domain} - {source}")]
SystemDnsFailed {
domain: String,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("Public DNS resolution failed for domain: {domain} - {source}")]
PublicDnsFailed {
domain: String,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error(
"All DNS resolution attempts failed for domain: {domain}. Please check your domain spelling, network connectivity, or DNS configuration"
)]
AllAttemptsFailed { domain: String },
#[error("DNS resolver initialization failed: {source}")]
InitializationFailed {
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("DNS configuration error: {source}")]
ConfigurationError {
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
}
/// Layered DNS resolver with caching and multiple fallback strategies
pub struct LayeredDnsResolver {
/// Local cache for resolved domains using Moka for high performance
cache: Cache<String, Vec<IpAddr>>,
/// System DNS resolver using hickory-resolver with default configuration
system_resolver: Resolver<TokioConnectionProvider>,
/// Public DNS resolver using hickory-resolver with Cloudflare DNS servers
public_resolver: Resolver<TokioConnectionProvider>,
}
impl LayeredDnsResolver {
/// Create a new layered DNS resolver with automatic DNS configuration detection
#[instrument(skip_all)]
pub async fn new() -> Result<Self, DnsError> {
info!("Initializing layered DNS resolver with hickory-resolver, Moka cache and public DNS fallback");
// Create Moka cache with TTL and size limits
let cache = Cache::builder()
.time_to_live(Duration::from_secs(CACHE_TTL_SECONDS))
.max_capacity(MAX_CACHE_SIZE)
.build();
// Create system DNS resolver with default configuration (auto-detects container/host DNS)
let system_resolver =
Resolver::builder_with_config(ResolverConfig::default(), TokioConnectionProvider::default()).build();
let mut config = ResolverConfig::cloudflare_tls();
for ns in ResolverConfig::google_tls().name_servers() {
config.add_name_server(ns.clone())
}
// Create public DNS resolver using Cloudflare DNS with TLS support
let public_resolver = Resolver::builder_with_config(config, TokioConnectionProvider::default()).build();
info!("DNS resolver initialized successfully with hickory-resolver system and Cloudflare TLS public fallback");
Ok(Self {
cache,
system_resolver,
public_resolver,
})
}
/// Validate domain format according to RFC standards
#[instrument(skip_all, fields(domain = %domain))]
fn validate_domain_format(domain: &str) -> Result<(), DnsError> {
// Check FQDN length
if domain.len() > MAX_FQDN_LENGTH {
return Err(DnsError::InvalidFormat {
reason: format!("FQDN must not exceed {} bytes, got {} bytes", MAX_FQDN_LENGTH, domain.len()),
});
}
// Check each label length
for label in domain.split('.') {
if label.len() > MAX_LABEL_LENGTH {
return Err(DnsError::InvalidFormat {
reason: format!(
"Each label must not exceed {} bytes, label '{}' has {} bytes",
MAX_LABEL_LENGTH,
label,
label.len()
),
});
}
}
// Check for empty labels (except trailing dot)
let labels: Vec<&str> = domain.trim_end_matches('.').split('.').collect();
for label in &labels {
if label.is_empty() {
return Err(DnsError::InvalidFormat {
reason: "Domain contains empty labels".to_string(),
});
}
}
Ok(())
}
/// Check local cache for resolved domain
#[instrument(skip_all, fields(domain = %domain))]
async fn check_cache(&self, domain: &str) -> Option<Vec<IpAddr>> {
match self.cache.get(domain).await {
Some(ips) => {
debug!("DNS cache hit for domain: {}, found {} IPs", domain, ips.len());
Some(ips)
}
None => {
debug!("DNS cache miss for domain: {}", domain);
None
}
}
}
/// Update local cache with resolved IPs
#[instrument(skip_all, fields(domain = %domain, ip_count = ips.len()))]
async fn update_cache(&self, domain: &str, ips: Vec<IpAddr>) {
self.cache.insert(domain.to_string(), ips.clone()).await;
debug!("DNS cache updated for domain: {} with {} IPs", domain, ips.len());
}
/// Get cache statistics for monitoring
#[instrument(skip_all)]
pub async fn cache_stats(&self) -> (u64, u64) {
let entry_count = self.cache.entry_count();
let weighted_size = self.cache.weighted_size();
debug!("DNS cache stats - entries: {}, weighted_size: {}", entry_count, weighted_size);
(entry_count, weighted_size)
}
/// Manually invalidate cache entries (useful for testing or forced refresh)
#[instrument(skip_all)]
pub async fn invalidate_cache(&self) {
self.cache.invalidate_all();
info!("DNS cache invalidated");
}
/// Resolve domain using system DNS (cluster/host DNS configuration) with hickory-resolver
#[instrument(skip_all, fields(domain = %domain))]
async fn resolve_with_system_dns(&self, domain: &str) -> Result<Vec<IpAddr>, DnsError> {
debug!("Attempting system DNS resolution for domain: {} using hickory-resolver", domain);
match self.system_resolver.lookup_ip(domain).await {
Ok(lookup) => {
let ips: Vec<IpAddr> = lookup.iter().collect();
if !ips.is_empty() {
info!("System DNS resolution successful for domain: {} -> {} IPs", domain, ips.len());
debug!("System DNS resolved IPs: {:?}", ips);
Ok(ips)
} else {
warn!("System DNS returned empty result for domain: {}", domain);
Err(DnsError::SystemDnsFailed {
domain: domain.to_string(),
source: "No IP addresses found".to_string().into(),
})
}
}
Err(e) => {
warn!("System DNS resolution failed for domain: {} - {}", domain, e);
Err(DnsError::SystemDnsFailed {
domain: domain.to_string(),
source: Box::new(e),
})
}
}
}
/// Resolve domain using public DNS servers (Cloudflare TLS DNS) with hickory-resolver
#[instrument(skip_all, fields(domain = %domain))]
async fn resolve_with_public_dns(&self, domain: &str) -> Result<Vec<IpAddr>, DnsError> {
debug!(
"Attempting public DNS resolution for domain: {} using hickory-resolver with TLS-enabled Cloudflare DNS",
domain
);
match self.public_resolver.lookup_ip(domain).await {
Ok(lookup) => {
let ips: Vec<IpAddr> = lookup.iter().collect();
if !ips.is_empty() {
info!("Public DNS resolution successful for domain: {} -> {} IPs", domain, ips.len());
debug!("Public DNS resolved IPs: {:?}", ips);
Ok(ips)
} else {
warn!("Public DNS returned empty result for domain: {}", domain);
Err(DnsError::PublicDnsFailed {
domain: domain.to_string(),
source: "No IP addresses found".to_string().into(),
})
}
}
Err(e) => {
error!("Public DNS resolution failed for domain: {} - {}", domain, e);
Err(DnsError::PublicDnsFailed {
domain: domain.to_string(),
source: Box::new(e),
})
}
}
}
/// Resolve domain with layered fallback strategy using hickory-resolver
///
/// Resolution order with detailed tracing:
/// 1. Local cache (Moka with TTL)
/// 2. System DNS (hickory-resolver with host/container adaptive configuration)
/// 3. Public DNS (hickory-resolver with TLS-enabled Cloudflare DNS fallback)
#[instrument(skip_all, fields(domain = %domain))]
pub async fn resolve(&self, domain: &str) -> Result<Vec<IpAddr>, DnsError> {
// Validate domain format first
Self::validate_domain_format(domain)?;
info!("Starting DNS resolution for domain: {}", domain);
// Step 1: Check local cache
if let Some(ips) = self.check_cache(domain).await {
info!("DNS resolution completed from cache for domain: {} -> {} IPs", domain, ips.len());
return Ok(ips);
}
debug!("Local cache miss for domain: {}, attempting system DNS", domain);
// Step 2: Try system DNS (cluster/host adaptive)
match self.resolve_with_system_dns(domain).await {
Ok(ips) => {
self.update_cache(domain, ips.clone()).await;
info!("DNS resolution completed via system DNS for domain: {} -> {} IPs", domain, ips.len());
return Ok(ips);
}
Err(system_err) => {
warn!("System DNS failed for domain: {} - {}", domain, system_err);
}
}
// Step 3: Fallback to public DNS
info!("Falling back to public DNS for domain: {}", domain);
match self.resolve_with_public_dns(domain).await {
Ok(ips) => {
self.update_cache(domain, ips.clone()).await;
info!("DNS resolution completed via public DNS for domain: {} -> {} IPs", domain, ips.len());
Ok(ips)
}
Err(public_err) => {
error!(
"All DNS resolution attempts failed for domain: {}. System DNS: failed, Public DNS: {}",
domain, public_err
);
Err(DnsError::AllAttemptsFailed {
domain: domain.to_string(),
})
}
}
}
}
/// Global DNS resolver instance
static GLOBAL_DNS_RESOLVER: OnceLock<LayeredDnsResolver> = OnceLock::new();
/// Initialize the global DNS resolver
#[instrument]
pub async fn init_global_dns_resolver() -> Result<(), DnsError> {
info!("Initializing global DNS resolver");
let resolver = LayeredDnsResolver::new().await?;
match GLOBAL_DNS_RESOLVER.set(resolver) {
Ok(()) => {
info!("Global DNS resolver initialized successfully");
Ok(())
}
Err(_) => {
warn!("Global DNS resolver was already initialized");
Ok(())
}
}
}
/// Get the global DNS resolver instance
pub fn get_global_dns_resolver() -> Option<&'static LayeredDnsResolver> {
GLOBAL_DNS_RESOLVER.get()
}
/// Resolve domain using the global DNS resolver with comprehensive tracing
#[instrument(skip_all, fields(domain = %domain))]
pub async fn resolve_domain(domain: &str) -> Result<Vec<IpAddr>, DnsError> {
match get_global_dns_resolver() {
Some(resolver) => resolver.resolve(domain).await,
None => Err(DnsError::InitializationFailed {
source: "Global DNS resolver not initialized. Call init_global_dns_resolver() first."
.to_string()
.into(),
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_domain_validation() {
// Valid domains
assert!(LayeredDnsResolver::validate_domain_format("example.com").is_ok());
assert!(LayeredDnsResolver::validate_domain_format("sub.example.com").is_ok());
assert!(LayeredDnsResolver::validate_domain_format("very.deep.sub.domain.example.com").is_ok());
// Invalid domains - too long FQDN
let long_domain = "a".repeat(254);
assert!(LayeredDnsResolver::validate_domain_format(&long_domain).is_err());
// Invalid domains - label too long
let long_label = format!("{}.com", "a".repeat(64));
assert!(LayeredDnsResolver::validate_domain_format(&long_label).is_err());
// Invalid domains - empty label
assert!(LayeredDnsResolver::validate_domain_format("example..com").is_err());
}
#[tokio::test]
async fn test_cache_functionality() {
let resolver = LayeredDnsResolver::new().await.unwrap();
// Test cache miss
assert!(resolver.check_cache("example.com").await.is_none());
// Update cache
let test_ips = vec![IpAddr::from([192, 0, 2, 1])];
resolver.update_cache("example.com", test_ips.clone()).await;
// Test cache hit
assert_eq!(resolver.check_cache("example.com").await, Some(test_ips));
// Test cache stats (note: moka cache might not immediately reflect changes)
let (total, _weighted_size) = resolver.cache_stats().await;
// Cache should have at least the entry we just added (might be 0 due to async nature)
assert!(total <= 1, "Cache should have at most 1 entry, got {}", total);
}
#[tokio::test]
async fn test_dns_resolution() {
let resolver = LayeredDnsResolver::new().await.unwrap();
// Test resolution of a known domain (localhost should always resolve)
match resolver.resolve("localhost").await {
Ok(ips) => {
assert!(!ips.is_empty());
println!("Resolved localhost to: {:?}", ips);
}
Err(e) => {
// In some test environments, even localhost might fail
// This is acceptable as long as our error handling works
println!("DNS resolution failed (might be expected in test environments): {}", e);
}
}
}
#[tokio::test]
async fn test_invalid_domain_resolution() {
let resolver = LayeredDnsResolver::new().await.unwrap();
// Test resolution of invalid domain
let result = resolver
.resolve("nonexistent.invalid.domain.example.thisdefinitelydoesnotexist")
.await;
assert!(result.is_err());
if let Err(e) = result {
println!("Expected error for invalid domain: {}", e);
// Should be AllAttemptsFailed since both system and public DNS should fail
assert!(matches!(e, DnsError::AllAttemptsFailed { .. }));
}
}
#[tokio::test]
async fn test_cache_invalidation() {
let resolver = LayeredDnsResolver::new().await.unwrap();
// Add entry to cache
let test_ips = vec![IpAddr::from([192, 0, 2, 1])];
resolver.update_cache("test.example.com", test_ips.clone()).await;
// Verify cache hit
assert_eq!(resolver.check_cache("test.example.com").await, Some(test_ips));
// Invalidate cache
resolver.invalidate_cache().await;
// Verify cache miss after invalidation
assert!(resolver.check_cache("test.example.com").await.is_none());
}
#[tokio::test]
async fn test_global_resolver_initialization() {
// Test initialization
assert!(init_global_dns_resolver().await.is_ok());
// Test that resolver is available
assert!(get_global_dns_resolver().is_some());
// Test domain resolution through global resolver
match resolve_domain("localhost").await {
Ok(ips) => {
assert!(!ips.is_empty());
println!("Global resolver resolved localhost to: {:?}", ips);
}
Err(e) => {
println!("Global resolver DNS resolution failed (might be expected in test environments): {}", e);
}
}
}
}
+4
View File
@@ -14,11 +14,15 @@
#[cfg(feature = "tls")]
pub mod certs;
#[cfg(feature = "net")]
pub mod dns_resolver;
#[cfg(feature = "ip")]
pub mod ip;
#[cfg(feature = "net")]
pub mod net;
#[cfg(feature = "net")]
pub use dns_resolver::*;
#[cfg(feature = "net")]
pub use net::*;
+44 -1
View File
@@ -86,7 +86,50 @@ pub fn is_local_host(host: Host<&str>, port: u16, local_port: u16) -> std::io::R
Ok(is_local_host)
}
/// returns IP address of given host.
/// returns IP address of given host using layered DNS resolution.
pub fn get_host_ip_async(host: Host<&str>) -> std::io::Result<HashSet<IpAddr>> {
match host {
Host::Domain(domain) => {
#[cfg(feature = "net")]
{
use crate::dns_resolver::resolve_domain;
let handle = tokio::runtime::Handle::current();
handle.block_on(async {
match resolve_domain(domain).await {
Ok(ips) => Ok(ips.into_iter().collect()),
Err(e) => Err(std::io::Error::other(format!("DNS resolution failed: {}", e))),
}
})
}
#[cfg(not(feature = "net"))]
{
// Fallback to standard resolution when DNS resolver is not available
match (domain, 0)
.to_socket_addrs()
.map(|v| v.map(|v| v.ip()).collect::<HashSet<_>>())
{
Ok(ips) => Ok(ips),
Err(err) => Err(std::io::Error::other(err)),
}
}
}
Host::Ipv4(ip) => {
let mut set = HashSet::with_capacity(1);
set.insert(IpAddr::V4(ip));
Ok(set)
}
Host::Ipv6(ip) => {
let mut set = HashSet::with_capacity(1);
set.insert(IpAddr::V6(ip));
Ok(set)
}
}
}
/// returns IP address of given host using standard resolution.
///
/// **Note**: This function uses standard library DNS resolution.
/// For enhanced DNS resolution with Kubernetes support, use `get_host_ip_async()`.
pub fn get_host_ip(host: Host<&str>) -> std::io::Result<HashSet<IpAddr>> {
match host {
Host::Domain(domain) => match (domain, 0)
+118
View File
@@ -0,0 +1,118 @@
# RustFS Environment Variables
This document describes the environment variables that can be used to configure RustFS behavior.
## Background Services Control
### RUSTFS_ENABLE_SCANNER
Controls whether the data scanner service should be started.
- **Default**: `true`
- **Valid values**: `true`, `false`
- **Description**: When enabled, the data scanner will run background scans to detect inconsistencies and corruption in stored data.
**Examples**:
```bash
# Disable scanner
export RUSTFS_ENABLE_SCANNER=false
# Enable scanner (default behavior)
export RUSTFS_ENABLE_SCANNER=true
```
### RUSTFS_ENABLE_HEAL
Controls whether the auto-heal service should be started.
- **Default**: `true`
- **Valid values**: `true`, `false`
- **Description**: When enabled, the heal manager will automatically repair detected data inconsistencies and corruption.
**Examples**:
```bash
# Disable auto-heal
export RUSTFS_ENABLE_HEAL=false
# Enable auto-heal (default behavior)
export RUSTFS_ENABLE_HEAL=true
```
### RUSTFS_ENABLE_LOCKS
Controls whether the distributed lock system should be enabled.
- **Default**: `true`
- **Valid values**: `true`, `false`, `1`, `0`, `yes`, `no`, `on`, `off`, `enabled`, `disabled` (case insensitive)
- **Description**: When enabled, provides distributed locking for concurrent object operations. When disabled, all lock operations immediately return success without actual locking.
**Examples**:
```bash
# Disable lock system
export RUSTFS_ENABLE_LOCKS=false
# Enable lock system (default behavior)
export RUSTFS_ENABLE_LOCKS=true
```
## Service Combinations
The scanner and heal services can be independently controlled:
| RUSTFS_ENABLE_SCANNER | RUSTFS_ENABLE_HEAL | Result |
|----------------------|-------------------|--------|
| `true` (default) | `true` (default) | Both scanner and heal are active |
| `true` | `false` | Scanner runs without heal capabilities |
| `false` | `true` | Heal manager is available but no scanning |
| `false` | `false` | No background maintenance services |
## Use Cases
### Development Environment
For development or testing environments where you don't need background maintenance:
```bash
export RUSTFS_ENABLE_SCANNER=false
export RUSTFS_ENABLE_HEAL=false
./rustfs --address 127.0.0.1:9000 ...
```
### Scan-Only Mode
For environments where you want to detect issues but not automatically fix them:
```bash
export RUSTFS_ENABLE_SCANNER=true
export RUSTFS_ENABLE_HEAL=false
./rustfs --address 127.0.0.1:9000 ...
```
### Heal-Only Mode
For environments where external tools trigger healing but no automatic scanning:
```bash
export RUSTFS_ENABLE_SCANNER=false
export RUSTFS_ENABLE_HEAL=true
./rustfs --address 127.0.0.1:9000 ...
```
### Production Environment (Default)
For production environments where both services should be active:
```bash
# These are the defaults, so no need to set explicitly
# export RUSTFS_ENABLE_SCANNER=true
# export RUSTFS_ENABLE_HEAL=true
./rustfs --address 127.0.0.1:9000 ...
```
### No-Lock Development
For single-node development where locking is not needed:
```bash
export RUSTFS_ENABLE_LOCKS=false
./rustfs --address 127.0.0.1:9000 ...
```
## Performance Impact
- **Scanner**: Light to moderate CPU/IO impact during scans
- **Heal**: Moderate to high CPU/IO impact during healing operations
- **Locks**: Minimal CPU/memory overhead for coordination; disabling can improve throughput in single-client scenarios
- **Memory**: Each service uses additional memory for processing queues and metadata
Disabling these services in resource-constrained environments can improve performance for primary storage operations.
+65
View File
@@ -0,0 +1,65 @@
use std::collections::HashMap;
use std::sync::Arc;
use http::HeaderMap;
use rustfs_iam::store::object::ObjectStore;
use rustfs_iam::sys::IamSys;
use rustfs_policy::auth;
use rustfs_policy::policy::Args;
use rustfs_policy::policy::action::Action;
use s3s::S3Result;
use s3s::s3_error;
use crate::auth::get_condition_values;
pub async fn validate_admin_request(
headers: &HeaderMap,
cred: &auth::Credentials,
is_owner: bool,
deny_only: bool,
actions: Vec<Action>,
) -> S3Result<()> {
let Ok(iam_store) = rustfs_iam::get() else {
return Err(s3_error!(InternalError, "iam not init"));
};
for action in actions {
match check_admin_request_auth(iam_store.clone(), headers, cred, is_owner, deny_only, action).await {
Ok(_) => return Ok(()),
Err(_) => {
continue;
}
}
}
Err(s3_error!(AccessDenied, "Access Denied"))
}
async fn check_admin_request_auth(
iam_store: Arc<IamSys<ObjectStore>>,
headers: &HeaderMap,
cred: &auth::Credentials,
is_owner: bool,
deny_only: bool,
action: Action,
) -> S3Result<()> {
let conditions = get_condition_values(headers, cred);
if !iam_store
.is_allowed(&Args {
account: &cred.access_key,
groups: &cred.groups,
action,
conditions: &conditions,
is_owner,
claims: cred.claims.as_ref().unwrap_or(&HashMap::new()),
deny_only,
bucket: "",
object: "",
})
.await
{
return Err(s3_error!(AccessDenied, "Access Denied"));
}
Ok(())
}
+143 -3
View File
@@ -13,6 +13,7 @@
// limitations under the License.
use super::router::Operation;
use crate::admin::auth::validate_admin_request;
use crate::auth::check_key_valid;
use crate::auth::get_condition_values;
use crate::auth::get_session_token;
@@ -45,6 +46,7 @@ use rustfs_madmin::utils::parse_duration;
use rustfs_policy::policy::Args;
use rustfs_policy::policy::BucketPolicy;
use rustfs_policy::policy::action::Action;
use rustfs_policy::policy::action::AdminAction;
use rustfs_policy::policy::action::S3Action;
use rustfs_policy::policy::default::DEFAULT_POLICIES;
use rustfs_utils::path::path_join;
@@ -300,7 +302,23 @@ pub struct ServerInfoHandler {}
#[async_trait::async_trait]
impl Operation for ServerInfoHandler {
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let Some(input_cred) = req.credentials else {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)],
)
.await?;
let info = get_server_info(true).await;
let data = serde_json::to_vec(&info)
@@ -328,9 +346,25 @@ pub struct StorageInfoHandler {}
#[async_trait::async_trait]
impl Operation for StorageInfoHandler {
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle StorageInfoHandler");
let Some(input_cred) = req.credentials else {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::StorageInfoAdminAction)],
)
.await?;
let Some(store) = new_object_layer_fn() else {
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
};
@@ -353,9 +387,25 @@ pub struct DataUsageInfoHandler {}
#[async_trait::async_trait]
impl Operation for DataUsageInfoHandler {
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle DataUsageInfoHandler");
let Some(input_cred) = req.credentials else {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::DataUsageInfoAdminAction)],
)
.await?;
let Some(store) = new_object_layer_fn() else {
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
};
@@ -365,6 +415,16 @@ impl Operation for DataUsageInfoHandler {
s3_error!(InternalError, "load_data_usage_from_backend failed")
})?;
// If no valid data exists, attempt real-time collection
if info.objects_total_count == 0 && info.buckets_count == 0 {
info!("No data usage statistics found, attempting real-time collection");
if let Err(e) = collect_realtime_data_usage(&mut info, store.clone()).await {
warn!("Failed to collect real-time data usage: {}", e);
}
}
// Set capacity information
let sinfo = store.storage_info().await;
info.total_capacity = get_total_usable_capacity(&sinfo.disks, &sinfo) as u64;
info.total_free_capacity = get_total_usable_capacity_free(&sinfo.disks, &sinfo) as u64;
@@ -1093,6 +1153,86 @@ impl Operation for RemoveRemoteTargetHandler {
}
}
/// Real-time data collection function
async fn collect_realtime_data_usage(
info: &mut rustfs_common::data_usage::DataUsageInfo,
store: Arc<rustfs_ecstore::store::ECStore>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Get bucket list and collect basic statistics
let buckets = store
.list_bucket(&rustfs_ecstore::store_api::BucketOptions::default())
.await?;
info.buckets_count = buckets.len() as u64;
info.last_update = Some(std::time::SystemTime::now());
let mut total_objects = 0u64;
let mut total_size = 0u64;
// For each bucket, try to get object count
for bucket_info in buckets {
let bucket_name = &bucket_info.name;
// Skip system buckets
if bucket_name.starts_with('.') {
continue;
}
// Try to count objects in this bucket
let (object_count, bucket_size) = count_bucket_objects(&store, bucket_name).await.unwrap_or((0, 0));
total_objects += object_count;
total_size += bucket_size;
let bucket_usage = rustfs_common::data_usage::BucketUsageInfo {
objects_count: object_count,
size: bucket_size,
versions_count: object_count, // Simplified: assume 1 version per object
..Default::default()
};
info.buckets_usage.insert(bucket_name.clone(), bucket_usage);
info.bucket_sizes.insert(bucket_name.clone(), bucket_size);
}
info.objects_total_count = total_objects;
info.objects_total_size = total_size;
info.versions_total_count = total_objects; // Simplified
Ok(())
}
/// Helper function to count objects in a bucket
async fn count_bucket_objects(
store: &Arc<rustfs_ecstore::store::ECStore>,
bucket_name: &str,
) -> Result<(u64, u64), Box<dyn std::error::Error + Send + Sync>> {
// Use list_objects_v2 to get actual object count
match store
.clone()
.list_objects_v2(
bucket_name,
"", // prefix
None, // continuation_token
None, // delimiter
1000, // max_keys - limit for performance
false, // fetch_owner
None, // start_after
)
.await
{
Ok(result) => {
let object_count = result.objects.len() as u64;
let total_size = result.objects.iter().map(|obj| obj.size as u64).sum();
Ok((object_count, total_size))
}
Err(e) => {
warn!("Failed to list objects in bucket {}: {}", bucket_name, e);
Ok((0, 0))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
+25 -4
View File
@@ -18,7 +18,7 @@ use std::{
};
use crate::{
admin::router::Operation,
admin::{auth::validate_admin_request, router::Operation},
auth::{check_key_valid, get_session_token},
};
@@ -44,7 +44,10 @@ use rustfs_ecstore::{
bucket::utils::{deserialize, serialize},
store_api::MakeBucketOptions,
};
use rustfs_policy::policy::BucketPolicy;
use rustfs_policy::policy::{
BucketPolicy,
action::{Action, AdminAction},
};
use rustfs_utils::path::{SLASH_SEPARATOR, path_join_buf};
use s3s::{
Body, S3Request, S3Response, S3Result,
@@ -85,9 +88,18 @@ impl Operation for ExportBucketMetadata {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (_cred, _owner) =
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::ExportBucketMetadataAction)],
)
.await?;
let Some(store) = new_object_layer_fn() else {
return Err(s3_error!(InvalidRequest, "object store not init"));
};
@@ -369,9 +381,18 @@ impl Operation for ImportBucketMetadata {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (_cred, _owner) =
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::ImportBucketMetadataAction)],
)
.await?;
let mut input = req.input;
let body = match input.store_all_unlimited().await {
Ok(b) => b,
+70 -2
View File
@@ -17,6 +17,7 @@ use matchit::Params;
use rustfs_ecstore::global::get_global_action_cred;
use rustfs_iam::error::{is_err_no_such_group, is_err_no_such_user};
use rustfs_madmin::GroupAddRemove;
use rustfs_policy::policy::action::{Action, AdminAction};
use s3s::{
Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result,
header::{CONTENT_LENGTH, CONTENT_TYPE},
@@ -26,7 +27,10 @@ use serde::Deserialize;
use serde_urlencoded::from_bytes;
use tracing::warn;
use crate::admin::{router::Operation, utils::has_space_be};
use crate::{
admin::{auth::validate_admin_request, router::Operation, utils::has_space_be},
auth::{check_key_valid, get_session_token},
};
#[derive(Debug, Deserialize, Default)]
pub struct GroupQuery {
@@ -37,9 +41,25 @@ pub struct GroupQuery {
pub struct ListGroups {}
#[async_trait::async_trait]
impl Operation for ListGroups {
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle ListGroups");
let Some(input_cred) = req.credentials else {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::ListGroupsAdminAction)],
)
.await?;
let Ok(iam_store) = rustfs_iam::get() else { return Err(s3_error!(InternalError, "iam not init")) };
let groups = iam_store.list_groups().await.map_err(|e| {
@@ -62,6 +82,22 @@ impl Operation for GetGroup {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle GetGroup");
let Some(input_cred) = req.credentials else {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::GetGroupAdminAction)],
)
.await?;
let query = {
if let Some(query) = req.uri.query() {
let input: GroupQuery =
@@ -93,6 +129,22 @@ impl Operation for SetGroupStatus {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle SetGroupStatus");
let Some(input_cred) = req.credentials else {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::EnableGroupAdminAction)],
)
.await?;
let query = {
if let Some(query) = req.uri.query() {
let input: GroupQuery =
@@ -144,6 +196,22 @@ impl Operation for UpdateGroupMembers {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle UpdateGroupMembers");
let Some(input_cred) = req.credentials else {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::AddUserToGroupAdminAction)],
)
.await?;
let mut input = req.input;
let body = match input.store_all_unlimited().await {
Ok(b) => b,
+88 -6
View File
@@ -12,13 +12,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::admin::{router::Operation, utils::has_space_be};
use crate::{
admin::{auth::validate_admin_request, router::Operation, utils::has_space_be},
auth::{check_key_valid, get_session_token},
};
use http::{HeaderMap, StatusCode};
use matchit::Params;
use rustfs_ecstore::global::get_global_action_cred;
use rustfs_iam::error::is_err_no_such_user;
use rustfs_iam::store::MappedPolicy;
use rustfs_policy::policy::Policy;
use rustfs_policy::policy::{
Policy,
action::{Action, AdminAction},
};
use s3s::{
Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result,
header::{CONTENT_LENGTH, CONTENT_TYPE},
@@ -40,6 +46,22 @@ impl Operation for ListCannedPolicies {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle ListCannedPolicies");
let Some(input_cred) = req.credentials else {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::ListUserPoliciesAdminAction)],
)
.await?;
let query = {
if let Some(query) = req.uri.query() {
let input: BucketQuery =
@@ -82,6 +104,22 @@ impl Operation for AddCannedPolicy {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle AddCannedPolicy");
let Some(input_cred) = req.credentials else {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::CreatePolicyAdminAction)],
)
.await?;
let query = {
if let Some(query) = req.uri.query() {
let input: PolicyNameQuery =
@@ -138,6 +176,22 @@ impl Operation for InfoCannedPolicy {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle InfoCannedPolicy");
let Some(input_cred) = req.credentials else {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::GetPolicyAdminAction)],
)
.await?;
let query = {
if let Some(query) = req.uri.query() {
let input: PolicyNameQuery =
@@ -179,6 +233,22 @@ impl Operation for RemoveCannedPolicy {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle RemoveCannedPolicy");
let Some(input_cred) = req.credentials else {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::DeletePolicyAdminAction)],
)
.await?;
let query = {
if let Some(query) = req.uri.query() {
let input: PolicyNameQuery =
@@ -223,6 +293,22 @@ impl Operation for SetPolicyForUserOrGroup {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle SetPolicyForUserOrGroup");
let Some(input_cred) = req.credentials else {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::AttachPolicyAdminAction)],
)
.await?;
let query = {
if let Some(query) = req.uri.query() {
let input: SetPolicyForUserOrGroupQuery =
@@ -233,10 +319,6 @@ impl Operation for SetPolicyForUserOrGroup {
}
};
if query.policy_name.is_empty() {
return Err(s3_error!(InvalidArgument, "policy name is empty"));
}
if query.user_or_group.is_empty() {
return Err(s3_error!(InvalidArgument, "user or group is empty"));
}
+77 -2
View File
@@ -15,13 +15,18 @@
use http::{HeaderMap, StatusCode};
use matchit::Params;
use rustfs_ecstore::{GLOBAL_Endpoints, new_object_layer_fn};
use rustfs_policy::policy::action::{Action, AdminAction};
use s3s::{Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result, header::CONTENT_TYPE, s3_error};
use serde::Deserialize;
use serde_urlencoded::from_bytes;
use tokio::sync::broadcast;
use tracing::warn;
use crate::{admin::router::Operation, error::ApiError};
use crate::{
admin::{auth::validate_admin_request, router::Operation},
auth::{check_key_valid, get_session_token},
error::ApiError,
};
pub struct ListPools {}
@@ -29,9 +34,28 @@ pub struct ListPools {}
impl Operation for ListPools {
// GET <endpoint>/<admin-API>/pools/list
#[tracing::instrument(skip_all)]
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle ListPools");
let Some(input_cred) = req.credentials else {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![
Action::AdminAction(AdminAction::ServerInfoAdminAction),
Action::AdminAction(AdminAction::DecommissionAdminAction),
],
)
.await?;
let Some(store) = new_object_layer_fn() else {
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
};
@@ -79,6 +103,25 @@ impl Operation for StatusPool {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle StatusPool");
let Some(input_cred) = req.credentials else {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![
Action::AdminAction(AdminAction::ServerInfoAdminAction),
Action::AdminAction(AdminAction::DecommissionAdminAction),
],
)
.await?;
let Some(endpoints) = GLOBAL_Endpoints.get() else {
return Err(s3_error!(NotImplemented));
};
@@ -138,6 +181,22 @@ impl Operation for StartDecommission {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle StartDecommission");
let Some(input_cred) = req.credentials else {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::DecommissionAdminAction)],
)
.await?;
let Some(endpoints) = GLOBAL_Endpoints.get() else {
return Err(s3_error!(NotImplemented));
};
@@ -221,6 +280,22 @@ impl Operation for CancelDecommission {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle CancelDecommission");
let Some(input_cred) = req.credentials else {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::DecommissionAdminAction)],
)
.await?;
let Some(endpoints) = GLOBAL_Endpoints.get() else {
return Err(s3_error!(NotImplemented));
};
+56 -4
View File
@@ -22,6 +22,7 @@ use rustfs_ecstore::{
rebalance::{DiskStat, RebalSaveOpt},
store_api::BucketOptions,
};
use rustfs_policy::policy::action::{Action, AdminAction};
use s3s::{
Body, S3Request, S3Response, S3Result,
header::{CONTENT_LENGTH, CONTENT_TYPE},
@@ -32,7 +33,10 @@ use std::time::Duration;
use time::OffsetDateTime;
use tracing::warn;
use crate::admin::router::Operation;
use crate::{
admin::{auth::validate_admin_request, router::Operation},
auth::{check_key_valid, get_session_token},
};
use rustfs_ecstore::rebalance::RebalanceMeta;
#[derive(Debug, Clone, Deserialize, Serialize)]
@@ -84,9 +88,25 @@ pub struct RebalanceStart {}
#[async_trait::async_trait]
impl Operation for RebalanceStart {
#[tracing::instrument(skip_all)]
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle RebalanceStart");
let Some(input_cred) = req.credentials else {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::RebalanceAdminAction)],
)
.await?;
let Some(store) = new_object_layer_fn() else {
return Err(s3_error!(InternalError, "Not init"));
};
@@ -145,9 +165,25 @@ pub struct RebalanceStatus {}
#[async_trait::async_trait]
impl Operation for RebalanceStatus {
#[tracing::instrument(skip_all)]
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle RebalanceStatus");
let Some(input_cred) = req.credentials else {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::RebalanceAdminAction)],
)
.await?;
let Some(store) = new_object_layer_fn() else {
return Err(s3_error!(InternalError, "Not init"));
};
@@ -246,9 +282,25 @@ pub struct RebalanceStop {}
#[async_trait::async_trait]
impl Operation for RebalanceStop {
#[tracing::instrument(skip_all)]
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle RebalanceStop");
let Some(input_cred) = req.credentials else {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::RebalanceAdminAction)],
)
.await?;
let Some(store) = new_object_layer_fn() else {
return Err(s3_error!(InternalError, "Not init"));
};
+37 -14
View File
@@ -16,6 +16,7 @@
use http::{HeaderMap, StatusCode};
//use iam::get_global_action_cred;
use matchit::Params;
use rustfs_policy::policy::action::{Action, AdminAction};
use s3s::{
Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result,
header::{CONTENT_LENGTH, CONTENT_TYPE},
@@ -26,7 +27,7 @@ use time::OffsetDateTime;
use tracing::{debug, warn};
use crate::{
admin::router::Operation,
admin::{auth::validate_admin_request, router::Operation},
auth::{check_key_valid, get_session_token},
};
@@ -88,9 +89,11 @@ impl Operation for AddTier {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, _owner) =
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(&req.headers, &cred, owner, false, vec![Action::AdminAction(AdminAction::SetTierAction)]).await?;
let mut input = req.input;
let body = match input.store_all_unlimited().await {
Ok(b) => b,
@@ -196,9 +199,11 @@ impl Operation for EditTier {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, _owner) =
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(&req.headers, &cred, owner, false, vec![Action::AdminAction(AdminAction::SetTierAction)]).await?;
let mut input = req.input;
let body = match input.store_all_unlimited().await {
Ok(b) => b,
@@ -265,6 +270,15 @@ impl Operation for ListTiers {
}
};
let Some(input_cred) = req.credentials else {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(&req.headers, &cred, owner, false, vec![Action::AdminAction(AdminAction::ListTierAction)]).await?;
let mut tier_config_mgr = GLOBAL_TierConfigMgr.read().await;
let tiers = tier_config_mgr.list_tiers();
@@ -296,11 +310,10 @@ impl Operation for RemoveTier {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, _owner) =
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
//let sys_cred = get_global_action_cred()
// .ok_or_else(|| S3Error::with_message(S3ErrorCode::InternalError, "get_global_action_cred failed"))?;
validate_admin_request(&req.headers, &cred, owner, false, vec![Action::AdminAction(AdminAction::SetTierAction)]).await?;
let mut force: bool = false;
let force_str = query.force.clone().unwrap_or_default();
@@ -360,11 +373,10 @@ impl Operation for VerifyTier {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, _owner) =
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
//let sys_cred = get_global_action_cred()
// .ok_or_else(|| S3Error::with_message(S3ErrorCode::InternalError, "get_global_action_cred failed"))?;
validate_admin_request(&req.headers, &cred, owner, false, vec![Action::AdminAction(AdminAction::ListTierAction)]).await?;
let mut tier_config_mgr = GLOBAL_TierConfigMgr.write().await;
tier_config_mgr.verify(&query.tier.unwrap()).await;
@@ -380,6 +392,15 @@ pub struct GetTierInfo {}
#[async_trait::async_trait]
impl Operation for GetTierInfo {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let Some(input_cred) = req.credentials else {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(&req.headers, &cred, owner, false, vec![Action::AdminAction(AdminAction::ListTierAction)]).await?;
let query = {
if let Some(query) = req.uri.query() {
let input: AddTierQuery =
@@ -423,12 +444,14 @@ impl Operation for ClearTier {
}
};
//let Some(input_cred) = req.credentials else {
// return Err(s3_error!(InvalidRequest, "get cred failed"));
//};
let Some(input_cred) = req.credentials else {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
//let (cred, _owner) =
// check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(&req.headers, &cred, owner, false, vec![Action::AdminAction(AdminAction::SetTierAction)]).await?;
let mut force: bool = false;
let force_str = query.force;
+86 -65
View File
@@ -13,8 +13,8 @@
// limitations under the License.
use crate::{
admin::{router::Operation, utils::has_space_be},
auth::{check_key_valid, get_condition_values, get_session_token},
admin::{auth::validate_admin_request, router::Operation, utils::has_space_be},
auth::{check_key_valid, get_session_token},
};
use http::{HeaderMap, StatusCode};
use matchit::Params;
@@ -27,10 +27,7 @@ use rustfs_madmin::{
AccountStatus, AddOrUpdateUserReq, IAMEntities, IAMErrEntities, IAMErrEntity, IAMErrPolicyEntity,
user::{ImportIAMResult, SRSessionPolicy, SRSvcAccCreate},
};
use rustfs_policy::policy::{
Args,
action::{Action, AdminAction},
};
use rustfs_policy::policy::action::{Action, AdminAction};
use rustfs_utils::path::path_join_buf;
use s3s::{
@@ -121,23 +118,14 @@ impl Operation for AddUser {
}
let deny_only = ak == cred.access_key;
let conditions = get_condition_values(&req.headers, &cred);
if !iam_store
.is_allowed(&Args {
account: &cred.access_key,
groups: &cred.groups,
action: Action::AdminAction(AdminAction::CreateUserAdminAction),
bucket: "",
conditions: &conditions,
is_owner: owner,
object: "",
claims: cred.claims.as_ref().unwrap_or(&HashMap::new()),
deny_only,
})
.await
{
return Err(s3_error!(AccessDenied, "access denied"));
}
validate_admin_request(
&req.headers,
&cred,
owner,
deny_only,
vec![Action::AdminAction(AdminAction::CreateUserAdminAction)],
)
.await?;
iam_store
.create_user(ak, &args)
@@ -179,6 +167,18 @@ impl Operation for SetUserStatus {
return Err(s3_error!(InvalidArgument, "can't change status of self"));
}
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::EnableUserAdminAction)],
)
.await?;
let status = AccountStatus::try_from(query.status.as_deref().unwrap_or_default())
.map_err(|e| S3Error::with_message(S3ErrorCode::InvalidArgument, e))?;
@@ -207,6 +207,22 @@ pub struct ListUsers {}
#[async_trait::async_trait]
impl Operation for ListUsers {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let Some(input_cred) = req.credentials else {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::ListUsersAdminAction)],
)
.await?;
let query = {
if let Some(query) = req.uri.query() {
let input: BucketQuery =
@@ -238,13 +254,6 @@ impl Operation for ListUsers {
let data = serde_json::to_vec(&users)
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("marshal users err {e}")))?;
// let Some(input_cred) = req.credentials else {
// return Err(s3_error!(InvalidRequest, "get cred failed"));
// };
// let body = encrypt_data(input_cred.secret_key.expose().as_bytes(), &data)
// .map_err(|e| S3Error::with_message(S3ErrorCode::InvalidArgument, format!("encrypt_data err {}", e)))?;
let mut header = HeaderMap::new();
header.insert(CONTENT_TYPE, "application/json".parse().unwrap());
@@ -256,6 +265,22 @@ pub struct RemoveUser {}
#[async_trait::async_trait]
impl Operation for RemoveUser {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let Some(input_cred) = req.credentials else {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::DeleteUserAdminAction)],
)
.await?;
let query = {
if let Some(query) = req.uri.query() {
let input: AddUserQuery =
@@ -272,6 +297,13 @@ impl Operation for RemoveUser {
return Err(s3_error!(InvalidArgument, "access key is empty"));
}
let sys_cred = get_global_action_cred()
.ok_or_else(|| S3Error::with_message(S3ErrorCode::InternalError, "get_global_action_cred failed"))?;
if ak == sys_cred.access_key || ak == cred.access_key || cred.parent_user == ak {
return Err(s3_error!(InvalidArgument, "can't remove self"));
}
let Ok(iam_store) = rustfs_iam::get() else {
return Err(s3_error!(InvalidRequest, "iam not init"));
};
@@ -285,22 +317,12 @@ impl Operation for RemoveUser {
return Err(s3_error!(InvalidArgument, "can't remove temp user"));
}
let Some(input_cred) = req.credentials else {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, _owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
let sys_cred = get_global_action_cred()
.ok_or_else(|| S3Error::with_message(S3ErrorCode::InternalError, "get_global_action_cred failed"))?;
if ak == sys_cred.access_key || ak == cred.access_key {
warn!(
"can't remove self or system access key {}, {}, {}",
ak, sys_cred.access_key, cred.access_key
);
return Err(s3_error!(InvalidArgument, "can't remove self"));
let (is_service_account, _) = iam_store
.is_service_account(ak)
.await
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("is_service_account err {e}")))?;
if is_service_account {
return Err(s3_error!(InvalidArgument, "can't remove service account"));
}
iam_store
@@ -308,6 +330,8 @@ impl Operation for RemoveUser {
.await
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("delete_user err {e}")))?;
// TODO: IAMChangeHook
let mut header = HeaderMap::new();
header.insert(CONTENT_TYPE, "application/json".parse().unwrap());
header.insert(CONTENT_LENGTH, "0".parse().unwrap());
@@ -347,23 +371,14 @@ impl Operation for GetUserInfo {
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
let deny_only = ak == cred.access_key;
let conditions = get_condition_values(&req.headers, &cred);
if !iam_store
.is_allowed(&Args {
account: &cred.access_key,
groups: &cred.groups,
action: Action::AdminAction(AdminAction::GetUserAdminAction),
bucket: "",
conditions: &conditions,
is_owner: owner,
object: "",
claims: cred.claims.as_ref().unwrap_or(&HashMap::new()),
deny_only,
})
.await
{
return Err(s3_error!(AccessDenied, "access denied"));
}
validate_admin_request(
&req.headers,
&cred,
owner,
deny_only,
vec![Action::AdminAction(AdminAction::GetUserAdminAction)],
)
.await?;
let info = iam_store
.get_user_info(ak)
@@ -408,9 +423,12 @@ impl Operation for ExportIam {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (_cred, _owner) =
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(&req.headers, &cred, owner, false, vec![Action::AdminAction(AdminAction::ExportIAMAction)])
.await?;
let Ok(iam_store) = rustfs_iam::get() else {
return Err(s3_error!(InvalidRequest, "iam not init"));
};
@@ -612,9 +630,12 @@ impl Operation for ImportIam {
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (_cred, _owner) =
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(&req.headers, &cred, owner, false, vec![Action::AdminAction(AdminAction::ExportIAMAction)])
.await?;
let mut input = req.input;
let body = match input.store_all_unlimited().await {
Ok(b) => b,
+1
View File
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
mod auth;
pub mod console;
pub mod handlers;
pub mod router;
+1
View File
@@ -81,6 +81,7 @@ impl From<StorageError> for ApiError {
StorageError::DataMovementOverwriteErr(_, _, _) => S3ErrorCode::InvalidArgument,
StorageError::ObjectExistsAsDirectory(_, _) => S3ErrorCode::InvalidArgument,
StorageError::InvalidPart(_, _, _) => S3ErrorCode::InvalidPart,
StorageError::EntityTooSmall(_, _, _) => S3ErrorCode::EntityTooSmall,
StorageError::PreconditionFailed => S3ErrorCode::PreconditionFailed,
_ => S3ErrorCode::InternalError,
};
+118 -15
View File
@@ -25,6 +25,7 @@ mod version;
// Ensure the correct path for parse_license is imported
use crate::server::{SHUTDOWN_TIMEOUT, ServiceState, ServiceStateManager, ShutdownSignal, start_http_server, wait_for_shutdown};
use crate::storage::ecfs::{process_lambda_configurations, process_queue_configurations, process_topic_configurations};
use chrono::Datelike;
use clap::Parser;
use license::init_license;
@@ -34,6 +35,7 @@ use rustfs_ahm::{
};
use rustfs_common::globals::set_global_addr;
use rustfs_config::DEFAULT_DELIMITER;
use rustfs_ecstore::bucket::metadata_sys;
use rustfs_ecstore::bucket::metadata_sys::init_bucket_metadata_sys;
use rustfs_ecstore::cmd::bucket_replication::init_bucket_replication_pool;
use rustfs_ecstore::config as ecconfig;
@@ -51,9 +53,14 @@ use rustfs_ecstore::{
update_erasure_type,
};
use rustfs_iam::init_iam_sys;
use rustfs_notify::global::notifier_instance;
use rustfs_obs::{init_obs, set_global_guard};
use rustfs_targets::arn::TargetID;
use rustfs_utils::dns_resolver::init_global_dns_resolver;
use rustfs_utils::net::parse_and_resolve_address;
use s3s::s3_error;
use std::io::{Error, Result};
use std::str::FromStr;
use std::sync::Arc;
use tracing::{debug, error, info, instrument, warn};
@@ -95,6 +102,11 @@ async fn main() -> Result<()> {
async fn run(opt: config::Opt) -> Result<()> {
debug!("opt: {:?}", &opt);
// Initialize global DNS resolver early for enhanced DNS resolution
if let Err(e) = init_global_dns_resolver().await {
warn!("Failed to initialize global DNS resolver: {}. Using standard DNS resolution.", e);
}
if let Some(region) = &opt.region {
rustfs_ecstore::global::set_global_region(region.clone());
}
@@ -172,26 +184,59 @@ async fn run(opt: config::Opt) -> Result<()> {
.await
.map_err(Error::other)?;
let buckets = buckets_list.into_iter().map(|v| v.name).collect();
// Collect bucket names into a vector
let buckets: Vec<String> = buckets_list.into_iter().map(|v| v.name).collect();
init_bucket_metadata_sys(store.clone(), buckets).await;
// Initialize the bucket metadata system
init_bucket_metadata_sys(store.clone(), buckets.clone()).await;
// Initialize the IAM system
init_iam_sys(store.clone()).await?;
// add bucket notification configuration
add_bucket_notification_configuration(buckets).await;
// Initialize the global notification system
new_global_notification_sys(endpoint_pools.clone()).await.map_err(|err| {
error!("new_global_notification_sys failed {:?}", &err);
Error::other(err)
})?;
// Create a cancellation token for AHM services
let _ = create_ahm_services_cancel_token();
// Initialize heal manager with channel processor
let heal_storage = Arc::new(ECStoreHealStorage::new(store.clone()));
let heal_manager = init_heal_manager(heal_storage, None).await?;
// Check environment variables to determine if scanner and heal should be enabled
let enable_scanner = parse_bool_env_var("RUSTFS_ENABLE_SCANNER", true);
let enable_heal = parse_bool_env_var("RUSTFS_ENABLE_HEAL", true);
let scanner = Scanner::new(Some(ScannerConfig::default()), Some(heal_manager));
scanner.start().await?;
info!("Background services configuration: scanner={}, heal={}", enable_scanner, enable_heal);
// Initialize heal manager and scanner based on environment variables
if enable_heal || enable_scanner {
if enable_heal {
// Initialize heal manager with channel processor
let heal_storage = Arc::new(ECStoreHealStorage::new(store.clone()));
let heal_manager = init_heal_manager(heal_storage, None).await?;
if enable_scanner {
info!("Starting scanner with heal manager...");
let scanner = Scanner::new(Some(ScannerConfig::default()), Some(heal_manager));
scanner.start().await?;
} else {
info!("Scanner disabled, but heal manager is initialized and available");
}
} else if enable_scanner {
info!("Starting scanner without heal manager...");
let scanner = Scanner::new(Some(ScannerConfig::default()), None);
scanner.start().await?;
}
} else {
info!("Both scanner and heal are disabled, skipping AHM service initialization");
}
// print server info
print_server_info();
// initialize bucket replication pool
init_bucket_replication_pool().await;
// Async update check (optional)
@@ -244,19 +289,37 @@ async fn run(opt: config::Opt) -> Result<()> {
Ok(())
}
/// Parse a boolean environment variable with default value
///
/// Returns true if the environment variable is not set or set to true/1/yes/on/enabled,
/// false if set to false/0/no/off/disabled
fn parse_bool_env_var(var_name: &str, default: bool) -> bool {
std::env::var(var_name)
.unwrap_or_else(|_| default.to_string())
.parse::<bool>()
.unwrap_or(default)
}
/// Handles the shutdown process of the server
async fn handle_shutdown(state_manager: &ServiceStateManager, shutdown_tx: &tokio::sync::broadcast::Sender<()>) {
info!("Shutdown signal received in main thread");
// update the status to stopping first
state_manager.update(ServiceState::Stopping);
// Stop background services (data scanner and auto heal) gracefully
info!("Stopping background services (data scanner and auto heal)...");
shutdown_background_services();
// Check environment variables to determine what services need to be stopped
let enable_scanner = parse_bool_env_var("RUSTFS_ENABLE_SCANNER", true);
let enable_heal = parse_bool_env_var("RUSTFS_ENABLE_HEAL", true);
// Stop AHM services gracefully
info!("Stopping AHM services...");
shutdown_ahm_services();
// Stop background services based on what was enabled
if enable_scanner || enable_heal {
info!("Stopping background services (data scanner and auto heal)...");
shutdown_background_services();
info!("Stopping AHM services...");
shutdown_ahm_services();
} else {
info!("Background services were disabled, skipping AHM shutdown");
}
// Stop the notification system
shutdown_event_notifier().await;
@@ -273,7 +336,7 @@ async fn handle_shutdown(state_manager: &ServiceStateManager, shutdown_tx: &toki
}
#[instrument]
pub(crate) async fn init_event_notifier() {
async fn init_event_notifier() {
info!("Initializing event notifier...");
// 1. Get the global configuration loaded by ecstore
@@ -311,8 +374,48 @@ pub(crate) async fn init_event_notifier() {
});
}
#[instrument(skip_all)]
async fn add_bucket_notification_configuration(buckets: Vec<String>) {
let region_opt = rustfs_ecstore::global::get_global_region();
let region = match region_opt {
Some(ref r) if !r.is_empty() => r,
_ => {
warn!("Global region is not set; attempting notification configuration for all buckets with an empty region.");
""
}
};
for bucket in buckets.iter() {
let has_notification_config = metadata_sys::get_notification_config(bucket).await.unwrap_or_else(|err| {
warn!("get_notification_config err {:?}", err);
None
});
match has_notification_config {
Some(cfg) => {
info!("Bucket '{}' has existing notification configuration: {:?}", bucket, cfg);
let mut event_rules = Vec::new();
process_queue_configurations(&mut event_rules, cfg.queue_configurations.clone(), TargetID::from_str);
process_topic_configurations(&mut event_rules, cfg.topic_configurations.clone(), TargetID::from_str);
process_lambda_configurations(&mut event_rules, cfg.lambda_function_configurations.clone(), TargetID::from_str);
if let Err(e) = notifier_instance()
.add_event_specific_rules(bucket, region, &event_rules)
.await
.map_err(|e| s3_error!(InternalError, "Failed to add rules: {e}"))
{
error!("Failed to add rules for bucket '{}': {:?}", bucket, e);
}
}
None => {
info!("Bucket '{}' has no existing notification configuration.", bucket);
}
}
}
}
/// Shuts down the event notifier system gracefully
pub async fn shutdown_event_notifier() {
async fn shutdown_event_notifier() {
info!("Shutting down event notifier system...");
if !rustfs_notify::is_notification_system_initialized() {
+75 -24
View File
@@ -292,6 +292,21 @@ impl FS {
}
}
/// Helper function to get store and validate bucket exists
async fn get_validated_store(bucket: &str) -> S3Result<Arc<rustfs_ecstore::store::ECStore>> {
let Some(store) = new_object_layer_fn() else {
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
};
// Validate bucket exists
store
.get_bucket_info(bucket, &BucketOptions::default())
.await
.map_err(ApiError::from)?;
Ok(store)
}
#[async_trait::async_trait]
impl S3 for FS {
#[tracing::instrument(
@@ -928,9 +943,7 @@ impl S3 for FS {
.await
.map_err(ApiError::from)?;
let Some(store) = new_object_layer_fn() else {
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
};
let store = get_validated_store(&bucket).await?;
let reader = store
.get_object_reader(bucket.as_str(), key.as_str(), rs.clone(), h, &opts)
@@ -1215,15 +1228,17 @@ impl S3 for FS {
} = req.input;
let prefix = prefix.unwrap_or_default();
let max_keys = max_keys.unwrap_or(1000);
let max_keys = match max_keys {
Some(v) if v > 0 && v <= 1000 => v,
None => 1000,
_ => return Err(s3_error!(InvalidArgument, "max-keys must be between 1 and 1000")),
};
let delimiter = delimiter.filter(|v| !v.is_empty());
let continuation_token = continuation_token.filter(|v| !v.is_empty());
let start_after = start_after.filter(|v| !v.is_empty());
let Some(store) = new_object_layer_fn() else {
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
};
let store = get_validated_store(&bucket).await?;
let object_infos = store
.list_objects_v2(
@@ -1310,9 +1325,7 @@ impl S3 for FS {
let version_id_marker = version_id_marker.filter(|v| !v.is_empty());
let delimiter = delimiter.filter(|v| !v.is_empty());
let Some(store) = new_object_layer_fn() else {
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
};
let store = get_validated_store(&bucket).await?;
let object_infos = store
.list_object_versions(&bucket, &prefix, key_marker, version_id_marker, delimiter.clone(), max_keys)
@@ -1423,9 +1436,7 @@ impl S3 for FS {
// let mut reader = PutObjReader::new(body, content_length as usize);
let Some(store) = new_object_layer_fn() else {
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
};
let store = get_validated_store(&bucket).await?;
let mut metadata = metadata.unwrap_or_default();
@@ -1879,7 +1890,15 @@ impl S3 for FS {
};
let part_number_marker = part_number_marker.map(|x| x as usize);
let max_parts = max_parts.map(|x| x as usize).unwrap_or(MAX_PARTS_COUNT);
let max_parts = match max_parts {
Some(parts) => {
if !(1..=1000).contains(&parts) {
return Err(s3_error!(InvalidArgument, "max-parts must be between 1 and 1000"));
}
parts as usize
}
None => 1000,
};
let res = store
.list_object_parts(&bucket, &key, &upload_id, part_number_marker, max_parts, &ObjectOptions::default())
@@ -2139,6 +2158,41 @@ impl S3 for FS {
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
};
// Validate tag_set length doesn't exceed 10
if tagging.tag_set.len() > 10 {
return Err(s3_error!(InvalidArgument, "Object tags cannot be greater than 10"));
}
let mut tag_keys = std::collections::HashSet::with_capacity(tagging.tag_set.len());
for tag in &tagging.tag_set {
let key = tag
.key
.as_ref()
.filter(|k| !k.is_empty())
.ok_or_else(|| s3_error!(InvalidTag, "Tag key cannot be empty"))?;
if key.len() > 128 {
return Err(s3_error!(InvalidTag, "Tag key is too long, maximum allowed length is 128 characters"));
}
let value = tag
.value
.as_ref()
.ok_or_else(|| s3_error!(InvalidTag, "Tag value cannot be null"))?;
if value.is_empty() {
return Err(s3_error!(InvalidTag, "Tag value cannot be empty"));
}
if value.len() > 256 {
return Err(s3_error!(InvalidTag, "Tag value is too long, maximum allowed length is 256 characters"));
}
if !tag_keys.insert(key) {
return Err(s3_error!(InvalidTag, "Cannot provide multiple Tags with the same key"));
}
}
let tags = encode_tags(tagging.tag_set);
// TODO: getOpts
@@ -2772,13 +2826,10 @@ impl S3 for FS {
.await
.map_err(ApiError::from)?;
let has_notification_config = match metadata_sys::get_notification_config(&bucket).await {
Ok(cfg) => cfg,
Err(err) => {
warn!("get_notification_config err {:?}", err);
None
}
};
let has_notification_config = metadata_sys::get_notification_config(&bucket).await.unwrap_or_else(|err| {
warn!("get_notification_config err {:?}", err);
None
});
// TODO: valid target list
@@ -3434,7 +3485,7 @@ fn extract_prefix_suffix(filter: Option<&NotificationConfigurationFilter>) -> (S
}
/// Auxiliary functions: Handle configuration
fn process_queue_configurations<F>(
pub(crate) fn process_queue_configurations<F>(
event_rules: &mut Vec<(Vec<EventName>, String, String, Vec<TargetID>)>,
configurations: Option<Vec<QueueConfiguration>>,
target_id_parser: F,
@@ -3451,7 +3502,7 @@ fn process_queue_configurations<F>(
}
}
fn process_topic_configurations<F>(
pub(crate) fn process_topic_configurations<F>(
event_rules: &mut Vec<(Vec<EventName>, String, String, Vec<TargetID>)>,
configurations: Option<Vec<TopicConfiguration>>,
target_id_parser: F,
@@ -3468,7 +3519,7 @@ fn process_topic_configurations<F>(
}
}
fn process_lambda_configurations<F>(
pub(crate) fn process_lambda_configurations<F>(
event_rules: &mut Vec<(Vec<EventName>, String, String, Vec<TargetID>)>,
configurations: Option<Vec<LambdaFunctionConfiguration>>,
target_id_parser: F,