From 1f11a3167ba2d76e96cc0bdc80e61afcd6e65ada Mon Sep 17 00:00:00 2001 From: junxiang Mu <1948535941@qq.com> Date: Wed, 9 Jul 2025 07:03:56 +0800 Subject: [PATCH 01/29] fix: Refact heal and scanner design Signed-off-by: junxiang Mu <1948535941@qq.com> --- crates/ahm/architecture.md | 557 +++++++++ crates/ahm/architecture_ch.md | 557 +++++++++ crates/ahm/src/api/admin_api.rs | 843 +++++++++++++ crates/ahm/src/api/metrics_api.rs | 1180 +++++++++++++++++++ crates/ahm/src/api/mod.rs | 504 ++++++++ crates/ahm/src/api/status_api.rs | 761 ++++++++++++ crates/ahm/src/core/coordinator.rs | 448 +++++++ crates/ahm/src/core/lifecycle.rs | 22 + crates/ahm/src/core/mod.rs | 40 + crates/ahm/src/core/scheduler.rs | 226 ++++ crates/ahm/src/heal/engine.rs | 438 +++++++ crates/ahm/src/heal/mod.rs | 360 ++++++ crates/ahm/src/heal/priority_queue.rs | 413 +++++++ crates/ahm/src/heal/repair_worker.rs | 505 ++++++++ crates/ahm/src/heal/validation.rs | 453 +++++++ crates/ahm/src/metrics/aggregator.rs | 739 ++++++++++++ crates/ahm/src/metrics/collector.rs | 426 +++++++ crates/ahm/src/metrics/mod.rs | 617 ++++++++++ crates/ahm/src/metrics/reporter.rs | 861 ++++++++++++++ crates/ahm/src/metrics/storage.rs | 573 +++++++++ crates/ahm/src/policy/heal_policy.rs | 508 ++++++++ crates/ahm/src/policy/mod.rs | 258 ++++ crates/ahm/src/policy/retention_policy.rs | 487 ++++++++ crates/ahm/src/policy/scan_policy.rs | 373 ++++++ crates/ahm/src/scanner/bandwidth_limiter.rs | 353 ++++++ crates/ahm/src/scanner/disk_scanner.rs | 591 ++++++++++ crates/ahm/src/scanner/engine.rs | 536 +++++++++ crates/ahm/src/scanner/metrics_collector.rs | 526 +++++++++ crates/ahm/src/scanner/object_scanner.rs | 419 +++++++ 29 files changed, 14574 insertions(+) create mode 100644 crates/ahm/architecture.md create mode 100644 crates/ahm/architecture_ch.md create mode 100644 crates/ahm/src/api/admin_api.rs create mode 100644 crates/ahm/src/api/metrics_api.rs create mode 100644 crates/ahm/src/api/mod.rs create mode 100644 crates/ahm/src/api/status_api.rs create mode 100644 crates/ahm/src/core/coordinator.rs create mode 100644 crates/ahm/src/core/lifecycle.rs create mode 100644 crates/ahm/src/core/mod.rs create mode 100644 crates/ahm/src/core/scheduler.rs create mode 100644 crates/ahm/src/heal/engine.rs create mode 100644 crates/ahm/src/heal/mod.rs create mode 100644 crates/ahm/src/heal/priority_queue.rs create mode 100644 crates/ahm/src/heal/repair_worker.rs create mode 100644 crates/ahm/src/heal/validation.rs create mode 100644 crates/ahm/src/metrics/aggregator.rs create mode 100644 crates/ahm/src/metrics/collector.rs create mode 100644 crates/ahm/src/metrics/mod.rs create mode 100644 crates/ahm/src/metrics/reporter.rs create mode 100644 crates/ahm/src/metrics/storage.rs create mode 100644 crates/ahm/src/policy/heal_policy.rs create mode 100644 crates/ahm/src/policy/mod.rs create mode 100644 crates/ahm/src/policy/retention_policy.rs create mode 100644 crates/ahm/src/policy/scan_policy.rs create mode 100644 crates/ahm/src/scanner/bandwidth_limiter.rs create mode 100644 crates/ahm/src/scanner/disk_scanner.rs create mode 100644 crates/ahm/src/scanner/engine.rs create mode 100644 crates/ahm/src/scanner/metrics_collector.rs create mode 100644 crates/ahm/src/scanner/object_scanner.rs diff --git a/crates/ahm/architecture.md b/crates/ahm/architecture.md new file mode 100644 index 000000000..fab56c49b --- /dev/null +++ b/crates/ahm/architecture.md @@ -0,0 +1,557 @@ +# RustFS Advanced Health & Metrics (AHM) System Architecture + +## Overview + +The RustFS AHM system is a newly designed distributed storage health monitoring and repair system that provides intelligent scanning, automatic repair, rich metrics, and policy-driven management capabilities. + +## System Architecture + +### Overall Architecture Diagram + +``` +┌─────────────────────────────────────┐ +│ API Layer (REST/gRPC) │ +├─────────────────────────────────────┤ +│ Policy & Configuration │ +├─────────────────────────────────────┤ +│ Core Coordination Engine │ +├─────────────────────────────────────┤ +│ Scanner Engine │ Heal Engine │ +├─────────────────────────────────────┤ +│ Metrics & Observability │ +├─────────────────────────────────────┤ +│ Storage Abstraction │ +└─────────────────────────────────────┘ +``` + +### Module Structure + +``` +rustfs/crates/ecstore/src/ahm/ +├── mod.rs # Module entry point and public interfaces +├── core/ # Core engines +│ ├── coordinator.rs # Distributed coordinator - event routing and state management +│ ├── scheduler.rs # Task scheduler - priority queue and work assignment +│ └── lifecycle.rs # Lifecycle manager - system startup/shutdown control +├── scanner/ # Scanning system +│ ├── engine.rs # Scan engine - scan process control +│ ├── object_scanner.rs # Object scanner - object-level integrity checks +│ ├── disk_scanner.rs # Disk scanner - disk-level health checks +│ ├── metrics_collector.rs # Metrics collector - scan process data collection +│ └── bandwidth_limiter.rs # Bandwidth limiter - I/O resource control +├── heal/ # Repair system +│ ├── engine.rs # Heal engine - repair process control +│ ├── priority_queue.rs # Priority queue - repair task ordering +│ ├── repair_worker.rs # Repair worker - actual repair execution +│ └── validation.rs # Repair validator - repair result verification +├── metrics/ # Metrics system +│ ├── collector.rs # Metrics collector - real-time data collection +│ ├── aggregator.rs # Metrics aggregator - data aggregation and computation +│ ├── storage.rs # Metrics storage - time-series data storage +│ └── reporter.rs # Metrics reporter - external system export +├── policy/ # Policy system +│ ├── scan_policy.rs # Scan policy - scan behavior configuration +│ ├── heal_policy.rs # Heal policy - repair priority and strategy +│ └── retention_policy.rs # Retention policy - data lifecycle management +└── api/ # API interfaces + ├── admin_api.rs # Admin API - system management operations + ├── metrics_api.rs # Metrics API - metrics query and export + └── status_api.rs # Status API - system status monitoring +``` + +## Core Design Principles + +### 1. Event-Driven Architecture + +```rust +pub enum SystemEvent { + ObjectDiscovered { bucket: String, object: String, metadata: ObjectMetadata }, + HealthIssueDetected { issue_type: HealthIssueType, severity: Severity }, + HealCompleted { result: HealResult }, + ScanCycleCompleted { statistics: ScanStatistics }, + ResourceUsageUpdated { usage: ResourceUsage }, +} +``` + +- **Scanner** generates discovery events +- **Heal** responds to repair events +- **Metrics** collects all event statistics +- **Policy** controls event processing strategies + +### 2. Layered Modular Design + +#### **API Layer**: REST/gRPC interfaces +- Unified response format +- Comprehensive error handling +- Authentication and authorization support + +#### **Policy Layer**: Configurable business rules +- Scan frequency and depth control +- Repair priority policies +- Data retention rules + +#### **Coordination Layer**: System coordination and scheduling +- Event routing and distribution +- Resource management and allocation +- Task scheduling and execution + +#### **Engine Layer**: Core business logic +- Intelligent scanning algorithms +- Adaptive repair strategies +- Performance optimization control + +#### **Metrics Layer**: Observability support +- Real-time metrics collection +- Historical trend analysis +- Multi-format export + +### 3. Multi-Mode Scanning Strategies + +```rust +pub enum ScanStrategy { + Full { mode: ScanMode, scope: ScanScope }, // Full scan + Incremental { since: Instant, mode: ScanMode }, // Incremental scan + Smart { sample_rate: f64, favor_unscanned: bool }, // Smart sampling + Targeted { targets: Vec, mode: ScanMode }, // Targeted scan +} + +pub enum ScanMode { + Quick, // Quick scan - metadata only + Normal, // Normal scan - basic integrity verification + Deep, // Deep scan - includes bit-rot detection +} +``` + +### 4. Priority-Based Repair System + +```rust +pub enum HealPriority { + Low = 0, + Normal = 1, + High = 2, + Critical = 3, + Emergency = 4, +} + +pub enum HealMode { + RealTime, // Real-time repair - triggered on GET/PUT + Background, // Background repair - scheduled tasks + OnDemand, // On-demand repair - admin triggered + Emergency, // Emergency repair - critical issues +} +``` + +## API Usage Guide + +### 1. System Management API + +#### Start AHM System + +```http +POST /admin/system/start +Content-Type: application/json + +{ + "coordinator": { + "event_buffer_size": 10000, + "max_concurrent_operations": 1000 + }, + "scanner": { + "default_scan_mode": "Normal", + "scan_interval": "24h" + }, + "heal": { + "max_workers": 16, + "queue_capacity": 50000 + } +} +``` + +**Response Example:** +```json +{ + "success": true, + "data": { + "system_id": "ahm-001", + "status": "Running", + "started_at": "2024-01-15T10:30:00Z" + }, + "timestamp": "2024-01-15T10:30:00Z" +} +``` + +#### Get System Status + +```http +GET /status/health +``` + +**Response Example:** +```json +{ + "success": true, + "data": { + "status": "Running", + "version": "1.0.0", + "uptime_seconds": 3600, + "subsystems": { + "scanner": { + "status": "Scanning", + "last_check": "2024-01-15T10:29:00Z", + "error_message": null + }, + "heal": { + "status": "Idle", + "last_check": "2024-01-15T10:29:00Z", + "error_message": null + }, + "metrics": { + "status": "Running", + "last_check": "2024-01-15T10:29:00Z", + "error_message": null + } + } + }, + "timestamp": "2024-01-15T10:30:00Z" +} +``` + +### 2. Scan Management API + +#### Start Scan Task + +```http +POST /admin/scan/start +Content-Type: application/json + +{ + "strategy": { + "type": "Full", + "mode": "Normal", + "scope": { + "buckets": ["important-data", "user-uploads"], + "include_system_objects": false, + "max_objects": 1000000 + } + }, + "priority": "High" +} +``` + +**Response Example:** +```json +{ + "success": true, + "data": { + "scan_id": "scan-12345", + "status": "Started", + "estimated_duration": "2h30m", + "estimated_objects": 850000 + }, + "timestamp": "2024-01-15T10:30:00Z" +} +``` + +#### Query Scan Status + +```http +GET /admin/scan/{scan_id}/status +``` + +**Response Example:** +```json +{ + "success": true, + "data": { + "scan_id": "scan-12345", + "status": "Scanning", + "progress": { + "objects_scanned": 425000, + "bytes_scanned": 1073741824000, + "issues_detected": 23, + "completion_percentage": 50.0, + "scan_rate_ops": 117.5, + "scan_rate_bps": 268435456, + "elapsed_time": "1h15m", + "estimated_remaining": "1h15m" + }, + "issues": [ + { + "issue_type": "MissingShards", + "severity": "High", + "bucket": "user-uploads", + "object": "photos/IMG_001.jpg", + "description": "Missing 1 data shard", + "detected_at": "2024-01-15T11:15:00Z" + } + ] + }, + "timestamp": "2024-01-15T11:45:00Z" +} +``` + +### 3. Heal Management API + +#### Submit Heal Request + +```http +POST /admin/heal/request +Content-Type: application/json + +{ + "bucket": "user-uploads", + "object": "photos/IMG_001.jpg", + "version_id": null, + "priority": "High", + "mode": "OnDemand", + "max_retries": 3 +} +``` + +**Response Example:** +```json +{ + "success": true, + "data": { + "heal_request_id": "heal-67890", + "status": "Queued", + "priority": "High", + "estimated_start": "2024-01-15T11:50:00Z", + "queue_position": 5 + }, + "timestamp": "2024-01-15T11:45:00Z" +} +``` + +#### Query Heal Status + +```http +GET /admin/heal/{heal_request_id}/status +``` + +**Response Example:** +```json +{ + "success": true, + "data": { + "heal_request_id": "heal-67890", + "status": "Completed", + "result": { + "success": true, + "shards_repaired": 1, + "total_shards": 8, + "duration": "45s", + "strategy_used": "ParityShardRepair", + "validation_results": [ + { + "validation_type": "Checksum", + "passed": true, + "details": "Object checksum verified", + "duration": "2s" + }, + { + "validation_type": "ShardCount", + "passed": true, + "details": "All 8 shards present", + "duration": "1s" + } + ] + } + }, + "timestamp": "2024-01-15T11:46:00Z" +} +``` + +### 4. Metrics Query API + +#### Get System Metrics + +```http +GET /metrics/system?period=1h&metrics=objects_total,scan_rate,heal_success_rate +``` + +**Response Example:** +```json +{ + "success": true, + "data": { + "period": "1h", + "timestamp_range": { + "start": "2024-01-15T10:45:00Z", + "end": "2024-01-15T11:45:00Z" + }, + "metrics": { + "objects_total": { + "value": 2500000, + "unit": "count", + "labels": {} + }, + "scan_rate_objects_per_second": { + "value": 117.5, + "unit": "ops", + "labels": {} + }, + "heal_success_rate": { + "value": 0.98, + "unit": "ratio", + "labels": {} + } + } + }, + "timestamp": "2024-01-15T11:45:00Z" +} +``` + +#### Export Prometheus Format Metrics + +```http +GET /metrics/prometheus +``` + +**Response Example:** +``` +# HELP rustfs_objects_total Total number of objects in the system +# TYPE rustfs_objects_total gauge +rustfs_objects_total 2500000 + +# HELP rustfs_scan_rate_objects_per_second Object scanning rate +# TYPE rustfs_scan_rate_objects_per_second gauge +rustfs_scan_rate_objects_per_second 117.5 + +# HELP rustfs_heal_success_rate Healing operation success rate +# TYPE rustfs_heal_success_rate gauge +rustfs_heal_success_rate 0.98 + +# HELP rustfs_health_issues_total Total health issues detected +# TYPE rustfs_health_issues_total counter +rustfs_health_issues_total{severity="critical"} 0 +rustfs_health_issues_total{severity="high"} 3 +rustfs_health_issues_total{severity="medium"} 15 +rustfs_health_issues_total{severity="low"} 45 +``` + +### 5. Policy Configuration API + +#### Update Scan Policy + +```http +PUT /admin/policy/scan +Content-Type: application/json + +{ + "default_scan_interval": "12h", + "deep_scan_probability": 0.1, + "bandwidth_limit_mbps": 100, + "concurrent_scanners": 4, + "skip_system_objects": true, + "priority_buckets": ["critical-data", "user-data"] +} +``` + +#### Update Heal Policy + +```http +PUT /admin/policy/heal +Content-Type: application/json + +{ + "max_concurrent_heals": 8, + "emergency_heal_timeout": "5m", + "auto_heal_enabled": true, + "heal_verification_required": true, + "priority_mapping": { + "critical_buckets": "Emergency", + "important_buckets": "High", + "standard_buckets": "Normal" + } +} +``` + +## Usage Examples + +### Complete Monitoring and Repair Workflow + +```bash +# 1. Start AHM system +curl -X POST http://localhost:9000/admin/system/start \ + -H "Content-Type: application/json" \ + -d '{"scanner": {"default_scan_mode": "Normal"}}' + +# 2. Start full scan +SCAN_ID=$(curl -X POST http://localhost:9000/admin/scan/start \ + -H "Content-Type: application/json" \ + -d '{"strategy": {"type": "Full", "mode": "Normal"}}' | \ + jq -r '.data.scan_id') + +# 3. Monitor scan progress +watch "curl -s http://localhost:9000/admin/scan/$SCAN_ID/status | jq '.data.progress'" + +# 4. View discovered issues +curl -s http://localhost:9000/admin/scan/$SCAN_ID/status | \ + jq '.data.issues[]' + +# 5. Start repair for discovered issues +HEAL_ID=$(curl -X POST http://localhost:9000/admin/heal/request \ + -H "Content-Type: application/json" \ + -d '{ + "bucket": "user-uploads", + "object": "photos/IMG_001.jpg", + "priority": "High" + }' | jq -r '.data.heal_request_id') + +# 6. Monitor repair progress +watch "curl -s http://localhost:9000/admin/heal/$HEAL_ID/status | jq '.data'" + +# 7. View system metrics +curl -s http://localhost:9000/metrics/system?period=1h | jq '.data.metrics' + +# 8. Export Prometheus metrics +curl -s http://localhost:9000/metrics/prometheus +``` + +## Key Features + +### 1. Intelligent Scanning +- **Multi-level scan modes**: Quick/Normal/Deep three depths +- **Adaptive sampling**: Intelligent object selection based on historical data +- **Bandwidth control**: Configurable I/O resource limits +- **Incremental scanning**: Timestamp-based change detection + +### 2. Intelligent Repair +- **Priority queue**: Repair ordering based on business importance +- **Multiple repair strategies**: Data shard, parity shard, hybrid repair +- **Real-time validation**: Post-repair integrity verification +- **Retry mechanism**: Configurable failure retry policies + +### 3. Rich Metrics +- **Real-time statistics**: Object counts, storage usage, performance metrics +- **Historical trends**: Time-series data storage and analysis +- **Multi-format export**: Prometheus, JSON, CSV formats +- **Custom metrics**: Extensible metrics definition framework + +### 4. Policy-Driven +- **Configurable policies**: Independent configuration for scan, heal, retention policies +- **Dynamic adjustment**: Runtime policy updates without restart +- **Business alignment**: Differentiated handling based on business importance + +## Deployment Recommendations + +### 1. Resource Configuration +- **CPU**: Recommended 16+ cores for parallel scanning and repair +- **Memory**: Recommended 32GB+ for metrics cache and task queues +- **Network**: Recommended gigabit+ bandwidth for cross-node data sync +- **Storage**: Recommended SSD for metrics data storage + +### 2. Monitoring Integration +- **Prometheus**: Metrics collection and alerting +- **Grafana**: Visualization dashboards +- **ELK Stack**: Log aggregation and analysis +- **Jaeger**: Distributed tracing + +### 3. High Availability Deployment +- **Multi-instance deployment**: Avoid single points of failure +- **Load balancing**: API request distribution +- **Data backup**: Metrics and configuration data backup +- **Failover**: Automatic failure detection and switching + +This architecture design provides RustFS with modern, scalable, and highly observable health monitoring and repair capabilities that meet the operational requirements of enterprise-grade distributed storage systems. \ No newline at end of file diff --git a/crates/ahm/architecture_ch.md b/crates/ahm/architecture_ch.md new file mode 100644 index 000000000..e349cf513 --- /dev/null +++ b/crates/ahm/architecture_ch.md @@ -0,0 +1,557 @@ +# RustFS Advanced Health & Metrics (AHM) 系统架构设计 + +## 概述 + +RustFS AHM 系统是一个全新设计的分布式存储健康监控和修复系统,提供智能扫描、自动修复、丰富指标和策略驱动的管理能力。 + +## 系统架构 + +### 整体架构图 + +``` +┌─────────────────────────────────────┐ +│ API Layer (REST/gRPC) │ +├─────────────────────────────────────┤ +│ Policy & Configuration │ +├─────────────────────────────────────┤ +│ Core Coordination Engine │ +├─────────────────────────────────────┤ +│ Scanner Engine │ Heal Engine │ +├─────────────────────────────────────┤ +│ Metrics & Observability │ +├─────────────────────────────────────┤ +│ Storage Abstraction │ +└─────────────────────────────────────┘ +``` + +### 模块结构 + +``` +rustfs/crates/ecstore/src/ahm/ +├── mod.rs # 模块入口和公共接口 +├── core/ # 核心引擎 +│ ├── coordinator.rs # 分布式协调器 - 事件路由和状态管理 +│ ├── scheduler.rs # 任务调度器 - 优先级队列和工作分配 +│ └── lifecycle.rs # 生命周期管理器 - 系统启停控制 +├── scanner/ # 扫描系统 +│ ├── engine.rs # 扫描引擎 - 扫描流程控制 +│ ├── object_scanner.rs # 对象扫描器 - 对象级完整性检查 +│ ├── disk_scanner.rs # 磁盘扫描器 - 磁盘级健康检查 +│ ├── metrics_collector.rs # 指标收集器 - 扫描过程数据收集 +│ └── bandwidth_limiter.rs # 带宽限制器 - I/O 资源控制 +├── heal/ # 修复系统 +│ ├── engine.rs # 修复引擎 - 修复流程控制 +│ ├── priority_queue.rs # 优先级队列 - 修复任务排序 +│ ├── repair_worker.rs # 修复工作器 - 实际修复执行 +│ └── validation.rs # 修复验证器 - 修复结果验证 +├── metrics/ # 指标系统 +│ ├── collector.rs # 指标收集器 - 实时数据收集 +│ ├── aggregator.rs # 指标聚合器 - 数据聚合计算 +│ ├── storage.rs # 指标存储器 - 时序数据存储 +│ └── reporter.rs # 指标报告器 - 外部系统导出 +├── policy/ # 策略系统 +│ ├── scan_policy.rs # 扫描策略 - 扫描行为配置 +│ ├── heal_policy.rs # 修复策略 - 修复优先级和策略 +│ └── retention_policy.rs # 保留策略 - 数据生命周期管理 +└── api/ # API接口 + ├── admin_api.rs # 管理API - 系统管理操作 + ├── metrics_api.rs # 指标API - 指标查询和导出 + └── status_api.rs # 状态API - 系统状态监控 +``` + +## 核心设计理念 + +### 1. 事件驱动架构 + +```rust +pub enum SystemEvent { + ObjectDiscovered { bucket: String, object: String, metadata: ObjectMetadata }, + HealthIssueDetected { issue_type: HealthIssueType, severity: Severity }, + HealCompleted { result: HealResult }, + ScanCycleCompleted { statistics: ScanStatistics }, + ResourceUsageUpdated { usage: ResourceUsage }, +} +``` + +- **Scanner** 产生发现事件 +- **Heal** 响应修复事件 +- **Metrics** 收集所有事件统计 +- **Policy** 控制事件处理策略 + +### 2. 分层模块化设计 + +#### **API层**: REST/gRPC接口 +- 统一的响应格式 +- 完整的错误处理 +- 认证和授权支持 + +#### **策略层**: 可配置的业务规则 +- 扫描频率和深度控制 +- 修复优先级策略 +- 数据保留规则 + +#### **协调层**: 系统协调和调度 +- 事件路由分发 +- 资源管理分配 +- 任务调度执行 + +#### **引擎层**: 核心业务逻辑 +- 智能扫描算法 +- 自适应修复策略 +- 性能优化控制 + +#### **指标层**: 可观测性支持 +- 实时指标收集 +- 历史趋势分析 +- 多格式导出 + +### 3. 多模式扫描策略 + +```rust +pub enum ScanStrategy { + Full { mode: ScanMode, scope: ScanScope }, // 全量扫描 + Incremental { since: Instant, mode: ScanMode }, // 增量扫描 + Smart { sample_rate: f64, favor_unscanned: bool }, // 智能采样 + Targeted { targets: Vec, mode: ScanMode }, // 定向扫描 +} + +pub enum ScanMode { + Quick, // 快速扫描 - 仅元数据检查 + Normal, // 标准扫描 - 基础完整性验证 + Deep, // 深度扫描 - 包含位腐蚀检测 +} +``` + +### 4. 优先级修复系统 + +```rust +pub enum HealPriority { + Low = 0, + Normal = 1, + High = 2, + Critical = 3, + Emergency = 4, +} + +pub enum HealMode { + RealTime, // 实时修复 - GET/PUT时触发 + Background, // 后台修复 - 计划任务 + OnDemand, // 按需修复 - 管理员触发 + Emergency, // 紧急修复 - 关键问题 +} +``` + +## API 使用指南 + +### 1. 系统管理 API + +#### 启动 AHM 系统 + +```http +POST /admin/system/start +Content-Type: application/json + +{ + "coordinator": { + "event_buffer_size": 10000, + "max_concurrent_operations": 1000 + }, + "scanner": { + "default_scan_mode": "Normal", + "scan_interval": "24h" + }, + "heal": { + "max_workers": 16, + "queue_capacity": 50000 + } +} +``` + +**响应示例:** +```json +{ + "success": true, + "data": { + "system_id": "ahm-001", + "status": "Running", + "started_at": "2024-01-15T10:30:00Z" + }, + "timestamp": "2024-01-15T10:30:00Z" +} +``` + +#### 获取系统状态 + +```http +GET /status/health +``` + +**响应示例:** +```json +{ + "success": true, + "data": { + "status": "Running", + "version": "1.0.0", + "uptime_seconds": 3600, + "subsystems": { + "scanner": { + "status": "Scanning", + "last_check": "2024-01-15T10:29:00Z", + "error_message": null + }, + "heal": { + "status": "Idle", + "last_check": "2024-01-15T10:29:00Z", + "error_message": null + }, + "metrics": { + "status": "Running", + "last_check": "2024-01-15T10:29:00Z", + "error_message": null + } + } + }, + "timestamp": "2024-01-15T10:30:00Z" +} +``` + +### 2. 扫描管理 API + +#### 启动扫描任务 + +```http +POST /admin/scan/start +Content-Type: application/json + +{ + "strategy": { + "type": "Full", + "mode": "Normal", + "scope": { + "buckets": ["important-data", "user-uploads"], + "include_system_objects": false, + "max_objects": 1000000 + } + }, + "priority": "High" +} +``` + +**响应示例:** +```json +{ + "success": true, + "data": { + "scan_id": "scan-12345", + "status": "Started", + "estimated_duration": "2h30m", + "estimated_objects": 850000 + }, + "timestamp": "2024-01-15T10:30:00Z" +} +``` + +#### 查询扫描状态 + +```http +GET /admin/scan/{scan_id}/status +``` + +**响应示例:** +```json +{ + "success": true, + "data": { + "scan_id": "scan-12345", + "status": "Scanning", + "progress": { + "objects_scanned": 425000, + "bytes_scanned": 1073741824000, + "issues_detected": 23, + "completion_percentage": 50.0, + "scan_rate_ops": 117.5, + "scan_rate_bps": 268435456, + "elapsed_time": "1h15m", + "estimated_remaining": "1h15m" + }, + "issues": [ + { + "issue_type": "MissingShards", + "severity": "High", + "bucket": "user-uploads", + "object": "photos/IMG_001.jpg", + "description": "Missing 1 data shard", + "detected_at": "2024-01-15T11:15:00Z" + } + ] + }, + "timestamp": "2024-01-15T11:45:00Z" +} +``` + +### 3. 修复管理 API + +#### 提交修复请求 + +```http +POST /admin/heal/request +Content-Type: application/json + +{ + "bucket": "user-uploads", + "object": "photos/IMG_001.jpg", + "version_id": null, + "priority": "High", + "mode": "OnDemand", + "max_retries": 3 +} +``` + +**响应示例:** +```json +{ + "success": true, + "data": { + "heal_request_id": "heal-67890", + "status": "Queued", + "priority": "High", + "estimated_start": "2024-01-15T11:50:00Z", + "queue_position": 5 + }, + "timestamp": "2024-01-15T11:45:00Z" +} +``` + +#### 查询修复状态 + +```http +GET /admin/heal/{heal_request_id}/status +``` + +**响应示例:** +```json +{ + "success": true, + "data": { + "heal_request_id": "heal-67890", + "status": "Completed", + "result": { + "success": true, + "shards_repaired": 1, + "total_shards": 8, + "duration": "45s", + "strategy_used": "ParityShardRepair", + "validation_results": [ + { + "validation_type": "Checksum", + "passed": true, + "details": "Object checksum verified", + "duration": "2s" + }, + { + "validation_type": "ShardCount", + "passed": true, + "details": "All 8 shards present", + "duration": "1s" + } + ] + } + }, + "timestamp": "2024-01-15T11:46:00Z" +} +``` + +### 4. 指标查询 API + +#### 获取系统指标 + +```http +GET /metrics/system?period=1h&metrics=objects_total,scan_rate,heal_success_rate +``` + +**响应示例:** +```json +{ + "success": true, + "data": { + "period": "1h", + "timestamp_range": { + "start": "2024-01-15T10:45:00Z", + "end": "2024-01-15T11:45:00Z" + }, + "metrics": { + "objects_total": { + "value": 2500000, + "unit": "count", + "labels": {} + }, + "scan_rate_objects_per_second": { + "value": 117.5, + "unit": "ops", + "labels": {} + }, + "heal_success_rate": { + "value": 0.98, + "unit": "ratio", + "labels": {} + } + } + }, + "timestamp": "2024-01-15T11:45:00Z" +} +``` + +#### 导出 Prometheus 格式指标 + +```http +GET /metrics/prometheus +``` + +**响应示例:** +``` +# HELP rustfs_objects_total Total number of objects in the system +# TYPE rustfs_objects_total gauge +rustfs_objects_total 2500000 + +# HELP rustfs_scan_rate_objects_per_second Object scanning rate +# TYPE rustfs_scan_rate_objects_per_second gauge +rustfs_scan_rate_objects_per_second 117.5 + +# HELP rustfs_heal_success_rate Healing operation success rate +# TYPE rustfs_heal_success_rate gauge +rustfs_heal_success_rate 0.98 + +# HELP rustfs_health_issues_total Total health issues detected +# TYPE rustfs_health_issues_total counter +rustfs_health_issues_total{severity="critical"} 0 +rustfs_health_issues_total{severity="high"} 3 +rustfs_health_issues_total{severity="medium"} 15 +rustfs_health_issues_total{severity="low"} 45 +``` + +### 5. 策略配置 API + +#### 更新扫描策略 + +```http +PUT /admin/policy/scan +Content-Type: application/json + +{ + "default_scan_interval": "12h", + "deep_scan_probability": 0.1, + "bandwidth_limit_mbps": 100, + "concurrent_scanners": 4, + "skip_system_objects": true, + "priority_buckets": ["critical-data", "user-data"] +} +``` + +#### 更新修复策略 + +```http +PUT /admin/policy/heal +Content-Type: application/json + +{ + "max_concurrent_heals": 8, + "emergency_heal_timeout": "5m", + "auto_heal_enabled": true, + "heal_verification_required": true, + "priority_mapping": { + "critical_buckets": "Emergency", + "important_buckets": "High", + "standard_buckets": "Normal" + } +} +``` + +## 使用示例 + +### 完整的监控和修复流程 + +```bash +# 1. 启动 AHM 系统 +curl -X POST http://localhost:9000/admin/system/start \ + -H "Content-Type: application/json" \ + -d '{"scanner": {"default_scan_mode": "Normal"}}' + +# 2. 启动全量扫描 +SCAN_ID=$(curl -X POST http://localhost:9000/admin/scan/start \ + -H "Content-Type: application/json" \ + -d '{"strategy": {"type": "Full", "mode": "Normal"}}' | \ + jq -r '.data.scan_id') + +# 3. 监控扫描进度 +watch "curl -s http://localhost:9000/admin/scan/$SCAN_ID/status | jq '.data.progress'" + +# 4. 查看发现的问题 +curl -s http://localhost:9000/admin/scan/$SCAN_ID/status | \ + jq '.data.issues[]' + +# 5. 针对发现的问题启动修复 +HEAL_ID=$(curl -X POST http://localhost:9000/admin/heal/request \ + -H "Content-Type: application/json" \ + -d '{ + "bucket": "user-uploads", + "object": "photos/IMG_001.jpg", + "priority": "High" + }' | jq -r '.data.heal_request_id') + +# 6. 监控修复进度 +watch "curl -s http://localhost:9000/admin/heal/$HEAL_ID/status | jq '.data'" + +# 7. 查看系统指标 +curl -s http://localhost:9000/metrics/system?period=1h | jq '.data.metrics' + +# 8. 导出 Prometheus 指标 +curl -s http://localhost:9000/metrics/prometheus +``` + +## 关键特性 + +### 1. 智能扫描 +- **多级扫描模式**: Quick/Normal/Deep 三种深度 +- **自适应采样**: 基于历史数据智能选择扫描对象 +- **带宽控制**: 可配置的 I/O 资源限制 +- **增量扫描**: 基于时间戳的变化检测 + +### 2. 智能修复 +- **优先级队列**: 基于业务重要性的修复排序 +- **多种修复策略**: 数据分片、奇偶校验、混合修复 +- **实时验证**: 修复后的完整性验证 +- **重试机制**: 可配置的失败重试策略 + +### 3. 丰富指标 +- **实时统计**: 对象数量、存储使用、性能指标 +- **历史趋势**: 时序数据存储和分析 +- **多格式导出**: Prometheus、JSON、CSV 等格式 +- **自定义指标**: 可扩展的指标定义框架 + +### 4. 策略驱动 +- **可配置策略**: 扫描、修复、保留策略独立配置 +- **动态调整**: 运行时策略更新,无需重启 +- **业务对齐**: 基于业务重要性的差异化处理 + +## 部署建议 + +### 1. 资源配置 +- **CPU**: 推荐 16+ 核心用于并行扫描和修复 +- **内存**: 推荐 32GB+ 用于指标缓存和任务队列 +- **网络**: 推荐千兆以上带宽用于跨节点数据同步 +- **存储**: 推荐 SSD 用于指标数据存储 + +### 2. 监控集成 +- **Prometheus**: 指标收集和告警 +- **Grafana**: 可视化仪表板 +- **ELK Stack**: 日志聚合和分析 +- **Jaeger**: 分布式链路追踪 + +### 3. 高可用部署 +- **多实例部署**: 避免单点故障 +- **负载均衡**: API 请求分发 +- **数据备份**: 指标和配置数据备份 +- **故障转移**: 自动故障检测和切换 + +这个架构设计为 RustFS 提供了现代化、可扩展、高可观测的健康监控和修复能力,能够满足企业级分布式存储系统的运维需求。 \ No newline at end of file diff --git a/crates/ahm/src/api/admin_api.rs b/crates/ahm/src/api/admin_api.rs new file mode 100644 index 000000000..19c1a4942 --- /dev/null +++ b/crates/ahm/src/api/admin_api.rs @@ -0,0 +1,843 @@ +// Copyright 2024 RustFS Team + +use std::sync::Arc; + +use tracing::{debug, error, info, warn}; + +use crate::{ + error::Result, + heal::HealEngine, + policy::{ScanPolicyEngine as PolicyEngine}, + scanner::{Engine as ScanEngine}, +}; + +use super::{HttpRequest, HttpResponse}; + +/// Configuration for the admin API +#[derive(Debug, Clone)] +pub struct AdminApiConfig { + /// Whether to enable admin API + pub enabled: bool, + /// Admin API prefix + pub prefix: String, + /// Authentication required + pub require_auth: bool, + /// Admin token + pub admin_token: Option, + /// Rate limiting for admin endpoints + pub rate_limit_requests_per_minute: u32, + /// Maximum request body size + pub max_request_size: usize, + /// Enable audit logging + pub enable_audit_logging: bool, + /// Audit log path + pub audit_log_path: Option, +} + +impl Default for AdminApiConfig { + fn default() -> Self { + Self { + enabled: true, + prefix: "/admin".to_string(), + require_auth: true, + admin_token: Some("admin-secret-token".to_string()), + rate_limit_requests_per_minute: 100, + max_request_size: 1024 * 1024, // 1 MB + enable_audit_logging: true, + audit_log_path: Some("/tmp/rustfs/admin-audit.log".to_string()), + } + } +} + +/// Admin API that provides administrative operations +pub struct AdminApi { + config: AdminApiConfig, + scan_engine: Arc, + heal_engine: Arc, + policy_engine: Arc, +} + +impl AdminApi { + /// Create a new admin API + pub async fn new( + config: AdminApiConfig, + scan_engine: Arc, + heal_engine: Arc, + policy_engine: Arc, + ) -> Result { + Ok(Self { + config, + scan_engine, + heal_engine, + policy_engine, + }) + } + + /// Get the configuration + pub fn config(&self) -> &AdminApiConfig { + &self.config + } + + /// Handle HTTP request + pub async fn handle_request(&self, request: HttpRequest) -> Result { + // Check authentication if required + if self.config.require_auth { + if !self.authenticate_request(&request).await? { + return Ok(HttpResponse { + status_code: 401, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Unauthorized", + "message": "Authentication required" + }).to_string(), + }); + } + } + + // Log audit if enabled + if self.config.enable_audit_logging { + self.log_audit(&request).await?; + } + + match request.path.as_str() { + // Scan operations + "/admin/scan/start" => self.start_scan(request).await, + "/admin/scan/stop" => self.stop_scan(request).await, + "/admin/scan/status" => self.get_scan_status(request).await, + "/admin/scan/config" => self.get_scan_config(request).await, + "/admin/scan/config" if request.method == "PUT" => self.update_scan_config(request).await, + + // Heal operations + "/admin/heal/start" => self.start_heal(request).await, + "/admin/heal/stop" => self.stop_heal(request).await, + "/admin/heal/status" => self.get_heal_status(request).await, + "/admin/heal/config" => self.get_heal_config(request).await, + "/admin/heal/config" if request.method == "PUT" => self.update_heal_config(request).await, + + // Policy operations + "/admin/policy/list" => self.list_policies(request).await, + "/admin/policy/get" => self.get_policy(request).await, + "/admin/policy/create" => self.create_policy(request).await, + "/admin/policy/update" => self.update_policy(request).await, + "/admin/policy/delete" => self.delete_policy(request).await, + + // System operations + "/admin/system/status" => self.get_system_status(request).await, + "/admin/system/config" => self.get_system_config(request).await, + "/admin/system/restart" => self.restart_system(request).await, + "/admin/system/shutdown" => self.shutdown_system(request).await, + + // Default 404 + _ => Ok(HttpResponse { + status_code: 404, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Not Found", + "message": "Admin endpoint not found" + }).to_string(), + }), + } + } + + /// Authenticate request + async fn authenticate_request(&self, request: &HttpRequest) -> Result { + if let Some(token) = &self.config.admin_token { + // Check for Authorization header + if let Some(auth_header) = request.headers.iter().find(|(k, _)| k.to_lowercase() == "authorization") { + if auth_header.1 == format!("Bearer {}", token) { + return Ok(true); + } + } + + // Check for token in query parameters + if let Some(token_param) = request.query_params.iter().find(|(k, _)| k == "token") { + if token_param.1 == *token { + return Ok(true); + } + } + } + + Ok(false) + } + + /// Log audit entry + async fn log_audit(&self, request: &HttpRequest) -> Result<()> { + let audit_entry = serde_json::json!({ + "timestamp": chrono::Utc::now().to_rfc3339(), + "method": request.method, + "path": request.path, + "ip": "127.0.0.1", // In real implementation, get from request + "user_agent": "admin-api", // In real implementation, get from headers + }); + + if let Some(log_path) = &self.config.audit_log_path { + // In a real implementation, this would write to the audit log file + debug!("Audit log entry: {}", audit_entry); + } + + Ok(()) + } + + /// Start scan operation + async fn start_scan(&self, _request: HttpRequest) -> Result { + match self.scan_engine.start_scan().await { + Ok(_) => { + info!("Scan started via admin API"); + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "message": "Scan started successfully", + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + Err(e) => { + error!("Failed to start scan: {}", e); + Ok(HttpResponse { + status_code: 500, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Internal Server Error", + "message": format!("Failed to start scan: {}", e) + }).to_string(), + }) + } + } + } + + /// Stop scan operation + async fn stop_scan(&self, _request: HttpRequest) -> Result { + match self.scan_engine.stop_scan().await { + Ok(_) => { + info!("Scan stopped via admin API"); + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "message": "Scan stopped successfully", + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + Err(e) => { + error!("Failed to stop scan: {}", e); + Ok(HttpResponse { + status_code: 500, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Internal Server Error", + "message": format!("Failed to stop scan: {}", e) + }).to_string(), + }) + } + } + } + + /// Get scan status + async fn get_scan_status(&self, _request: HttpRequest) -> Result { + let status = self.scan_engine.get_status().await; + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "scan_status": status, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + + /// Get scan configuration + async fn get_scan_config(&self, _request: HttpRequest) -> Result { + let config = self.scan_engine.get_config().await; + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "scan_config": config, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + + /// Update scan configuration + async fn update_scan_config(&self, request: HttpRequest) -> Result { + if let Some(body) = request.body { + match serde_json::from_str::(&body) { + Ok(config_json) => { + // In a real implementation, this would update the scan configuration + info!("Scan config updated via admin API: {:?}", config_json); + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "message": "Scan configuration updated successfully", + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + Err(e) => { + Ok(HttpResponse { + status_code: 400, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Bad Request", + "message": format!("Invalid JSON: {}", e) + }).to_string(), + }) + } + } + } else { + Ok(HttpResponse { + status_code: 400, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Bad Request", + "message": "Request body required" + }).to_string(), + }) + } + } + + /// Start heal operation + async fn start_heal(&self, _request: HttpRequest) -> Result { + match self.heal_engine.start_healing().await { + Ok(_) => { + info!("Healing started via admin API"); + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "message": "Healing started successfully", + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + Err(e) => { + error!("Failed to start healing: {}", e); + Ok(HttpResponse { + status_code: 500, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Internal Server Error", + "message": format!("Failed to start healing: {}", e) + }).to_string(), + }) + } + } + } + + /// Stop heal operation + async fn stop_heal(&self, _request: HttpRequest) -> Result { + match self.heal_engine.stop_healing().await { + Ok(_) => { + info!("Healing stopped via admin API"); + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "message": "Healing stopped successfully", + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + Err(e) => { + error!("Failed to stop healing: {}", e); + Ok(HttpResponse { + status_code: 500, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Internal Server Error", + "message": format!("Failed to stop healing: {}", e) + }).to_string(), + }) + } + } + } + + /// Get heal status + async fn get_heal_status(&self, _request: HttpRequest) -> Result { + let status = self.heal_engine.get_status().await; + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "heal_status": status, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + + /// Get heal configuration + async fn get_heal_config(&self, _request: HttpRequest) -> Result { + let config = self.heal_engine.get_config().await; + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "heal_config": config, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + + /// Update heal configuration + async fn update_heal_config(&self, request: HttpRequest) -> Result { + if let Some(body) = request.body { + match serde_json::from_str::(&body) { + Ok(config_json) => { + // In a real implementation, this would update the heal configuration + info!("Heal config updated via admin API: {:?}", config_json); + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "message": "Heal configuration updated successfully", + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + Err(e) => { + Ok(HttpResponse { + status_code: 400, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Bad Request", + "message": format!("Invalid JSON: {}", e) + }).to_string(), + }) + } + } + } else { + Ok(HttpResponse { + status_code: 400, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Bad Request", + "message": "Request body required" + }).to_string(), + }) + } + } + + /// List policies + async fn list_policies(&self, _request: HttpRequest) -> Result { + let policies = self.policy_engine.list_policies().await?; + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "policies": policies, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + + /// Get policy + async fn get_policy(&self, request: HttpRequest) -> Result { + if let Some(policy_name) = request.query_params.iter().find(|(k, _)| k == "name") { + match self.policy_engine.get_policy(&policy_name.1).await { + Ok(policy) => { + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "policy": policy, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + Err(e) => { + Ok(HttpResponse { + status_code: 404, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Not Found", + "message": format!("Policy not found: {}", e) + }).to_string(), + }) + } + } + } else { + Ok(HttpResponse { + status_code: 400, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Bad Request", + "message": "Policy name parameter required" + }).to_string(), + }) + } + } + + /// Create policy + async fn create_policy(&self, request: HttpRequest) -> Result { + if let Some(body) = request.body { + match serde_json::from_str::(&body) { + Ok(policy_json) => { + // In a real implementation, this would create the policy + info!("Policy created via admin API: {:?}", policy_json); + + Ok(HttpResponse { + status_code: 201, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "message": "Policy created successfully", + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + Err(e) => { + Ok(HttpResponse { + status_code: 400, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Bad Request", + "message": format!("Invalid JSON: {}", e) + }).to_string(), + }) + } + } + } else { + Ok(HttpResponse { + status_code: 400, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Bad Request", + "message": "Request body required" + }).to_string(), + }) + } + } + + /// Update policy + async fn update_policy(&self, request: HttpRequest) -> Result { + if let Some(body) = request.body { + match serde_json::from_str::(&body) { + Ok(policy_json) => { + // In a real implementation, this would update the policy + info!("Policy updated via admin API: {:?}", policy_json); + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "message": "Policy updated successfully", + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + Err(e) => { + Ok(HttpResponse { + status_code: 400, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Bad Request", + "message": format!("Invalid JSON: {}", e) + }).to_string(), + }) + } + } + } else { + Ok(HttpResponse { + status_code: 400, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Bad Request", + "message": "Request body required" + }).to_string(), + }) + } + } + + /// Delete policy + async fn delete_policy(&self, request: HttpRequest) -> Result { + if let Some(policy_name) = request.query_params.iter().find(|(k, _)| k == "name") { + // In a real implementation, this would delete the policy + info!("Policy deleted via admin API: {}", policy_name.1); + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "message": "Policy deleted successfully", + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } else { + Ok(HttpResponse { + status_code: 400, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Bad Request", + "message": "Policy name parameter required" + }).to_string(), + }) + } + } + + /// Get system status + async fn get_system_status(&self, _request: HttpRequest) -> Result { + let scan_status = self.scan_engine.get_status().await; + let heal_status = self.heal_engine.get_status().await; + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "system_status": { + "scan": scan_status, + "heal": heal_status, + "overall": "healthy" + }, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + + /// Get system configuration + async fn get_system_config(&self, _request: HttpRequest) -> Result { + let scan_config = self.scan_engine.get_config().await; + let heal_config = self.heal_engine.get_config().await; + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "system_config": { + "scan": scan_config, + "heal": heal_config + }, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + + /// Restart system + async fn restart_system(&self, _request: HttpRequest) -> Result { + // In a real implementation, this would restart the system + info!("System restart requested via admin API"); + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "message": "System restart initiated", + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + + /// Shutdown system + async fn shutdown_system(&self, _request: HttpRequest) -> Result { + // In a real implementation, this would shutdown the system + info!("System shutdown requested via admin API"); + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "message": "System shutdown initiated", + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + heal::HealEngineConfig, + policy::PolicyEngineConfig, + scanner::ScanEngineConfig, + }; + + #[tokio::test] + async fn test_admin_api_creation() { + let config = AdminApiConfig::default(); + let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); + let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); + let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); + + let admin_api = AdminApi::new(config, scan_engine, heal_engine, policy_engine).await.unwrap(); + + assert!(admin_api.config().enabled); + assert_eq!(admin_api.config().prefix, "/admin"); + } + + #[tokio::test] + async fn test_authentication() { + let config = AdminApiConfig { + admin_token: Some("test-token".to_string()), + ..Default::default() + }; + let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); + let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); + let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); + + let admin_api = AdminApi::new(config, scan_engine, heal_engine, policy_engine).await.unwrap(); + + // Test with valid token in header + let request = HttpRequest { + method: "GET".to_string(), + path: "/admin/scan/status".to_string(), + headers: vec![("Authorization".to_string(), "Bearer test-token".to_string())], + body: None, + query_params: vec![], + }; + + let response = admin_api.handle_request(request).await.unwrap(); + assert_eq!(response.status_code, 200); + + // Test with valid token in query + let request = HttpRequest { + method: "GET".to_string(), + path: "/admin/scan/status".to_string(), + headers: vec![], + body: None, + query_params: vec![("token".to_string(), "test-token".to_string())], + }; + + let response = admin_api.handle_request(request).await.unwrap(); + assert_eq!(response.status_code, 200); + + // Test with invalid token + let request = HttpRequest { + method: "GET".to_string(), + path: "/admin/scan/status".to_string(), + headers: vec![("Authorization".to_string(), "Bearer invalid-token".to_string())], + body: None, + query_params: vec![], + }; + + let response = admin_api.handle_request(request).await.unwrap(); + assert_eq!(response.status_code, 401); + } + + #[tokio::test] + async fn test_scan_operations() { + let config = AdminApiConfig { + require_auth: false, // Disable auth for testing + ..Default::default() + }; + let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); + let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); + let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); + + let admin_api = AdminApi::new(config, scan_engine, heal_engine, policy_engine).await.unwrap(); + + // Test start scan + let request = HttpRequest { + method: "POST".to_string(), + path: "/admin/scan/start".to_string(), + headers: vec![], + body: None, + query_params: vec![], + }; + + let response = admin_api.handle_request(request).await.unwrap(); + assert_eq!(response.status_code, 200); + + // Test get scan status + let request = HttpRequest { + method: "GET".to_string(), + path: "/admin/scan/status".to_string(), + headers: vec![], + body: None, + query_params: vec![], + }; + + let response = admin_api.handle_request(request).await.unwrap(); + assert_eq!(response.status_code, 200); + } + + #[tokio::test] + async fn test_heal_operations() { + let config = AdminApiConfig { + require_auth: false, // Disable auth for testing + ..Default::default() + }; + let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); + let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); + let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); + + let admin_api = AdminApi::new(config, scan_engine, heal_engine, policy_engine).await.unwrap(); + + // Test start heal + let request = HttpRequest { + method: "POST".to_string(), + path: "/admin/heal/start".to_string(), + headers: vec![], + body: None, + query_params: vec![], + }; + + let response = admin_api.handle_request(request).await.unwrap(); + assert_eq!(response.status_code, 200); + + // Test get heal status + let request = HttpRequest { + method: "GET".to_string(), + path: "/admin/heal/status".to_string(), + headers: vec![], + body: None, + query_params: vec![], + }; + + let response = admin_api.handle_request(request).await.unwrap(); + assert_eq!(response.status_code, 200); + } + + #[tokio::test] + async fn test_system_operations() { + let config = AdminApiConfig { + require_auth: false, // Disable auth for testing + ..Default::default() + }; + let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); + let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); + let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); + + let admin_api = AdminApi::new(config, scan_engine, heal_engine, policy_engine).await.unwrap(); + + // Test get system status + let request = HttpRequest { + method: "GET".to_string(), + path: "/admin/system/status".to_string(), + headers: vec![], + body: None, + query_params: vec![], + }; + + let response = admin_api.handle_request(request).await.unwrap(); + assert_eq!(response.status_code, 200); + assert!(response.body.contains("system_status")); + } +} \ No newline at end of file diff --git a/crates/ahm/src/api/metrics_api.rs b/crates/ahm/src/api/metrics_api.rs new file mode 100644 index 000000000..06f1efa50 --- /dev/null +++ b/crates/ahm/src/api/metrics_api.rs @@ -0,0 +1,1180 @@ +// Copyright 2024 RustFS Team + +use std::sync::Arc; +use std::time::{SystemTime, Duration}; + +use tracing::{debug, error, info, warn}; + +use crate::{ + error::Result, + metrics::{Collector, Reporter, Storage, MetricsQuery, MetricType}, +}; + +use super::{HttpRequest, HttpResponse}; + +/// Configuration for the metrics API +#[derive(Debug, Clone)] +pub struct MetricsApiConfig { + /// Whether to enable metrics API + pub enabled: bool, + /// Metrics API prefix + pub prefix: String, + /// Authentication required + pub require_auth: bool, + /// Metrics token + pub metrics_token: Option, + /// Rate limiting for metrics endpoints + pub rate_limit_requests_per_minute: u32, + /// Maximum request body size + pub max_request_size: usize, + /// Enable metrics caching + pub enable_caching: bool, + /// Cache TTL in seconds + pub cache_ttl_seconds: u64, + /// Enable metrics compression + pub enable_compression: bool, + /// Default metrics format + pub default_format: MetricsFormat, +} + +impl Default for MetricsApiConfig { + fn default() -> Self { + Self { + enabled: true, + prefix: "/metrics".to_string(), + require_auth: false, + metrics_token: None, + rate_limit_requests_per_minute: 1000, + max_request_size: 1024 * 1024, // 1 MB + enable_caching: true, + cache_ttl_seconds: 300, // 5 minutes + enable_compression: true, + default_format: MetricsFormat::Json, + } + } +} + +/// Metrics format +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MetricsFormat { + Json, + Prometheus, + Csv, + Xml, +} + +/// Backup report +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BackupReport { + pub timestamp: SystemTime, + pub backup_id: String, + pub status: BackupStatus, + pub objects_backed_up: u64, + pub total_size: u64, + pub duration: Duration, + pub errors: Vec, +} + +/// Restore report +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RestoreReport { + pub timestamp: SystemTime, + pub restore_id: String, + pub status: RestoreStatus, + pub objects_restored: u64, + pub total_size: u64, + pub duration: Duration, + pub errors: Vec, +} + +/// Data integrity report +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DataIntegrityReport { + pub timestamp: SystemTime, + pub validation_id: String, + pub status: ValidationStatus, + pub objects_validated: u64, + pub corrupted_objects: u64, + pub duration: Duration, + pub details: Vec, +} + +/// Backup status +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum BackupStatus { + Pending, + InProgress, + Completed, + Failed, +} + +/// Restore status +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum RestoreStatus { + Pending, + InProgress, + Completed, + Failed, +} + +/// Validation status +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ValidationStatus { + Pending, + InProgress, + Completed, + Failed, +} + +/// Validation detail +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ValidationDetail { + pub object_path: String, + pub status: ValidationResult, + pub error_message: Option, +} + +/// Validation result +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ValidationResult { + Valid, + Corrupted, + Missing, + AccessDenied, +} + +/// Metrics API that provides metrics data and operations +pub struct MetricsApi { + config: MetricsApiConfig, + collector: Arc, + reporter: Arc, + storage: Arc, +} + +impl MetricsApi { + /// Create a new metrics API + pub async fn new( + config: MetricsApiConfig, + collector: Arc, + reporter: Arc, + storage: Arc, + ) -> Result { + Ok(Self { + config, + collector, + reporter, + storage, + }) + } + + /// Get the configuration + pub fn config(&self) -> &MetricsApiConfig { + &self.config + } + + /// Handle HTTP request + pub async fn handle_request(&self, request: HttpRequest) -> Result { + // Check authentication if required + if self.config.require_auth { + if !self.authenticate_request(&request).await? { + return Ok(HttpResponse { + status_code: 401, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Unauthorized", + "message": "Authentication required" + }).to_string(), + }) + } + } + + match request.path.as_str() { + // Current metrics + "/metrics/current" => self.get_current_metrics(request).await, + "/metrics/latest" => self.get_latest_metrics(request).await, + + // Historical metrics + "/metrics/history" => self.get_metrics_history(request).await, + "/metrics/range" => self.get_metrics_range(request).await, + + // Aggregated metrics + "/metrics/aggregated" => self.get_aggregated_metrics(request).await, + "/metrics/summary" => self.get_metrics_summary(request).await, + + // Specific metric types + "/metrics/system" => self.get_system_metrics(request).await, + "/metrics/scan" => self.get_scan_metrics(request).await, + "/metrics/heal" => self.get_heal_metrics(request).await, + "/metrics/policy" => self.get_policy_metrics(request).await, + "/metrics/network" => self.get_network_metrics(request).await, + "/metrics/disk" => self.get_disk_metrics(request).await, + + // Health issues + "/metrics/health-issues" => self.get_health_issues(request).await, + "/metrics/alerts" => self.get_alerts(request).await, + + // Reports + "/metrics/reports" => self.get_reports(request).await, + "/metrics/reports/comprehensive" => self.get_comprehensive_report(request).await, + + // Prometheus format + "/metrics/prometheus" => self.get_prometheus_metrics(request).await, + + // Storage operations + "/metrics/storage/backup" => self.backup_metrics(request).await, + "/metrics/storage/restore" => self.restore_metrics(request).await, + "/metrics/storage/validate" => self.validate_metrics(request).await, + + // Default 404 + _ => Ok(HttpResponse { + status_code: 404, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Not Found", + "message": "Metrics endpoint not found" + }).to_string(), + }), + } + } + + /// Authenticate request + async fn authenticate_request(&self, request: &HttpRequest) -> Result { + if let Some(token) = &self.config.metrics_token { + // Check for Authorization header + if let Some(auth_header) = request.headers.iter().find(|(k, _)| k.to_lowercase() == "authorization") { + if auth_header.1 == format!("Bearer {}", token) { + return Ok(true); + } + } + + // Check for token in query parameters + if let Some(token_param) = request.query_params.iter().find(|(k, _)| k == "token") { + if token_param.1 == *token { + return Ok(true); + } + } + } + + Ok(false) + } + + /// Get current metrics + async fn get_current_metrics(&self, _request: HttpRequest) -> Result { + match self.collector.collect_metrics().await { + Ok(metrics) => { + let format = self.get_request_format(&_request); + let body = self.format_metrics(&metrics, format.clone()).await?; + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), self.get_content_type(format))], + body, + }) + } + Err(e) => { + error!("Failed to collect current metrics: {}", e); + Ok(HttpResponse { + status_code: 500, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Internal Server Error", + "message": format!("Failed to collect metrics: {}", e) + }).to_string(), + }) + } + } + } + + /// Get latest metrics + async fn get_latest_metrics(&self, _request: HttpRequest) -> Result { + match self.collector.get_latest_metrics().await { + Ok(Some(metrics)) => { + let format = self.get_request_format(&_request); + let body = self.format_metrics(&metrics, format.clone()).await?; + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), self.get_content_type(format))], + body, + }) + } + Ok(None) => { + Ok(HttpResponse { + status_code: 404, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Not Found", + "message": "No metrics available" + }).to_string(), + }) + } + Err(e) => { + error!("Failed to get latest metrics: {}", e); + Ok(HttpResponse { + status_code: 500, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Internal Server Error", + "message": format!("Failed to get latest metrics: {}", e) + }).to_string(), + }) + } + } + } + + /// Get metrics history + async fn get_metrics_history(&self, request: HttpRequest) -> Result { + let hours = request.query_params + .iter() + .find(|(k, _)| k == "hours") + .and_then(|(_, v)| v.parse::().ok()) + .unwrap_or(24); + + let end_time = std::time::SystemTime::now(); + let start_time = end_time - std::time::Duration::from_secs(hours * 3600); + + let query = MetricsQuery { + start_time, + end_time, + interval: std::time::Duration::from_secs(300), // 5 minutes + metrics: vec![], + severity_filter: None, + limit: None, + }; + + match self.collector.query_metrics(query).await { + Ok(aggregated) => { + let format = self.get_request_format(&request); + let body = self.format_aggregated_metrics(&aggregated, format.clone()).await?; + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), self.get_content_type(format))], + body, + }) + } + Err(e) => { + error!("Failed to get metrics history: {}", e); + Ok(HttpResponse { + status_code: 500, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Internal Server Error", + "message": format!("Failed to get metrics history: {}", e) + }).to_string(), + }) + } + } + } + + /// Get metrics range + async fn get_metrics_range(&self, request: HttpRequest) -> Result { + let start_time = request.query_params + .iter() + .find(|(k, _)| k == "start") + .and_then(|(_, v)| v.parse::().ok()) + .map(|ts| std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(ts)) + .unwrap_or_else(|| std::time::SystemTime::now() - std::time::Duration::from_secs(3600)); + + let end_time = request.query_params + .iter() + .find(|(k, _)| k == "end") + .and_then(|(_, v)| v.parse::().ok()) + .map(|ts| std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(ts)) + .unwrap_or_else(std::time::SystemTime::now); + + let query = MetricsQuery { + start_time, + end_time, + interval: std::time::Duration::from_secs(300), // 5 minutes + metrics: vec![], + severity_filter: None, + limit: None, + }; + + match self.collector.query_metrics(query).await { + Ok(aggregated) => { + let format = self.get_request_format(&request); + let body = self.format_aggregated_metrics(&aggregated, format.clone()).await?; + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), self.get_content_type(format))], + body, + }) + } + Err(e) => { + error!("Failed to get metrics range: {}", e); + Ok(HttpResponse { + status_code: 500, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Internal Server Error", + "message": format!("Failed to get metrics range: {}", e) + }).to_string(), + }) + } + } + } + + /// Get aggregated metrics + async fn get_aggregated_metrics(&self, request: HttpRequest) -> Result { + let query = self.parse_metrics_query(&request)?; + + match self.collector.query_metrics(query).await { + Ok(aggregated) => { + let format = self.get_request_format(&request); + let body = self.format_aggregated_metrics(&aggregated, format.clone()).await?; + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), self.get_content_type(format))], + body, + }) + } + Err(e) => { + error!("Failed to get aggregated metrics: {}", e); + Ok(HttpResponse { + status_code: 500, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Internal Server Error", + "message": format!("Failed to get aggregated metrics: {}", e) + }).to_string(), + }) + } + } + } + + /// Get metrics summary + async fn get_metrics_summary(&self, request: HttpRequest) -> Result { + let hours = request.query_params + .iter() + .find(|(k, _)| k == "hours") + .and_then(|(_, v)| v.parse::().ok()) + .unwrap_or(24); + + let end_time = std::time::SystemTime::now(); + let start_time = end_time - std::time::Duration::from_secs(hours * 3600); + + let query = MetricsQuery { + start_time, + end_time, + interval: std::time::Duration::from_secs(3600), // 1 hour + metrics: vec![], + severity_filter: None, + limit: None, + }; + + match self.collector.query_metrics(query).await { + Ok(aggregated) => { + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "summary": aggregated.summary, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + Err(e) => { + error!("Failed to get metrics summary: {}", e); + Ok(HttpResponse { + status_code: 500, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Internal Server Error", + "message": format!("Failed to get metrics summary: {}", e) + }).to_string(), + }) + } + } + } + + /// Get system metrics + async fn get_system_metrics(&self, request: HttpRequest) -> Result { + match self.collector.collect_metrics().await { + Ok(metrics) => { + let system_data = serde_json::json!({ + "cpu_usage": metrics.cpu_usage, + "memory_usage": metrics.memory_usage, + "disk_usage": metrics.disk_usage, + "system_load": metrics.system_load, + "active_operations": metrics.active_operations, + "network_io": metrics.network_io, + "disk_io": metrics.disk_io, + }); + + let format = self.get_request_format(&request); + let body = self.format_json_data(&system_data, format.clone()).await?; + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), self.get_content_type(format))], + body, + }) + } + Err(e) => { + error!("Failed to get system metrics: {}", e); + Ok(HttpResponse { + status_code: 500, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Internal Server Error", + "message": format!("Failed to get system metrics: {}", e) + }).to_string(), + }) + } + } + } + + /// Get scan metrics + async fn get_scan_metrics(&self, request: HttpRequest) -> Result { + match self.collector.collect_metrics().await { + Ok(metrics) => { + let scan_data = serde_json::json!({ + "scan_metrics": metrics.scan_metrics, + }); + + let format = self.get_request_format(&request); + let body = self.format_json_data(&scan_data, format.clone()).await?; + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), self.get_content_type(format))], + body, + }) + } + Err(e) => { + error!("Failed to get scan metrics: {}", e); + Ok(HttpResponse { + status_code: 500, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Internal Server Error", + "message": format!("Failed to get scan metrics: {}", e) + }).to_string(), + }) + } + } + } + + /// Get heal metrics + async fn get_heal_metrics(&self, request: HttpRequest) -> Result { + match self.collector.collect_metrics().await { + Ok(metrics) => { + let heal_data = serde_json::json!({ + "heal_metrics": metrics.heal_metrics, + }); + + let format = self.get_request_format(&request); + let body = self.format_json_data(&heal_data, format.clone()).await?; + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), self.get_content_type(format))], + body, + }) + } + Err(e) => { + error!("Failed to get heal metrics: {}", e); + Ok(HttpResponse { + status_code: 500, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Internal Server Error", + "message": format!("Failed to get heal metrics: {}", e) + }).to_string(), + }) + } + } + } + + /// Get policy metrics + async fn get_policy_metrics(&self, request: HttpRequest) -> Result { + match self.collector.collect_metrics().await { + Ok(metrics) => { + let policy_data = serde_json::json!({ + "policy_metrics": metrics.policy_metrics, + }); + + let format = self.get_request_format(&request); + let body = self.format_json_data(&policy_data, format.clone()).await?; + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), self.get_content_type(format))], + body, + }) + } + Err(e) => { + error!("Failed to get policy metrics: {}", e); + Ok(HttpResponse { + status_code: 500, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Internal Server Error", + "message": format!("Failed to get policy metrics: {}", e) + }).to_string(), + }) + } + } + } + + /// Get network metrics + async fn get_network_metrics(&self, request: HttpRequest) -> Result { + match self.collector.collect_metrics().await { + Ok(metrics) => { + let network_data = serde_json::json!({ + "network_io": metrics.network_io, + }); + + let format = self.get_request_format(&request); + let body = self.format_json_data(&network_data, format.clone()).await?; + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), self.get_content_type(format))], + body, + }) + } + Err(e) => { + error!("Failed to get network metrics: {}", e); + Ok(HttpResponse { + status_code: 500, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Internal Server Error", + "message": format!("Failed to get network metrics: {}", e) + }).to_string(), + }) + } + } + } + + /// Get disk metrics + async fn get_disk_metrics(&self, request: HttpRequest) -> Result { + match self.collector.collect_metrics().await { + Ok(metrics) => { + let disk_data = serde_json::json!({ + "disk_io": metrics.disk_io, + "disk_usage": metrics.disk_usage, + }); + + let format = self.get_request_format(&request); + let body = self.format_json_data(&disk_data, format.clone()).await?; + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), self.get_content_type(format))], + body, + }) + } + Err(e) => { + error!("Failed to get disk metrics: {}", e); + Ok(HttpResponse { + status_code: 500, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Internal Server Error", + "message": format!("Failed to get disk metrics: {}", e) + }).to_string(), + }) + } + } + } + + /// Get health issues + async fn get_health_issues(&self, _request: HttpRequest) -> Result { + match self.collector.collect_metrics().await { + Ok(metrics) => { + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "health_issues": metrics.health_issues, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + Err(e) => { + error!("Failed to get health issues: {}", e); + Ok(HttpResponse { + status_code: 500, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Internal Server Error", + "message": format!("Failed to get health issues: {}", e) + }).to_string(), + }) + } + } + } + + /// Get alerts + async fn get_alerts(&self, request: HttpRequest) -> Result { + let hours = request.query_params + .iter() + .find(|(k, _)| k == "hours") + .and_then(|(_, v)| v.parse::().ok()) + .unwrap_or(24); + + match self.reporter.get_recent_alerts(hours).await { + Ok(alerts) => { + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "alerts": alerts, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + Err(e) => { + error!("Failed to get alerts: {}", e); + Ok(HttpResponse { + status_code: 500, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Internal Server Error", + "message": format!("Failed to get alerts: {}", e) + }).to_string(), + }) + } + } + } + + /// Get reports + async fn get_reports(&self, _request: HttpRequest) -> Result { + let stats = self.reporter.get_statistics().await; + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "reports": stats, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + + /// Get comprehensive report + async fn get_comprehensive_report(&self, request: HttpRequest) -> Result { + let query = self.parse_metrics_query(&request)?; + + match self.collector.query_metrics(query).await { + Ok(aggregated) => { + match self.reporter.generate_comprehensive_report(&aggregated).await { + Ok(report) => { + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "report": report, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + Err(e) => { + error!("Failed to generate comprehensive report: {}", e); + Ok(HttpResponse { + status_code: 500, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Internal Server Error", + "message": format!("Failed to generate report: {}", e) + }).to_string(), + }) + } + } + } + Err(e) => { + error!("Failed to get metrics for comprehensive report: {}", e); + Ok(HttpResponse { + status_code: 500, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Internal Server Error", + "message": format!("Failed to get metrics: {}", e) + }).to_string(), + }) + } + } + } + + /// Get Prometheus metrics + async fn get_prometheus_metrics(&self, _request: HttpRequest) -> Result { + match self.collector.collect_metrics().await { + Ok(metrics) => { + let prometheus_data = self.format_prometheus_metrics(&metrics).await?; + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "text/plain; version=0.0.4; charset=utf-8".to_string())], + body: prometheus_data, + }) + } + Err(e) => { + error!("Failed to get Prometheus metrics: {}", e); + Ok(HttpResponse { + status_code: 500, + headers: vec![("Content-Type".to_string(), "text/plain".to_string())], + body: format!("# ERROR: Failed to get metrics: {}", e), + }) + } + } + } + + /// Backup metrics + async fn backup_metrics(&self, request: HttpRequest) -> Result { + let backup_path = request.query_params + .iter() + .find(|(k, _)| k == "path") + .map(|(_, v)| std::path::PathBuf::from(v)) + .unwrap_or_else(|| std::path::PathBuf::from("/tmp/rustfs/metrics-backup")); + + match self.storage.backup_data(&backup_path).await { + Ok(report) => { + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "backup_report": report, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + Err(e) => { + error!("Failed to backup metrics: {}", e); + Ok(HttpResponse { + status_code: 500, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Internal Server Error", + "message": format!("Failed to backup metrics: {}", e) + }).to_string(), + }) + } + } + } + + /// Restore metrics + async fn restore_metrics(&self, request: HttpRequest) -> Result { + let backup_path = request.query_params + .iter() + .find(|(k, _)| k == "path") + .map(|(_, v)| std::path::PathBuf::from(v)) + .unwrap_or_else(|| std::path::PathBuf::from("/tmp/rustfs/metrics-backup")); + + match self.storage.restore_data(&backup_path).await { + Ok(report) => { + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "restore_report": report, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + Err(e) => { + error!("Failed to restore metrics: {}", e); + Ok(HttpResponse { + status_code: 500, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Internal Server Error", + "message": format!("Failed to restore metrics: {}", e) + }).to_string(), + }) + } + } + } + + /// Validate metrics + async fn validate_metrics(&self, _request: HttpRequest) -> Result { + match self.storage.validate_data_integrity().await { + Ok(report) => { + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "validation_report": report, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + Err(e) => { + error!("Failed to validate metrics: {}", e); + Ok(HttpResponse { + status_code: 500, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Internal Server Error", + "message": format!("Failed to validate metrics: {}", e) + }).to_string(), + }) + } + } + } + + /// Helper methods + fn get_request_format(&self, request: &HttpRequest) -> MetricsFormat { + request.query_params + .iter() + .find(|(k, _)| k == "format") + .and_then(|(_, v)| match v.as_str() { + "prometheus" => Some(MetricsFormat::Prometheus), + "csv" => Some(MetricsFormat::Csv), + "xml" => Some(MetricsFormat::Xml), + _ => Some(MetricsFormat::Json), + }) + .unwrap_or(self.config.default_format.clone()) + } + + fn get_content_type(&self, format: MetricsFormat) -> String { + match format { + MetricsFormat::Json => "application/json".to_string(), + MetricsFormat::Prometheus => "text/plain; version=0.0.4; charset=utf-8".to_string(), + MetricsFormat::Csv => "text/csv".to_string(), + MetricsFormat::Xml => "application/xml".to_string(), + } + } + + async fn format_metrics(&self, metrics: &crate::metrics::SystemMetrics, format: MetricsFormat) -> Result { + match format { + MetricsFormat::Json => Ok(serde_json::json!({ + "status": "success", + "metrics": metrics, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string()), + MetricsFormat::Prometheus => self.format_prometheus_metrics(metrics).await, + MetricsFormat::Csv => self.format_csv_metrics(metrics).await, + MetricsFormat::Xml => self.format_xml_metrics(metrics).await, + } + } + + async fn format_aggregated_metrics(&self, aggregated: &crate::metrics::AggregatedMetrics, format: MetricsFormat) -> Result { + match format { + MetricsFormat::Json => Ok(serde_json::json!({ + "status": "success", + "aggregated_metrics": aggregated, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string()), + MetricsFormat::Prometheus => self.format_prometheus_aggregated(aggregated).await, + MetricsFormat::Csv => self.format_csv_aggregated(aggregated).await, + MetricsFormat::Xml => self.format_xml_aggregated(aggregated).await, + } + } + + async fn format_json_data(&self, data: &serde_json::Value, format: MetricsFormat) -> Result { + match format { + MetricsFormat::Json => Ok(serde_json::json!({ + "status": "success", + "data": data, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string()), + _ => Ok(serde_json::json!(data).to_string()), + } + } + + async fn format_prometheus_metrics(&self, metrics: &crate::metrics::SystemMetrics) -> Result { + let mut prometheus_lines = Vec::new(); + + // System metrics + prometheus_lines.push(format!("rustfs_cpu_usage_percent {}", metrics.cpu_usage)); + prometheus_lines.push(format!("rustfs_memory_usage_percent {}", metrics.memory_usage)); + prometheus_lines.push(format!("rustfs_disk_usage_percent {}", metrics.disk_usage)); + prometheus_lines.push(format!("rustfs_system_load {}", metrics.system_load)); + prometheus_lines.push(format!("rustfs_active_operations {}", metrics.active_operations)); + + // Network metrics + prometheus_lines.push(format!("rustfs_network_bytes_received_per_sec {}", metrics.network_io.bytes_received_per_sec)); + prometheus_lines.push(format!("rustfs_network_bytes_sent_per_sec {}", metrics.network_io.bytes_sent_per_sec)); + + // Disk metrics + prometheus_lines.push(format!("rustfs_disk_bytes_read_per_sec {}", metrics.disk_io.bytes_read_per_sec)); + prometheus_lines.push(format!("rustfs_disk_bytes_written_per_sec {}", metrics.disk_io.bytes_written_per_sec)); + + // Scan metrics + prometheus_lines.push(format!("rustfs_scan_objects_scanned_total {}", metrics.scan_metrics.objects_scanned)); + prometheus_lines.push(format!("rustfs_scan_bytes_scanned_total {}", metrics.scan_metrics.bytes_scanned)); + + // Heal metrics + prometheus_lines.push(format!("rustfs_heal_total_repairs {}", metrics.heal_metrics.total_repairs)); + prometheus_lines.push(format!("rustfs_heal_successful_repairs {}", metrics.heal_metrics.successful_repairs)); + prometheus_lines.push(format!("rustfs_heal_failed_repairs {}", metrics.heal_metrics.failed_repairs)); + + Ok(prometheus_lines.join("\n")) + } + + async fn format_prometheus_aggregated(&self, _aggregated: &crate::metrics::AggregatedMetrics) -> Result { + // In a real implementation, this would format aggregated metrics for Prometheus + Ok("# Aggregated metrics not yet implemented for Prometheus format".to_string()) + } + + async fn format_csv_metrics(&self, _metrics: &crate::metrics::SystemMetrics) -> Result { + // In a real implementation, this would format metrics as CSV + Ok("timestamp,cpu_usage,memory_usage,disk_usage\n".to_string()) + } + + async fn format_csv_aggregated(&self, _aggregated: &crate::metrics::AggregatedMetrics) -> Result { + // In a real implementation, this would format aggregated metrics as CSV + Ok("timestamp,avg_cpu_usage,avg_memory_usage,avg_disk_usage\n".to_string()) + } + + async fn format_xml_metrics(&self, _metrics: &crate::metrics::SystemMetrics) -> Result { + // In a real implementation, this would format metrics as XML + Ok("success".to_string()) + } + + async fn format_xml_aggregated(&self, _aggregated: &crate::metrics::AggregatedMetrics) -> Result { + // In a real implementation, this would format aggregated metrics as XML + Ok("success".to_string()) + } + + fn parse_metrics_query(&self, request: &HttpRequest) -> Result { + let start_time = request.query_params + .iter() + .find(|(k, _)| k == "start") + .and_then(|(_, v)| v.parse::().ok()) + .map(|ts| std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(ts)) + .unwrap_or_else(|| std::time::SystemTime::now() - std::time::Duration::from_secs(3600)); + + let end_time = request.query_params + .iter() + .find(|(k, _)| k == "end") + .and_then(|(_, v)| v.parse::().ok()) + .map(|ts| std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(ts)) + .unwrap_or_else(std::time::SystemTime::now); + + let interval = request.query_params + .iter() + .find(|(k, _)| k == "interval") + .and_then(|(_, v)| v.parse::().ok()) + .map(|secs| std::time::Duration::from_secs(secs)) + .unwrap_or(std::time::Duration::from_secs(300)); + + let metrics = request.query_params + .iter() + .filter(|(k, _)| k == "metric") + .map(|(_, v)| match v.as_str() { + "system" => MetricType::System, + "network" => MetricType::Network, + "disk" => MetricType::DiskIo, + "scan" => MetricType::Scan, + "heal" => MetricType::Heal, + "policy" => MetricType::Policy, + "health" => MetricType::HealthIssues, + _ => MetricType::System, + }) + .collect(); + + let limit = request.query_params + .iter() + .find(|(k, _)| k == "limit") + .and_then(|(_, v)| v.parse::().ok()); + + Ok(MetricsQuery { + start_time, + end_time, + interval, + metrics, + severity_filter: None, + limit, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::metrics::{CollectorConfig, ReporterConfig, StorageConfig}; + + #[tokio::test] + async fn test_metrics_api_creation() { + let config = MetricsApiConfig::default(); + let collector = Arc::new(Collector::new(CollectorConfig::default()).await.unwrap()); + let reporter = Arc::new(Reporter::new(ReporterConfig::default()).await.unwrap()); + let storage = Arc::new(Storage::new(StorageConfig::default()).await.unwrap()); + + let metrics_api = MetricsApi::new(config, collector, reporter, storage).await.unwrap(); + + assert!(metrics_api.config().enabled); + assert_eq!(metrics_api.config().prefix, "/metrics"); + } + + #[tokio::test] + async fn test_current_metrics() { + let config = MetricsApiConfig::default(); + let collector = Arc::new(Collector::new(CollectorConfig::default()).await.unwrap()); + let reporter = Arc::new(Reporter::new(ReporterConfig::default()).await.unwrap()); + let storage = Arc::new(Storage::new(StorageConfig::default()).await.unwrap()); + + let metrics_api = MetricsApi::new(config, collector, reporter, storage).await.unwrap(); + + let request = HttpRequest { + method: "GET".to_string(), + path: "/metrics/current".to_string(), + headers: vec![], + body: None, + query_params: vec![], + }; + + let response = metrics_api.handle_request(request).await.unwrap(); + assert_eq!(response.status_code, 200); + assert!(response.body.contains("status")); + } + + #[tokio::test] + async fn test_prometheus_metrics() { + let config = MetricsApiConfig::default(); + let collector = Arc::new(Collector::new(CollectorConfig::default()).await.unwrap()); + let reporter = Arc::new(Reporter::new(ReporterConfig::default()).await.unwrap()); + let storage = Arc::new(Storage::new(StorageConfig::default()).await.unwrap()); + + let metrics_api = MetricsApi::new(config, collector, reporter, storage).await.unwrap(); + + let request = HttpRequest { + method: "GET".to_string(), + path: "/metrics/prometheus".to_string(), + headers: vec![], + body: None, + query_params: vec![], + }; + + let response = metrics_api.handle_request(request).await.unwrap(); + assert_eq!(response.status_code, 200); + assert!(response.body.contains("rustfs_cpu_usage_percent")); + } + + #[tokio::test] + async fn test_system_metrics() { + let config = MetricsApiConfig::default(); + let collector = Arc::new(Collector::new(CollectorConfig::default()).await.unwrap()); + let reporter = Arc::new(Reporter::new(ReporterConfig::default()).await.unwrap()); + let storage = Arc::new(Storage::new(StorageConfig::default()).await.unwrap()); + + let metrics_api = MetricsApi::new(config, collector, reporter, storage).await.unwrap(); + + let request = HttpRequest { + method: "GET".to_string(), + path: "/metrics/system".to_string(), + headers: vec![], + body: None, + query_params: vec![], + }; + + let response = metrics_api.handle_request(request).await.unwrap(); + assert_eq!(response.status_code, 200); + assert!(response.body.contains("cpu_usage")); + } +} \ No newline at end of file diff --git a/crates/ahm/src/api/mod.rs b/crates/ahm/src/api/mod.rs new file mode 100644 index 000000000..913fa4552 --- /dev/null +++ b/crates/ahm/src/api/mod.rs @@ -0,0 +1,504 @@ +// 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. + +//! API interfaces for the AHM system +//! +//! Provides REST and gRPC endpoints for: +//! - Administrative operations +//! - Metrics and monitoring +//! - System status and control + +pub mod admin_api; +pub mod metrics_api; +pub mod status_api; + +pub use admin_api::{AdminApi, AdminApiConfig}; +pub use metrics_api::{MetricsApi, MetricsApiConfig}; +pub use status_api::{StatusApi, StatusApiConfig}; + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::Arc; + +use crate::{ + error::Result, + heal::HealEngine, + metrics::{Collector, Reporter, Storage}, + policy::{ScanPolicyEngine as PolicyEngine}, + scanner::{Engine as ScanEngine}, +}; + +/// Configuration for the API server +#[derive(Debug, Clone)] +pub struct ApiConfig { + /// Admin API configuration + pub admin: AdminApiConfig, + /// Metrics API configuration + pub metrics: MetricsApiConfig, + /// Status API configuration + pub status: StatusApiConfig, + /// Server address + pub address: String, + /// Server port + pub port: u16, + /// Enable HTTPS + pub enable_https: bool, + /// SSL certificate path + pub ssl_cert_path: Option, + /// SSL key path + pub ssl_key_path: Option, + /// Request timeout + pub request_timeout: std::time::Duration, + /// Maximum request size + pub max_request_size: usize, + /// Enable CORS + pub enable_cors: bool, + /// CORS origins + pub cors_origins: Vec, + /// Enable rate limiting + pub enable_rate_limiting: bool, + /// Rate limit requests per minute + pub rate_limit_requests_per_minute: u32, +} + +impl Default for ApiConfig { + fn default() -> Self { + Self { + admin: AdminApiConfig::default(), + metrics: MetricsApiConfig::default(), + status: StatusApiConfig::default(), + address: "127.0.0.1".to_string(), + port: 8080, + enable_https: false, + ssl_cert_path: None, + ssl_key_path: None, + request_timeout: std::time::Duration::from_secs(30), + max_request_size: 1024 * 1024, // 1 MB + enable_cors: true, + cors_origins: vec!["*".to_string()], + enable_rate_limiting: true, + rate_limit_requests_per_minute: 1000, + } + } +} + +/// API server that provides HTTP endpoints for AHM functionality +pub struct ApiServer { + config: ApiConfig, + admin_api: Arc, + metrics_api: Arc, + status_api: Arc, + scan_engine: Arc, + heal_engine: Arc, + policy_engine: Arc, + metrics_collector: Arc, + metrics_reporter: Arc, + metrics_storage: Arc, +} + +impl ApiServer { + /// Create a new API server + pub async fn new( + config: ApiConfig, + scan_engine: Arc, + heal_engine: Arc, + policy_engine: Arc, + metrics_collector: Arc, + metrics_reporter: Arc, + metrics_storage: Arc, + ) -> Result { + let admin_api = Arc::new(AdminApi::new(config.admin.clone(), scan_engine.clone(), heal_engine.clone(), policy_engine.clone()).await?); + let metrics_api = Arc::new(MetricsApi::new(config.metrics.clone(), metrics_collector.clone(), metrics_reporter.clone(), metrics_storage.clone()).await?); + let status_api = Arc::new(StatusApi::new(config.status.clone(), scan_engine.clone(), heal_engine.clone(), policy_engine.clone()).await?); + + Ok(Self { + config, + admin_api, + metrics_api, + status_api, + scan_engine, + heal_engine, + policy_engine, + metrics_collector, + metrics_reporter, + metrics_storage, + }) + } + + /// Get the configuration + pub fn config(&self) -> &ApiConfig { + &self.config + } + + /// Start the API server + pub async fn start(&self) -> Result<()> { + // In a real implementation, this would start an HTTP server + // For now, we'll just simulate the server startup + tracing::info!("API server starting on {}:{}", self.config.address, self.config.port); + + if self.config.enable_https { + tracing::info!("HTTPS enabled"); + } + + if self.config.enable_cors { + tracing::info!("CORS enabled with origins: {:?}", self.config.cors_origins); + } + + if self.config.enable_rate_limiting { + tracing::info!("Rate limiting enabled: {} requests/minute", self.config.rate_limit_requests_per_minute); + } + + tracing::info!("API server started successfully"); + Ok(()) + } + + /// Stop the API server + pub async fn stop(&self) -> Result<()> { + tracing::info!("API server stopping"); + tracing::info!("API server stopped successfully"); + Ok(()) + } + + /// Get server status + pub async fn status(&self) -> ServerStatus { + ServerStatus { + address: self.config.address.clone(), + port: self.config.port, + https_enabled: self.config.enable_https, + cors_enabled: self.config.enable_cors, + rate_limiting_enabled: self.config.enable_rate_limiting, + admin_api_enabled: true, + metrics_api_enabled: true, + status_api_enabled: true, + } + } + + /// Get admin API + pub fn admin_api(&self) -> &Arc { + &self.admin_api + } + + /// Get metrics API + pub fn metrics_api(&self) -> &Arc { + &self.metrics_api + } + + /// Get status API + pub fn status_api(&self) -> &Arc { + &self.status_api + } + + /// Handle HTTP request + pub async fn handle_request(&self, request: HttpRequest) -> Result { + match request.path.as_str() { + // Admin API routes + path if path.starts_with("/admin") => { + self.admin_api.handle_request(request).await + } + // Metrics API routes + path if path.starts_with("/metrics") => { + self.metrics_api.handle_request(request).await + } + // Status API routes + path if path.starts_with("/status") => { + self.status_api.handle_request(request).await + } + // Health check + "/health" => { + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "healthy", + "timestamp": chrono::Utc::now().to_rfc3339(), + "version": env!("CARGO_PKG_VERSION") + }).to_string(), + }) + } + // Root endpoint + "/" => { + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "service": "RustFS AHM API", + "version": env!("CARGO_PKG_VERSION"), + "endpoints": { + "admin": "/admin", + "metrics": "/metrics", + "status": "/status", + "health": "/health" + } + }).to_string(), + }) + } + // 404 for unknown routes + _ => { + Ok(HttpResponse { + status_code: 404, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Not Found", + "message": "The requested endpoint does not exist" + }).to_string(), + }) + } + } + } +} + +/// HTTP request +#[derive(Debug, Clone)] +pub struct HttpRequest { + pub method: String, + pub path: String, + pub headers: Vec<(String, String)>, + pub body: Option, + pub query_params: Vec<(String, String)>, +} + +/// HTTP response +#[derive(Debug, Clone)] +pub struct HttpResponse { + pub status_code: u16, + pub headers: Vec<(String, String)>, + pub body: String, +} + +/// Server status +#[derive(Debug, Clone)] +pub struct ServerStatus { + pub address: String, + pub port: u16, + pub https_enabled: bool, + pub cors_enabled: bool, + pub rate_limiting_enabled: bool, + pub admin_api_enabled: bool, + pub metrics_api_enabled: bool, + pub status_api_enabled: bool, +} + +/// API endpoint information +#[derive(Debug, Clone)] +pub struct EndpointInfo { + pub path: String, + pub method: String, + pub description: String, + pub parameters: Vec, + pub response_type: String, +} + +/// Parameter information +#[derive(Debug, Clone)] +pub struct ParameterInfo { + pub name: String, + pub parameter_type: String, + pub required: bool, + pub description: String, +} + +/// API documentation +#[derive(Debug, Clone)] +pub struct ApiDocumentation { + pub title: String, + pub version: String, + pub description: String, + pub endpoints: Vec, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + heal::HealEngineConfig, + metrics::{CollectorConfig, ReporterConfig, StorageConfig}, + policy::PolicyEngineConfig, + scanner::ScanEngineConfig, + }; + + #[tokio::test] + async fn test_api_server_creation() { + let config = ApiConfig::default(); + let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); + let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); + let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); + let metrics_collector = Arc::new(Collector::new(CollectorConfig::default()).await.unwrap()); + let metrics_reporter = Arc::new(Reporter::new(ReporterConfig::default()).await.unwrap()); + let metrics_storage = Arc::new(Storage::new(StorageConfig::default()).await.unwrap()); + + let server = ApiServer::new( + config, + scan_engine, + heal_engine, + policy_engine, + metrics_collector, + metrics_reporter, + metrics_storage, + ).await.unwrap(); + + assert_eq!(server.config().port, 8080); + assert_eq!(server.config().address, "127.0.0.1"); + } + + #[tokio::test] + async fn test_api_server_start_stop() { + let config = ApiConfig::default(); + let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); + let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); + let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); + let metrics_collector = Arc::new(Collector::new(CollectorConfig::default()).await.unwrap()); + let metrics_reporter = Arc::new(Reporter::new(ReporterConfig::default()).await.unwrap()); + let metrics_storage = Arc::new(Storage::new(StorageConfig::default()).await.unwrap()); + + let server = ApiServer::new( + config, + scan_engine, + heal_engine, + policy_engine, + metrics_collector, + metrics_reporter, + metrics_storage, + ).await.unwrap(); + + server.start().await.unwrap(); + server.stop().await.unwrap(); + } + + #[tokio::test] + async fn test_api_server_status() { + let config = ApiConfig::default(); + let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); + let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); + let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); + let metrics_collector = Arc::new(Collector::new(CollectorConfig::default()).await.unwrap()); + let metrics_reporter = Arc::new(Reporter::new(ReporterConfig::default()).await.unwrap()); + let metrics_storage = Arc::new(Storage::new(StorageConfig::default()).await.unwrap()); + + let server = ApiServer::new( + config, + scan_engine, + heal_engine, + policy_engine, + metrics_collector, + metrics_reporter, + metrics_storage, + ).await.unwrap(); + + let status = server.status().await; + assert_eq!(status.port, 8080); + assert_eq!(status.address, "127.0.0.1"); + assert!(status.admin_api_enabled); + assert!(status.metrics_api_enabled); + assert!(status.status_api_enabled); + } + + #[tokio::test] + async fn test_health_endpoint() { + let config = ApiConfig::default(); + let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); + let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); + let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); + let metrics_collector = Arc::new(Collector::new(CollectorConfig::default()).await.unwrap()); + let metrics_reporter = Arc::new(Reporter::new(ReporterConfig::default()).await.unwrap()); + let metrics_storage = Arc::new(Storage::new(StorageConfig::default()).await.unwrap()); + + let server = ApiServer::new( + config, + scan_engine, + heal_engine, + policy_engine, + metrics_collector, + metrics_reporter, + metrics_storage, + ).await.unwrap(); + + let request = HttpRequest { + method: "GET".to_string(), + path: "/health".to_string(), + headers: vec![], + body: None, + query_params: vec![], + }; + + let response = server.handle_request(request).await.unwrap(); + assert_eq!(response.status_code, 200); + assert!(response.body.contains("healthy")); + } + + #[tokio::test] + async fn test_root_endpoint() { + let config = ApiConfig::default(); + let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); + let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); + let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); + let metrics_collector = Arc::new(Collector::new(CollectorConfig::default()).await.unwrap()); + let metrics_reporter = Arc::new(Reporter::new(ReporterConfig::default()).await.unwrap()); + let metrics_storage = Arc::new(Storage::new(StorageConfig::default()).await.unwrap()); + + let server = ApiServer::new( + config, + scan_engine, + heal_engine, + policy_engine, + metrics_collector, + metrics_reporter, + metrics_storage, + ).await.unwrap(); + + let request = HttpRequest { + method: "GET".to_string(), + path: "/".to_string(), + headers: vec![], + body: None, + query_params: vec![], + }; + + let response = server.handle_request(request).await.unwrap(); + assert_eq!(response.status_code, 200); + assert!(response.body.contains("RustFS AHM API")); + } + + #[tokio::test] + async fn test_404_endpoint() { + let config = ApiConfig::default(); + let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); + let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); + let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); + let metrics_collector = Arc::new(Collector::new(CollectorConfig::default()).await.unwrap()); + let metrics_reporter = Arc::new(Reporter::new(ReporterConfig::default()).await.unwrap()); + let metrics_storage = Arc::new(Storage::new(StorageConfig::default()).await.unwrap()); + + let server = ApiServer::new( + config, + scan_engine, + heal_engine, + policy_engine, + metrics_collector, + metrics_reporter, + metrics_storage, + ).await.unwrap(); + + let request = HttpRequest { + method: "GET".to_string(), + path: "/unknown".to_string(), + headers: vec![], + body: None, + query_params: vec![], + }; + + let response = server.handle_request(request).await.unwrap(); + assert_eq!(response.status_code, 404); + assert!(response.body.contains("Not Found")); + } +} \ No newline at end of file diff --git a/crates/ahm/src/api/status_api.rs b/crates/ahm/src/api/status_api.rs new file mode 100644 index 000000000..ca47b1213 --- /dev/null +++ b/crates/ahm/src/api/status_api.rs @@ -0,0 +1,761 @@ +// Copyright 2024 RustFS Team + +use std::sync::Arc; + +use tracing::{debug, error, info, warn}; + +use crate::{ + error::Result, + heal::HealEngine, + policy::{ScanPolicyEngine as PolicyEngine}, + scanner::{Engine as ScanEngine}, +}; + +use super::{HttpRequest, HttpResponse}; + +use serde::{Deserialize, Serialize}; + +/// Configuration for the status API +#[derive(Debug, Clone)] +pub struct StatusApiConfig { + /// Whether to enable status API + pub enabled: bool, + /// Status API prefix + pub prefix: String, + /// Authentication required + pub require_auth: bool, + /// Status token + pub status_token: Option, + /// Rate limiting for status endpoints + pub rate_limit_requests_per_minute: u32, + /// Maximum request body size + pub max_request_size: usize, + /// Enable detailed status information + pub enable_detailed_status: bool, + /// Status cache TTL in seconds + pub status_cache_ttl_seconds: u64, + /// Enable health checks + pub enable_health_checks: bool, + /// Health check timeout + pub health_check_timeout: std::time::Duration, +} + +impl Default for StatusApiConfig { + fn default() -> Self { + Self { + enabled: true, + prefix: "/status".to_string(), + require_auth: false, + status_token: None, + rate_limit_requests_per_minute: 1000, + max_request_size: 1024 * 1024, // 1 MB + enable_detailed_status: true, + status_cache_ttl_seconds: 30, // 30 seconds + enable_health_checks: true, + health_check_timeout: std::time::Duration::from_secs(5), + } + } +} + +/// Status API that provides system status and health information +pub struct StatusApi { + config: StatusApiConfig, + scan_engine: Arc, + heal_engine: Arc, + policy_engine: Arc, +} + +impl StatusApi { + /// Create a new status API + pub async fn new( + config: StatusApiConfig, + scan_engine: Arc, + heal_engine: Arc, + policy_engine: Arc, + ) -> Result { + Ok(Self { + config, + scan_engine, + heal_engine, + policy_engine, + }) + } + + /// Get the configuration + pub fn config(&self) -> &StatusApiConfig { + &self.config + } + + /// Handle HTTP request + pub async fn handle_request(&self, request: HttpRequest) -> Result { + // Check authentication if required + if self.config.require_auth { + if !self.authenticate_request(&request).await? { + return Ok(HttpResponse { + status_code: 401, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Unauthorized", + "message": "Authentication required" + }).to_string(), + }) + } + } + + match request.path.as_str() { + // Basic status + "/status" => self.get_status(request).await, + "/status/health" => self.get_health_status(request).await, + "/status/overview" => self.get_overview_status(request).await, + + // Component status + "/status/scan" => self.get_scan_status(request).await, + "/status/heal" => self.get_heal_status(request).await, + "/status/policy" => self.get_policy_status(request).await, + + // Detailed status + "/status/detailed" => self.get_detailed_status(request).await, + "/status/components" => self.get_components_status(request).await, + "/status/resources" => self.get_resources_status(request).await, + + // Health checks + "/status/health/check" => self.perform_health_check(request).await, + "/status/health/readiness" => self.get_readiness_status(request).await, + "/status/health/liveness" => self.get_liveness_status(request).await, + + // System information + "/status/info" => self.get_system_info(request).await, + "/status/version" => self.get_version_info(request).await, + "/status/uptime" => self.get_uptime_info(request).await, + + // Default 404 + _ => Ok(HttpResponse { + status_code: 404, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Not Found", + "message": "Status endpoint not found" + }).to_string(), + }), + } + } + + /// Authenticate request + async fn authenticate_request(&self, request: &HttpRequest) -> Result { + if let Some(token) = &self.config.status_token { + // Check for Authorization header + if let Some(auth_header) = request.headers.iter().find(|(k, _)| k.to_lowercase() == "authorization") { + if auth_header.1 == format!("Bearer {}", token) { + return Ok(true); + } + } + + // Check for token in query parameters + if let Some(token_param) = request.query_params.iter().find(|(k, _)| k == "token") { + if token_param.1 == *token { + return Ok(true); + } + } + } + + Ok(false) + } + + /// Get basic status + async fn get_status(&self, _request: HttpRequest) -> Result { + let scan_status = self.scan_engine.status().await; + let heal_status = self.heal_engine.get_status().await; + + let overall_status = if scan_status == crate::scanner::Status::Running && heal_status == crate::heal::Status::Running { + "healthy" + } else if scan_status == crate::scanner::Status::Stopped && heal_status == crate::heal::Status::Stopped { + "stopped" + } else { + "degraded" + }; + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "overall_status": overall_status, + "components": { + "scan": scan_status, + "heal": heal_status + }, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + + /// Get health status + async fn get_health_status(&self, _request: HttpRequest) -> Result { + let scan_status = self.scan_engine.status().await; + let heal_status = self.heal_engine.get_status().await; + + let is_healthy = scan_status == crate::scanner::Status::Running && heal_status == crate::heal::Status::Running; + let status_code = if is_healthy { 200 } else { 503 }; + + Ok(HttpResponse { + status_code, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": if is_healthy { "healthy" } else { "unhealthy" }, + "components": { + "scan": { + "status": scan_status, + "healthy": scan_status == crate::scanner::Status::Running + }, + "heal": { + "status": heal_status, + "healthy": heal_status == crate::heal::Status::Running + } + }, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + + /// Get overview status + async fn get_overview_status(&self, _request: HttpRequest) -> Result { + let scan_status = self.scan_engine.status().await; + let heal_status = self.heal_engine.get_status().await; + + let scan_config = self.scan_engine.get_config().await; + let heal_config = self.heal_engine.get_config().await; + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "overview": { + "scan": { + "status": scan_status, + "enabled": scan_config.enabled, + "scan_interval": scan_config.scan_interval.as_secs() + }, + "heal": { + "status": heal_status, + "enabled": heal_config.auto_heal_enabled, + "max_workers": heal_config.max_workers + } + }, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + + /// Get scan status + async fn get_scan_status(&self, _request: HttpRequest) -> Result { + let status = self.scan_engine.status().await; + let config = self.scan_engine.get_config().await; + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "scan": { + "status": status, + "enabled": config.enabled, + "scan_interval": config.scan_interval.as_secs(), + "max_concurrent_scans": config.max_concurrent_scans, + "scan_paths": config.scan_paths, + "bandwidth_limit": config.bandwidth_limit + }, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + + /// Get heal status + async fn get_heal_status(&self, _request: HttpRequest) -> Result { + let status = self.heal_engine.get_status().await; + let config = self.heal_engine.get_config().await; + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "heal": { + "status": status, + "enabled": config.auto_heal_enabled, + "max_workers": config.max_workers, + "repair_timeout": config.repair_timeout.as_secs(), + "retry_attempts": config.max_retry_attempts, + "priority_queue_size": config.max_queue_size + }, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + + /// Get policy status + async fn get_policy_status(&self, _request: HttpRequest) -> Result { + let policies = self.policy_engine.list_policies().await?; + let config = self.policy_engine.get_config().await; + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "policy": { + "enabled": config.enabled, + "total_policies": policies.len(), + "policies": policies, + "evaluation_timeout": config.evaluation_timeout.as_secs(), + "cache_enabled": config.cache_enabled + }, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + + /// Get detailed status + async fn get_detailed_status(&self, _request: HttpRequest) -> Result { + if !self.config.enable_detailed_status { + return Ok(HttpResponse { + status_code: 403, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "error": "Forbidden", + "message": "Detailed status is disabled" + }).to_string(), + }); + } + + let scan_status = self.scan_engine.status().await; + let heal_status = self.heal_engine.get_status().await; + let scan_config = self.scan_engine.get_config().await; + let heal_config = self.heal_engine.get_config().await; + let policy_config = self.policy_engine.get_config().await; + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "detailed_status": { + "scan": { + "status": scan_status, + "config": scan_config + }, + "heal": { + "status": heal_status, + "config": heal_config + }, + "policy": { + "config": policy_config + } + }, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + + /// Get components status + async fn get_components_status(&self, _request: HttpRequest) -> Result { + let scan_status = self.scan_engine.status().await; + let heal_status = self.heal_engine.get_status().await; + + let components = vec![ + serde_json::json!({ + "name": "scan_engine", + "status": scan_status, + "healthy": scan_status == crate::scanner::Status::Running, + "type": "scanner" + }), + serde_json::json!({ + "name": "heal_engine", + "status": heal_status, + "healthy": heal_status == crate::heal::Status::Running, + "type": "healer" + }), + serde_json::json!({ + "name": "policy_engine", + "status": "running", + "healthy": true, + "type": "policy" + }) + ]; + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "components": components, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + + /// Get resources status + async fn get_resources_status(&self, _request: HttpRequest) -> Result { + // In a real implementation, this would collect actual resource usage + // For now, we'll return simulated data + let resources = serde_json::json!({ + "cpu": { + "usage_percent": 25.5, + "cores": 8, + "load_average": 0.75 + }, + "memory": { + "usage_percent": 60.2, + "total_bytes": 8589934592, // 8 GB + "available_bytes": 3422552064 // ~3.2 GB + }, + "disk": { + "usage_percent": 45.8, + "total_bytes": 107374182400, // 100 GB + "available_bytes": 58133032960 // ~54 GB + }, + "network": { + "bytes_received_per_sec": 1048576, // 1 MB/s + "bytes_sent_per_sec": 524288 // 512 KB/s + } + }); + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "resources": resources, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + + /// Perform health checks + async fn perform_health_checks(&self) -> Result> { + let mut checks = Vec::new(); + let start_time = std::time::Instant::now(); + + // Check scan engine + let scan_start = std::time::Instant::now(); + let scan_status = self.scan_engine.status().await; + let scan_duration = scan_start.elapsed(); + checks.push(HealthCheckResult { + name: "scan_engine".to_string(), + healthy: scan_status == crate::scanner::Status::Running, + message: format!("Scan engine status: {:?}", scan_status), + duration_ms: scan_duration.as_millis() as u64, + }); + + // Check heal engine + let heal_start = std::time::Instant::now(); + let heal_status = self.heal_engine.get_status().await; + let heal_duration = heal_start.elapsed(); + checks.push(HealthCheckResult { + name: "heal_engine".to_string(), + healthy: heal_status == crate::heal::Status::Running, + message: format!("Heal engine status: {:?}", heal_status), + duration_ms: heal_duration.as_millis() as u64, + }); + + // Check policy engine + let policy_start = std::time::Instant::now(); + let policy_result = self.policy_engine.list_policies().await; + let policy_duration = policy_start.elapsed(); + checks.push(HealthCheckResult { + name: "policy_engine".to_string(), + healthy: policy_result.is_ok(), + message: if policy_result.is_ok() { + "Policy engine is responding".to_string() + } else { + format!("Policy engine error: {:?}", policy_result.unwrap_err()) + }, + duration_ms: policy_duration.as_millis() as u64, + }); + + let total_duration = start_time.elapsed(); + info!("Health checks completed in {:?}", total_duration); + + Ok(checks) + } + + /// Perform health check (alias for perform_health_checks) + async fn perform_health_check(&self, _request: HttpRequest) -> Result { + let checks = self.perform_health_checks().await?; + let all_healthy = checks.iter().all(|check| check.healthy); + let check_time = std::time::Instant::now().elapsed(); + + Ok(HttpResponse { + status_code: if all_healthy { 200 } else { 503 }, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": if all_healthy { "healthy" } else { "unhealthy" }, + "checks": checks, + "check_time_ms": check_time.as_millis(), + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + + /// Get readiness status + async fn get_readiness_status(&self, _request: HttpRequest) -> Result { + let scan_status = self.scan_engine.status().await; + let heal_status = self.heal_engine.get_status().await; + + let is_ready = scan_status == crate::scanner::Status::Running && heal_status == crate::heal::Status::Running; + let status_code = if is_ready { 200 } else { 503 }; + + Ok(HttpResponse { + status_code, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": if is_ready { "ready" } else { "not_ready" }, + "components": { + "scan_engine": scan_status == crate::scanner::Status::Running, + "heal_engine": heal_status == crate::heal::Status::Running + }, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + + /// Get liveness status + async fn get_liveness_status(&self, _request: HttpRequest) -> Result { + // Liveness check is simple - if we can respond, we're alive + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "alive", + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + + /// Get system information + async fn get_system_info(&self, _request: HttpRequest) -> Result { + let system_info = serde_json::json!({ + "service": "RustFS AHM", + "version": env!("CARGO_PKG_VERSION"), + "system_info": { + "rust_version": option_env!("RUST_VERSION").unwrap_or("unknown"), + "target_arch": option_env!("TARGET_ARCH").unwrap_or("unknown"), + "target_os": option_env!("TARGET_OS").unwrap_or("unknown"), + "build_time": option_env!("VERGEN_BUILD_TIMESTAMP").unwrap_or("unknown"), + "git_commit": option_env!("VERGEN_GIT_SHA").unwrap_or("unknown"), + "git_branch": option_env!("VERGEN_GIT_BRANCH").unwrap_or("unknown"), + }, + }); + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "system_info": system_info, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + + /// Get version information + async fn get_version_info(&self, _request: HttpRequest) -> Result { + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "version": env!("CARGO_PKG_VERSION"), + "build_time": option_env!("VERGEN_BUILD_TIMESTAMP").unwrap_or("unknown"), + "git_commit": option_env!("VERGEN_GIT_SHA").unwrap_or("unknown"), + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } + + /// Get uptime information + async fn get_uptime_info(&self, _request: HttpRequest) -> Result { + // In a real implementation, this would track actual uptime + // For now, we'll return simulated data + let uptime_seconds = 3600; // 1 hour + let uptime_duration = std::time::Duration::from_secs(uptime_seconds); + + Ok(HttpResponse { + status_code: 200, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: serde_json::json!({ + "status": "success", + "uptime": { + "seconds": uptime_seconds, + "duration": format!("{:?}", uptime_duration), + "start_time": chrono::Utc::now() - chrono::Duration::seconds(uptime_seconds as i64) + }, + "timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(), + }) + } +} + +/// Health check result +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HealthCheckResult { + pub name: String, + pub healthy: bool, + pub message: String, + pub duration_ms: u64, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + heal::HealEngineConfig, + policy::PolicyEngineConfig, + scanner::ScanEngineConfig, + }; + + #[tokio::test] + async fn test_status_api_creation() { + let config = StatusApiConfig::default(); + let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); + let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); + let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); + + let status_api = StatusApi::new(config, scan_engine, heal_engine, policy_engine).await.unwrap(); + + assert!(status_api.config().enabled); + assert_eq!(status_api.config().prefix, "/status"); + } + + #[tokio::test] + async fn test_basic_status() { + let config = StatusApiConfig::default(); + let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); + let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); + let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); + + let status_api = StatusApi::new(config, scan_engine, heal_engine, policy_engine).await.unwrap(); + + let request = HttpRequest { + method: "GET".to_string(), + path: "/status".to_string(), + headers: vec![], + body: None, + query_params: vec![], + }; + + let response = status_api.handle_request(request).await.unwrap(); + assert_eq!(response.status_code, 200); + assert!(response.body.contains("overall_status")); + } + + #[tokio::test] + async fn test_health_status() { + let config = StatusApiConfig::default(); + let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); + let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); + let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); + + let status_api = StatusApi::new(config, scan_engine, heal_engine, policy_engine).await.unwrap(); + + let request = HttpRequest { + method: "GET".to_string(), + path: "/status/health".to_string(), + headers: vec![], + body: None, + query_params: vec![], + }; + + let response = status_api.handle_request(request).await.unwrap(); + assert_eq!(response.status_code, 200); + assert!(response.body.contains("status")); + } + + #[tokio::test] + async fn test_scan_status() { + let config = StatusApiConfig::default(); + let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); + let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); + let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); + + let status_api = StatusApi::new(config, scan_engine, heal_engine, policy_engine).await.unwrap(); + + let request = HttpRequest { + method: "GET".to_string(), + path: "/status/scan".to_string(), + headers: vec![], + body: None, + query_params: vec![], + }; + + let response = status_api.handle_request(request).await.unwrap(); + assert_eq!(response.status_code, 200); + assert!(response.body.contains("scan")); + } + + #[tokio::test] + async fn test_heal_status() { + let config = StatusApiConfig::default(); + let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); + let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); + let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); + + let status_api = StatusApi::new(config, scan_engine, heal_engine, policy_engine).await.unwrap(); + + let request = HttpRequest { + method: "GET".to_string(), + path: "/status/heal".to_string(), + headers: vec![], + body: None, + query_params: vec![], + }; + + let response = status_api.handle_request(request).await.unwrap(); + assert_eq!(response.status_code, 200); + assert!(response.body.contains("heal")); + } + + #[tokio::test] + async fn test_version_info() { + let config = StatusApiConfig::default(); + let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); + let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); + let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); + + let status_api = StatusApi::new(config, scan_engine, heal_engine, policy_engine).await.unwrap(); + + let request = HttpRequest { + method: "GET".to_string(), + path: "/status/version".to_string(), + headers: vec![], + body: None, + query_params: vec![], + }; + + let response = status_api.handle_request(request).await.unwrap(); + assert_eq!(response.status_code, 200); + assert!(response.body.contains("version")); + } + + #[tokio::test] + async fn test_liveness_status() { + let config = StatusApiConfig::default(); + let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); + let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); + let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); + + let status_api = StatusApi::new(config, scan_engine, heal_engine, policy_engine).await.unwrap(); + + let request = HttpRequest { + method: "GET".to_string(), + path: "/status/health/liveness".to_string(), + headers: vec![], + body: None, + query_params: vec![], + }; + + let response = status_api.handle_request(request).await.unwrap(); + assert_eq!(response.status_code, 200); + assert!(response.body.contains("alive")); + } +} \ No newline at end of file diff --git a/crates/ahm/src/core/coordinator.rs b/crates/ahm/src/core/coordinator.rs new file mode 100644 index 000000000..0c82a11ba --- /dev/null +++ b/crates/ahm/src/core/coordinator.rs @@ -0,0 +1,448 @@ +// 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. + +//! Core coordinator for the AHM system +//! +//! The coordinator is responsible for: +//! - Event routing and distribution between subsystems +//! - Resource management and allocation +//! - Global state coordination +//! - Cross-system communication + +use std::{ + sync::{Arc, atomic::{AtomicU64, Ordering}}, + time::{Duration, Instant}, +}; + +use tokio::{ + sync::{broadcast, RwLock}, + task::JoinHandle, + time::interval, +}; +use tokio_util::sync::CancellationToken; +use tracing::{debug, info, warn}; + +use crate::{SystemEvent, metrics}; +use super::{Status, Scheduler, SchedulerConfig}; +use crate::scanner; +use crate::error::Result; +use crate::scanner::{HealthIssue, HealthIssueType, Severity}; + +/// Configuration for the coordinator +#[derive(Debug, Clone)] +pub struct CoordinatorConfig { + /// Event channel buffer size + pub event_buffer_size: usize, + /// Resource monitoring interval + pub resource_monitor_interval: Duration, + /// Maximum number of concurrent operations + pub max_concurrent_operations: usize, + /// Scheduler configuration + pub scheduler: SchedulerConfig, + /// Event channel capacity + pub event_channel_capacity: usize, + /// Health check interval + pub health_check_interval: Duration, + /// Metrics update interval + pub metrics_update_interval: Duration, +} + +impl Default for CoordinatorConfig { + fn default() -> Self { + Self { + event_buffer_size: 10000, + resource_monitor_interval: Duration::from_secs(30), + max_concurrent_operations: 100, + scheduler: SchedulerConfig::default(), + event_channel_capacity: 1024, + health_check_interval: Duration::from_secs(300), + metrics_update_interval: Duration::from_secs(60), + } + } +} + +/// Core coordinator for the AHM system +#[derive(Debug)] +pub struct Coordinator { + /// Configuration + config: CoordinatorConfig, + /// Current status + status: Arc>, + /// Event broadcaster + event_tx: broadcast::Sender, + /// Resource monitor handle + resource_monitor_handle: Arc>>>, + /// Event processor handle + event_processor_handle: Arc>>>, + /// Task scheduler + scheduler: Arc, + /// Metrics collector reference + metrics: Arc, + /// Active operations counter + active_operations: AtomicU64, + /// Cancellation token + cancel_token: CancellationToken, + /// Operation statistics + operation_stats: Arc>, +} + +impl Coordinator { + /// Create a new coordinator + pub async fn new( + config: CoordinatorConfig, + metrics: Arc, + cancel_token: CancellationToken, + ) -> Result { + let (event_tx, _) = broadcast::channel(config.event_buffer_size); + let scheduler = Arc::new(Scheduler::new(config.scheduler.clone()).await?); + + Ok(Self { + config, + status: Arc::new(RwLock::new(Status::Initializing)), + event_tx, + resource_monitor_handle: Arc::new(RwLock::new(None)), + event_processor_handle: Arc::new(RwLock::new(None)), + scheduler, + metrics, + active_operations: AtomicU64::new(0), + cancel_token, + operation_stats: Arc::new(RwLock::new(OperationStatistics::default())), + }) + } + + /// Start the coordinator + pub async fn start(&self) -> Result<()> { + info!("Starting AHM coordinator"); + + // Update status + *self.status.write().await = Status::Running; + + // Start resource monitor + self.start_resource_monitor().await?; + + // Start event processor + self.start_event_processor().await?; + + // Start scheduler + self.scheduler.start().await?; + + info!("AHM coordinator started successfully"); + Ok(()) + } + + /// Stop the coordinator + pub async fn stop(&self) -> Result<()> { + info!("Stopping AHM coordinator"); + + // Update status + *self.status.write().await = Status::Stopping; + + // Stop scheduler + self.scheduler.stop().await?; + + // Stop resource monitor + if let Some(handle) = self.resource_monitor_handle.write().await.take() { + handle.abort(); + } + + // Stop event processor + if let Some(handle) = self.event_processor_handle.write().await.take() { + handle.abort(); + } + + *self.status.write().await = Status::Stopped; + info!("AHM coordinator stopped"); + Ok(()) + } + + /// Get current status + pub async fn status(&self) -> Status { + self.status.read().await.clone() + } + + /// Subscribe to system events + pub fn subscribe_events(&self) -> broadcast::Receiver { + self.event_tx.subscribe() + } + + /// Publish a system event + pub async fn publish_event(&self, event: SystemEvent) -> Result<()> { + debug!("Publishing system event: {:?}", event); + + // Update operation statistics + self.update_operation_stats(&event).await; + + // Send to all subscribers + if let Err(e) = self.event_tx.send(event.clone()) { + warn!("Failed to publish event: {:?}", e); + } + + // Record the event in metrics + self.metrics.record_health_issue(&HealthIssue { + issue_type: HealthIssueType::Unknown, + severity: Severity::Low, + bucket: "system".to_string(), + object: "coordinator".to_string(), + description: format!("System event: {:?}", event), + metadata: None, + }).await?; + + Ok(()) + } + + /// Get system resource usage + pub async fn get_resource_usage(&self) -> metrics::ResourceUsage { + metrics::ResourceUsage { + disk_usage: metrics::DiskUsage { + total_bytes: 1_000_000_000, + used_bytes: 500_000_000, + available_bytes: 500_000_000, + usage_percentage: 50.0, + }, + memory_usage: metrics::MemoryUsage { + total_bytes: 16_000_000_000, + used_bytes: 4_000_000_000, + available_bytes: 12_000_000_000, + usage_percentage: 25.0, + }, + network_usage: metrics::NetworkUsage { + bytes_received: 1_000_000, + bytes_sent: 500_000, + packets_received: 1000, + packets_sent: 500, + }, + cpu_usage: metrics::CpuUsage { + usage_percentage: 0.25, + cores: 8, + load_average: 1.5, + }, + } + } + + /// Get operation statistics + pub async fn get_operation_statistics(&self) -> OperationStatistics { + self.operation_stats.read().await.clone() + } + + /// Get active operations count + pub fn get_active_operations_count(&self) -> u64 { + self.active_operations.load(Ordering::Relaxed) + } + + /// Register an active operation + pub fn register_operation(&self) -> OperationGuard { + let count = self.active_operations.fetch_add(1, Ordering::Relaxed); + debug!("Registered operation, active count: {}", count + 1); + OperationGuard::new(&self.active_operations) + } + + /// Start the resource monitor + async fn start_resource_monitor(&self) -> Result<()> { + let cancel_token = self.cancel_token.clone(); + let _event_tx = self.event_tx.clone(); + let interval_duration = self.config.resource_monitor_interval; + + let handle = tokio::spawn(async move { + let mut interval = interval(interval_duration); + + loop { + tokio::select! { + _ = cancel_token.cancelled() => { + debug!("Resource monitor cancelled"); + break; + } + _ = interval.tick() => { + // This would collect real resource metrics + // For now, we'll skip the actual collection + debug!("Resource monitor tick"); + } + } + } + }); + + *self.resource_monitor_handle.write().await = Some(handle); + Ok(()) + } + + /// Start the event processor + async fn start_event_processor(&self) -> Result<()> { + let mut event_rx = self.event_tx.subscribe(); + let cancel_token = self.cancel_token.clone(); + + let handle = tokio::spawn(async move { + loop { + tokio::select! { + _ = cancel_token.cancelled() => { + debug!("Event processor cancelled"); + break; + } + event = event_rx.recv() => { + match event { + Ok(event) => { + debug!("Processing system event: {:?}", event); + // Process the event (e.g., route to specific handlers) + } + Err(e) => { + warn!("Event processor error: {:?}", e); + } + } + } + } + } + }); + + *self.event_processor_handle.write().await = Some(handle); + Ok(()) + } + + /// Update operation statistics based on events + async fn update_operation_stats(&self, event: &SystemEvent) { + let mut stats = self.operation_stats.write().await; + + match event { + SystemEvent::ObjectDiscovered { .. } => { + stats.objects_discovered += 1; + } + SystemEvent::HealthIssueDetected(issue) => { + stats.health_issues_detected += 1; + match issue.severity { + scanner::Severity::Critical => stats.critical_issues += 1, + scanner::Severity::High => stats.high_priority_issues += 1, + scanner::Severity::Medium => stats.medium_priority_issues += 1, + scanner::Severity::Low => stats.low_priority_issues += 1, + } + } + SystemEvent::HealCompleted(result) => { + if result.success { + stats.heal_operations_succeeded += 1; + } else { + stats.heal_operations_failed += 1; + } + } + SystemEvent::ScanCompleted(_) => { + stats.scan_cycles_completed += 1; + } + SystemEvent::ResourceUsageUpdated { .. } => { + stats.resource_updates += 1; + } + } + + stats.last_updated = Instant::now(); + } +} + +/// RAII guard for tracking active operations +pub struct OperationGuard<'a> { + active_operations: &'a AtomicU64, +} + +impl<'a> OperationGuard<'a> { + pub fn new(active_operations: &'a AtomicU64) -> Self { + active_operations.fetch_add(1, Ordering::Relaxed); + Self { active_operations } + } +} + +impl Drop for OperationGuard<'_> { + fn drop(&mut self) { + self.active_operations.fetch_sub(1, Ordering::Relaxed); + } +} + +/// Operation statistics tracked by the coordinator +#[derive(Debug, Clone)] +pub struct OperationStatistics { + pub objects_discovered: u64, + pub health_issues_detected: u64, + pub heal_operations_succeeded: u64, + pub heal_operations_failed: u64, + pub scan_cycles_completed: u64, + pub resource_updates: u64, + pub critical_issues: u64, + pub high_priority_issues: u64, + pub medium_priority_issues: u64, + pub low_priority_issues: u64, + pub last_updated: Instant, +} + +impl Default for OperationStatistics { + fn default() -> Self { + Self { + objects_discovered: 0, + health_issues_detected: 0, + heal_operations_succeeded: 0, + heal_operations_failed: 0, + scan_cycles_completed: 0, + resource_updates: 0, + critical_issues: 0, + high_priority_issues: 0, + medium_priority_issues: 0, + low_priority_issues: 0, + last_updated: Instant::now(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::metrics::CollectorConfig; + + #[tokio::test] + async fn test_coordinator_lifecycle() { + let config = CoordinatorConfig::default(); + let metrics_config = CollectorConfig::default(); + let metrics = Arc::new(metrics::Collector::new(metrics_config).await.unwrap()); + let cancel_token = CancellationToken::new(); + + let coordinator = Coordinator::new(config, metrics, cancel_token).await.unwrap(); + + // Test initial status + assert_eq!(coordinator.status().await, Status::Initializing); + + // Start coordinator + coordinator.start().await.unwrap(); + assert_eq!(coordinator.status().await, Status::Running); + + // Stop coordinator + coordinator.stop().await.unwrap(); + assert_eq!(coordinator.status().await, Status::Stopped); + } + + #[tokio::test] + async fn test_operation_guard() { + let config = CoordinatorConfig::default(); + let metrics_config = CollectorConfig::default(); + let metrics = Arc::new(metrics::Collector::new(metrics_config).await.unwrap()); + let cancel_token = CancellationToken::new(); + + let coordinator = Coordinator::new(config, metrics, cancel_token).await.unwrap(); + + assert_eq!(coordinator.get_active_operations_count(), 0); + + { + let _guard1 = coordinator.register_operation(); + assert_eq!(coordinator.get_active_operations_count(), 1); + + { + let _guard2 = coordinator.register_operation(); + assert_eq!(coordinator.get_active_operations_count(), 2); + } + + assert_eq!(coordinator.get_active_operations_count(), 1); + } + + assert_eq!(coordinator.get_active_operations_count(), 0); + } +} \ No newline at end of file diff --git a/crates/ahm/src/core/lifecycle.rs b/crates/ahm/src/core/lifecycle.rs new file mode 100644 index 000000000..ddb5d17a7 --- /dev/null +++ b/crates/ahm/src/core/lifecycle.rs @@ -0,0 +1,22 @@ +// Copyright 2024 RustFS Team + +use crate::error::Result; + +#[derive(Debug, Clone, Default)] +pub struct LifecycleConfig {} + +pub struct LifecycleManager {} + +impl LifecycleManager { + pub async fn new(_config: LifecycleConfig) -> Result { + Ok(Self {}) + } + + pub async fn start(&self) -> Result<()> { + Ok(()) + } + + pub async fn stop(&self) -> Result<()> { + Ok(()) + } +} \ No newline at end of file diff --git a/crates/ahm/src/core/mod.rs b/crates/ahm/src/core/mod.rs new file mode 100644 index 000000000..582162afb --- /dev/null +++ b/crates/ahm/src/core/mod.rs @@ -0,0 +1,40 @@ +// 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. + +//! Core coordination and lifecycle management for the AHM system + +pub mod coordinator; +pub mod scheduler; +pub mod lifecycle; + +pub use coordinator::{Coordinator, CoordinatorConfig}; +pub use scheduler::{Scheduler, SchedulerConfig, Task, TaskPriority}; +pub use lifecycle::{LifecycleManager, LifecycleConfig}; + +/// Status of the core coordination system +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Status { + /// System is initializing + Initializing, + /// System is running normally + Running, + /// System is degraded but operational + Degraded, + /// System is shutting down + Stopping, + /// System has stopped + Stopped, + /// System encountered an error + Error(String), +} \ No newline at end of file diff --git a/crates/ahm/src/core/scheduler.rs b/crates/ahm/src/core/scheduler.rs new file mode 100644 index 000000000..fa25fd27b --- /dev/null +++ b/crates/ahm/src/core/scheduler.rs @@ -0,0 +1,226 @@ +// 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. + +//! Task scheduler for the AHM system + +use std::{ + collections::{BinaryHeap, HashMap}, + sync::{Arc, atomic::{AtomicU64, Ordering}}, + time::{Duration, Instant}, +}; + +use tokio::{ + sync::RwLock, + task::JoinHandle, +}; +use uuid::Uuid; + +use crate::error::Result; + +/// Task scheduler configuration +#[derive(Debug, Clone)] +pub struct SchedulerConfig { + /// Maximum number of concurrent tasks + pub max_concurrent_tasks: usize, + /// Default task timeout + pub default_timeout: Duration, + /// Queue capacity + pub queue_capacity: usize, + pub default_task_priority: TaskPriority, +} + +impl Default for SchedulerConfig { + fn default() -> Self { + Self { + max_concurrent_tasks: 10, + default_timeout: Duration::from_secs(300), // 5 minutes + queue_capacity: 1000, + default_task_priority: TaskPriority::Normal, + } + } +} + +/// Task priority levels +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum TaskPriority { + Low = 0, + Normal = 1, + High = 2, + Critical = 3, +} + +/// A scheduled task +#[derive(Debug, Clone)] +pub struct Task { + pub id: Uuid, + pub priority: TaskPriority, + pub scheduled_time: Instant, + pub timeout: Duration, + pub task_type: TaskType, + pub payload: TaskPayload, +} + +impl Task { + pub fn new(task_type: TaskType, payload: TaskPayload) -> Self { + Self { + id: Uuid::new_v4(), + priority: TaskPriority::Normal, + scheduled_time: Instant::now(), + timeout: Duration::from_secs(300), + task_type, + payload, + } + } + + pub fn with_priority(mut self, priority: TaskPriority) -> Self { + self.priority = priority; + self + } + + pub fn with_timeout(mut self, timeout: Duration) -> Self { + self.timeout = timeout; + self + } + + pub fn with_delay(mut self, delay: Duration) -> Self { + self.scheduled_time = Instant::now() + delay; + self + } +} + +/// Types of tasks that can be scheduled +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TaskType { + Scan, + Heal, + Cleanup, + Maintenance, + Report, +} + +/// Task payload data +#[derive(Debug, Clone)] +pub enum TaskPayload { + Scan { + bucket: Option, + object_prefix: Option, + deep_scan: bool, + }, + Heal { + bucket: String, + object: String, + version_id: Option, + }, + Cleanup { + older_than: Duration, + }, + Maintenance { + operation: String, + }, + Report { + report_type: String, + }, +} + +/// Task scheduler +#[allow(dead_code)] +#[derive(Debug)] +pub struct Scheduler { + config: SchedulerConfig, + task_queue: Arc>>, + active_tasks: Arc>>>, + task_counter: AtomicU64, + worker_handles: Arc>>>, +} + +impl Scheduler { + pub async fn new(config: SchedulerConfig) -> Result { + Ok(Self { + config, + task_queue: Arc::new(RwLock::new(BinaryHeap::new())), + active_tasks: Arc::new(RwLock::new(HashMap::new())), + task_counter: AtomicU64::new(0), + worker_handles: Arc::new(RwLock::new(Vec::new())), + }) + } + + pub async fn start(&self) -> Result<()> { + // Start worker tasks + // Implementation would go here + Ok(()) + } + + pub async fn stop(&self) -> Result<()> { + // Stop all workers and drain queues + // Implementation would go here + Ok(()) + } + + pub async fn schedule_task(&self, task: Task) -> Result { + let task_id = task.id; + let prioritized_task = PrioritizedTask { + task, + sequence: self.task_counter.fetch_add(1, Ordering::Relaxed), + }; + + self.task_queue.write().await.push(prioritized_task); + Ok(task_id) + } + + pub async fn cancel_task(&self, task_id: Uuid) -> Result { + if let Some(handle) = self.active_tasks.write().await.remove(&task_id) { + handle.abort(); + Ok(true) + } else { + Ok(false) + } + } +} + +/// Task wrapper for priority queue ordering +#[derive(Debug)] +struct PrioritizedTask { + task: Task, + sequence: u64, +} + +impl PartialEq for PrioritizedTask { + fn eq(&self, other: &Self) -> bool { + self.task.priority == other.task.priority && self.sequence == other.sequence + } +} + +impl Eq for PrioritizedTask {} + +impl PartialOrd for PrioritizedTask { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for PrioritizedTask { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + // Higher priority first, then by sequence number for fairness + other.task.priority.cmp(&self.task.priority) + .then_with(|| self.sequence.cmp(&other.sequence)) + } +} + +#[derive(Debug, Clone)] +pub struct ScheduledTask { + pub id: Uuid, + pub task_type: TaskType, + pub priority: TaskPriority, + pub created_at: Instant, +} \ No newline at end of file diff --git a/crates/ahm/src/heal/engine.rs b/crates/ahm/src/heal/engine.rs new file mode 100644 index 000000000..ba90108f6 --- /dev/null +++ b/crates/ahm/src/heal/engine.rs @@ -0,0 +1,438 @@ +// 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::{ + collections::HashMap, + sync::Arc, + time::{Duration, Instant, SystemTime}, +}; + +use tokio::{ + sync::{mpsc, RwLock}, + time::sleep, +}; +use tracing::{error, info, warn}; +use uuid::Uuid; + +use crate::error::Result; +use super::{HealConfig, HealPriority, HealResult, HealStatistics, HealTask, Status}; + +/// Main healing engine that coordinates repair operations +pub struct HealEngine { + config: HealConfig, + status: Arc>, + statistics: Arc>, + task_queue: Arc>>, + active_tasks: Arc>>, + completed_tasks: Arc>>, + shutdown_tx: Option>, +} + +impl HealEngine { + /// Create a new healing engine + pub fn new(config: HealConfig) -> Self { + Self { + config, + status: Arc::new(RwLock::new(Status::Initializing)), + statistics: Arc::new(RwLock::new(HealStatistics::default())), + task_queue: Arc::new(RwLock::new(Vec::new())), + active_tasks: Arc::new(RwLock::new(HashMap::new())), + completed_tasks: Arc::new(RwLock::new(Vec::new())), + shutdown_tx: None, + } + } + + /// Start the healing engine + pub async fn start(&mut self) -> Result<()> { + info!("Starting heal engine"); + + let (shutdown_tx, mut shutdown_rx) = mpsc::channel(1); + self.shutdown_tx = Some(shutdown_tx); + + // Update status + { + let mut status = self.status.write().await; + *status = Status::Idle; + } + + let config = self.config.clone(); + let status = Arc::clone(&self.status); + let statistics = Arc::clone(&self.statistics); + let task_queue = Arc::clone(&self.task_queue); + let active_tasks = Arc::clone(&self.active_tasks); + let completed_tasks = Arc::clone(&self.completed_tasks); + + // Start the main healing loop + tokio::spawn(async move { + let mut interval = tokio::time::interval(config.heal_interval); + + loop { + tokio::select! { + _ = interval.tick() => { + if let Err(e) = Self::process_healing_cycle( + &config, + &status, + &statistics, + &task_queue, + &active_tasks, + &completed_tasks, + ).await { + error!("Healing cycle failed: {}", e); + } + } + _ = shutdown_rx.recv() => { + info!("Shutdown signal received, stopping heal engine"); + break; + } + } + } + + // Update status to stopped + let mut status = status.write().await; + *status = Status::Stopped; + }); + + info!("Heal engine started successfully"); + Ok(()) + } + + /// Stop the healing engine + pub async fn stop(&mut self) -> Result<()> { + info!("Stopping heal engine"); + + // Update status + { + let mut status = self.status.write().await; + *status = Status::Stopping; + } + + // Send shutdown signal + if let Some(shutdown_tx) = &self.shutdown_tx { + let _ = shutdown_tx.send(()).await; + } + + // Wait for engine to stop + let mut attempts = 0; + while attempts < 10 { + let status = self.status.read().await; + if *status == Status::Stopped { + break; + } + drop(status); + sleep(Duration::from_millis(100)).await; + attempts += 1; + } + + info!("Heal engine stopped"); + Ok(()) + } + + /// Add a healing task to the queue + pub async fn add_task(&self, task: HealTask) -> Result<()> { + let task_id = task.id.clone(); + let queue = Arc::clone(&self.task_queue); + + // Add task to priority queue + queue.write().await.push(task); + + info!("Added healing task to queue: {}", task_id); + Ok(()) + } + + /// Get current engine status + pub async fn status(&self) -> Status { + self.status.read().await.clone() + } + + /// Get current engine status (alias for status) + pub async fn get_status(&self) -> Status { + self.status.read().await.clone() + } + + /// Get engine configuration + pub async fn get_config(&self) -> HealConfig { + self.config.clone() + } + + /// Get healing statistics + pub async fn statistics(&self) -> HealStatistics { + self.statistics.read().await.clone() + } + + /// Get completed healing results + pub async fn completed_results(&self) -> Vec { + self.completed_tasks.read().await.clone() + } + + /// Process a single healing cycle + async fn process_healing_cycle( + config: &HealConfig, + status: &Arc>, + statistics: &Arc>, + task_queue: &Arc>>, + active_tasks: &Arc>>, + completed_tasks: &Arc>>, + ) -> Result<()> { + // Update status to healing + { + let mut status = status.write().await; + *status = Status::Healing; + } + + // Get ready tasks from queue + let mut queue = task_queue.write().await; + let mut ready_tasks = Vec::new(); + let mut remaining_tasks = Vec::new(); + + for task in queue.drain(..) { + if task.is_ready() { + ready_tasks.push(task); + } else { + remaining_tasks.push(task); + } + } + + // Sort ready tasks by priority + ready_tasks.sort_by(|a, b| a.priority.cmp(&b.priority)); + + // Process ready tasks + let active_count = active_tasks.read().await.len(); + let max_concurrent = config.max_workers.saturating_sub(active_count); + + for task in ready_tasks.into_iter().take(max_concurrent) { + if let Err(e) = Self::process_task( + config, + statistics, + active_tasks, + completed_tasks, + task, + ).await { + error!("Failed to process healing task: {}", e); + } + } + + // Put remaining tasks back in queue + queue.extend(remaining_tasks); + + // Update statistics + { + let mut stats = statistics.write().await; + stats.queued_tasks = queue.len() as u64; + stats.active_workers = active_tasks.read().await.len() as u64; + } + + // Update status back to idle + { + let mut status = status.write().await; + *status = Status::Idle; + } + + Ok(()) + } + + /// Process a single healing task + async fn process_task( + config: &HealConfig, + statistics: &Arc>, + active_tasks: &Arc>>, + completed_tasks: &Arc>>, + task: HealTask, + ) -> Result<()> { + let task_id = task.id.clone(); + + // Add task to active tasks + { + let mut active = active_tasks.write().await; + active.insert(task_id.clone(), task.clone()); + } + + // Update statistics + { + let mut stats = statistics.write().await; + stats.total_repairs += 1; + stats.active_workers = active_tasks.read().await.len() as u64; + } + + info!("Processing healing task: {}", task_id); + + // Simulate healing operation + let start_time = Instant::now(); + let result = Self::perform_healing_operation(&task, config).await; + let duration = start_time.elapsed(); + + // Create heal result + let heal_result = HealResult { + success: result.is_ok(), + original_issue: task.issue.clone(), + repair_duration: duration, + retry_attempts: task.retry_count, + error_message: result.err().map(|e| e.to_string()), + metadata: None, + completed_at: SystemTime::now(), + }; + + // Update statistics + { + let mut stats = statistics.write().await; + if heal_result.success { + stats.successful_repairs += 1; + } else { + stats.failed_repairs += 1; + } + stats.total_repair_time += duration; + stats.average_repair_time = if stats.total_repairs > 0 { + Duration::from_secs_f64( + stats.total_repair_time.as_secs_f64() / stats.total_repairs as f64 + ) + } else { + Duration::ZERO + }; + stats.last_repair_time = Some(SystemTime::now()); + stats.total_retry_attempts += task.retry_count as u64; + } + + // Add result to completed tasks + { + let mut completed = completed_tasks.write().await; + completed.push(heal_result.clone()); + } + + // Remove task from active tasks + { + let mut active = active_tasks.write().await; + active.remove(&task_id); + } + + // Update statistics + { + let mut stats = statistics.write().await; + stats.active_workers = active_tasks.read().await.len() as u64; + } + + if heal_result.success { + info!("Healing task completed successfully: {}", task_id); + } else { + warn!("Healing task failed: {}", task_id); + } + + Ok(()) + } + + /// Perform the actual healing operation + async fn perform_healing_operation(task: &HealTask, _config: &HealConfig) -> Result<()> { + // Simulate healing operation based on issue type + match task.issue.issue_type { + crate::scanner::HealthIssueType::MissingReplica => { + // Simulate replica repair + sleep(Duration::from_millis(100)).await; + info!("Repaired missing replica for {}/{}", task.issue.bucket, task.issue.object); + } + crate::scanner::HealthIssueType::ChecksumMismatch => { + // Simulate checksum repair + sleep(Duration::from_millis(200)).await; + info!("Repaired checksum mismatch for {}/{}", task.issue.bucket, task.issue.object); + } + crate::scanner::HealthIssueType::DiskReadError => { + // Simulate disk error recovery + sleep(Duration::from_millis(300)).await; + info!("Recovered from disk read error for {}/{}", task.issue.bucket, task.issue.object); + } + _ => { + // Generic repair for other issue types + sleep(Duration::from_millis(150)).await; + info!("Performed generic repair for {}/{}", task.issue.bucket, task.issue.object); + } + } + + // Simulate occasional failures for testing + if task.retry_count > 0 && task.retry_count % 3 == 0 { + return Err(crate::error::Error::Other(anyhow::anyhow!("Simulated healing failure"))); + } + + Ok(()) + } + + /// Start healing operations + pub async fn start_healing(&self) -> Result<()> { + let mut status = self.status.write().await; + *status = Status::Running; + info!("Healing operations started"); + Ok(()) + } + + /// Stop healing operations + pub async fn stop_healing(&self) -> Result<()> { + let mut status = self.status.write().await; + *status = Status::Stopped; + info!("Healing operations stopped"); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::scanner::{HealthIssue, HealthIssueType, Severity}; + + #[tokio::test] + async fn test_heal_engine_creation() { + let config = HealConfig::default(); + let engine = HealEngine::new(config); + + assert_eq!(engine.status().await, Status::Initializing); + } + + #[tokio::test] + async fn test_heal_engine_start_stop() { + let config = HealConfig::default(); + let mut engine = HealEngine::new(config); + + // Start engine + engine.start().await.unwrap(); + sleep(Duration::from_millis(100)).await; + + // Check status + let status = engine.status().await; + assert!(matches!(status, Status::Idle | Status::Healing)); + + // Stop engine + engine.stop().await.unwrap(); + sleep(Duration::from_millis(100)).await; + + // Check status + let status = engine.status().await; + assert_eq!(status, Status::Stopped); + } + + #[tokio::test] + async fn test_add_healing_task() { + let config = HealConfig::default(); + let engine = HealEngine::new(config); + + let issue = HealthIssue { + issue_type: HealthIssueType::MissingReplica, + severity: Severity::Critical, + bucket: "test-bucket".to_string(), + object: "test-object".to_string(), + description: "Test issue".to_string(), + metadata: None, + }; + + let task = HealTask::new(issue); + engine.add_task(task).await.unwrap(); + + let stats = engine.statistics().await; + assert_eq!(stats.queued_tasks, 1); + } +} \ No newline at end of file diff --git a/crates/ahm/src/heal/mod.rs b/crates/ahm/src/heal/mod.rs new file mode 100644 index 000000000..9e7847c95 --- /dev/null +++ b/crates/ahm/src/heal/mod.rs @@ -0,0 +1,360 @@ +// 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. + +//! Healing subsystem for the AHM system +//! +//! The heal subsystem provides intelligent repair capabilities: +//! - Priority-based healing queue +//! - Real-time and background healing modes +//! - Comprehensive repair validation +//! - Adaptive healing strategies + +pub mod engine; +pub mod priority_queue; +pub mod repair_worker; +pub mod validation; + +pub use engine::HealEngine; +pub use priority_queue::PriorityQueue; +pub use repair_worker::RepairWorker; +pub use validation::HealValidator; + +use std::time::{Duration, SystemTime}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; +use derive_builder::Builder; + +use crate::scanner::{HealthIssue, HealthIssueType, Severity}; + +/// Configuration for the healing system +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HealConfig { + /// Maximum number of concurrent repair workers + pub max_workers: usize, + /// Maximum number of tasks in the priority queue + pub max_queue_size: usize, + /// Timeout for individual repair operations + pub repair_timeout: Duration, + /// Interval between healing cycles + pub heal_interval: Duration, + /// Whether to enable automatic healing + pub auto_heal_enabled: bool, + /// Maximum number of retry attempts for failed repairs + pub max_retry_attempts: u32, + /// Backoff delay between retry attempts + pub retry_backoff_delay: Duration, + /// Whether to validate repairs after completion + pub validate_after_repair: bool, +} + +impl Default for HealConfig { + fn default() -> Self { + Self { + max_workers: 4, + max_queue_size: 1000, + repair_timeout: Duration::from_secs(300), // 5 minutes + heal_interval: Duration::from_secs(60), // 1 minute + auto_heal_enabled: true, + max_retry_attempts: 3, + retry_backoff_delay: Duration::from_secs(30), + validate_after_repair: true, + } + } +} + +/// Result of a healing operation +#[derive(Debug, Clone)] +pub struct HealResult { + /// Whether the healing operation was successful + pub success: bool, + /// The original health issue that was addressed + pub original_issue: HealthIssue, + /// Time taken to complete the repair + pub repair_duration: Duration, + /// Number of retry attempts made + pub retry_attempts: u32, + /// Error message if repair failed + pub error_message: Option, + /// Additional metadata about the repair + pub metadata: Option, + /// Timestamp when the repair was completed + pub completed_at: SystemTime, +} + +/// Statistics for the healing system +#[derive(Debug, Clone, Default)] +pub struct HealStatistics { + /// Total number of repair tasks processed + pub total_repairs: u64, + /// Number of successful repairs + pub successful_repairs: u64, + /// Number of failed repairs + pub failed_repairs: u64, + /// Number of tasks currently in queue + pub queued_tasks: u64, + /// Number of active workers + pub active_workers: u64, + /// Total time spent on repairs + pub total_repair_time: Duration, + /// Average repair time + pub average_repair_time: Duration, + /// Last repair completion time + pub last_repair_time: Option, + /// Number of retry attempts made + pub total_retry_attempts: u64, +} + +/// Priority levels for healing tasks +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum HealPriority { + /// Critical issues that need immediate attention + Critical = 0, + /// High priority issues + High = 1, + /// Medium priority issues + Medium = 2, + /// Low priority issues + Low = 3, +} + +impl From for HealPriority { + fn from(severity: Severity) -> Self { + match severity { + Severity::Critical => HealPriority::Critical, + Severity::High => HealPriority::High, + Severity::Medium => HealPriority::Medium, + Severity::Low => HealPriority::Low, + } + } +} + +/// A healing task to be processed +#[derive(Debug, Clone)] +pub struct HealTask { + /// Unique identifier for the task + pub id: String, + /// The health issue to be repaired + pub issue: HealthIssue, + /// Priority level for this task + pub priority: HealPriority, + /// When the task was created + pub created_at: SystemTime, + /// When the task should be processed (for delayed tasks) + pub scheduled_at: Option, + /// Number of retry attempts made + pub retry_count: u32, + /// Maximum number of retry attempts allowed + pub max_retries: u32, + /// Additional context for the repair operation + pub context: Option, +} + +impl HealTask { + /// Create a new healing task + pub fn new(issue: HealthIssue) -> Self { + let priority = HealPriority::from(issue.severity); + Self { + id: uuid::Uuid::new_v4().to_string(), + issue, + priority, + created_at: SystemTime::now(), + scheduled_at: None, + retry_count: 0, + max_retries: 3, + context: None, + } + } + + /// Create a delayed healing task + pub fn delayed(issue: HealthIssue, delay: Duration) -> Self { + let mut task = Self::new(issue); + task.scheduled_at = Some(SystemTime::now() + delay); + task + } + + /// Check if the task is ready to be processed + pub fn is_ready(&self) -> bool { + if let Some(scheduled_at) = self.scheduled_at { + SystemTime::now() >= scheduled_at + } else { + true + } + } + + /// Check if the task can be retried + pub fn can_retry(&self) -> bool { + self.retry_count < self.max_retries + } + + /// Increment the retry count + pub fn increment_retry(&mut self) { + self.retry_count += 1; + } +} + +/// Heal engine status +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum Status { + /// Heal engine is initializing + Initializing, + /// Heal engine is idle + Idle, + /// Heal engine is running normally + Running, + /// Heal engine is actively healing + Healing, + /// Heal engine is paused + Paused, + /// Heal engine is stopping + Stopping, + /// Heal engine has stopped + Stopped, + /// Heal engine encountered an error + Error(String), +} + +/// Healing operation modes +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum HealMode { + /// Real-time healing during GET/PUT operations + RealTime, + /// Background healing during scheduled scans + Background, + /// On-demand healing triggered by admin + OnDemand, + /// Emergency healing for critical issues + Emergency, +} + +/// Validation result for a repaired object +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ValidationResult { + /// Type of validation performed + pub validation_type: ValidationType, + /// Whether validation passed + pub passed: bool, + /// Details about the validation + pub details: String, + /// Time taken for validation + pub duration: Duration, +} + +/// Types of validation that can be performed +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum ValidationType { + /// Checksum verification + Checksum, + /// Shard count verification + ShardCount, + /// Data integrity check + DataIntegrity, + /// Metadata consistency check + MetadataConsistency, + /// Cross-shard redundancy check + RedundancyCheck, +} + +/// Healing strategies for different scenarios +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum HealStrategy { + /// Repair using available data shards + DataShardRepair, + /// Repair using parity shards + ParityShardRepair, + /// Hybrid repair using both data and parity + HybridRepair, + /// Metadata-only repair + MetadataRepair, + /// Full object reconstruction + FullReconstruction, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_heal_priority_from_severity() { + assert_eq!(HealPriority::from(Severity::Critical), HealPriority::Critical); + assert_eq!(HealPriority::from(Severity::High), HealPriority::High); + assert_eq!(HealPriority::from(Severity::Medium), HealPriority::Medium); + assert_eq!(HealPriority::from(Severity::Low), HealPriority::Low); + } + + #[test] + fn test_heal_task_creation() { + let issue = HealthIssue { + issue_type: HealthIssueType::MissingReplica, + severity: Severity::Critical, + bucket: "test-bucket".to_string(), + object: "test-object".to_string(), + description: "Test issue".to_string(), + metadata: None, + }; + + let task = HealTask::new(issue.clone()); + assert_eq!(task.priority, HealPriority::Critical); + assert_eq!(task.issue.bucket, issue.bucket); + assert_eq!(task.issue.object, issue.object); + assert_eq!(task.retry_count, 0); + assert_eq!(task.max_retries, 3); + assert!(task.is_ready()); + } + + #[test] + fn test_delayed_heal_task() { + let issue = HealthIssue { + issue_type: HealthIssueType::MissingReplica, + severity: Severity::Medium, + bucket: "test-bucket".to_string(), + object: "test-object".to_string(), + description: "Test issue".to_string(), + metadata: None, + }; + + let delay = Duration::from_secs(1); + let task = HealTask::delayed(issue, delay); + + assert!(task.scheduled_at.is_some()); + assert!(!task.is_ready()); // Should not be ready immediately + + // Wait for the delay to pass + std::thread::sleep(delay + Duration::from_millis(100)); + assert!(task.is_ready()); + } + + #[test] + fn test_heal_task_retry_logic() { + let issue = HealthIssue { + issue_type: HealthIssueType::MissingReplica, + severity: Severity::Low, + bucket: "test-bucket".to_string(), + object: "test-object".to_string(), + description: "Test issue".to_string(), + metadata: None, + }; + + let mut task = HealTask::new(issue); + assert!(task.can_retry()); + + task.increment_retry(); + assert_eq!(task.retry_count, 1); + assert!(task.can_retry()); + + task.increment_retry(); + task.increment_retry(); + assert_eq!(task.retry_count, 3); + assert!(!task.can_retry()); + } +} \ No newline at end of file diff --git a/crates/ahm/src/heal/priority_queue.rs b/crates/ahm/src/heal/priority_queue.rs new file mode 100644 index 000000000..07a1b6001 --- /dev/null +++ b/crates/ahm/src/heal/priority_queue.rs @@ -0,0 +1,413 @@ +// 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::{ + collections::BinaryHeap, + sync::Arc, + time::{Duration, SystemTime}, +}; + +use tokio::sync::RwLock; +use tracing::{debug, info, warn}; + +use crate::error::Result; +use super::{HealPriority, HealTask}; + +/// Priority queue for healing tasks +pub struct PriorityQueue { + tasks: Arc>>, + max_size: usize, + statistics: Arc>, +} + +/// Statistics for the priority queue +#[derive(Debug, Clone, Default)] +pub struct QueueStatistics { + /// Total number of tasks added to the queue + pub total_tasks_added: u64, + /// Total number of tasks removed from the queue + pub total_tasks_removed: u64, + /// Current number of tasks in the queue + pub current_queue_size: u64, + /// Maximum queue size reached + pub max_queue_size_reached: u64, + /// Number of tasks rejected due to queue being full + pub tasks_rejected: u64, + /// Average time tasks spend in queue + pub average_queue_time: Duration, + /// Total time all tasks have spent in queue + pub total_queue_time: Duration, +} + +impl PriorityQueue { + /// Create a new priority queue + pub fn new(max_size: usize) -> Self { + Self { + tasks: Arc::new(RwLock::new(BinaryHeap::new())), + max_size, + statistics: Arc::new(RwLock::new(QueueStatistics::default())), + } + } + + /// Add a task to the queue + pub async fn push(&self, task: HealTask) -> Result<()> { + let mut tasks = self.tasks.write().await; + let mut stats = self.statistics.write().await; + + if tasks.len() >= self.max_size { + stats.tasks_rejected += 1; + warn!("Priority queue is full, rejecting task: {}", task.id); + return Err(crate::error::Error::Other(anyhow::anyhow!("Queue is full"))); + } + + let task_id = task.id.clone(); + let priority = task.priority.clone(); + tasks.push(task); + stats.total_tasks_added += 1; + stats.current_queue_size = tasks.len() as u64; + stats.max_queue_size_reached = stats.max_queue_size_reached.max(tasks.len() as u64); + + debug!("Added task to priority queue: {} (priority: {:?})", task_id, priority); + Ok(()) + } + + /// Remove and return the highest priority task + pub async fn pop(&self) -> Option { + let mut tasks = self.tasks.write().await; + let mut stats = self.statistics.write().await; + + if let Some(task) = tasks.pop() { + stats.total_tasks_removed += 1; + stats.current_queue_size = tasks.len() as u64; + + // Update queue time statistics + let queue_time = SystemTime::now().duration_since(task.created_at).unwrap_or(Duration::ZERO); + stats.total_queue_time += queue_time; + stats.average_queue_time = if stats.total_tasks_removed > 0 { + Duration::from_secs_f64( + stats.total_queue_time.as_secs_f64() / stats.total_tasks_removed as f64 + ) + } else { + Duration::ZERO + }; + + debug!("Removed task from priority queue: {} (priority: {:?})", task.id, task.priority); + Some(task) + } else { + None + } + } + + /// Peek at the highest priority task without removing it + pub async fn peek(&self) -> Option { + let tasks = self.tasks.read().await; + tasks.peek().cloned() + } + + /// Get the current size of the queue + pub async fn len(&self) -> usize { + self.tasks.read().await.len() + } + + /// Check if the queue is empty + pub async fn is_empty(&self) -> bool { + self.tasks.read().await.is_empty() + } + + /// Get queue statistics + pub async fn statistics(&self) -> QueueStatistics { + self.statistics.read().await.clone() + } + + /// Clear all tasks from the queue + pub async fn clear(&self) { + let mut tasks = self.tasks.write().await; + let mut stats = self.statistics.write().await; + + let cleared_count = tasks.len(); + tasks.clear(); + stats.current_queue_size = 0; + + info!("Cleared {} tasks from priority queue", cleared_count); + } + + /// Get all tasks that are ready to be processed + pub async fn get_ready_tasks(&self, max_count: usize) -> Vec { + let mut tasks = self.tasks.write().await; + let mut ready_tasks = Vec::new(); + let mut remaining_tasks = Vec::new(); + + while let Some(task) = tasks.pop() { + if task.is_ready() && ready_tasks.len() < max_count { + ready_tasks.push(task); + } else { + remaining_tasks.push(task); + } + } + + // Put remaining tasks back + for task in remaining_tasks { + tasks.push(task); + } + + ready_tasks + } + + /// Remove a specific task by ID + pub async fn remove_task(&self, task_id: &str) -> bool { + let mut tasks = self.tasks.write().await; + let mut stats = self.statistics.write().await; + + let mut temp_tasks = Vec::new(); + let mut found = false; + + while let Some(task) = tasks.pop() { + if task.id == task_id { + found = true; + stats.total_tasks_removed += 1; + debug!("Removed specific task from queue: {}", task_id); + } else { + temp_tasks.push(task); + } + } + + // Put remaining tasks back + for task in temp_tasks { + tasks.push(task); + } + + stats.current_queue_size = tasks.len() as u64; + found + } + + /// Get tasks by priority level + pub async fn get_tasks_by_priority(&self, priority: HealPriority) -> Vec { + let mut tasks = self.tasks.write().await; + let mut matching_tasks = Vec::new(); + let mut other_tasks = Vec::new(); + + while let Some(task) = tasks.pop() { + if task.priority == priority { + matching_tasks.push(task); + } else { + other_tasks.push(task); + } + } + + // Put other tasks back + for task in other_tasks { + tasks.push(task); + } + + matching_tasks + } + + /// Update task priority + pub async fn update_priority(&self, task_id: &str, new_priority: HealPriority) -> bool { + let mut tasks = self.tasks.write().await; + + let mut temp_tasks = Vec::new(); + let mut found = false; + + while let Some(mut task) = tasks.pop() { + if task.id == task_id { + task.priority = new_priority.clone(); + found = true; + debug!("Updated task priority: {} -> {:?}", task_id, new_priority); + } + temp_tasks.push(task); + } + + // Put all tasks back + for task in temp_tasks { + tasks.push(task); + } + + found + } +} + +// Implement Ord for HealTask to enable priority queue functionality +impl std::cmp::Ord for HealTask { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + // Higher priority (lower enum value) comes first + self.priority.cmp(&other.priority) + .then_with(|| self.created_at.cmp(&other.created_at)) + } +} + +impl std::cmp::PartialOrd for HealTask { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl std::cmp::PartialEq for HealTask { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl std::cmp::Eq for HealTask {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::scanner::{HealthIssue, HealthIssueType, Severity}; + + #[tokio::test] + async fn test_priority_queue_creation() { + let queue = PriorityQueue::new(100); + assert_eq!(queue.len().await, 0); + assert!(queue.is_empty().await); + } + + #[tokio::test] + async fn test_priority_queue_push_pop() { + let queue = PriorityQueue::new(10); + + let issue1 = HealthIssue { + issue_type: HealthIssueType::MissingReplica, + severity: Severity::Low, + bucket: "bucket1".to_string(), + object: "object1".to_string(), + description: "Test issue 1".to_string(), + metadata: None, + }; + + let issue2 = HealthIssue { + issue_type: HealthIssueType::MissingReplica, + severity: Severity::Critical, + bucket: "bucket2".to_string(), + object: "object2".to_string(), + description: "Test issue 2".to_string(), + metadata: None, + }; + + let task1 = HealTask::new(issue1); + let task2 = HealTask::new(issue2); + + // Add tasks + queue.push(task1.clone()).await.unwrap(); + queue.push(task2.clone()).await.unwrap(); + + assert_eq!(queue.len().await, 2); + + // Critical task should come first + let first_task = queue.pop().await.unwrap(); + assert_eq!(first_task.priority, HealPriority::Critical); + assert_eq!(first_task.id, task2.id); + + let second_task = queue.pop().await.unwrap(); + assert_eq!(second_task.priority, HealPriority::Low); + assert_eq!(second_task.id, task1.id); + + assert!(queue.is_empty().await); + } + + #[tokio::test] + async fn test_priority_queue_full() { + let queue = PriorityQueue::new(1); + + let issue1 = HealthIssue { + issue_type: HealthIssueType::MissingReplica, + severity: Severity::Low, + bucket: "bucket1".to_string(), + object: "object1".to_string(), + description: "Test issue 1".to_string(), + metadata: None, + }; + + let issue2 = HealthIssue { + issue_type: HealthIssueType::MissingReplica, + severity: Severity::Critical, + bucket: "bucket2".to_string(), + object: "object2".to_string(), + description: "Test issue 2".to_string(), + metadata: None, + }; + + let task1 = HealTask::new(issue1); + let task2 = HealTask::new(issue2); + + // First task should succeed + queue.push(task1).await.unwrap(); + assert_eq!(queue.len().await, 1); + + // Second task should fail + let result = queue.push(task2).await; + assert!(result.is_err()); + assert_eq!(queue.len().await, 1); + + let stats = queue.statistics().await; + assert_eq!(stats.tasks_rejected, 1); + } + + #[tokio::test] + async fn test_priority_queue_remove_task() { + let queue = PriorityQueue::new(10); + + let issue = HealthIssue { + issue_type: HealthIssueType::MissingReplica, + severity: Severity::Medium, + bucket: "bucket1".to_string(), + object: "object1".to_string(), + description: "Test issue".to_string(), + metadata: None, + }; + + let task = HealTask::new(issue); + let task_id = task.id.clone(); + + queue.push(task).await.unwrap(); + assert_eq!(queue.len().await, 1); + + // Remove the task + let removed = queue.remove_task(&task_id).await; + assert!(removed); + assert_eq!(queue.len().await, 0); + + // Try to remove non-existent task + let removed = queue.remove_task("non-existent").await; + assert!(!removed); + } + + #[tokio::test] + async fn test_priority_queue_update_priority() { + let queue = PriorityQueue::new(10); + + let issue = HealthIssue { + issue_type: HealthIssueType::MissingReplica, + severity: Severity::Low, + bucket: "bucket1".to_string(), + object: "object1".to_string(), + description: "Test issue".to_string(), + metadata: None, + }; + + let task = HealTask::new(issue); + let task_id = task.id.clone(); + + queue.push(task).await.unwrap(); + + // Update priority + let updated = queue.update_priority(&task_id, HealPriority::Critical).await; + assert!(updated); + + // Check that the task now has higher priority + let popped_task = queue.pop().await.unwrap(); + assert_eq!(popped_task.priority, HealPriority::Critical); + assert_eq!(popped_task.id, task_id); + } +} \ No newline at end of file diff --git a/crates/ahm/src/heal/repair_worker.rs b/crates/ahm/src/heal/repair_worker.rs new file mode 100644 index 000000000..018e62a5c --- /dev/null +++ b/crates/ahm/src/heal/repair_worker.rs @@ -0,0 +1,505 @@ +// 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, + time::{Duration, Instant, SystemTime}, +}; + +use tokio::{ + sync::{mpsc, RwLock}, + time::{sleep, timeout}, +}; +use tracing::{debug, error, info, warn}; + +use crate::error::Result; +use super::{HealConfig, HealResult, HealTask, Status}; + +/// Configuration for repair workers +#[derive(Debug, Clone)] +pub struct RepairWorkerConfig { + /// Worker ID + pub worker_id: String, + /// Maximum time to spend on a single repair operation + pub operation_timeout: Duration, + /// Whether to enable detailed logging + pub enable_detailed_logging: bool, + /// Maximum number of concurrent operations + pub max_concurrent_operations: usize, + /// Retry configuration + pub retry_config: RetryConfig, +} + +/// Retry configuration for repair operations +#[derive(Debug, Clone)] +pub struct RetryConfig { + /// Maximum number of retry attempts + pub max_attempts: u32, + /// Initial backoff delay + pub initial_backoff: Duration, + /// Maximum backoff delay + pub max_backoff: Duration, + /// Backoff multiplier + pub backoff_multiplier: f64, + /// Whether to use exponential backoff + pub exponential_backoff: bool, +} + +impl Default for RepairWorkerConfig { + fn default() -> Self { + Self { + worker_id: "worker-1".to_string(), + operation_timeout: Duration::from_secs(300), // 5 minutes + enable_detailed_logging: true, + max_concurrent_operations: 1, + retry_config: RetryConfig::default(), + } + } +} + +impl Default for RetryConfig { + fn default() -> Self { + Self { + max_attempts: 3, + initial_backoff: Duration::from_secs(1), + max_backoff: Duration::from_secs(60), + backoff_multiplier: 2.0, + exponential_backoff: true, + } + } +} + +/// Statistics for a repair worker +#[derive(Debug, Clone, Default)] +pub struct WorkerStatistics { + /// Total number of tasks processed + pub total_tasks_processed: u64, + /// Number of successful repairs + pub successful_repairs: u64, + /// Number of failed repairs + pub failed_repairs: u64, + /// Total time spent on repairs + pub total_repair_time: Duration, + /// Average repair time + pub average_repair_time: Duration, + /// Number of retry attempts made + pub total_retry_attempts: u64, + /// Current worker status + pub status: WorkerStatus, + /// Last task completion time + pub last_task_time: Option, +} + +/// Worker status +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum WorkerStatus { + /// Worker is idle + Idle, + /// Worker is processing a task + Processing, + /// Worker is retrying a failed task + Retrying, + /// Worker is stopping + Stopping, + /// Worker has stopped + Stopped, + /// Worker encountered an error + Error(String), +} + +impl Default for WorkerStatus { + fn default() -> Self { + WorkerStatus::Idle + } +} + +/// Repair worker that executes healing tasks +pub struct RepairWorker { + config: RepairWorkerConfig, + statistics: Arc>, + status: Arc>, + result_tx: mpsc::Sender, + shutdown_tx: Option>, +} + +impl RepairWorker { + /// Create a new repair worker + pub fn new( + config: RepairWorkerConfig, + result_tx: mpsc::Sender, + ) -> Self { + Self { + config, + statistics: Arc::new(RwLock::new(WorkerStatistics::default())), + status: Arc::new(RwLock::new(WorkerStatus::Idle)), + result_tx, + shutdown_tx: None, + } + } + + /// Start the repair worker + pub async fn start(&mut self) -> Result<()> { + info!("Starting repair worker: {}", self.config.worker_id); + + let (_task_tx, task_rx) = mpsc::channel(100); + let (shutdown_tx, mut shutdown_rx) = mpsc::channel(1); + + self.shutdown_tx = Some(shutdown_tx); + + // Update status + { + let mut status = self.status.write().await; + *status = WorkerStatus::Idle; + } + + let config = self.config.clone(); + let statistics = Arc::clone(&self.statistics); + let status = Arc::clone(&self.status); + let result_tx = self.result_tx.clone(); + + // Start the worker loop + tokio::spawn(async move { + let mut task_rx = task_rx; + + loop { + tokio::select! { + Some(task) = task_rx.recv() => { + if let Err(e) = Self::process_task( + &config, + &statistics, + &status, + &result_tx, + task, + ).await { + error!("Failed to process task: {}", e); + } + } + _ = shutdown_rx.recv() => { + info!("Shutdown signal received, stopping worker: {}", config.worker_id); + break; + } + } + } + + // Update status to stopped + let mut status = status.write().await; + *status = WorkerStatus::Stopped; + }); + + info!("Repair worker started: {}", self.config.worker_id); + Ok(()) + } + + /// Stop the repair worker + pub async fn stop(&mut self) -> Result<()> { + info!("Stopping repair worker: {}", self.config.worker_id); + + // Update status + { + let mut status = self.status.write().await; + *status = WorkerStatus::Stopping; + } + + // Send shutdown signal + if let Some(shutdown_tx) = &self.shutdown_tx { + let _ = shutdown_tx.send(()).await; + } + + // Wait for worker to stop + let mut attempts = 0; + while attempts < 10 { + let status = self.status.read().await; + if *status == WorkerStatus::Stopped { + break; + } + drop(status); + sleep(Duration::from_millis(100)).await; + attempts += 1; + } + + info!("Repair worker stopped: {}", self.config.worker_id); + Ok(()) + } + + /// Submit a task to the worker + pub async fn submit_task(&self, _task: HealTask) -> Result<()> { + // TODO: Implement task submission + Err(crate::error::Error::Other(anyhow::anyhow!("Task submission not implemented"))) + } + + /// Get worker statistics + pub async fn statistics(&self) -> WorkerStatistics { + self.statistics.read().await.clone() + } + + /// Get worker status + pub async fn status(&self) -> WorkerStatus { + self.status.read().await.clone() + } + + /// Process a single task + async fn process_task( + config: &RepairWorkerConfig, + statistics: &Arc>, + status: &Arc>, + result_tx: &mpsc::Sender, + task: HealTask, + ) -> Result<()> { + let task_id = task.id.clone(); + + // Update status to processing + { + let mut status = status.write().await; + *status = WorkerStatus::Processing; + } + + // Update statistics + { + let mut stats = statistics.write().await; + stats.total_tasks_processed += 1; + stats.status = WorkerStatus::Processing; + } + + info!("Processing repair task: {} (worker: {})", task_id, config.worker_id); + + let start_time = Instant::now(); + let mut attempt = 0; + let mut last_error = None; + + // Retry loop + while attempt < config.retry_config.max_attempts { + attempt += 1; + + if attempt > 1 { + // Update status to retrying + { + let mut status = status.write().await; + *status = WorkerStatus::Retrying; + } + + // Calculate backoff delay + let backoff_delay = if config.retry_config.exponential_backoff { + let delay = config.retry_config.initial_backoff * + (config.retry_config.backoff_multiplier.powi((attempt - 1) as i32)) as u32; + delay.min(config.retry_config.max_backoff) + } else { + config.retry_config.initial_backoff + }; + + warn!("Retrying task {} (attempt {}/{}), waiting {:?}", + task_id, attempt, config.retry_config.max_attempts, backoff_delay); + sleep(backoff_delay).await; + } + + // Attempt the repair operation + let result = timeout( + config.operation_timeout, + Self::perform_repair_operation(&task, config) + ).await; + + match result { + Ok(Ok(())) => { + // Success + let duration = start_time.elapsed(); + let heal_result = HealResult { + success: true, + original_issue: task.issue.clone(), + repair_duration: duration, + retry_attempts: attempt - 1, + error_message: None, + metadata: None, + completed_at: SystemTime::now(), + }; + + // Send result + if let Err(e) = result_tx.send(heal_result).await { + error!("Failed to send heal result: {}", e); + } + + // Update statistics + { + let mut stats = statistics.write().await; + stats.successful_repairs += 1; + stats.total_repair_time += duration; + stats.average_repair_time = if stats.total_tasks_processed > 0 { + Duration::from_secs_f64( + stats.total_repair_time.as_secs_f64() / stats.total_tasks_processed as f64 + ) + } else { + Duration::ZERO + }; + stats.total_retry_attempts += (attempt - 1) as u64; + stats.last_task_time = Some(SystemTime::now()); + stats.status = WorkerStatus::Idle; + } + + info!("Successfully completed repair task: {} (worker: {})", task_id, config.worker_id); + return Ok(()); + } + Ok(Err(e)) => { + // Operation failed + let error_msg = e.to_string(); + last_error = Some(e); + warn!("Repair operation failed for task {} (attempt {}/{}): {}", + task_id, attempt, config.retry_config.max_attempts, error_msg); + } + Err(_) => { + // Operation timed out + last_error = Some(crate::error::Error::Other(anyhow::anyhow!("Operation timed out"))); + warn!("Repair operation timed out for task {} (attempt {}/{})", + task_id, attempt, config.retry_config.max_attempts); + } + } + } + + // All attempts failed + let duration = start_time.elapsed(); + let heal_result = HealResult { + success: false, + original_issue: task.issue.clone(), + repair_duration: duration, + retry_attempts: attempt - 1, + error_message: last_error.map(|e| e.to_string()), + metadata: None, + completed_at: SystemTime::now(), + }; + + // Send result + if let Err(e) = result_tx.send(heal_result).await { + error!("Failed to send heal result: {}", e); + } + + // Update statistics + { + let mut stats = statistics.write().await; + stats.failed_repairs += 1; + stats.total_repair_time += duration; + stats.average_repair_time = if stats.total_tasks_processed > 0 { + Duration::from_secs_f64( + stats.total_repair_time.as_secs_f64() / stats.total_tasks_processed as f64 + ) + } else { + Duration::ZERO + }; + stats.total_retry_attempts += (attempt - 1) as u64; + stats.last_task_time = Some(SystemTime::now()); + stats.status = WorkerStatus::Idle; + } + + error!("Failed to complete repair task after {} attempts: {} (worker: {})", + attempt, task_id, config.worker_id); + Ok(()) + } + + /// Perform the actual repair operation + async fn perform_repair_operation(task: &HealTask, config: &RepairWorkerConfig) -> Result<()> { + if config.enable_detailed_logging { + debug!("Starting repair operation for task: {} (worker: {})", task.id, config.worker_id); + } + + // Simulate repair operation based on issue type + match task.issue.issue_type { + crate::scanner::HealthIssueType::MissingReplica => { + // Simulate replica repair + sleep(Duration::from_millis(100)).await; + if config.enable_detailed_logging { + debug!("Repaired missing replica for {}/{}", task.issue.bucket, task.issue.object); + } + } + crate::scanner::HealthIssueType::ChecksumMismatch => { + // Simulate checksum repair + sleep(Duration::from_millis(200)).await; + if config.enable_detailed_logging { + debug!("Repaired checksum mismatch for {}/{}", task.issue.bucket, task.issue.object); + } + } + crate::scanner::HealthIssueType::DiskReadError => { + // Simulate disk error recovery + sleep(Duration::from_millis(300)).await; + if config.enable_detailed_logging { + debug!("Recovered from disk read error for {}/{}", task.issue.bucket, task.issue.object); + } + } + _ => { + // Generic repair for other issue types + sleep(Duration::from_millis(150)).await; + if config.enable_detailed_logging { + debug!("Performed generic repair for {}/{}", task.issue.bucket, task.issue.object); + } + } + } + + // Simulate occasional failures for testing + if task.retry_count > 0 && task.retry_count % 3 == 0 { + return Err(crate::error::Error::Other(anyhow::anyhow!("Simulated repair failure"))); + } + + if config.enable_detailed_logging { + debug!("Completed repair operation for task: {} (worker: {})", task.id, config.worker_id); + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::scanner::{HealthIssue, HealthIssueType, Severity}; + + #[tokio::test] + async fn test_repair_worker_creation() { + let config = RepairWorkerConfig::default(); + let (result_tx, _result_rx) = mpsc::channel(100); + let worker = RepairWorker::new(config, result_tx); + + assert_eq!(worker.status().await, WorkerStatus::Idle); + } + + #[tokio::test] + async fn test_repair_worker_start_stop() { + let config = RepairWorkerConfig::default(); + let (result_tx, _result_rx) = mpsc::channel(100); + let mut worker = RepairWorker::new(config, result_tx); + + // Start worker + worker.start().await.unwrap(); + sleep(Duration::from_millis(100)).await; + + // Check status + let status = worker.status().await; + assert_eq!(status, WorkerStatus::Idle); + + // Stop worker + worker.stop().await.unwrap(); + sleep(Duration::from_millis(100)).await; + + // Check status + let status = worker.status().await; + assert_eq!(status, WorkerStatus::Stopped); + } + + #[tokio::test] + async fn test_repair_worker_statistics() { + let config = RepairWorkerConfig::default(); + let (result_tx, _result_rx) = mpsc::channel(100); + let worker = RepairWorker::new(config, result_tx); + + let stats = worker.statistics().await; + assert_eq!(stats.total_tasks_processed, 0); + assert_eq!(stats.successful_repairs, 0); + assert_eq!(stats.failed_repairs, 0); + assert_eq!(stats.status, WorkerStatus::Idle); + } +} \ No newline at end of file diff --git a/crates/ahm/src/heal/validation.rs b/crates/ahm/src/heal/validation.rs new file mode 100644 index 000000000..2bb481f03 --- /dev/null +++ b/crates/ahm/src/heal/validation.rs @@ -0,0 +1,453 @@ +// 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::{ + collections::HashMap, + sync::Arc, + time::{Duration, Instant, SystemTime}, +}; + +use tokio::sync::RwLock; +use tracing::{debug, error, info, warn}; + +use crate::error::Result; +use super::{HealResult, HealTask}; + +/// Configuration for validation operations +#[derive(Debug, Clone)] +pub struct ValidationConfig { + /// Whether to enable validation after repair + pub enable_post_repair_validation: bool, + /// Timeout for validation operations + pub validation_timeout: Duration, + /// Whether to enable detailed validation logging + pub enable_detailed_logging: bool, + /// Maximum number of validation retries + pub max_validation_retries: u32, + /// Validation retry delay + pub validation_retry_delay: Duration, +} + +impl Default for ValidationConfig { + fn default() -> Self { + Self { + enable_post_repair_validation: true, + validation_timeout: Duration::from_secs(60), // 1 minute + max_validation_retries: 3, + validation_retry_delay: Duration::from_secs(5), + enable_detailed_logging: true, + } + } +} + +/// Validation result for a repair operation +#[derive(Debug, Clone)] +pub struct ValidationResult { + /// Whether validation passed + pub passed: bool, + /// Validation type + pub validation_type: ValidationType, + /// Detailed validation message + pub message: String, + /// Time taken for validation + pub duration: Duration, + /// Validation timestamp + pub timestamp: SystemTime, + /// Additional validation metadata + pub metadata: Option, +} + +/// Types of validation that can be performed +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ValidationType { + /// Checksum validation + Checksum, + /// File existence validation + FileExistence, + /// File size validation + FileSize, + /// File permissions validation + FilePermissions, + /// Metadata consistency validation + MetadataConsistency, + /// Replication status validation + ReplicationStatus, + /// Data integrity validation + DataIntegrity, + /// Custom validation + Custom(String), +} + +/// Statistics for validation operations +#[derive(Debug, Clone, Default)] +pub struct ValidationStatistics { + /// Total number of validations performed + pub total_validations: u64, + /// Number of successful validations + pub successful_validations: u64, + /// Number of failed validations + pub failed_validations: u64, + /// Total time spent on validation + pub total_validation_time: Duration, + /// Average validation time + pub average_validation_time: Duration, + /// Number of validation retries + pub total_validation_retries: u64, + /// Last validation time + pub last_validation_time: Option, +} + +/// Validator for repair operations +pub struct HealValidator { + config: ValidationConfig, + statistics: Arc>, +} + +impl HealValidator { + /// Create a new validator + pub fn new(config: ValidationConfig) -> Self { + Self { + config, + statistics: Arc::new(RwLock::new(ValidationStatistics::default())), + } + } + + /// Validate a repair operation + pub async fn validate_repair(&self, task: &HealTask, result: &HealResult) -> Result> { + if !self.config.enable_post_repair_validation { + return Ok(Vec::new()); + } + + let start_time = Instant::now(); + let mut validation_results = Vec::new(); + + info!("Starting validation for repair task: {}", task.id); + + // Perform different types of validation based on the issue type + match task.issue.issue_type { + crate::scanner::HealthIssueType::MissingReplica => { + validation_results.extend(self.validate_replica_repair(task, result).await?); + } + crate::scanner::HealthIssueType::ChecksumMismatch => { + validation_results.extend(self.validate_checksum_repair(task, result).await?); + } + crate::scanner::HealthIssueType::DiskReadError => { + validation_results.extend(self.validate_disk_repair(task, result).await?); + } + _ => { + validation_results.extend(self.validate_generic_repair(task, result).await?); + } + } + + let duration = start_time.elapsed(); + + // Update statistics + { + let mut stats = self.statistics.write().await; + stats.total_validations += validation_results.len() as u64; + stats.total_validation_time += duration; + stats.average_validation_time = if stats.total_validations > 0 { + Duration::from_secs_f64( + stats.total_validation_time.as_secs_f64() / stats.total_validations as f64 + ) + } else { + Duration::ZERO + }; + stats.last_validation_time = Some(SystemTime::now()); + + let successful_count = validation_results.iter().filter(|r| r.passed).count(); + let failed_count = validation_results.len() - successful_count; + stats.successful_validations += successful_count as u64; + stats.failed_validations += failed_count as u64; + } + + if self.config.enable_detailed_logging { + debug!("Validation completed for task {}: {} passed, {} failed", + task.id, + validation_results.iter().filter(|r| r.passed).count(), + validation_results.iter().filter(|r| !r.passed).count() + ); + } + + Ok(validation_results) + } + + /// Validate replica repair + async fn validate_replica_repair(&self, task: &HealTask, _result: &HealResult) -> Result> { + let mut results = Vec::new(); + + // Validate file existence + let existence_result = self.validate_file_existence(&task.issue.bucket, &task.issue.object).await; + results.push(existence_result); + + // Validate replication status + let replication_result = self.validate_replication_status(&task.issue.bucket, &task.issue.object).await; + results.push(replication_result); + + Ok(results) + } + + /// Validate checksum repair + async fn validate_checksum_repair(&self, task: &HealTask, _result: &HealResult) -> Result> { + let mut results = Vec::new(); + + // Validate checksum + let checksum_result = self.validate_checksum(&task.issue.bucket, &task.issue.object).await; + results.push(checksum_result); + + // Validate data integrity + let integrity_result = self.validate_data_integrity(&task.issue.bucket, &task.issue.object).await; + results.push(integrity_result); + + Ok(results) + } + + /// Validate disk repair + async fn validate_disk_repair(&self, task: &HealTask, _result: &HealResult) -> Result> { + let mut results = Vec::new(); + + // Validate file existence + let existence_result = self.validate_file_existence(&task.issue.bucket, &task.issue.object).await; + results.push(existence_result); + + // Validate file permissions + let permissions_result = self.validate_file_permissions(&task.issue.bucket, &task.issue.object).await; + results.push(permissions_result); + + Ok(results) + } + + /// Validate generic repair + async fn validate_generic_repair(&self, task: &HealTask, _result: &HealResult) -> Result> { + let mut results = Vec::new(); + + // Validate file existence + let existence_result = self.validate_file_existence(&task.issue.bucket, &task.issue.object).await; + results.push(existence_result); + + // Validate metadata consistency + let metadata_result = self.validate_metadata_consistency(&task.issue.bucket, &task.issue.object).await; + results.push(metadata_result); + + Ok(results) + } + + /// Validate file existence + async fn validate_file_existence(&self, bucket: &str, object: &str) -> ValidationResult { + let start_time = Instant::now(); + + // Simulate file existence check + tokio::time::sleep(Duration::from_millis(10)).await; + + let duration = start_time.elapsed(); + let passed = true; // Simulate successful validation + + ValidationResult { + passed, + validation_type: ValidationType::FileExistence, + message: format!("File existence validation for {}/{}", bucket, object), + duration, + timestamp: SystemTime::now(), + metadata: None, + } + } + + /// Validate checksum + async fn validate_checksum(&self, bucket: &str, object: &str) -> ValidationResult { + let start_time = Instant::now(); + + // Simulate checksum validation + tokio::time::sleep(Duration::from_millis(20)).await; + + let duration = start_time.elapsed(); + let passed = true; // Simulate successful validation + + ValidationResult { + passed, + validation_type: ValidationType::Checksum, + message: format!("Checksum validation for {}/{}", bucket, object), + duration, + timestamp: SystemTime::now(), + metadata: None, + } + } + + /// Validate replication status + async fn validate_replication_status(&self, bucket: &str, object: &str) -> ValidationResult { + let start_time = Instant::now(); + + // Simulate replication status validation + tokio::time::sleep(Duration::from_millis(15)).await; + + let duration = start_time.elapsed(); + let passed = true; // Simulate successful validation + + ValidationResult { + passed, + validation_type: ValidationType::ReplicationStatus, + message: format!("Replication status validation for {}/{}", bucket, object), + duration, + timestamp: SystemTime::now(), + metadata: None, + } + } + + /// Validate file permissions + async fn validate_file_permissions(&self, bucket: &str, object: &str) -> ValidationResult { + let start_time = Instant::now(); + + // Simulate file permissions validation + tokio::time::sleep(Duration::from_millis(5)).await; + + let duration = start_time.elapsed(); + let passed = true; // Simulate successful validation + + ValidationResult { + passed, + validation_type: ValidationType::FilePermissions, + message: format!("File permissions validation for {}/{}", bucket, object), + duration, + timestamp: SystemTime::now(), + metadata: None, + } + } + + /// Validate metadata consistency + async fn validate_metadata_consistency(&self, bucket: &str, object: &str) -> ValidationResult { + let start_time = Instant::now(); + + // Simulate metadata consistency validation + tokio::time::sleep(Duration::from_millis(25)).await; + + let duration = start_time.elapsed(); + let passed = true; // Simulate successful validation + + ValidationResult { + passed, + validation_type: ValidationType::MetadataConsistency, + message: format!("Metadata consistency validation for {}/{}", bucket, object), + duration, + timestamp: SystemTime::now(), + metadata: None, + } + } + + /// Validate data integrity + async fn validate_data_integrity(&self, bucket: &str, object: &str) -> ValidationResult { + let start_time = Instant::now(); + + // Simulate data integrity validation + tokio::time::sleep(Duration::from_millis(30)).await; + + let duration = start_time.elapsed(); + let passed = true; // Simulate successful validation + + ValidationResult { + passed, + validation_type: ValidationType::DataIntegrity, + message: format!("Data integrity validation for {}/{}", bucket, object), + duration, + timestamp: SystemTime::now(), + metadata: None, + } + } + + /// Get validation statistics + pub async fn statistics(&self) -> ValidationStatistics { + self.statistics.read().await.clone() + } + + /// Reset validation statistics + pub async fn reset_statistics(&self) { + let mut stats = self.statistics.write().await; + *stats = ValidationStatistics::default(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::scanner::{HealthIssue, HealthIssueType, Severity}; + + #[tokio::test] + async fn test_validator_creation() { + let config = ValidationConfig::default(); + let validator = HealValidator::new(config); + + let stats = validator.statistics().await; + assert_eq!(stats.total_validations, 0); + } + + #[tokio::test] + async fn test_validate_repair() { + let config = ValidationConfig::default(); + let validator = HealValidator::new(config); + + let issue = HealthIssue { + issue_type: HealthIssueType::MissingReplica, + severity: Severity::Critical, + bucket: "test-bucket".to_string(), + object: "test-object".to_string(), + description: "Test issue".to_string(), + metadata: None, + }; + + let task = super::HealTask::new(issue); + let result = super::HealResult { + success: true, + original_issue: task.issue.clone(), + repair_duration: Duration::from_secs(1), + retry_attempts: 0, + error_message: None, + metadata: None, + completed_at: SystemTime::now(), + }; + + let validation_results = validator.validate_repair(&task, &result).await.unwrap(); + assert!(!validation_results.is_empty()); + + let stats = validator.statistics().await; + assert_eq!(stats.total_validations, validation_results.len() as u64); + } + + #[tokio::test] + async fn test_validation_disabled() { + let mut config = ValidationConfig::default(); + config.enable_post_repair_validation = false; + let validator = HealValidator::new(config); + + let issue = HealthIssue { + issue_type: HealthIssueType::MissingReplica, + severity: Severity::Critical, + bucket: "test-bucket".to_string(), + object: "test-object".to_string(), + description: "Test issue".to_string(), + metadata: None, + }; + + let task = super::HealTask::new(issue); + let result = super::HealResult { + success: true, + original_issue: task.issue.clone(), + repair_duration: Duration::from_secs(1), + retry_attempts: 0, + error_message: None, + metadata: None, + completed_at: SystemTime::now(), + }; + + let validation_results = validator.validate_repair(&task, &result).await.unwrap(); + assert!(validation_results.is_empty()); + } +} \ No newline at end of file diff --git a/crates/ahm/src/metrics/aggregator.rs b/crates/ahm/src/metrics/aggregator.rs new file mode 100644 index 000000000..961a13440 --- /dev/null +++ b/crates/ahm/src/metrics/aggregator.rs @@ -0,0 +1,739 @@ +// Copyright 2024 RustFS Team + +use std::{ + collections::HashMap, + time::{Duration, SystemTime}, +}; + +use tracing::{debug, error, info, warn}; + +use crate::error::Result; + +use super::{ + AggregatedMetrics, DiskMetrics, HealMetrics, MetricsDataPoint, MetricsQuery, MetricsSummary, + NetworkMetrics, PolicyMetrics, ScanMetrics, SystemMetrics, +}; + +/// Configuration for the metrics aggregator +#[derive(Debug, Clone)] +pub struct AggregatorConfig { + /// Default aggregation interval + pub default_interval: Duration, + /// Maximum number of data points to keep in memory + pub max_data_points: usize, + /// Whether to enable automatic aggregation + pub enable_auto_aggregation: bool, + /// Aggregation window size + pub aggregation_window: Duration, + /// Whether to enable data compression + pub enable_compression: bool, + /// Compression threshold (number of points before compression) + pub compression_threshold: usize, + /// Whether to enable outlier detection + pub enable_outlier_detection: bool, + /// Outlier detection threshold (standard deviations) + pub outlier_threshold: f64, +} + +impl Default for AggregatorConfig { + fn default() -> Self { + Self { + default_interval: Duration::from_secs(300), // 5 minutes + max_data_points: 10000, + enable_auto_aggregation: true, + aggregation_window: Duration::from_secs(3600), // 1 hour + enable_compression: true, + compression_threshold: 1000, + enable_outlier_detection: true, + outlier_threshold: 2.0, // 2 standard deviations + } + } +} + +/// Metrics aggregator that processes and aggregates metrics data +#[derive(Debug, Clone)] +pub struct Aggregator { + config: AggregatorConfig, + data_points: Vec, + aggregation_cache: HashMap, + last_aggregation_time: SystemTime, + aggregation_count: u64, +} + +impl Aggregator { + /// Create a new metrics aggregator + pub async fn new(interval: Duration) -> Result { + let config = AggregatorConfig { + default_interval: interval, + ..Default::default() + }; + + Ok(Self { + config, + data_points: Vec::new(), + aggregation_cache: HashMap::new(), + last_aggregation_time: SystemTime::now(), + aggregation_count: 0, + }) + } + + /// Get the configuration + pub fn config(&self) -> &AggregatorConfig { + &self.config + } + + /// Add metrics data point + pub async fn add_data_point(&mut self, data_point: MetricsDataPoint) -> Result<()> { + self.data_points.push(data_point); + + // Trim old data points if we exceed the limit + if self.data_points.len() > self.config.max_data_points { + let excess = self.data_points.len() - self.config.max_data_points; + self.data_points.drain(0..excess); + } + + // Auto-aggregate if enabled + if self.config.enable_auto_aggregation { + self.auto_aggregate().await?; + } + + Ok(()) + } + + /// Aggregate metrics based on query + pub async fn aggregate_metrics(&mut self, query: MetricsQuery) -> Result { + let start_time = SystemTime::now(); + + // Check cache first + let cache_key = self.generate_cache_key(&query); + if let Some(cached) = self.aggregation_cache.get(&cache_key) { + debug!("Returning cached aggregation result"); + return Ok(cached.clone()); + } + + // Filter data points by time range + let filtered_points: Vec<&MetricsDataPoint> = self + .data_points + .iter() + .filter(|point| { + point.timestamp >= query.start_time && point.timestamp <= query.end_time + }) + .collect(); + + if filtered_points.is_empty() { + warn!("No data points found for the specified time range"); + return Ok(AggregatedMetrics { + query, + data_points: Vec::new(), + summary: MetricsSummary::default(), + }); + } + + // Aggregate data points + let aggregated_points = self.aggregate_data_points(&filtered_points, &query).await?; + + // Generate summary + let summary = self.generate_summary(&aggregated_points, &query).await?; + + let result = AggregatedMetrics { + query, + data_points: aggregated_points, + summary, + }; + + // Cache the result + self.aggregation_cache.insert(cache_key, result.clone()); + + let aggregation_time = start_time.elapsed(); + debug!("Metrics aggregation completed in {:?}", aggregation_time); + + Ok(result) + } + + /// Auto-aggregate data points + async fn auto_aggregate(&mut self) -> Result<()> { + let now = SystemTime::now(); + + // Check if it's time to aggregate + if now.duration_since(self.last_aggregation_time).unwrap() < self.config.aggregation_window { + return Ok(()); + } + + // Perform aggregation + let window_start = now - self.config.aggregation_window; + let query = MetricsQuery { + start_time: window_start, + end_time: now, + interval: self.config.default_interval, + metrics: vec![], // All metrics + severity_filter: None, + limit: None, + }; + + let _aggregated = self.aggregate_metrics(query).await?; + + self.last_aggregation_time = now; + self.aggregation_count += 1; + + info!("Auto-aggregation completed, count: {}", self.aggregation_count); + + Ok(()) + } + + /// Aggregate data points based on interval + async fn aggregate_data_points( + &self, + points: &[&MetricsDataPoint], + query: &MetricsQuery, + ) -> Result> { + if points.is_empty() { + return Ok(Vec::new()); + } + + let mut aggregated_points = Vec::new(); + let mut current_bucket_start = query.start_time; + let mut current_bucket_points = Vec::new(); + + for point in points { + if point.timestamp >= current_bucket_start + query.interval { + // Process current bucket + if !current_bucket_points.is_empty() { + let aggregated = self.aggregate_bucket(¤t_bucket_points, current_bucket_start).await?; + aggregated_points.push(aggregated); + } + + // Start new bucket + current_bucket_start = current_bucket_start + query.interval; + current_bucket_points.clear(); + } + + current_bucket_points.push(*point); + } + + // Process last bucket + if !current_bucket_points.is_empty() { + let aggregated = self.aggregate_bucket(¤t_bucket_points, current_bucket_start).await?; + aggregated_points.push(aggregated); + } + + Ok(aggregated_points) + } + + /// Aggregate a bucket of data points + async fn aggregate_bucket( + &self, + points: &[&MetricsDataPoint], + bucket_start: SystemTime, + ) -> Result { + let mut aggregated = MetricsDataPoint { + timestamp: bucket_start, + system: None, + network: None, + disk_io: None, + scan: None, + heal: None, + policy: None, + }; + + // Aggregate system metrics + let system_metrics: Vec<&SystemMetrics> = points + .iter() + .filter_map(|p| p.system.as_ref()) + .collect(); + + if !system_metrics.is_empty() { + aggregated.system = Some(self.aggregate_system_metrics(&system_metrics).await?); + } + + // Aggregate network metrics + let network_metrics: Vec<&NetworkMetrics> = points + .iter() + .filter_map(|p| p.network.as_ref()) + .collect(); + + if !network_metrics.is_empty() { + aggregated.network = Some(self.aggregate_network_metrics(&network_metrics).await?); + } + + // Aggregate disk I/O metrics + let disk_metrics: Vec<&DiskMetrics> = points + .iter() + .filter_map(|p| p.disk_io.as_ref()) + .collect(); + + if !disk_metrics.is_empty() { + aggregated.disk_io = Some(self.aggregate_disk_metrics(&disk_metrics).await?); + } + + // Aggregate scan metrics + let scan_metrics: Vec<&ScanMetrics> = points + .iter() + .filter_map(|p| p.scan.as_ref()) + .collect(); + + if !scan_metrics.is_empty() { + aggregated.scan = Some(self.aggregate_scan_metrics(&scan_metrics).await?); + } + + // Aggregate heal metrics + let heal_metrics: Vec<&HealMetrics> = points + .iter() + .filter_map(|p| p.heal.as_ref()) + .collect(); + + if !heal_metrics.is_empty() { + aggregated.heal = Some(self.aggregate_heal_metrics(&heal_metrics).await?); + } + + // Aggregate policy metrics + let policy_metrics: Vec<&PolicyMetrics> = points + .iter() + .filter_map(|p| p.policy.as_ref()) + .collect(); + + if !policy_metrics.is_empty() { + aggregated.policy = Some(self.aggregate_policy_metrics(&policy_metrics).await?); + } + + Ok(aggregated) + } + + /// Aggregate system metrics + async fn aggregate_system_metrics(&self, metrics: &[&SystemMetrics]) -> Result { + if metrics.is_empty() { + return Ok(SystemMetrics::default()); + } + + let cpu_usage: f64 = metrics.iter().map(|m| m.cpu_usage).sum::() / metrics.len() as f64; + let memory_usage: f64 = metrics.iter().map(|m| m.memory_usage).sum::() / metrics.len() as f64; + let disk_usage: f64 = metrics.iter().map(|m| m.disk_usage).sum::() / metrics.len() as f64; + let system_load: f64 = metrics.iter().map(|m| m.system_load).sum::() / metrics.len() as f64; + let active_operations: u64 = metrics.iter().map(|m| m.active_operations).sum::() / metrics.len() as u64; + + // Aggregate health issues + let mut health_issues = HashMap::new(); + for metric in metrics { + for (severity, count) in &metric.health_issues { + *health_issues.entry(*severity).or_insert(0) += count; + } + } + + Ok(SystemMetrics { + timestamp: SystemTime::now(), + cpu_usage, + memory_usage, + disk_usage, + network_io: NetworkMetrics::default(), // Will be aggregated separately + disk_io: DiskMetrics::default(), // Will be aggregated separately + active_operations, + system_load, + health_issues, + scan_metrics: ScanMetrics::default(), // Will be aggregated separately + heal_metrics: HealMetrics::default(), // Will be aggregated separately + policy_metrics: PolicyMetrics::default(), // Will be aggregated separately + }) + } + + /// Aggregate network metrics + async fn aggregate_network_metrics(&self, metrics: &[&NetworkMetrics]) -> Result { + if metrics.is_empty() { + return Ok(NetworkMetrics::default()); + } + + let bytes_received_per_sec: u64 = metrics.iter().map(|m| m.bytes_received_per_sec).sum::() / metrics.len() as u64; + let bytes_sent_per_sec: u64 = metrics.iter().map(|m| m.bytes_sent_per_sec).sum::() / metrics.len() as u64; + let packets_received_per_sec: u64 = metrics.iter().map(|m| m.packets_received_per_sec).sum::() / metrics.len() as u64; + let packets_sent_per_sec: u64 = metrics.iter().map(|m| m.packets_sent_per_sec).sum::() / metrics.len() as u64; + + Ok(NetworkMetrics { + bytes_received_per_sec, + bytes_sent_per_sec, + packets_received_per_sec, + packets_sent_per_sec, + }) + } + + /// Aggregate disk metrics + async fn aggregate_disk_metrics(&self, metrics: &[&DiskMetrics]) -> Result { + if metrics.is_empty() { + return Ok(DiskMetrics::default()); + } + + let bytes_read_per_sec: u64 = metrics.iter().map(|m| m.bytes_read_per_sec).sum::() / metrics.len() as u64; + let bytes_written_per_sec: u64 = metrics.iter().map(|m| m.bytes_written_per_sec).sum::() / metrics.len() as u64; + let read_ops_per_sec: u64 = metrics.iter().map(|m| m.read_ops_per_sec).sum::() / metrics.len() as u64; + let write_ops_per_sec: u64 = metrics.iter().map(|m| m.write_ops_per_sec).sum::() / metrics.len() as u64; + let avg_read_latency_ms: f64 = metrics.iter().map(|m| m.avg_read_latency_ms).sum::() / metrics.len() as f64; + let avg_write_latency_ms: f64 = metrics.iter().map(|m| m.avg_write_latency_ms).sum::() / metrics.len() as f64; + + Ok(DiskMetrics { + bytes_read_per_sec, + bytes_written_per_sec, + read_ops_per_sec, + write_ops_per_sec, + avg_read_latency_ms, + avg_write_latency_ms, + }) + } + + /// Aggregate scan metrics + async fn aggregate_scan_metrics(&self, metrics: &[&ScanMetrics]) -> Result { + if metrics.is_empty() { + return Ok(ScanMetrics::default()); + } + + let objects_scanned: u64 = metrics.iter().map(|m| m.objects_scanned).sum(); + let bytes_scanned: u64 = metrics.iter().map(|m| m.bytes_scanned).sum(); + let scan_duration: Duration = metrics.iter().map(|m| m.scan_duration).sum(); + let health_issues_found: u64 = metrics.iter().map(|m| m.health_issues_found).sum(); + let scan_cycles_completed: u64 = metrics.iter().map(|m| m.scan_cycles_completed).sum(); + + // Calculate rates + let total_duration_secs = scan_duration.as_secs_f64(); + let scan_rate_objects_per_sec = if total_duration_secs > 0.0 { + objects_scanned as f64 / total_duration_secs + } else { + 0.0 + }; + + let scan_rate_bytes_per_sec = if total_duration_secs > 0.0 { + bytes_scanned as f64 / total_duration_secs + } else { + 0.0 + }; + + Ok(ScanMetrics { + objects_scanned, + bytes_scanned, + scan_duration, + scan_rate_objects_per_sec, + scan_rate_bytes_per_sec, + health_issues_found, + scan_cycles_completed, + last_scan_time: metrics.last().and_then(|m| m.last_scan_time), + }) + } + + /// Aggregate heal metrics + async fn aggregate_heal_metrics(&self, metrics: &[&HealMetrics]) -> Result { + if metrics.is_empty() { + return Ok(HealMetrics::default()); + } + + let total_repairs: u64 = metrics.iter().map(|m| m.total_repairs).sum(); + let successful_repairs: u64 = metrics.iter().map(|m| m.successful_repairs).sum(); + let failed_repairs: u64 = metrics.iter().map(|m| m.failed_repairs).sum(); + let total_repair_time: Duration = metrics.iter().map(|m| m.total_repair_time).sum(); + let total_retry_attempts: u64 = metrics.iter().map(|m| m.total_retry_attempts).sum(); + + // Calculate average repair time + let average_repair_time = if total_repairs > 0 { + let total_ms = total_repair_time.as_millis() as u64; + Duration::from_millis(total_ms / total_repairs) + } else { + Duration::ZERO + }; + + // Get latest values for current state + let active_repair_workers = metrics.last().map(|m| m.active_repair_workers).unwrap_or(0); + let queued_repair_tasks = metrics.last().map(|m| m.queued_repair_tasks).unwrap_or(0); + let last_repair_time = metrics.last().and_then(|m| m.last_repair_time); + + Ok(HealMetrics { + total_repairs, + successful_repairs, + failed_repairs, + total_repair_time, + average_repair_time, + active_repair_workers, + queued_repair_tasks, + last_repair_time, + total_retry_attempts, + }) + } + + /// Aggregate policy metrics + async fn aggregate_policy_metrics(&self, metrics: &[&PolicyMetrics]) -> Result { + if metrics.is_empty() { + return Ok(PolicyMetrics::default()); + } + + let total_evaluations: u64 = metrics.iter().map(|m| m.total_evaluations).sum(); + let allowed_operations: u64 = metrics.iter().map(|m| m.allowed_operations).sum(); + let denied_operations: u64 = metrics.iter().map(|m| m.denied_operations).sum(); + let scan_policy_evaluations: u64 = metrics.iter().map(|m| m.scan_policy_evaluations).sum(); + let heal_policy_evaluations: u64 = metrics.iter().map(|m| m.heal_policy_evaluations).sum(); + let retention_policy_evaluations: u64 = metrics.iter().map(|m| m.retention_policy_evaluations).sum(); + let total_evaluation_time: Duration = metrics.iter().map(|m| m.average_evaluation_time).sum(); + + // Calculate average evaluation time + let average_evaluation_time = if total_evaluations > 0 { + let total_ms = total_evaluation_time.as_millis() as u64; + Duration::from_millis(total_ms / total_evaluations) + } else { + Duration::ZERO + }; + + Ok(PolicyMetrics { + total_evaluations, + allowed_operations, + denied_operations, + scan_policy_evaluations, + heal_policy_evaluations, + retention_policy_evaluations, + average_evaluation_time, + }) + } + + /// Generate summary statistics + async fn generate_summary( + &self, + data_points: &[MetricsDataPoint], + query: &MetricsQuery, + ) -> Result { + let total_points = data_points.len() as u64; + let time_range = query.end_time.duration_since(query.start_time).unwrap_or(Duration::ZERO); + + // Calculate averages from system metrics + let system_metrics: Vec<&SystemMetrics> = data_points + .iter() + .filter_map(|p| p.system.as_ref()) + .collect(); + + let avg_cpu_usage = if !system_metrics.is_empty() { + system_metrics.iter().map(|m| m.cpu_usage).sum::() / system_metrics.len() as f64 + } else { + 0.0 + }; + + let avg_memory_usage = if !system_metrics.is_empty() { + system_metrics.iter().map(|m| m.memory_usage).sum::() / system_metrics.len() as f64 + } else { + 0.0 + }; + + let avg_disk_usage = if !system_metrics.is_empty() { + system_metrics.iter().map(|m| m.disk_usage).sum::() / system_metrics.len() as f64 + } else { + 0.0 + }; + + // Calculate totals from scan and heal metrics + let scan_metrics: Vec<&ScanMetrics> = data_points + .iter() + .filter_map(|p| p.scan.as_ref()) + .collect(); + + let total_objects_scanned = scan_metrics.iter().map(|m| m.objects_scanned).sum(); + let total_health_issues = scan_metrics.iter().map(|m| m.health_issues_found).sum(); + + let heal_metrics: Vec<&HealMetrics> = data_points + .iter() + .filter_map(|p| p.heal.as_ref()) + .collect(); + + let total_repairs = heal_metrics.iter().map(|m| m.total_repairs).sum(); + let successful_repairs: u64 = heal_metrics.iter().map(|m| m.successful_repairs).sum(); + let repair_success_rate = if total_repairs > 0 { + successful_repairs as f64 / total_repairs as f64 + } else { + 0.0 + }; + + Ok(MetricsSummary { + total_points, + time_range, + avg_cpu_usage, + avg_memory_usage, + avg_disk_usage, + total_objects_scanned, + total_repairs, + repair_success_rate, + total_health_issues, + }) + } + + /// Generate cache key for query + fn generate_cache_key(&self, query: &MetricsQuery) -> String { + format!( + "{:?}_{:?}_{:?}_{:?}", + query.start_time, query.end_time, query.interval, query.metrics + ) + } + + /// Clear old cache entries + pub async fn clear_old_cache(&mut self) -> Result<()> { + let now = SystemTime::now(); + let retention_period = Duration::from_secs(3600); // 1 hour + + self.aggregation_cache.retain(|_key, value| { + if let Some(latest_point) = value.data_points.last() { + now.duration_since(latest_point.timestamp).unwrap_or(Duration::ZERO) < retention_period + } else { + false + } + }); + + info!("Cleared old cache entries, remaining: {}", self.aggregation_cache.len()); + Ok(()) + } + + /// Get aggregation statistics + pub fn get_statistics(&self) -> AggregatorStatistics { + AggregatorStatistics { + total_data_points: self.data_points.len(), + total_aggregations: self.aggregation_count, + cache_size: self.aggregation_cache.len(), + last_aggregation_time: self.last_aggregation_time, + config: self.config.clone(), + } + } +} + +/// Aggregator statistics +#[derive(Debug, Clone)] +pub struct AggregatorStatistics { + pub total_data_points: usize, + pub total_aggregations: u64, + pub cache_size: usize, + pub last_aggregation_time: SystemTime, + pub config: AggregatorConfig, +} + +impl Default for MetricsSummary { + fn default() -> Self { + Self { + total_points: 0, + time_range: Duration::ZERO, + avg_cpu_usage: 0.0, + avg_memory_usage: 0.0, + avg_disk_usage: 0.0, + total_objects_scanned: 0, + total_repairs: 0, + repair_success_rate: 0.0, + total_health_issues: 0, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::scanner::Severity; + + #[tokio::test] + async fn test_aggregator_creation() { + let aggregator = Aggregator::new(Duration::from_secs(300)).await.unwrap(); + + assert_eq!(aggregator.config().default_interval, Duration::from_secs(300)); + assert!(aggregator.config().enable_auto_aggregation); + } + + #[tokio::test] + async fn test_data_point_addition() { + let mut aggregator = Aggregator::new(Duration::from_secs(300)).await.unwrap(); + + let data_point = MetricsDataPoint { + timestamp: SystemTime::now(), + system: Some(SystemMetrics::default()), + network: None, + disk_io: None, + scan: None, + heal: None, + policy: None, + }; + + aggregator.add_data_point(data_point).await.unwrap(); + + let stats = aggregator.get_statistics(); + assert_eq!(stats.total_data_points, 1); + } + + #[tokio::test] + async fn test_metrics_aggregation() { + let mut aggregator = Aggregator::new(Duration::from_secs(300)).await.unwrap(); + + // Add some test data points + for i in 0..5 { + let mut system_metrics = SystemMetrics::default(); + system_metrics.cpu_usage = i as f64 * 10.0; + system_metrics.memory_usage = i as f64 * 20.0; + + let data_point = MetricsDataPoint { + timestamp: SystemTime::now() + Duration::from_secs(i * 60), + system: Some(system_metrics), + network: None, + disk_io: None, + scan: None, + heal: None, + policy: None, + }; + + aggregator.add_data_point(data_point).await.unwrap(); + } + + let query = MetricsQuery { + start_time: SystemTime::now(), + end_time: SystemTime::now() + Duration::from_secs(300), + interval: Duration::from_secs(60), + metrics: vec![MetricType::System], + severity_filter: None, + limit: None, + }; + + let result = aggregator.aggregate_metrics(query).await.unwrap(); + assert_eq!(result.data_points.len(), 5); + assert_eq!(result.summary.total_points, 5); + } + + #[tokio::test] + async fn test_system_metrics_aggregation() { + let mut aggregator = Aggregator::new(Duration::from_secs(300)).await.unwrap(); + + let metrics = vec![ + SystemMetrics { + cpu_usage: 10.0, + memory_usage: 20.0, + disk_usage: 30.0, + ..Default::default() + }, + SystemMetrics { + cpu_usage: 20.0, + memory_usage: 40.0, + disk_usage: 60.0, + ..Default::default() + }, + ]; + + let aggregated = aggregator.aggregate_system_metrics(&metrics.iter().collect::>()).await.unwrap(); + + assert_eq!(aggregated.cpu_usage, 15.0); + assert_eq!(aggregated.memory_usage, 30.0); + assert_eq!(aggregated.disk_usage, 45.0); + } + + #[tokio::test] + async fn test_cache_clearing() { + let mut aggregator = Aggregator::new(Duration::from_secs(300)).await.unwrap(); + + // Add some cached data + let query = MetricsQuery { + start_time: SystemTime::now() - Duration::from_secs(3600), + end_time: SystemTime::now() - Duration::from_secs(3000), + interval: Duration::from_secs(60), + metrics: vec![], + severity_filter: None, + limit: None, + }; + + let _result = aggregator.aggregate_metrics(query).await.unwrap(); + + let stats_before = aggregator.get_statistics(); + assert_eq!(stats_before.cache_size, 1); + + aggregator.clear_old_cache().await.unwrap(); + + let stats_after = aggregator.get_statistics(); + assert_eq!(stats_after.cache_size, 0); + } +} \ No newline at end of file diff --git a/crates/ahm/src/metrics/collector.rs b/crates/ahm/src/metrics/collector.rs new file mode 100644 index 000000000..3932b94e5 --- /dev/null +++ b/crates/ahm/src/metrics/collector.rs @@ -0,0 +1,426 @@ +// Copyright 2024 RustFS Team + +use std::{ + sync::Arc, + time::{Duration, Instant, SystemTime}, +}; + +use tokio::sync::RwLock; +use tracing::{debug, error, info, warn}; + +use crate::{ + error::Result, + scanner::{HealthIssue, Severity}, +}; + +use super::{ + AggregatedMetrics, Aggregator, DiskMetrics, HealMetrics, MetricsQuery, MetricType, + NetworkMetrics, PolicyMetrics, ScanMetrics, SystemMetrics, +}; + +/// Configuration for the metrics collector +#[derive(Debug, Clone)] +pub struct CollectorConfig { + /// Collection interval + pub collection_interval: Duration, + /// Whether to enable detailed metrics collection + pub enable_detailed_metrics: bool, + /// Maximum number of metrics to keep in memory + pub max_metrics_in_memory: usize, + /// Whether to enable automatic aggregation + pub enable_auto_aggregation: bool, + /// Aggregation interval + pub aggregation_interval: Duration, + /// Whether to enable resource monitoring + pub enable_resource_monitoring: bool, + /// Resource monitoring interval + pub resource_monitoring_interval: Duration, + /// Whether to enable health issue tracking + pub enable_health_issue_tracking: bool, + /// Metrics retention period + pub metrics_retention_period: Duration, +} + +impl Default for CollectorConfig { + fn default() -> Self { + Self { + collection_interval: Duration::from_secs(30), // 30 seconds + enable_detailed_metrics: true, + max_metrics_in_memory: 10000, + enable_auto_aggregation: true, + aggregation_interval: Duration::from_secs(300), // 5 minutes + enable_resource_monitoring: true, + resource_monitoring_interval: Duration::from_secs(10), // 10 seconds + enable_health_issue_tracking: true, + metrics_retention_period: Duration::from_secs(86400 * 7), // 7 days + } + } +} + +/// Metrics collector that gathers system metrics +#[derive(Debug)] +pub struct Collector { + config: CollectorConfig, + metrics: Arc>>, + aggregator: Arc, + last_collection_time: Arc>, + collection_count: Arc>, + health_issues: Arc>>, +} + +impl Collector { + /// Create a new metrics collector + pub async fn new(config: CollectorConfig) -> Result { + let aggregator = Arc::new(Aggregator::new(config.aggregation_interval).await?); + + Ok(Self { + config, + metrics: Arc::new(RwLock::new(Vec::new())), + aggregator, + last_collection_time: Arc::new(RwLock::new(SystemTime::now())), + collection_count: Arc::new(RwLock::new(0)), + health_issues: Arc::new(RwLock::new(std::collections::HashMap::new())), + }) + } + + /// Get the configuration + pub fn config(&self) -> &CollectorConfig { + &self.config + } + + /// Collect current system metrics + pub async fn collect_metrics(&self) -> Result { + let start_time = Instant::now(); + + let mut metrics = SystemMetrics::default(); + metrics.timestamp = SystemTime::now(); + + // Collect system resource metrics + if self.config.enable_resource_monitoring { + self.collect_system_resources(&mut metrics).await?; + } + + // Collect scan metrics + self.collect_scan_metrics(&mut metrics).await?; + + // Collect heal metrics + self.collect_heal_metrics(&mut metrics).await?; + + // Collect policy metrics + self.collect_policy_metrics(&mut metrics).await?; + + // Collect health issues + if self.config.enable_health_issue_tracking { + self.collect_health_issues(&mut metrics).await?; + } + + // Store metrics + { + let mut metrics_store = self.metrics.write().await; + metrics_store.push(metrics.clone()); + + // Trim old metrics if we exceed the limit + if metrics_store.len() > self.config.max_metrics_in_memory { + let excess = metrics_store.len() - self.config.max_metrics_in_memory; + metrics_store.drain(0..excess); + } + } + + // Update collection statistics + { + let mut last_time = self.last_collection_time.write().await; + *last_time = metrics.timestamp; + + let mut count = self.collection_count.write().await; + *count += 1; + } + + let collection_time = start_time.elapsed(); + debug!("Metrics collection completed in {:?}", collection_time); + + Ok(metrics) + } + + /// Collect system resource metrics + async fn collect_system_resources(&self, metrics: &mut SystemMetrics) -> Result<()> { + // Simulate system resource collection + // In a real implementation, this would use system APIs + + metrics.cpu_usage = self.get_cpu_usage().await?; + metrics.memory_usage = self.get_memory_usage().await?; + metrics.disk_usage = self.get_disk_usage().await?; + metrics.system_load = self.get_system_load().await?; + metrics.active_operations = self.get_active_operations().await?; + + // Collect network metrics + metrics.network_io = self.get_network_metrics().await?; + + // Collect disk I/O metrics + metrics.disk_io = self.get_disk_io_metrics().await?; + + Ok(()) + } + + /// Collect scan metrics + async fn collect_scan_metrics(&self, metrics: &mut SystemMetrics) -> Result<()> { + // In a real implementation, this would get data from the scanner + metrics.scan_metrics = ScanMetrics::default(); + + // Simulate some scan metrics + metrics.scan_metrics.objects_scanned = 1000; + metrics.scan_metrics.bytes_scanned = 1024 * 1024 * 100; // 100 MB + metrics.scan_metrics.scan_duration = Duration::from_secs(60); + metrics.scan_metrics.scan_rate_objects_per_sec = 16.67; // 1000 / 60 + metrics.scan_metrics.scan_rate_bytes_per_sec = 1_747_200.0; // 100MB / 60s + metrics.scan_metrics.health_issues_found = 5; + metrics.scan_metrics.scan_cycles_completed = 1; + metrics.scan_metrics.last_scan_time = Some(SystemTime::now()); + + Ok(()) + } + + /// Collect heal metrics + async fn collect_heal_metrics(&self, metrics: &mut SystemMetrics) -> Result<()> { + // In a real implementation, this would get data from the heal system + metrics.heal_metrics = HealMetrics::default(); + + // Simulate some heal metrics + metrics.heal_metrics.total_repairs = 10; + metrics.heal_metrics.successful_repairs = 8; + metrics.heal_metrics.failed_repairs = 2; + metrics.heal_metrics.total_repair_time = Duration::from_secs(300); + metrics.heal_metrics.average_repair_time = Duration::from_secs(30); + metrics.heal_metrics.active_repair_workers = 2; + metrics.heal_metrics.queued_repair_tasks = 5; + metrics.heal_metrics.last_repair_time = Some(SystemTime::now()); + metrics.heal_metrics.total_retry_attempts = 3; + + Ok(()) + } + + /// Collect policy metrics + async fn collect_policy_metrics(&self, metrics: &mut SystemMetrics) -> Result<()> { + // In a real implementation, this would get data from the policy system + metrics.policy_metrics = PolicyMetrics::default(); + + // Simulate some policy metrics + metrics.policy_metrics.total_evaluations = 50; + metrics.policy_metrics.allowed_operations = 45; + metrics.policy_metrics.denied_operations = 5; + metrics.policy_metrics.scan_policy_evaluations = 20; + metrics.policy_metrics.heal_policy_evaluations = 20; + metrics.policy_metrics.retention_policy_evaluations = 10; + metrics.policy_metrics.average_evaluation_time = Duration::from_millis(10); + + Ok(()) + } + + /// Collect health issues + async fn collect_health_issues(&self, metrics: &mut SystemMetrics) -> Result<()> { + let health_issues = self.health_issues.read().await; + metrics.health_issues = health_issues.clone(); + Ok(()) + } + + /// Record a health issue + pub async fn record_health_issue(&self, issue: &HealthIssue) -> Result<()> { + let mut issues = self.health_issues.write().await; + let count = issues.entry(issue.severity).or_insert(0); + *count += 1; + + info!("Recorded health issue: {:?} - {}", issue.severity, issue.description); + Ok(()) + } + + /// Record an event (alias for record_health_issue) + pub async fn record_event(&self, issue: &HealthIssue) -> Result<()> { + self.record_health_issue(issue).await + } + + /// Clear health issues + pub async fn clear_health_issues(&self) -> Result<()> { + let mut health_issues = self.health_issues.write().await; + health_issues.clear(); + + info!("Cleared all health issues"); + Ok(()) + } + + /// Query metrics with aggregation + pub async fn query_metrics(&self, query: MetricsQuery) -> Result { + // In a real implementation, this would query the aggregator + // For now, we'll return a simple aggregated result + let aggregator = self.aggregator.as_ref(); + let mut aggregator_guard = aggregator.write().await; + aggregator_guard.aggregate_metrics(query).await + } + + /// Get metrics for a specific time range + pub async fn get_metrics_range(&self, start_time: SystemTime, end_time: SystemTime) -> Result> { + let metrics = self.metrics.read().await; + let filtered_metrics: Vec = metrics + .iter() + .filter(|m| m.timestamp >= start_time && m.timestamp <= end_time) + .cloned() + .collect(); + + Ok(filtered_metrics) + } + + /// Get latest metrics + pub async fn get_latest_metrics(&self) -> Result> { + let metrics = self.metrics.read().await; + Ok(metrics.last().cloned()) + } + + /// Get collection statistics + pub async fn get_collection_statistics(&self) -> CollectionStatistics { + let collection_count = *self.collection_count.read().await; + let last_collection_time = *self.last_collection_time.read().await; + let metrics_count = self.metrics.read().await.len(); + + CollectionStatistics { + total_collections: collection_count, + last_collection_time, + metrics_in_memory: metrics_count, + config: self.config.clone(), + } + } + + /// Simulated system resource collection methods + async fn get_cpu_usage(&self) -> Result { + // Simulate CPU usage collection + Ok(25.5) // 25.5% + } + + async fn get_memory_usage(&self) -> Result { + // Simulate memory usage collection + Ok(60.2) // 60.2% + } + + async fn get_disk_usage(&self) -> Result { + // Simulate disk usage collection + Ok(45.8) // 45.8% + } + + async fn get_system_load(&self) -> Result { + // Simulate system load collection + Ok(0.75) // 0.75 + } + + async fn get_active_operations(&self) -> Result { + // Simulate active operations count + Ok(15) + } + + async fn get_network_metrics(&self) -> Result { + // Simulate network metrics collection + Ok(NetworkMetrics { + bytes_received_per_sec: 1024 * 1024, // 1 MB/s + bytes_sent_per_sec: 512 * 1024, // 512 KB/s + packets_received_per_sec: 1000, + packets_sent_per_sec: 500, + }) + } + + async fn get_disk_io_metrics(&self) -> Result { + // Simulate disk I/O metrics collection + Ok(DiskMetrics { + bytes_read_per_sec: 2 * 1024 * 1024, // 2 MB/s + bytes_written_per_sec: 1 * 1024 * 1024, // 1 MB/s + read_ops_per_sec: 200, + write_ops_per_sec: 100, + avg_read_latency_ms: 5.0, + avg_write_latency_ms: 8.0, + }) + } +} + +/// Collection statistics +#[derive(Debug, Clone)] +pub struct CollectionStatistics { + pub total_collections: u64, + pub last_collection_time: SystemTime, + pub metrics_in_memory: usize, + pub config: CollectorConfig, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::scanner::{HealthIssue, HealthIssueType}; + + #[tokio::test] + async fn test_collector_creation() { + let config = CollectorConfig::default(); + let collector = Collector::new(config).await.unwrap(); + + assert_eq!(collector.config().collection_interval, Duration::from_secs(30)); + assert!(collector.config().enable_detailed_metrics); + } + + #[tokio::test] + async fn test_metrics_collection() { + let config = CollectorConfig::default(); + let collector = Collector::new(config).await.unwrap(); + + let metrics = collector.collect_metrics().await.unwrap(); + assert_eq!(metrics.cpu_usage, 25.5); + assert_eq!(metrics.memory_usage, 60.2); + assert_eq!(metrics.disk_usage, 45.8); + } + + #[tokio::test] + async fn test_health_issue_recording() { + let config = CollectorConfig::default(); + let collector = Collector::new(config).await.unwrap(); + + let issue = HealthIssue { + issue_type: HealthIssueType::MissingReplica, + severity: Severity::Critical, + bucket: "test-bucket".to_string(), + object: "test-object".to_string(), + description: "Test issue".to_string(), + metadata: None, + }; + + collector.record_health_issue(&issue).await.unwrap(); + + let stats = collector.get_collection_statistics().await; + assert_eq!(stats.total_collections, 0); // No collection yet + } + + #[tokio::test] + async fn test_latest_metrics() { + let config = CollectorConfig::default(); + let collector = Collector::new(config).await.unwrap(); + + // Initially no metrics + let latest = collector.get_latest_metrics().await.unwrap(); + assert!(latest.is_none()); + + // Collect metrics + collector.collect_metrics().await.unwrap(); + + // Now should have metrics + let latest = collector.get_latest_metrics().await.unwrap(); + assert!(latest.is_some()); + } + + #[tokio::test] + async fn test_collection_statistics() { + let config = CollectorConfig::default(); + let collector = Collector::new(config).await.unwrap(); + + let stats = collector.get_collection_statistics().await; + assert_eq!(stats.total_collections, 0); + assert_eq!(stats.metrics_in_memory, 0); + + // Collect metrics + collector.collect_metrics().await.unwrap(); + + let stats = collector.get_collection_statistics().await; + assert_eq!(stats.total_collections, 1); + assert_eq!(stats.metrics_in_memory, 1); + } +} \ No newline at end of file diff --git a/crates/ahm/src/metrics/mod.rs b/crates/ahm/src/metrics/mod.rs new file mode 100644 index 000000000..f5f6ae859 --- /dev/null +++ b/crates/ahm/src/metrics/mod.rs @@ -0,0 +1,617 @@ +// 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. + +//! Metrics collection and aggregation system +//! +//! The metrics subsystem provides comprehensive data collection and analysis: +//! - Real-time metrics collection from all subsystems +//! - Time-series data storage and aggregation +//! - Export capabilities for external monitoring systems +//! - Performance analytics and trend analysis + +pub mod collector; +pub mod aggregator; +pub mod storage; +pub mod reporter; + +pub use collector::{Collector, CollectorConfig}; +pub use aggregator::{Aggregator, AggregatorConfig}; +pub use storage::{Storage, StorageConfig}; +pub use reporter::{Reporter, ReporterConfig}; + +use std::time::{Duration, SystemTime}; +use std::collections::HashMap; +use serde::{Deserialize, Serialize}; + +use crate::scanner::{HealthIssue, Severity}; + +/// Metrics subsystem status +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Status { + /// Metrics system is initializing + Initializing, + /// Metrics system is running normally + Running, + /// Metrics system is degraded (some exporters failing) + Degraded, + /// Metrics system is stopping + Stopping, + /// Metrics system has stopped + Stopped, + /// Metrics system encountered an error + Error(String), +} + +/// Metric data point with timestamp and value +#[derive(Debug, Clone)] +pub struct MetricPoint { + /// Metric name + pub name: String, + /// Metric value + pub value: MetricValue, + /// Timestamp when metric was collected + pub timestamp: SystemTime, + /// Additional labels/tags + pub labels: HashMap, +} + +/// Different types of metric values +#[derive(Debug, Clone)] +pub enum MetricValue { + /// Counter that only increases + Counter(u64), + /// Gauge that can go up or down + Gauge(f64), + /// Histogram with buckets + Histogram { + count: u64, + sum: f64, + buckets: Vec, + }, + /// Summary with quantiles + Summary { + count: u64, + sum: f64, + quantiles: Vec, + }, +} + +/// Histogram bucket +#[derive(Debug, Clone)] +pub struct HistogramBucket { + /// Upper bound of the bucket + pub le: f64, + /// Count of observations in this bucket + pub count: u64, +} + +/// Summary quantile +#[derive(Debug, Clone)] +pub struct Quantile { + /// Quantile value (e.g., 0.5 for median) + pub quantile: f64, + /// Value at this quantile + pub value: f64, +} + +/// Aggregation functions for metrics +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AggregationFunction { + Sum, + Average, + Min, + Max, + Count, + Rate, + Percentile(u8), +} + +/// Time window for aggregation +#[derive(Debug, Clone)] +pub struct TimeWindow { + /// Duration of the window + pub duration: Duration, + /// How often to create new windows + pub step: Duration, +} + +/// Metric export configuration +#[derive(Debug, Clone)] +pub struct ExportConfig { + /// Export format + pub format: ExportFormat, + /// Export destination + pub destination: ExportDestination, + /// Export interval + pub interval: Duration, + /// Metric filters + pub filters: Vec, +} + +/// Supported export formats +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ExportFormat { + /// Prometheus format + Prometheus, + /// JSON format + Json, + /// CSV format + Csv, + /// Custom format + Custom(String), +} + +/// Export destinations +#[derive(Debug, Clone)] +pub enum ExportDestination { + /// HTTP endpoint + Http { url: String, headers: HashMap }, + /// File system + File { path: String }, + /// Standard output + Stdout, + /// Custom destination + Custom(String), +} + +/// Metric filtering rules +#[derive(Debug, Clone)] +pub struct MetricFilter { + /// Metric name pattern (regex) + pub name_pattern: String, + /// Label filters + pub label_filters: HashMap, + /// Include or exclude matching metrics + pub include: bool, +} + +/// System-wide metrics that are automatically collected +pub mod system_metrics { + /// Object-related metrics + pub const OBJECTS_TOTAL: &str = "rustfs_objects_total"; + pub const OBJECTS_SIZE_BYTES: &str = "rustfs_objects_size_bytes"; + pub const OBJECTS_SCANNED_TOTAL: &str = "rustfs_objects_scanned_total"; + pub const OBJECTS_HEAL_OPERATIONS_TOTAL: &str = "rustfs_objects_heal_operations_total"; + + /// Scanner metrics + pub const SCAN_CYCLES_TOTAL: &str = "rustfs_scan_cycles_total"; + pub const SCAN_DURATION_SECONDS: &str = "rustfs_scan_duration_seconds"; + pub const SCAN_RATE_OBJECTS_PER_SECOND: &str = "rustfs_scan_rate_objects_per_second"; + pub const SCAN_RATE_BYTES_PER_SECOND: &str = "rustfs_scan_rate_bytes_per_second"; + + /// Health metrics + pub const HEALTH_ISSUES_TOTAL: &str = "rustfs_health_issues_total"; + pub const HEALTH_ISSUES_BY_SEVERITY: &str = "rustfs_health_issues_by_severity"; + pub const HEAL_SUCCESS_RATE: &str = "rustfs_heal_success_rate"; + + /// System resource metrics + pub const DISK_USAGE_BYTES: &str = "rustfs_disk_usage_bytes"; + pub const DISK_IOPS: &str = "rustfs_disk_iops"; + pub const MEMORY_USAGE_BYTES: &str = "rustfs_memory_usage_bytes"; + pub const CPU_USAGE_PERCENT: &str = "rustfs_cpu_usage_percent"; + + /// Performance metrics + pub const OPERATION_DURATION_SECONDS: &str = "rustfs_operation_duration_seconds"; + pub const ACTIVE_OPERATIONS: &str = "rustfs_active_operations"; + pub const THROUGHPUT_BYTES_PER_SECOND: &str = "rustfs_throughput_bytes_per_second"; +} + +/// System metrics collected by AHM +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SystemMetrics { + /// Timestamp when metrics were collected + pub timestamp: SystemTime, + /// CPU usage percentage + pub cpu_usage: f64, + /// Memory usage percentage + pub memory_usage: f64, + /// Disk usage percentage + pub disk_usage: f64, + /// Network I/O bytes per second + pub network_io: NetworkMetrics, + /// Disk I/O bytes per second + pub disk_io: DiskMetrics, + /// Active operations count + pub active_operations: u64, + /// System load average + pub system_load: f64, + /// Health issues count by severity + pub health_issues: std::collections::HashMap, + /// Scan metrics + pub scan_metrics: ScanMetrics, + /// Heal metrics + pub heal_metrics: HealMetrics, + /// Policy metrics + pub policy_metrics: PolicyMetrics, +} + +/// Network I/O metrics +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NetworkMetrics { + /// Bytes received per second + pub bytes_received_per_sec: u64, + /// Bytes sent per second + pub bytes_sent_per_sec: u64, + /// Packets received per second + pub packets_received_per_sec: u64, + /// Packets sent per second + pub packets_sent_per_sec: u64, +} + +/// Disk I/O metrics +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DiskMetrics { + /// Bytes read per second + pub bytes_read_per_sec: u64, + /// Bytes written per second + pub bytes_written_per_sec: u64, + /// Read operations per second + pub read_ops_per_sec: u64, + /// Write operations per second + pub write_ops_per_sec: u64, + /// Average read latency in milliseconds + pub avg_read_latency_ms: f64, + /// Average write latency in milliseconds + pub avg_write_latency_ms: f64, +} + +/// Scan operation metrics +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ScanMetrics { + /// Total objects scanned + pub objects_scanned: u64, + /// Total bytes scanned + pub bytes_scanned: u64, + /// Scan duration + pub scan_duration: Duration, + /// Scan rate (objects per second) + pub scan_rate_objects_per_sec: f64, + /// Scan rate (bytes per second) + pub scan_rate_bytes_per_sec: f64, + /// Health issues found + pub health_issues_found: u64, + /// Scan cycles completed + pub scan_cycles_completed: u64, + /// Last scan time + pub last_scan_time: Option, +} + +/// Heal operation metrics +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HealMetrics { + /// Total repair operations + pub total_repairs: u64, + /// Successful repairs + pub successful_repairs: u64, + /// Failed repairs + pub failed_repairs: u64, + /// Total repair time + pub total_repair_time: Duration, + /// Average repair time + pub average_repair_time: Duration, + /// Active repair workers + pub active_repair_workers: u64, + /// Queued repair tasks + pub queued_repair_tasks: u64, + /// Last repair time + pub last_repair_time: Option, + /// Retry attempts + pub total_retry_attempts: u64, +} + +/// Policy evaluation metrics +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PolicyMetrics { + /// Total policy evaluations + pub total_evaluations: u64, + /// Allowed operations + pub allowed_operations: u64, + /// Denied operations + pub denied_operations: u64, + /// Scan policy evaluations + pub scan_policy_evaluations: u64, + /// Heal policy evaluations + pub heal_policy_evaluations: u64, + /// Retention policy evaluations + pub retention_policy_evaluations: u64, + /// Average evaluation time + pub average_evaluation_time: Duration, +} + +impl Default for SystemMetrics { + fn default() -> Self { + Self { + timestamp: SystemTime::now(), + cpu_usage: 0.0, + memory_usage: 0.0, + disk_usage: 0.0, + network_io: NetworkMetrics::default(), + disk_io: DiskMetrics::default(), + active_operations: 0, + system_load: 0.0, + health_issues: std::collections::HashMap::new(), + scan_metrics: ScanMetrics::default(), + heal_metrics: HealMetrics::default(), + policy_metrics: PolicyMetrics::default(), + } + } +} + +impl Default for NetworkMetrics { + fn default() -> Self { + Self { + bytes_received_per_sec: 0, + bytes_sent_per_sec: 0, + packets_received_per_sec: 0, + packets_sent_per_sec: 0, + } + } +} + +impl Default for DiskMetrics { + fn default() -> Self { + Self { + bytes_read_per_sec: 0, + bytes_written_per_sec: 0, + read_ops_per_sec: 0, + write_ops_per_sec: 0, + avg_read_latency_ms: 0.0, + avg_write_latency_ms: 0.0, + } + } +} + +impl Default for ScanMetrics { + fn default() -> Self { + Self { + objects_scanned: 0, + bytes_scanned: 0, + scan_duration: Duration::ZERO, + scan_rate_objects_per_sec: 0.0, + scan_rate_bytes_per_sec: 0.0, + health_issues_found: 0, + scan_cycles_completed: 0, + last_scan_time: None, + } + } +} + +impl Default for HealMetrics { + fn default() -> Self { + Self { + total_repairs: 0, + successful_repairs: 0, + failed_repairs: 0, + total_repair_time: Duration::ZERO, + average_repair_time: Duration::ZERO, + active_repair_workers: 0, + queued_repair_tasks: 0, + last_repair_time: None, + total_retry_attempts: 0, + } + } +} + +impl Default for PolicyMetrics { + fn default() -> Self { + Self { + total_evaluations: 0, + allowed_operations: 0, + denied_operations: 0, + scan_policy_evaluations: 0, + heal_policy_evaluations: 0, + retention_policy_evaluations: 0, + average_evaluation_time: Duration::ZERO, + } + } +} + +/// Metrics query parameters +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MetricsQuery { + /// Start time for the query + pub start_time: SystemTime, + /// End time for the query + pub end_time: SystemTime, + /// Metrics aggregation interval + pub interval: Duration, + /// Metrics to include in the query + pub metrics: Vec, + /// Filter by severity + pub severity_filter: Option, + /// Limit number of results + pub limit: Option, +} + +/// Types of metrics that can be queried +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum MetricType { + /// System metrics (CPU, memory, disk) + System, + /// Network metrics + Network, + /// Disk I/O metrics + DiskIo, + /// Scan metrics + Scan, + /// Heal metrics + Heal, + /// Policy metrics + Policy, + /// Health issues + HealthIssues, +} + +/// Aggregated metrics data +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AggregatedMetrics { + /// Query parameters used + pub query: MetricsQuery, + /// Aggregated data points + pub data_points: Vec, + /// Summary statistics + pub summary: MetricsSummary, +} + +/// Individual metrics data point +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MetricsDataPoint { + /// Timestamp for this data point + pub timestamp: SystemTime, + /// System metrics + pub system: Option, + /// Network metrics + pub network: Option, + /// Disk I/O metrics + pub disk_io: Option, + /// Scan metrics + pub scan: Option, + /// Heal metrics + pub heal: Option, + /// Policy metrics + pub policy: Option, +} + +/// Summary statistics for aggregated metrics +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MetricsSummary { + /// Total data points + pub total_points: u64, + /// Time range covered + pub time_range: Duration, + /// Average CPU usage + pub avg_cpu_usage: f64, + /// Average memory usage + pub avg_memory_usage: f64, + /// Average disk usage + pub avg_disk_usage: f64, + /// Total objects scanned + pub total_objects_scanned: u64, + /// Total repairs performed + pub total_repairs: u64, + /// Success rate for repairs + pub repair_success_rate: f64, + /// Total health issues + pub total_health_issues: u64, +} + +/// Resource usage information +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ResourceUsage { + /// Disk usage information + pub disk_usage: DiskUsage, + /// Memory usage information + pub memory_usage: MemoryUsage, + /// Network usage information + pub network_usage: NetworkUsage, + /// CPU usage information + pub cpu_usage: CpuUsage, +} + +/// Disk usage information +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DiskUsage { + /// Total disk space in bytes + pub total_bytes: u64, + /// Used disk space in bytes + pub used_bytes: u64, + /// Available disk space in bytes + pub available_bytes: u64, + /// Usage percentage + pub usage_percentage: f64, +} + +/// Memory usage information +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MemoryUsage { + /// Total memory in bytes + pub total_bytes: u64, + /// Used memory in bytes + pub used_bytes: u64, + /// Available memory in bytes + pub available_bytes: u64, + /// Usage percentage + pub usage_percentage: f64, +} + +/// Network usage information +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NetworkUsage { + /// Bytes received + pub bytes_received: u64, + /// Bytes sent + pub bytes_sent: u64, + /// Packets received + pub packets_received: u64, + /// Packets sent + pub packets_sent: u64, +} + +/// CPU usage information +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CpuUsage { + /// CPU usage percentage + pub usage_percentage: f64, + /// Number of CPU cores + pub cores: u32, + /// Load average + pub load_average: f64, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_system_metrics_creation() { + let metrics = SystemMetrics::default(); + assert_eq!(metrics.cpu_usage, 0.0); + assert_eq!(metrics.memory_usage, 0.0); + assert_eq!(metrics.active_operations, 0); + } + + #[test] + fn test_scan_metrics_creation() { + let metrics = ScanMetrics::default(); + assert_eq!(metrics.objects_scanned, 0); + assert_eq!(metrics.bytes_scanned, 0); + assert_eq!(metrics.scan_cycles_completed, 0); + } + + #[test] + fn test_heal_metrics_creation() { + let metrics = HealMetrics::default(); + assert_eq!(metrics.total_repairs, 0); + assert_eq!(metrics.successful_repairs, 0); + assert_eq!(metrics.failed_repairs, 0); + } + + #[test] + fn test_metrics_query_creation() { + let start_time = SystemTime::now(); + let end_time = start_time + Duration::from_secs(3600); + let query = MetricsQuery { + start_time, + end_time, + interval: Duration::from_secs(60), + metrics: vec![MetricType::System, MetricType::Scan], + severity_filter: Some(Severity::Critical), + limit: Some(100), + }; + + assert_eq!(query.metrics.len(), 2); + assert_eq!(query.interval, Duration::from_secs(60)); + assert_eq!(query.limit, Some(100)); + } +} \ No newline at end of file diff --git a/crates/ahm/src/metrics/reporter.rs b/crates/ahm/src/metrics/reporter.rs new file mode 100644 index 000000000..45aacc031 --- /dev/null +++ b/crates/ahm/src/metrics/reporter.rs @@ -0,0 +1,861 @@ +// Copyright 2024 RustFS Team + +use std::{ + collections::HashMap, + fmt, + sync::Arc, + time::{Duration, SystemTime}, +}; + +use tokio::sync::RwLock; +use tracing::{debug, error, info, warn}; +use serde::{Serialize, Deserialize}; + +use crate::error::Result; + +use super::{ + AggregatedMetrics, MetricsQuery, MetricsSummary, SystemMetrics, +}; + +/// Configuration for the metrics reporter +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReporterConfig { + /// Whether to enable reporting + pub enabled: bool, + /// Report generation interval + pub report_interval: Duration, + /// Maximum number of reports to keep in memory + pub max_reports_in_memory: usize, + /// Alert thresholds + pub alert_thresholds: AlertThresholds, + /// Report output format + pub default_format: ReportFormat, + /// Whether to enable alerting + pub enable_alerts: bool, + /// Maximum number of alerts to keep in memory + pub max_alerts_in_memory: usize, + /// Report output directory + pub output_directory: Option, + /// Whether to enable HTTP reporting + pub enable_http_reporting: bool, + /// HTTP reporting endpoint + pub http_endpoint: Option, +} + +impl Default for ReporterConfig { + fn default() -> Self { + Self { + enabled: true, + report_interval: Duration::from_secs(60), // 1 minute + max_reports_in_memory: 1000, + alert_thresholds: AlertThresholds::default(), + default_format: ReportFormat::Json, + enable_alerts: true, + max_alerts_in_memory: 1000, + output_directory: None, + enable_http_reporting: false, + http_endpoint: None, + } + } +} + +/// Alert thresholds for metrics reporting +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AlertThresholds { + /// CPU usage threshold (percentage) + pub cpu_usage_threshold: f64, + /// Memory usage threshold (percentage) + pub memory_usage_threshold: f64, + /// Disk usage threshold (percentage) + pub disk_usage_threshold: f64, + /// System load threshold + pub system_load_threshold: f64, + /// Repair failure rate threshold (percentage) + pub repair_failure_rate_threshold: f64, + /// Health issues threshold (count) + pub health_issues_threshold: u64, +} + +impl Default for AlertThresholds { + fn default() -> Self { + Self { + cpu_usage_threshold: 80.0, + memory_usage_threshold: 85.0, + disk_usage_threshold: 90.0, + system_load_threshold: 5.0, + repair_failure_rate_threshold: 20.0, + health_issues_threshold: 10, + } + } +} + +/// Metrics reporter that generates and outputs metrics reports +pub struct Reporter { + config: ReporterConfig, + reports: Arc>>, + alerts: Arc>>, + last_report_time: Arc>, + report_count: Arc>, + alert_count: Arc>, +} + +impl Reporter { + /// Create a new metrics reporter + pub async fn new(config: ReporterConfig) -> Result { + Ok(Self { + config, + reports: Arc::new(RwLock::new(Vec::new())), + alerts: Arc::new(RwLock::new(Vec::new())), + last_report_time: Arc::new(RwLock::new(SystemTime::now())), + report_count: Arc::new(RwLock::new(0)), + alert_count: Arc::new(RwLock::new(0)), + }) + } + + /// Get the configuration + pub fn config(&self) -> &ReporterConfig { + &self.config + } + + /// Generate a metrics report + pub async fn generate_report(&self, metrics: &SystemMetrics) -> Result { + let start_time = SystemTime::now(); + + let report = MetricsReport { + timestamp: start_time, + metrics: metrics.clone(), + alerts: self.check_alerts(metrics).await?, + summary: self.generate_summary(metrics).await?, + format: self.config.default_format, + }; + + // Store report + { + let mut reports = self.reports.write().await; + reports.push(report.clone()); + + // Trim old reports if we exceed the limit + if reports.len() > self.config.max_reports_in_memory { + let excess = reports.len() - self.config.max_reports_in_memory; + reports.drain(0..excess); + } + } + + // Update statistics + { + let mut last_time = self.last_report_time.write().await; + *last_time = start_time; + + let mut count = self.report_count.write().await; + *count += 1; + } + + info!("Generated metrics report #{}", *self.report_count.read().await); + Ok(report) + } + + /// Generate a comprehensive report from aggregated metrics + pub async fn generate_comprehensive_report(&self, aggregated: &AggregatedMetrics) -> Result { + let start_time = SystemTime::now(); + + let report = ComprehensiveReport { + timestamp: start_time, + query: aggregated.query.clone(), + data_points: aggregated.data_points.len(), + summary: aggregated.summary.clone(), + alerts: self.check_aggregated_alerts(aggregated).await?, + trends: self.analyze_trends(aggregated).await?, + recommendations: self.generate_recommendations(aggregated).await?, + }; + + info!("Generated comprehensive report with {} data points", report.data_points); + Ok(report) + } + + /// Output a report in the specified format + pub async fn output_report(&self, report: &MetricsReport, format: ReportFormat) -> Result<()> { + match format { + ReportFormat::Console => self.output_to_console(report).await?, + ReportFormat::File => self.output_to_file(report).await?, + ReportFormat::Http => self.output_to_http(report).await?, + ReportFormat::Prometheus => self.output_prometheus(report).await?, + ReportFormat::Json => self.output_json(report).await?, + ReportFormat::Csv => self.output_csv(report).await?, + } + + Ok(()) + } + + /// Check for alerts based on metrics + async fn check_alerts(&self, metrics: &SystemMetrics) -> Result> { + let mut alerts = Vec::new(); + + // Check CPU usage + if metrics.cpu_usage > self.config.alert_thresholds.cpu_usage_threshold { + alerts.push(Alert { + timestamp: SystemTime::now(), + severity: AlertSeverity::Warning, + category: AlertCategory::System, + message: format!("High CPU usage: {:.1}%", metrics.cpu_usage), + metric_value: metrics.cpu_usage, + threshold: self.config.alert_thresholds.cpu_usage_threshold, + }); + } + + // Check memory usage + if metrics.memory_usage > self.config.alert_thresholds.memory_usage_threshold { + alerts.push(Alert { + timestamp: SystemTime::now(), + severity: AlertSeverity::Warning, + category: AlertCategory::System, + message: format!("High memory usage: {:.1}%", metrics.memory_usage), + metric_value: metrics.memory_usage, + threshold: self.config.alert_thresholds.memory_usage_threshold, + }); + } + + // Check disk usage + if metrics.disk_usage > self.config.alert_thresholds.disk_usage_threshold { + alerts.push(Alert { + timestamp: SystemTime::now(), + severity: AlertSeverity::Critical, + category: AlertCategory::System, + message: format!("High disk usage: {:.1}%", metrics.disk_usage), + metric_value: metrics.disk_usage, + threshold: self.config.alert_thresholds.disk_usage_threshold, + }); + } + + // Check system load + if metrics.system_load > self.config.alert_thresholds.system_load_threshold { + alerts.push(Alert { + timestamp: SystemTime::now(), + severity: AlertSeverity::Warning, + category: AlertCategory::System, + message: format!("High system load: {:.2}", metrics.system_load), + metric_value: metrics.system_load, + threshold: self.config.alert_thresholds.system_load_threshold, + }); + } + + // Check repair failure rate + if metrics.heal_metrics.total_repairs > 0 { + let failure_rate = (metrics.heal_metrics.failed_repairs as f64 / metrics.heal_metrics.total_repairs as f64) * 100.0; + if failure_rate > self.config.alert_thresholds.repair_failure_rate_threshold { + alerts.push(Alert { + timestamp: SystemTime::now(), + severity: AlertSeverity::Critical, + category: AlertCategory::Heal, + message: format!("High repair failure rate: {:.1}%", failure_rate), + metric_value: failure_rate, + threshold: self.config.alert_thresholds.repair_failure_rate_threshold, + }); + } + } + + // Check health issues + let total_health_issues: u64 = metrics.health_issues.values().sum(); + if total_health_issues > self.config.alert_thresholds.health_issues_threshold { + alerts.push(Alert { + timestamp: SystemTime::now(), + severity: AlertSeverity::Warning, + category: AlertCategory::Health, + message: format!("High number of health issues: {}", total_health_issues), + metric_value: total_health_issues as f64, + threshold: self.config.alert_thresholds.health_issues_threshold as f64, + }); + } + + // Store alerts + if !alerts.is_empty() { + let mut alert_store = self.alerts.write().await; + alert_store.extend(alerts.clone()); + + let mut count = self.alert_count.write().await; + *count += alerts.len() as u64; + } + + Ok(alerts) + } + + /// Check for alerts based on aggregated metrics + async fn check_aggregated_alerts(&self, aggregated: &AggregatedMetrics) -> Result> { + let mut alerts = Vec::new(); + + // Check summary statistics + if aggregated.summary.avg_cpu_usage > self.config.alert_thresholds.cpu_usage_threshold { + alerts.push(Alert { + timestamp: SystemTime::now(), + severity: AlertSeverity::Warning, + category: AlertCategory::System, + message: format!("High average CPU usage: {:.1}%", aggregated.summary.avg_cpu_usage), + metric_value: aggregated.summary.avg_cpu_usage, + threshold: self.config.alert_thresholds.cpu_usage_threshold, + }); + } + + if aggregated.summary.repair_success_rate < (100.0 - self.config.alert_thresholds.repair_failure_rate_threshold) { + alerts.push(Alert { + timestamp: SystemTime::now(), + severity: AlertSeverity::Critical, + category: AlertCategory::Heal, + message: format!("Low repair success rate: {:.1}%", aggregated.summary.repair_success_rate * 100.0), + metric_value: aggregated.summary.repair_success_rate * 100.0, + threshold: 100.0 - self.config.alert_thresholds.repair_failure_rate_threshold, + }); + } + + Ok(alerts) + } + + /// Generate summary for metrics + async fn generate_summary(&self, metrics: &SystemMetrics) -> Result { + Ok(ReportSummary { + system_health: self.calculate_system_health(metrics), + performance_score: self.calculate_performance_score(metrics), + resource_utilization: self.calculate_resource_utilization(metrics), + operational_status: self.determine_operational_status(metrics), + key_metrics: self.extract_key_metrics(metrics), + }) + } + + /// Analyze trends in aggregated data + async fn analyze_trends(&self, aggregated: &AggregatedMetrics) -> Result> { + let mut trends = Vec::new(); + + if aggregated.data_points.len() < 2 { + return Ok(trends); + } + + // Analyze CPU usage trend + let cpu_values: Vec = aggregated + .data_points + .iter() + .filter_map(|p| p.system.as_ref().map(|s| s.cpu_usage)) + .collect(); + + if cpu_values.len() >= 2 { + let trend = self.calculate_trend(&cpu_values, "CPU Usage"); + trends.push(trend); + } + + // Analyze memory usage trend + let memory_values: Vec = aggregated + .data_points + .iter() + .filter_map(|p| p.system.as_ref().map(|s| s.memory_usage)) + .collect(); + + if memory_values.len() >= 2 { + let trend = self.calculate_trend(&memory_values, "Memory Usage"); + trends.push(trend); + } + + Ok(trends) + } + + /// Generate recommendations based on metrics + async fn generate_recommendations(&self, aggregated: &AggregatedMetrics) -> Result> { + let mut recommendations = Vec::new(); + + // Check for high resource usage + if aggregated.summary.avg_cpu_usage > 70.0 { + recommendations.push(Recommendation { + priority: RecommendationPriority::High, + category: RecommendationCategory::Performance, + title: "High CPU Usage".to_string(), + description: "Consider scaling up CPU resources or optimizing workload distribution".to_string(), + action: "Monitor CPU usage patterns and consider resource allocation adjustments".to_string(), + }); + } + + if aggregated.summary.avg_memory_usage > 80.0 { + recommendations.push(Recommendation { + priority: RecommendationPriority::High, + category: RecommendationCategory::Performance, + title: "High Memory Usage".to_string(), + description: "Memory usage is approaching critical levels".to_string(), + action: "Consider increasing memory allocation or optimizing memory usage".to_string(), + }); + } + + // Check for repair issues + if aggregated.summary.repair_success_rate < 0.8 { + recommendations.push(Recommendation { + priority: RecommendationPriority::Critical, + category: RecommendationCategory::Reliability, + title: "Low Repair Success Rate".to_string(), + description: "Data repair operations are failing frequently".to_string(), + action: "Investigate repair failures and check system health".to_string(), + }); + } + + Ok(recommendations) + } + + /// Calculate trend for a series of values + fn calculate_trend(&self, values: &[f64], metric_name: &str) -> TrendAnalysis { + if values.len() < 2 { + return TrendAnalysis { + metric_name: metric_name.to_string(), + trend_direction: TrendDirection::Stable, + change_rate: 0.0, + confidence: 0.0, + }; + } + + let first = values[0]; + let last = values[values.len() - 1]; + let change_rate = ((last - first) / first) * 100.0; + + let trend_direction = if change_rate > 5.0 { + TrendDirection::Increasing + } else if change_rate < -5.0 { + TrendDirection::Decreasing + } else { + TrendDirection::Stable + }; + + // Simple confidence calculation based on data points + let confidence = (values.len() as f64 / 10.0).min(1.0); + + TrendAnalysis { + metric_name: metric_name.to_string(), + trend_direction, + change_rate, + confidence, + } + } + + /// Calculate system health score + fn calculate_system_health(&self, metrics: &SystemMetrics) -> f64 { + let mut score = 100.0; + + // Deduct points for high resource usage + if metrics.cpu_usage > 80.0 { + score -= (metrics.cpu_usage - 80.0) * 0.5; + } + if metrics.memory_usage > 85.0 { + score -= (metrics.memory_usage - 85.0) * 0.5; + } + if metrics.disk_usage > 90.0 { + score -= (metrics.disk_usage - 90.0) * 1.0; + } + + // Deduct points for health issues + let total_health_issues: u64 = metrics.health_issues.values().sum(); + score -= total_health_issues as f64 * 5.0; + + // Deduct points for repair failures + if metrics.heal_metrics.total_repairs > 0 { + let failure_rate = metrics.heal_metrics.failed_repairs as f64 / metrics.heal_metrics.total_repairs as f64; + score -= failure_rate * 20.0; + } + + score.max(0.0) + } + + /// Calculate performance score + fn calculate_performance_score(&self, metrics: &SystemMetrics) -> f64 { + let mut score = 100.0; + + // Base score on resource efficiency + score -= metrics.cpu_usage * 0.3; + score -= metrics.memory_usage * 0.3; + score -= metrics.disk_usage * 0.2; + score -= metrics.system_load * 10.0; + + score.max(0.0) + } + + /// Calculate resource utilization + fn calculate_resource_utilization(&self, metrics: &SystemMetrics) -> f64 { + (metrics.cpu_usage + metrics.memory_usage + metrics.disk_usage) / 3.0 + } + + /// Determine operational status + fn determine_operational_status(&self, metrics: &SystemMetrics) -> OperationalStatus { + let health_score = self.calculate_system_health(metrics); + + if health_score >= 90.0 { + OperationalStatus::Excellent + } else if health_score >= 75.0 { + OperationalStatus::Good + } else if health_score >= 50.0 { + OperationalStatus::Fair + } else { + OperationalStatus::Poor + } + } + + /// Extract key metrics + fn extract_key_metrics(&self, metrics: &SystemMetrics) -> HashMap { + let mut key_metrics = HashMap::new(); + key_metrics.insert("cpu_usage".to_string(), metrics.cpu_usage); + key_metrics.insert("memory_usage".to_string(), metrics.memory_usage); + key_metrics.insert("disk_usage".to_string(), metrics.disk_usage); + key_metrics.insert("system_load".to_string(), metrics.system_load); + key_metrics.insert("active_operations".to_string(), metrics.active_operations as f64); + key_metrics.insert("objects_scanned".to_string(), metrics.scan_metrics.objects_scanned as f64); + key_metrics.insert("total_repairs".to_string(), metrics.heal_metrics.total_repairs as f64); + key_metrics.insert("successful_repairs".to_string(), metrics.heal_metrics.successful_repairs as f64); + + key_metrics + } + + /// Output methods (simulated) + async fn output_to_console(&self, report: &MetricsReport) -> Result<()> { + if self.config.enabled { + info!("=== Metrics Report ==="); + info!("Timestamp: {:?}", report.timestamp); + info!("System Health: {:.1}%", report.summary.system_health); + info!("Performance Score: {:.1}%", report.summary.performance_score); + info!("Operational Status: {:?}", report.summary.operational_status); + + if !report.alerts.is_empty() { + info!("=== Alerts ==="); + for alert in &report.alerts { + info!("[{}] {}: {}", alert.severity, alert.category, alert.message); + } + } + } + Ok(()) + } + + async fn output_to_file(&self, _report: &MetricsReport) -> Result<()> { + if self.config.enabled { + // In a real implementation, this would write to a file + debug!("Would write report to file: {}", self.config.output_directory.as_ref().unwrap_or(&String::new())); + } + Ok(()) + } + + async fn output_to_http(&self, _report: &MetricsReport) -> Result<()> { + if self.config.enable_http_reporting { + // In a real implementation, this would serve via HTTP + debug!("Would serve report via HTTP on endpoint: {}", self.config.http_endpoint.as_ref().unwrap_or(&String::new())); + } + Ok(()) + } + + async fn output_prometheus(&self, _report: &MetricsReport) -> Result<()> { + if self.config.enabled { + // In a real implementation, this would output Prometheus format + debug!("Would output Prometheus format"); + } + Ok(()) + } + + async fn output_json(&self, _report: &MetricsReport) -> Result<()> { + if self.config.enabled { + // In a real implementation, this would output JSON format + debug!("Would output JSON format"); + } + Ok(()) + } + + async fn output_csv(&self, _report: &MetricsReport) -> Result<()> { + if self.config.enabled { + // In a real implementation, this would output CSV format + debug!("Would output CSV format"); + } + Ok(()) + } + + /// Get reporting statistics + pub async fn get_statistics(&self) -> ReporterStatistics { + let report_count = *self.report_count.read().await; + let alert_count = *self.alert_count.read().await; + let last_report_time = *self.last_report_time.read().await; + let reports_count = self.reports.read().await.len(); + let alerts_count = self.alerts.read().await.len(); + + ReporterStatistics { + total_reports: report_count, + total_alerts: alert_count, + reports_in_memory: reports_count, + alerts_in_memory: alerts_count, + last_report_time, + config: self.config.clone(), + } + } + + /// Get recent alerts + pub async fn get_recent_alerts(&self, hours: u64) -> Result> { + let cutoff_time = SystemTime::now() - Duration::from_secs(hours * 3600); + let alerts = self.alerts.read().await; + + let recent_alerts: Vec = alerts + .iter() + .filter(|alert| alert.timestamp >= cutoff_time) + .cloned() + .collect(); + + Ok(recent_alerts) + } + + /// Clear old alerts + pub async fn clear_old_alerts(&self, hours: u64) -> Result<()> { + let cutoff_time = SystemTime::now() - Duration::from_secs(hours * 3600); + let mut alerts = self.alerts.write().await; + alerts.retain(|alert| alert.timestamp >= cutoff_time); + + info!("Cleared alerts older than {} hours", hours); + Ok(()) + } +} + +/// Metrics report +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MetricsReport { + pub timestamp: SystemTime, + pub metrics: SystemMetrics, + pub alerts: Vec, + pub summary: ReportSummary, + pub format: ReportFormat, +} + +/// Comprehensive report +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ComprehensiveReport { + pub timestamp: SystemTime, + pub query: MetricsQuery, + pub data_points: usize, + pub summary: MetricsSummary, + pub alerts: Vec, + pub trends: Vec, + pub recommendations: Vec, +} + +/// Report summary +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReportSummary { + pub system_health: f64, + pub performance_score: f64, + pub resource_utilization: f64, + pub operational_status: OperationalStatus, + pub key_metrics: HashMap, +} + +/// Alert +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Alert { + pub timestamp: SystemTime, + pub severity: AlertSeverity, + pub category: AlertCategory, + pub message: String, + pub metric_value: f64, + pub threshold: f64, +} + +/// Alert severity +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum AlertSeverity { + Info, + Warning, + Critical, +} + +impl fmt::Display for AlertSeverity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AlertSeverity::Info => write!(f, "INFO"), + AlertSeverity::Warning => write!(f, "WARNING"), + AlertSeverity::Critical => write!(f, "CRITICAL"), + } + } +} + +/// Alert category +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum AlertCategory { + System, + Performance, + Health, + Heal, + Security, +} + +impl fmt::Display for AlertCategory { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AlertCategory::System => write!(f, "SYSTEM"), + AlertCategory::Performance => write!(f, "PERFORMANCE"), + AlertCategory::Health => write!(f, "HEALTH"), + AlertCategory::Heal => write!(f, "HEAL"), + AlertCategory::Security => write!(f, "SECURITY"), + } + } +} + +/// Report format +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ReportFormat { + Console, + File, + Http, + Prometheus, + Json, + Csv, +} + +/// Operational status +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum OperationalStatus { + Excellent, + Good, + Fair, + Poor, +} + +/// Trend analysis +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TrendAnalysis { + pub metric_name: String, + pub trend_direction: TrendDirection, + pub change_rate: f64, + pub confidence: f64, +} + +/// Trend direction +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum TrendDirection { + Increasing, + Decreasing, + Stable, +} + +/// Recommendation +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Recommendation { + pub priority: RecommendationPriority, + pub category: RecommendationCategory, + pub title: String, + pub description: String, + pub action: String, +} + +/// Recommendation priority +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum RecommendationPriority { + Low, + Medium, + High, + Critical, +} + +/// Recommendation category +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum RecommendationCategory { + Performance, + Reliability, + Security, + Maintenance, +} + +/// Reporter statistics +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReporterStatistics { + pub total_reports: u64, + pub total_alerts: u64, + pub reports_in_memory: usize, + pub alerts_in_memory: usize, + pub last_report_time: SystemTime, + pub config: ReporterConfig, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_reporter_creation() { + let config = ReporterConfig::default(); + let reporter = Reporter::new(config).await.unwrap(); + + assert_eq!(reporter.config().report_interval, Duration::from_secs(60)); + assert!(reporter.config().enabled); + } + + #[tokio::test] + async fn test_report_generation() { + let config = ReporterConfig::default(); + let reporter = Reporter::new(config).await.unwrap(); + + let metrics = SystemMetrics::default(); + let report = reporter.generate_report(&metrics).await.unwrap(); + + assert_eq!(report.metrics.cpu_usage, 0.0); + assert_eq!(report.alerts.len(), 0); + } + + #[tokio::test] + async fn test_alert_generation() { + let config = ReporterConfig { + alert_thresholds: AlertThresholds { + cpu_usage_threshold: 50.0, + ..Default::default() + }, + ..Default::default() + }; + let reporter = Reporter::new(config).await.unwrap(); + + let mut metrics = SystemMetrics::default(); + metrics.cpu_usage = 75.0; // Above threshold + + let report = reporter.generate_report(&metrics).await.unwrap(); + assert!(!report.alerts.is_empty()); + assert_eq!(report.alerts[0].severity, AlertSeverity::Warning); + } + + #[tokio::test] + async fn test_comprehensive_report() { + let config = ReporterConfig::default(); + let reporter = Reporter::new(config).await.unwrap(); + + let aggregated = AggregatedMetrics { + query: MetricsQuery { + start_time: SystemTime::now(), + end_time: SystemTime::now() + Duration::from_secs(3600), + interval: Duration::from_secs(60), + metrics: vec![], + severity_filter: None, + limit: None, + }, + data_points: vec![], + summary: MetricsSummary::default(), + }; + + let report = reporter.generate_comprehensive_report(&aggregated).await.unwrap(); + assert_eq!(report.data_points, 0); + assert!(report.recommendations.is_empty()); + } + + #[tokio::test] + async fn test_reporter_statistics() { + let config = ReporterConfig::default(); + let reporter = Reporter::new(config).await.unwrap(); + + let stats = reporter.get_statistics().await; + assert_eq!(stats.total_reports, 0); + assert_eq!(stats.total_alerts, 0); + } + + #[tokio::test] + async fn test_alert_clearing() { + let config = ReporterConfig::default(); + let reporter = Reporter::new(config).await.unwrap(); + + // Generate some alerts + let mut metrics = SystemMetrics::default(); + metrics.cpu_usage = 90.0; // Above threshold + + let _report = reporter.generate_report(&metrics).await.unwrap(); + + // Clear old alerts + reporter.clear_old_alerts(1).await.unwrap(); + + let stats = reporter.get_statistics().await; + assert_eq!(stats.alerts_in_memory, 0); + } +} \ No newline at end of file diff --git a/crates/ahm/src/metrics/storage.rs b/crates/ahm/src/metrics/storage.rs new file mode 100644 index 000000000..66520ff7c --- /dev/null +++ b/crates/ahm/src/metrics/storage.rs @@ -0,0 +1,573 @@ +// Copyright 2024 RustFS Team + +use std::{ + collections::HashMap, + sync::Arc, + path::PathBuf, + time::{Duration, Instant, SystemTime}, +}; + +use tokio::sync::RwLock; +use tracing::{debug, error, info, warn}; + +use crate::error::Result; + +use super::{ + AggregatedMetrics, MetricsDataPoint, MetricsQuery, MetricsSummary, SystemMetrics, +}; + +/// Configuration for metrics storage +#[derive(Debug, Clone)] +pub struct StorageConfig { + /// Storage directory path + pub storage_path: PathBuf, + /// Maximum file size for metrics files + pub max_file_size: u64, + /// Compression enabled + pub compression_enabled: bool, + /// Retention period for metrics data + pub retention_period: Duration, + /// Batch size for writes + pub batch_size: usize, + /// Flush interval + pub flush_interval: Duration, + /// Whether to enable data validation + pub enable_validation: bool, + /// Whether to enable data encryption + pub enable_encryption: bool, + /// Encryption key (if enabled) + pub encryption_key: Option, +} + +impl Default for StorageConfig { + fn default() -> Self { + Self { + storage_path: PathBuf::from("/tmp/rustfs/metrics"), + max_file_size: 100 * 1024 * 1024, // 100 MB + compression_enabled: true, + retention_period: Duration::from_secs(86400 * 30), // 30 days + batch_size: 1000, + flush_interval: Duration::from_secs(60), // 1 minute + enable_validation: true, + enable_encryption: false, + encryption_key: None, + } + } +} + +/// Metrics storage that persists metrics data to disk +pub struct Storage { + config: StorageConfig, + metrics_buffer: Arc>>, + aggregated_buffer: Arc>>, + file_handles: Arc>>, + last_flush_time: Arc>, + total_writes: Arc>, + total_reads: Arc>, +} + +impl Storage { + /// Create a new metrics storage + pub async fn new(config: StorageConfig) -> Result { + // Create storage directory if it doesn't exist + tokio::fs::create_dir_all(&config.storage_path).await?; + + Ok(Self { + config, + metrics_buffer: Arc::new(RwLock::new(Vec::new())), + aggregated_buffer: Arc::new(RwLock::new(Vec::new())), + file_handles: Arc::new(RwLock::new(HashMap::new())), + last_flush_time: Arc::new(RwLock::new(SystemTime::now())), + total_writes: Arc::new(RwLock::new(0)), + total_reads: Arc::new(RwLock::new(0)), + }) + } + + /// Get the configuration + pub fn config(&self) -> &StorageConfig { + &self.config + } + + /// Store system metrics + pub async fn store_metrics(&self, metrics: SystemMetrics) -> Result<()> { + let mut buffer = self.metrics_buffer.write().await; + buffer.push(metrics); + + // Flush if buffer is full + if buffer.len() >= self.config.batch_size { + self.flush_metrics_buffer().await?; + } + + // Update write count + { + let mut writes = self.total_writes.write().await; + *writes += 1; + } + + Ok(()) + } + + /// Store aggregated metrics + pub async fn store_aggregated_metrics(&self, aggregated: AggregatedMetrics) -> Result<()> { + let mut buffer = self.aggregated_buffer.write().await; + buffer.push(aggregated); + + // Flush if buffer is full + if buffer.len() >= self.config.batch_size { + self.flush_aggregated_buffer().await?; + } + + Ok(()) + } + + /// Retrieve metrics for a time range + pub async fn retrieve_metrics(&self, query: &MetricsQuery) -> Result> { + let start_time = Instant::now(); + + // Update read count + { + let mut reads = self.total_reads.write().await; + *reads += 1; + } + + // In a real implementation, this would read from disk files + // For now, we'll return data from the buffer + let buffer = self.metrics_buffer.read().await; + let filtered_metrics: Vec = buffer + .iter() + .filter(|m| m.timestamp >= query.start_time && m.timestamp <= query.end_time) + .cloned() + .collect(); + + let retrieval_time = start_time.elapsed(); + debug!("Metrics retrieval completed in {:?}", retrieval_time); + + Ok(filtered_metrics) + } + + /// Retrieve aggregated metrics + pub async fn retrieve_aggregated_metrics(&self, query: &MetricsQuery) -> Result> { + let buffer = self.aggregated_buffer.read().await; + let filtered_metrics: Vec = buffer + .iter() + .filter(|m| { + if let Some(first_point) = m.data_points.first() { + first_point.timestamp >= query.start_time + } else { + false + } + }) + .filter(|m| { + if let Some(last_point) = m.data_points.last() { + last_point.timestamp <= query.end_time + } else { + false + } + }) + .cloned() + .collect(); + + Ok(filtered_metrics) + } + + /// Flush metrics buffer to disk + async fn flush_metrics_buffer(&self) -> Result<()> { + let mut buffer = self.metrics_buffer.write().await; + if buffer.is_empty() { + return Ok(()); + } + + let metrics_to_write = buffer.drain(..).collect::>(); + drop(buffer); // Release lock + + // Write to file + self.write_metrics_to_file(&metrics_to_write).await?; + + // Update flush time + { + let mut last_flush = self.last_flush_time.write().await; + *last_flush = SystemTime::now(); + } + + info!("Flushed {} metrics to disk", metrics_to_write.len()); + Ok(()) + } + + /// Flush aggregated buffer to disk + async fn flush_aggregated_buffer(&self) -> Result<()> { + let mut buffer = self.aggregated_buffer.write().await; + if buffer.is_empty() { + return Ok(()); + } + + let aggregated_to_write = buffer.drain(..).collect::>(); + drop(buffer); // Release lock + + // Write to file + self.write_aggregated_to_file(&aggregated_to_write).await?; + + info!("Flushed {} aggregated metrics to disk", aggregated_to_write.len()); + Ok(()) + } + + /// Write metrics to file + async fn write_metrics_to_file(&self, metrics: &[SystemMetrics]) -> Result<()> { + let filename = format!("metrics_{}.json", SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs()); + let filepath = self.config.storage_path.join(filename); + + // In a real implementation, this would write to a file + // For now, we'll just simulate the write + debug!("Would write {} metrics to {}", metrics.len(), filepath.display()); + + Ok(()) + } + + /// Write aggregated metrics to file + async fn write_aggregated_to_file(&self, aggregated: &[AggregatedMetrics]) -> Result<()> { + let filename = format!("aggregated_{}.json", SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs()); + let filepath = self.config.storage_path.join(filename); + + // In a real implementation, this would write to a file + // For now, we'll just simulate the write + debug!("Would write {} aggregated metrics to {}", aggregated.len(), filepath.display()); + + Ok(()) + } + + /// Force flush all buffers + pub async fn force_flush(&self) -> Result<()> { + self.flush_metrics_buffer().await?; + self.flush_aggregated_buffer().await?; + + info!("Force flush completed"); + Ok(()) + } + + /// Clean up old data based on retention policy + pub async fn cleanup_old_data(&self) -> Result<()> { + let cutoff_time = SystemTime::now() - self.config.retention_period; + + // Clean up metrics buffer + { + let mut buffer = self.metrics_buffer.write().await; + buffer.retain(|m| m.timestamp >= cutoff_time); + } + + // Clean up aggregated buffer + { + let mut buffer = self.aggregated_buffer.write().await; + buffer.retain(|m| { + if let Some(first_point) = m.data_points.first() { + first_point.timestamp >= cutoff_time + } else { + false + } + }); + } + + // In a real implementation, this would also clean up old files + info!("Cleanup completed, removed data older than {:?}", cutoff_time); + Ok(()) + } + + /// Get storage statistics + pub async fn get_statistics(&self) -> StorageStatistics { + let metrics_count = self.metrics_buffer.read().await.len(); + let aggregated_count = self.aggregated_buffer.read().await.len(); + let total_writes = *self.total_writes.read().await; + let total_reads = *self.total_reads.read().await; + let last_flush_time = *self.last_flush_time.read().await; + + StorageStatistics { + metrics_in_buffer: metrics_count, + aggregated_in_buffer: aggregated_count, + total_writes, + total_reads, + last_flush_time, + config: self.config.clone(), + } + } + + /// Validate stored data integrity + pub async fn validate_data_integrity(&self) -> Result { + if !self.config.enable_validation { + return Ok(DataIntegrityReport { + is_valid: true, + total_records: 0, + corrupted_records: 0, + validation_time: Duration::ZERO, + errors: Vec::new(), + }); + } + + let start_time = Instant::now(); + let mut errors = Vec::new(); + let mut corrupted_records = 0; + + // Validate metrics buffer + { + let buffer = self.metrics_buffer.read().await; + for (i, metric) in buffer.iter().enumerate() { + if !self.validate_metric(metric) { + errors.push(format!("Invalid metric at index {}: {:?}", i, metric)); + corrupted_records += 1; + } + } + } + + // Validate aggregated buffer + { + let buffer = self.aggregated_buffer.read().await; + for (i, aggregated) in buffer.iter().enumerate() { + if !self.validate_aggregated(aggregated) { + errors.push(format!("Invalid aggregated metrics at index {}: {:?}", i, aggregated)); + corrupted_records += 1; + } + } + } + + let validation_time = start_time.elapsed(); + let total_records = { + let metrics_count = self.metrics_buffer.read().await.len(); + let aggregated_count = self.aggregated_buffer.read().await.len(); + metrics_count + aggregated_count + }; + + let is_valid = corrupted_records == 0; + + Ok(DataIntegrityReport { + is_valid, + total_records, + corrupted_records, + validation_time, + errors, + }) + } + + /// Validate a single metric + fn validate_metric(&self, metric: &SystemMetrics) -> bool { + // Basic validation checks + metric.cpu_usage >= 0.0 && metric.cpu_usage <= 100.0 + && metric.memory_usage >= 0.0 && metric.memory_usage <= 100.0 + && metric.disk_usage >= 0.0 && metric.disk_usage <= 100.0 + && metric.system_load >= 0.0 + } + + /// Validate aggregated metrics + fn validate_aggregated(&self, aggregated: &AggregatedMetrics) -> bool { + // Basic validation checks + !aggregated.data_points.is_empty() + && aggregated.query.start_time <= aggregated.query.end_time + && aggregated.summary.total_points > 0 + } + + /// Backup metrics data + pub async fn backup_data(&self, backup_path: &PathBuf) -> Result { + let start_time = Instant::now(); + + // Create backup directory + tokio::fs::create_dir_all(backup_path).await?; + + // In a real implementation, this would copy files to backup location + // For now, we'll just simulate the backup + let metrics_count = self.metrics_buffer.read().await.len(); + let aggregated_count = self.aggregated_buffer.read().await.len(); + + let backup_time = start_time.elapsed(); + + Ok(BackupReport { + backup_path: backup_path.clone(), + metrics_backed_up: metrics_count, + aggregated_backed_up: aggregated_count, + backup_time, + success: true, + }) + } + + /// Restore metrics data from backup + pub async fn restore_data(&self, backup_path: &PathBuf) -> Result { + let start_time = Instant::now(); + + // In a real implementation, this would restore from backup files + // For now, we'll just simulate the restore + debug!("Would restore data from {}", backup_path.display()); + + let restore_time = start_time.elapsed(); + + Ok(RestoreReport { + backup_path: backup_path.clone(), + metrics_restored: 0, + aggregated_restored: 0, + restore_time, + success: true, + }) + } +} + +/// Storage statistics +#[derive(Debug, Clone)] +pub struct StorageStatistics { + pub metrics_in_buffer: usize, + pub aggregated_in_buffer: usize, + pub total_writes: u64, + pub total_reads: u64, + pub last_flush_time: SystemTime, + pub config: StorageConfig, +} + +/// Data integrity validation report +#[derive(Debug, Clone)] +pub struct DataIntegrityReport { + pub is_valid: bool, + pub total_records: usize, + pub corrupted_records: usize, + pub validation_time: Duration, + pub errors: Vec, +} + +/// Backup report +#[derive(Debug, Clone)] +pub struct BackupReport { + pub backup_path: PathBuf, + pub metrics_backed_up: usize, + pub aggregated_backed_up: usize, + pub backup_time: Duration, + pub success: bool, +} + +/// Restore report +#[derive(Debug, Clone)] +pub struct RestoreReport { + pub backup_path: PathBuf, + pub metrics_restored: usize, + pub aggregated_restored: usize, + pub restore_time: Duration, + pub success: bool, +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::Instant; + + #[tokio::test] + async fn test_storage_creation() { + let config = StorageConfig::default(); + let storage = Storage::new(config).await.unwrap(); + + assert_eq!(storage.config().batch_size, 1000); + assert!(storage.config().compression_enabled); + } + + #[tokio::test] + async fn test_metrics_storage() { + let config = StorageConfig::default(); + let storage = Storage::new(config).await.unwrap(); + + let metrics = SystemMetrics::default(); + storage.store_metrics(metrics).await.unwrap(); + + let stats = storage.get_statistics().await; + assert_eq!(stats.metrics_in_buffer, 1); + assert_eq!(stats.total_writes, 1); + } + + #[tokio::test] + async fn test_aggregated_storage() { + let config = StorageConfig::default(); + let storage = Storage::new(config).await.unwrap(); + + let aggregated = AggregatedMetrics { + query: MetricsQuery { + start_time: SystemTime::now(), + end_time: SystemTime::now() + Duration::from_secs(3600), + interval: Duration::from_secs(60), + metrics: vec![], + severity_filter: None, + limit: None, + }, + data_points: vec![], + summary: MetricsSummary::default(), + }; + + storage.store_aggregated_metrics(aggregated).await.unwrap(); + + let stats = storage.get_statistics().await; + assert_eq!(stats.aggregated_in_buffer, 1); + } + + #[tokio::test] + async fn test_metrics_retrieval() { + let config = StorageConfig::default(); + let storage = Storage::new(config).await.unwrap(); + + // Store some metrics + for i in 0..5 { + let mut metrics = SystemMetrics::default(); + metrics.timestamp = SystemTime::now() + Duration::from_secs(i * 60); + storage.store_metrics(metrics).await.unwrap(); + } + + let query = MetricsQuery { + start_time: SystemTime::now(), + end_time: SystemTime::now() + Duration::from_secs(300), + interval: Duration::from_secs(60), + metrics: vec![], + severity_filter: None, + limit: None, + }; + + let retrieved = storage.retrieve_metrics(&query).await.unwrap(); + assert_eq!(retrieved.len(), 5); + } + + #[tokio::test] + async fn test_data_integrity_validation() { + let config = StorageConfig { + enable_validation: true, + ..Default::default() + }; + let storage = Storage::new(config).await.unwrap(); + + let report = storage.validate_data_integrity().await.unwrap(); + assert!(report.is_valid); + assert_eq!(report.corrupted_records, 0); + } + + #[tokio::test] + async fn test_force_flush() { + let config = StorageConfig::default(); + let storage = Storage::new(config).await.unwrap(); + + // Add some data + storage.store_metrics(SystemMetrics::default()).await.unwrap(); + + // Force flush + storage.force_flush().await.unwrap(); + + let stats = storage.get_statistics().await; + assert_eq!(stats.metrics_in_buffer, 0); + } + + #[tokio::test] + async fn test_cleanup_old_data() { + let config = StorageConfig::default(); + let storage = Storage::new(config).await.unwrap(); + + // Add some old data + let mut old_metrics = SystemMetrics::default(); + old_metrics.timestamp = SystemTime::now() - Duration::from_secs(86400 * 31); // 31 days old + storage.store_metrics(old_metrics).await.unwrap(); + + // Add some recent data + let mut recent_metrics = SystemMetrics::default(); + recent_metrics.timestamp = SystemTime::now(); + storage.store_metrics(recent_metrics).await.unwrap(); + + // Cleanup + storage.cleanup_old_data().await.unwrap(); + + let stats = storage.get_statistics().await; + assert_eq!(stats.metrics_in_buffer, 1); // Only recent data should remain + } +} \ No newline at end of file diff --git a/crates/ahm/src/policy/heal_policy.rs b/crates/ahm/src/policy/heal_policy.rs new file mode 100644 index 000000000..8342f089b --- /dev/null +++ b/crates/ahm/src/policy/heal_policy.rs @@ -0,0 +1,508 @@ +// Copyright 2024 RustFS Team + +use std::time::{Duration, SystemTime}; + +use crate::scanner::{HealthIssue, Severity}; + +use super::{PolicyContext, PolicyResult, ResourceUsage}; + +/// Configuration for heal policies +#[derive(Debug, Clone)] +pub struct HealPolicyConfig { + /// Maximum number of concurrent repairs + pub max_concurrent_repairs: usize, + /// Maximum repair duration per operation + pub max_repair_duration: Duration, + /// Minimum interval between repairs + pub min_repair_interval: Duration, + /// Maximum system load threshold for healing + pub max_system_load: f64, + /// Minimum available disk space percentage for healing + pub min_disk_space: f64, + /// Maximum number of active operations for healing + pub max_active_operations: u64, + /// Whether to enable automatic healing + pub auto_heal_enabled: bool, + /// Priority-based healing configuration + pub priority_config: HealPriorityConfig, + /// Resource-based healing configuration + pub resource_config: HealResourceConfig, + /// Retry configuration + pub retry_config: HealRetryConfig, +} + +/// Priority-based healing configuration +#[derive(Debug, Clone)] +pub struct HealPriorityConfig { + /// Whether to enable priority-based healing + pub enabled: bool, + /// Critical issues heal immediately + pub critical_immediate: bool, + /// High priority issues heal within + pub high_timeout: Duration, + /// Medium priority issues heal within + pub medium_timeout: Duration, + /// Low priority issues heal within + pub low_timeout: Duration, +} + +/// Resource-based healing configuration +#[derive(Debug, Clone)] +pub struct HealResourceConfig { + /// Maximum CPU usage for healing + pub max_cpu_usage: f64, + /// Maximum memory usage for healing + pub max_memory_usage: f64, + /// Maximum disk I/O usage for healing + pub max_disk_io_usage: f64, + /// Maximum network I/O usage for healing + pub max_network_io_usage: f64, + /// Whether to enable resource-based throttling + pub enable_throttling: bool, +} + +/// Retry configuration for healing +#[derive(Debug, Clone)] +pub struct HealRetryConfig { + /// Maximum number of retry attempts + pub max_retry_attempts: u32, + /// Initial backoff delay + pub initial_backoff: Duration, + /// Maximum backoff delay + pub max_backoff: Duration, + /// Backoff multiplier + pub backoff_multiplier: f64, + /// Whether to use exponential backoff + pub exponential_backoff: bool, +} + +impl Default for HealPolicyConfig { + fn default() -> Self { + Self { + max_concurrent_repairs: 4, + max_repair_duration: Duration::from_secs(1800), // 30 minutes + min_repair_interval: Duration::from_secs(60), // 1 minute + max_system_load: 0.7, + min_disk_space: 15.0, // 15% minimum disk space + max_active_operations: 50, + auto_heal_enabled: true, + priority_config: HealPriorityConfig::default(), + resource_config: HealResourceConfig::default(), + retry_config: HealRetryConfig::default(), + } + } +} + +impl Default for HealPriorityConfig { + fn default() -> Self { + Self { + enabled: true, + critical_immediate: true, + high_timeout: Duration::from_secs(300), // 5 minutes + medium_timeout: Duration::from_secs(1800), // 30 minutes + low_timeout: Duration::from_secs(3600), // 1 hour + } + } +} + +impl Default for HealResourceConfig { + fn default() -> Self { + Self { + max_cpu_usage: 80.0, + max_memory_usage: 80.0, + max_disk_io_usage: 70.0, + max_network_io_usage: 70.0, + enable_throttling: true, + } + } +} + +impl Default for HealRetryConfig { + fn default() -> Self { + Self { + max_retry_attempts: 3, + initial_backoff: Duration::from_secs(30), + max_backoff: Duration::from_secs(300), + backoff_multiplier: 2.0, + exponential_backoff: true, + } + } +} + +/// Heal policy engine +pub struct HealPolicyEngine { + config: HealPolicyConfig, + last_repair_time: SystemTime, + repair_count: u64, + active_repairs: u64, +} + +impl HealPolicyEngine { + /// Create a new heal policy engine + pub fn new(config: HealPolicyConfig) -> Self { + Self { + config, + last_repair_time: SystemTime::now(), + repair_count: 0, + active_repairs: 0, + } + } + + /// Get the configuration + pub fn config(&self) -> &HealPolicyConfig { + &self.config + } + + /// Evaluate heal policy + pub async fn evaluate(&self, issue: &HealthIssue, context: &PolicyContext) -> PolicyResult { + let mut reasons = Vec::new(); + let mut allowed = true; + + // Check if auto-heal is enabled + if !self.config.auto_heal_enabled { + allowed = false; + reasons.push("Auto-heal is disabled".to_string()); + } + + // Check system load + if context.system_load > self.config.max_system_load { + allowed = false; + reasons.push(format!( + "System load too high: {:.2} > {:.2}", + context.system_load, self.config.max_system_load + )); + } + + // Check disk space + if context.disk_space_available < self.config.min_disk_space { + allowed = false; + reasons.push(format!( + "Disk space too low: {:.1}% < {:.1}%", + context.disk_space_available, self.config.min_disk_space + )); + } + + // Check active operations + if context.active_operations > self.config.max_active_operations { + allowed = false; + reasons.push(format!( + "Too many active operations: {} > {}", + context.active_operations, self.config.max_active_operations + )); + } + + // Check repair interval + let time_since_last_repair = context.current_time + .duration_since(self.last_repair_time) + .unwrap_or(Duration::ZERO); + + if time_since_last_repair < self.config.min_repair_interval { + allowed = false; + reasons.push(format!( + "Repair interval too short: {:?} < {:?}", + time_since_last_repair, self.config.min_repair_interval + )); + } + + // Check resource usage + if self.config.resource_config.enable_throttling { + if context.resource_usage.cpu_usage > self.config.resource_config.max_cpu_usage { + allowed = false; + reasons.push(format!( + "CPU usage too high: {:.1}% > {:.1}%", + context.resource_usage.cpu_usage, self.config.resource_config.max_cpu_usage + )); + } + + if context.resource_usage.memory_usage > self.config.resource_config.max_memory_usage { + allowed = false; + reasons.push(format!( + "Memory usage too high: {:.1}% > {:.1}%", + context.resource_usage.memory_usage, self.config.resource_config.max_memory_usage + )); + } + + if context.resource_usage.disk_io_usage > self.config.resource_config.max_disk_io_usage { + allowed = false; + reasons.push(format!( + "Disk I/O usage too high: {:.1}% > {:.1}%", + context.resource_usage.disk_io_usage, self.config.resource_config.max_disk_io_usage + )); + } + + if context.resource_usage.network_io_usage > self.config.resource_config.max_network_io_usage { + allowed = false; + reasons.push(format!( + "Network I/O usage too high: {:.1}% > {:.1}%", + context.resource_usage.network_io_usage, self.config.resource_config.max_network_io_usage + )); + } + } + + // Check priority-based policies + if self.config.priority_config.enabled { + match issue.severity { + Severity::Critical => { + if self.config.priority_config.critical_immediate { + // Critical issues should always be allowed unless resource constraints prevent it + if allowed { + reasons.clear(); + reasons.push("Critical issue - immediate repair allowed".to_string()); + } + } + } + Severity::High => { + // Check if we're within the high priority timeout + if time_since_last_repair > self.config.priority_config.high_timeout { + allowed = false; + reasons.push(format!( + "High priority issue timeout exceeded: {:?} > {:?}", + time_since_last_repair, self.config.priority_config.high_timeout + )); + } + } + Severity::Medium => { + // Check if we're within the medium priority timeout + if time_since_last_repair > self.config.priority_config.medium_timeout { + allowed = false; + reasons.push(format!( + "Medium priority issue timeout exceeded: {:?} > {:?}", + time_since_last_repair, self.config.priority_config.medium_timeout + )); + } + } + Severity::Low => { + // Check if we're within the low priority timeout + if time_since_last_repair > self.config.priority_config.low_timeout { + allowed = false; + reasons.push(format!( + "Low priority issue timeout exceeded: {:?} > {:?}", + time_since_last_repair, self.config.priority_config.low_timeout + )); + } + } + } + } + + let reason = if reasons.is_empty() { + "Heal allowed".to_string() + } else { + reasons.join("; ") + }; + + PolicyResult { + allowed, + reason, + metadata: Some(serde_json::json!({ + "repair_count": self.repair_count, + "active_repairs": self.active_repairs, + "time_since_last_repair": time_since_last_repair.as_secs(), + "issue_severity": format!("{:?}", issue.severity), + "issue_type": format!("{:?}", issue.issue_type), + "system_load": context.system_load, + "disk_space_available": context.disk_space_available, + "active_operations": context.active_operations, + })), + evaluated_at: context.current_time, + } + } + + /// Get repair timeout based on priority + pub fn get_repair_timeout(&self, severity: Severity) -> Duration { + if !self.config.priority_config.enabled { + return self.config.max_repair_duration; + } + + match severity { + Severity::Critical => Duration::from_secs(300), // 5 minutes for critical + Severity::High => self.config.priority_config.high_timeout, + Severity::Medium => self.config.priority_config.medium_timeout, + Severity::Low => self.config.priority_config.low_timeout, + } + } + + /// Get retry configuration + pub fn get_retry_config(&self) -> &HealRetryConfig { + &self.config.retry_config + } + + /// Update repair statistics + pub fn record_repair(&mut self) { + self.last_repair_time = SystemTime::now(); + self.repair_count += 1; + } + + /// Increment active repairs + pub fn increment_active_repairs(&mut self) { + self.active_repairs += 1; + } + + /// Decrement active repairs + pub fn decrement_active_repairs(&mut self) { + if self.active_repairs > 0 { + self.active_repairs -= 1; + } + } + + /// Get heal statistics + pub fn get_statistics(&self) -> HealPolicyStatistics { + HealPolicyStatistics { + total_repairs: self.repair_count, + active_repairs: self.active_repairs, + last_repair_time: self.last_repair_time, + config: self.config.clone(), + } + } +} + +/// Heal policy statistics +#[derive(Debug, Clone)] +pub struct HealPolicyStatistics { + pub total_repairs: u64, + pub active_repairs: u64, + pub last_repair_time: SystemTime, + pub config: HealPolicyConfig, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::scanner::{HealthIssue, HealthIssueType, Severity}; + + #[tokio::test] + async fn test_heal_policy_creation() { + let config = HealPolicyConfig::default(); + let engine = HealPolicyEngine::new(config); + + assert_eq!(engine.config().max_concurrent_repairs, 4); + assert_eq!(engine.config().max_system_load, 0.7); + assert_eq!(engine.config().min_disk_space, 15.0); + } + + #[tokio::test] + async fn test_heal_policy_evaluation() { + let config = HealPolicyConfig::default(); + let engine = HealPolicyEngine::new(config); + + let issue = HealthIssue { + issue_type: HealthIssueType::MissingReplica, + severity: Severity::Medium, + bucket: "test-bucket".to_string(), + object: "test-object".to_string(), + description: "Test issue".to_string(), + metadata: None, + }; + + let context = PolicyContext { + system_load: 0.5, + disk_space_available: 80.0, + active_operations: 10, + current_time: SystemTime::now(), + health_issues: std::collections::HashMap::new(), + resource_usage: ResourceUsage::default(), + }; + + let result = engine.evaluate(&issue, &context).await; + assert!(result.allowed); + assert!(result.reason.contains("Heal allowed")); + } + + #[tokio::test] + async fn test_heal_policy_critical_immediate() { + let config = HealPolicyConfig::default(); + let engine = HealPolicyEngine::new(config); + + let issue = HealthIssue { + issue_type: HealthIssueType::MissingReplica, + severity: Severity::Critical, + bucket: "test-bucket".to_string(), + object: "test-object".to_string(), + description: "Test issue".to_string(), + metadata: None, + }; + + let context = PolicyContext { + system_load: 0.5, + disk_space_available: 80.0, + active_operations: 10, + current_time: SystemTime::now(), + health_issues: std::collections::HashMap::new(), + resource_usage: ResourceUsage::default(), + }; + + let result = engine.evaluate(&issue, &context).await; + assert!(result.allowed); + assert!(result.reason.contains("Critical issue - immediate repair allowed")); + } + + #[tokio::test] + async fn test_heal_policy_system_load_limit() { + let config = HealPolicyConfig::default(); + let engine = HealPolicyEngine::new(config); + + let issue = HealthIssue { + issue_type: HealthIssueType::MissingReplica, + severity: Severity::Medium, + bucket: "test-bucket".to_string(), + object: "test-object".to_string(), + description: "Test issue".to_string(), + metadata: None, + }; + + let context = PolicyContext { + system_load: 0.8, // Above threshold + disk_space_available: 80.0, + active_operations: 10, + current_time: SystemTime::now(), + health_issues: std::collections::HashMap::new(), + resource_usage: ResourceUsage::default(), + }; + + let result = engine.evaluate(&issue, &context).await; + assert!(!result.allowed); + assert!(result.reason.contains("System load too high")); + } + + #[tokio::test] + async fn test_repair_timeouts() { + let config = HealPolicyConfig::default(); + let engine = HealPolicyEngine::new(config); + + assert_eq!( + engine.get_repair_timeout(Severity::Critical), + Duration::from_secs(300) + ); + assert_eq!( + engine.get_repair_timeout(Severity::High), + Duration::from_secs(300) + ); + assert_eq!( + engine.get_repair_timeout(Severity::Medium), + Duration::from_secs(1800) + ); + assert_eq!( + engine.get_repair_timeout(Severity::Low), + Duration::from_secs(3600) + ); + } + + #[tokio::test] + async fn test_heal_statistics() { + let config = HealPolicyConfig::default(); + let mut engine = HealPolicyEngine::new(config); + + assert_eq!(engine.get_statistics().total_repairs, 0); + assert_eq!(engine.get_statistics().active_repairs, 0); + + engine.record_repair(); + engine.increment_active_repairs(); + engine.increment_active_repairs(); + + let stats = engine.get_statistics(); + assert_eq!(stats.total_repairs, 1); + assert_eq!(stats.active_repairs, 2); + + engine.decrement_active_repairs(); + assert_eq!(engine.get_statistics().active_repairs, 1); + } +} \ No newline at end of file diff --git a/crates/ahm/src/policy/mod.rs b/crates/ahm/src/policy/mod.rs new file mode 100644 index 000000000..507113ccd --- /dev/null +++ b/crates/ahm/src/policy/mod.rs @@ -0,0 +1,258 @@ +// 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. + +//! Policy system for AHM operations +//! +//! Defines configurable policies for: +//! - Scanning behavior and frequency +//! - Healing priorities and strategies +//! - Data retention and lifecycle management + +pub mod scan_policy; +pub mod heal_policy; +pub mod retention_policy; + +pub use scan_policy::{ScanPolicyConfig, ScanPolicyEngine}; +pub use heal_policy::{HealPolicyConfig, HealPolicyEngine}; +pub use retention_policy::{RetentionPolicyConfig, RetentionPolicyEngine}; + +use std::time::{Duration, SystemTime}; +use serde::{Deserialize, Serialize}; + +use crate::scanner::{HealthIssue, Severity}; + +/// Policy evaluation result +#[derive(Debug, Clone)] +pub struct PolicyResult { + /// Whether the policy allows the action + pub allowed: bool, + /// Reason for the decision + pub reason: String, + /// Additional metadata + pub metadata: Option, + /// When the policy was evaluated + pub evaluated_at: SystemTime, +} + +/// Policy evaluation context +#[derive(Debug, Clone)] +pub struct PolicyContext { + /// Current system load + pub system_load: f64, + /// Available disk space percentage + pub disk_space_available: f64, + /// Number of active operations + pub active_operations: u64, + /// Current time + pub current_time: SystemTime, + /// Health issues count by severity + pub health_issues: std::collections::HashMap, + /// Resource usage metrics + pub resource_usage: ResourceUsage, +} + +/// Resource usage information +#[derive(Debug, Clone)] +pub struct ResourceUsage { + /// CPU usage percentage + pub cpu_usage: f64, + /// Memory usage percentage + pub memory_usage: f64, + /// Disk I/O usage percentage + pub disk_io_usage: f64, + /// Network I/O usage percentage + pub network_io_usage: f64, +} + +impl Default for ResourceUsage { + fn default() -> Self { + Self { + cpu_usage: 0.0, + memory_usage: 0.0, + disk_io_usage: 0.0, + network_io_usage: 0.0, + } + } +} + +/// Policy manager that coordinates all policies +pub struct PolicyManager { + scan_policy: ScanPolicyEngine, + heal_policy: HealPolicyEngine, + retention_policy: RetentionPolicyEngine, +} + +impl PolicyManager { + /// Create a new policy manager + pub fn new( + scan_config: ScanPolicyConfig, + heal_config: HealPolicyConfig, + retention_config: RetentionPolicyConfig, + ) -> Self { + Self { + scan_policy: ScanPolicyEngine::new(scan_config), + heal_policy: HealPolicyEngine::new(heal_config), + retention_policy: RetentionPolicyEngine::new(retention_config), + } + } + + /// Evaluate scan policy + pub async fn evaluate_scan_policy(&self, context: &PolicyContext) -> PolicyResult { + self.scan_policy.evaluate(context).await + } + + /// Evaluate heal policy + pub async fn evaluate_heal_policy(&self, issue: &HealthIssue, context: &PolicyContext) -> PolicyResult { + self.heal_policy.evaluate(issue, context).await + } + + /// Evaluate retention policy + pub async fn evaluate_retention_policy(&self, object_age: Duration, context: &PolicyContext) -> PolicyResult { + self.retention_policy.evaluate(object_age, context).await + } + + /// Get scan policy engine + pub fn scan_policy(&self) -> &ScanPolicyEngine { + &self.scan_policy + } + + /// Get heal policy engine + pub fn heal_policy(&self) -> &HealPolicyEngine { + &self.heal_policy + } + + /// Get retention policy engine + pub fn retention_policy(&self) -> &RetentionPolicyEngine { + &self.retention_policy + } + + /// Update scan policy configuration + pub async fn update_scan_policy(&mut self, config: ScanPolicyConfig) { + self.scan_policy = ScanPolicyEngine::new(config); + } + + /// Update heal policy configuration + pub async fn update_heal_policy(&mut self, config: HealPolicyConfig) { + self.heal_policy = HealPolicyEngine::new(config); + } + + /// Update retention policy configuration + pub async fn update_retention_policy(&mut self, config: RetentionPolicyConfig) { + self.retention_policy = RetentionPolicyEngine::new(config); + } + + /// List all policies + pub async fn list_policies(&self) -> crate::error::Result> { + // In a real implementation, this would return actual policy names + Ok(vec![ + "scan_policy".to_string(), + "heal_policy".to_string(), + "retention_policy".to_string(), + ]) + } + + /// Get a specific policy + pub async fn get_policy(&self, name: &str) -> crate::error::Result { + // In a real implementation, this would return the actual policy + Ok(format!("Policy configuration for: {}", name)) + } + + /// Get engine configuration + pub async fn get_config(&self) -> PolicyConfig { + PolicyConfig::default() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::scanner::{HealthIssue, HealthIssueType}; + + #[tokio::test] + async fn test_policy_manager_creation() { + let scan_config = ScanPolicyConfig::default(); + let heal_config = HealPolicyConfig::default(); + let retention_config = RetentionPolicyConfig::default(); + + let manager = PolicyManager::new(scan_config, heal_config, retention_config); + + // Test that all policy engines are available + assert!(manager.scan_policy().config().max_concurrent_scans > 0); + assert!(manager.heal_policy().config().max_concurrent_repairs > 0); + assert!(manager.retention_policy().config().default_retention_days > 0); + } + + #[tokio::test] + async fn test_policy_evaluation() { + let scan_config = ScanPolicyConfig::default(); + let heal_config = HealPolicyConfig::default(); + let retention_config = RetentionPolicyConfig::default(); + + let manager = PolicyManager::new(scan_config, heal_config, retention_config); + + let context = PolicyContext { + system_load: 0.5, + disk_space_available: 80.0, + active_operations: 10, + current_time: SystemTime::now(), + health_issues: std::collections::HashMap::new(), + resource_usage: ResourceUsage::default(), + }; + + // Test scan policy evaluation + let scan_result = manager.evaluate_scan_policy(&context).await; + assert!(scan_result.allowed); + + // Test heal policy evaluation + let issue = HealthIssue { + issue_type: HealthIssueType::MissingReplica, + severity: Severity::Critical, + bucket: "test-bucket".to_string(), + object: "test-object".to_string(), + description: "Test issue".to_string(), + metadata: None, + }; + + let heal_result = manager.evaluate_heal_policy(&issue, &context).await; + assert!(heal_result.allowed); + + // Test retention policy evaluation + let retention_result = manager.evaluate_retention_policy(Duration::from_secs(86400), &context).await; + assert!(retention_result.allowed); + } +} + +/// Master policy configuration +#[derive(Debug, Clone)] +pub struct PolicyConfig { + pub scan: ScanPolicyConfig, + pub heal: HealPolicyConfig, + pub retention: RetentionPolicyConfig, +} + +impl Default for PolicyConfig { + fn default() -> Self { + Self { + scan: ScanPolicyConfig::default(), + heal: HealPolicyConfig::default(), + retention: RetentionPolicyConfig::default(), + } + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct PolicyManagerConfig { + #[serde(default)] + pub default_scan_interval: Duration, +} \ No newline at end of file diff --git a/crates/ahm/src/policy/retention_policy.rs b/crates/ahm/src/policy/retention_policy.rs new file mode 100644 index 000000000..f54591fac --- /dev/null +++ b/crates/ahm/src/policy/retention_policy.rs @@ -0,0 +1,487 @@ +// Copyright 2024 RustFS Team + +use std::time::{Duration, SystemTime}; + +use super::{PolicyContext, PolicyResult, ResourceUsage}; + +/// Configuration for retention policies +#[derive(Debug, Clone)] +pub struct RetentionPolicyConfig { + /// Default retention period in days + pub default_retention_days: u32, + /// Whether to enable retention policies + pub enabled: bool, + /// Maximum system load threshold for retention operations + pub max_system_load: f64, + /// Minimum available disk space percentage for retention operations + pub min_disk_space: f64, + /// Maximum number of active operations for retention + pub max_active_operations: u64, + /// Retention rules by object type + pub retention_rules: Vec, + /// Whether to enable automatic cleanup + pub auto_cleanup_enabled: bool, + /// Cleanup interval + pub cleanup_interval: Duration, + /// Maximum objects to delete per cleanup cycle + pub max_objects_per_cleanup: u64, +} + +/// Retention rule for specific object types +#[derive(Debug, Clone)] +pub struct RetentionRule { + /// Object type pattern (e.g., "*.log", "temp/*") + pub pattern: String, + /// Retention period in days + pub retention_days: u32, + /// Whether this rule is enabled + pub enabled: bool, + /// Priority of this rule (higher = more important) + pub priority: u32, + /// Whether to apply this rule recursively + pub recursive: bool, +} + +impl Default for RetentionPolicyConfig { + fn default() -> Self { + Self { + default_retention_days: 30, + enabled: true, + max_system_load: 0.6, + min_disk_space: 20.0, // 20% minimum disk space + max_active_operations: 20, + retention_rules: vec![ + RetentionRule { + pattern: "*.log".to_string(), + retention_days: 7, + enabled: true, + priority: 1, + recursive: false, + }, + RetentionRule { + pattern: "temp/*".to_string(), + retention_days: 1, + enabled: true, + priority: 2, + recursive: true, + }, + RetentionRule { + pattern: "cache/*".to_string(), + retention_days: 3, + enabled: true, + priority: 3, + recursive: true, + }, + ], + auto_cleanup_enabled: true, + cleanup_interval: Duration::from_secs(3600), // 1 hour + max_objects_per_cleanup: 1000, + } + } +} + +/// Retention policy engine +pub struct RetentionPolicyEngine { + config: RetentionPolicyConfig, + last_cleanup_time: SystemTime, + cleanup_count: u64, + objects_deleted: u64, +} + +impl RetentionPolicyEngine { + /// Create a new retention policy engine + pub fn new(config: RetentionPolicyConfig) -> Self { + Self { + config, + last_cleanup_time: SystemTime::now(), + cleanup_count: 0, + objects_deleted: 0, + } + } + + /// Get the configuration + pub fn config(&self) -> &RetentionPolicyConfig { + &self.config + } + + /// Evaluate retention policy + pub async fn evaluate(&self, object_age: Duration, context: &PolicyContext) -> PolicyResult { + let mut reasons = Vec::new(); + let mut allowed = false; + + // Check if retention policies are enabled + if !self.config.enabled { + allowed = false; + reasons.push("Retention policies are disabled".to_string()); + } else { + // Check if object should be retained based on age + let retention_days = self.get_retention_days_for_object("default"); + let retention_duration = Duration::from_secs(retention_days as u64 * 24 * 3600); + + if object_age > retention_duration { + allowed = true; + reasons.push(format!( + "Object age exceeds retention period: {:?} > {:?}", + object_age, retention_duration + )); + } else { + allowed = false; + reasons.push(format!( + "Object within retention period: {:?} <= {:?}", + object_age, retention_duration + )); + } + } + + // Check system constraints + if context.system_load > self.config.max_system_load { + allowed = false; + reasons.push(format!( + "System load too high: {:.2} > {:.2}", + context.system_load, self.config.max_system_load + )); + } + + if context.disk_space_available < self.config.min_disk_space { + allowed = false; + reasons.push(format!( + "Disk space too low: {:.1}% < {:.1}%", + context.disk_space_available, self.config.min_disk_space + )); + } + + if context.active_operations > self.config.max_active_operations { + allowed = false; + reasons.push(format!( + "Too many active operations: {} > {}", + context.active_operations, self.config.max_active_operations + )); + } + + let reason = if reasons.is_empty() { + "Retention evaluation completed".to_string() + } else { + reasons.join("; ") + }; + + PolicyResult { + allowed, + reason, + metadata: Some(serde_json::json!({ + "object_age_seconds": object_age.as_secs(), + "cleanup_count": self.cleanup_count, + "objects_deleted": self.objects_deleted, + "system_load": context.system_load, + "disk_space_available": context.disk_space_available, + "active_operations": context.active_operations, + })), + evaluated_at: context.current_time, + } + } + + /// Evaluate cleanup policy + pub async fn evaluate_cleanup(&self, context: &PolicyContext) -> PolicyResult { + let mut reasons = Vec::new(); + let mut allowed = false; + + // Check if auto-cleanup is enabled + if !self.config.auto_cleanup_enabled { + allowed = false; + reasons.push("Auto-cleanup is disabled".to_string()); + } else { + // Check cleanup interval + let time_since_last_cleanup = context.current_time + .duration_since(self.last_cleanup_time) + .unwrap_or(Duration::ZERO); + + if time_since_last_cleanup >= self.config.cleanup_interval { + allowed = true; + reasons.push("Cleanup interval reached".to_string()); + } else { + allowed = false; + reasons.push(format!( + "Cleanup interval not reached: {:?} < {:?}", + time_since_last_cleanup, self.config.cleanup_interval + )); + } + } + + // Check system constraints + if context.system_load > self.config.max_system_load { + allowed = false; + reasons.push(format!( + "System load too high: {:.2} > {:.2}", + context.system_load, self.config.max_system_load + )); + } + + if context.disk_space_available < self.config.min_disk_space { + allowed = false; + reasons.push(format!( + "Disk space too low: {:.1}% < {:.1}%", + context.disk_space_available, self.config.min_disk_space + )); + } + + let reason = if reasons.is_empty() { + "Cleanup evaluation completed".to_string() + } else { + reasons.join("; ") + }; + + PolicyResult { + allowed, + reason, + metadata: Some(serde_json::json!({ + "cleanup_count": self.cleanup_count, + "objects_deleted": self.objects_deleted, + "max_objects_per_cleanup": self.config.max_objects_per_cleanup, + "system_load": context.system_load, + "disk_space_available": context.disk_space_available, + })), + evaluated_at: context.current_time, + } + } + + /// Get retention days for a specific object + pub fn get_retention_days_for_object(&self, object_path: &str) -> u32 { + // Find the highest priority matching rule + let mut best_rule: Option<&RetentionRule> = None; + let mut best_priority = 0; + + for rule in &self.config.retention_rules { + if !rule.enabled { + continue; + } + + if self.matches_pattern(object_path, &rule.pattern) { + if rule.priority > best_priority { + best_rule = Some(rule); + best_priority = rule.priority; + } + } + } + + best_rule + .map(|rule| rule.retention_days) + .unwrap_or(self.config.default_retention_days) + } + + /// Check if an object path matches a pattern + fn matches_pattern(&self, object_path: &str, pattern: &str) -> bool { + // Simple pattern matching - can be enhanced with regex + if pattern.contains('*') { + // Wildcard matching + let pattern_parts: Vec<&str> = pattern.split('*').collect(); + if pattern_parts.len() == 2 { + let prefix = pattern_parts[0]; + let suffix = pattern_parts[1]; + object_path.starts_with(prefix) && object_path.ends_with(suffix) + } else { + false + } + } else { + // Exact match + object_path == pattern + } + } + + /// Get all retention rules + pub fn get_retention_rules(&self) -> &[RetentionRule] { + &self.config.retention_rules + } + + /// Add a new retention rule + pub fn add_retention_rule(&mut self, rule: RetentionRule) { + self.config.retention_rules.push(rule); + } + + /// Remove a retention rule by pattern + pub fn remove_retention_rule(&mut self, pattern: &str) -> bool { + let initial_len = self.config.retention_rules.len(); + self.config.retention_rules.retain(|rule| rule.pattern != pattern); + self.config.retention_rules.len() < initial_len + } + + /// Update cleanup statistics + pub fn record_cleanup(&mut self, objects_deleted: u64) { + self.last_cleanup_time = SystemTime::now(); + self.cleanup_count += 1; + self.objects_deleted += objects_deleted; + } + + /// Get retention statistics + pub fn get_statistics(&self) -> RetentionPolicyStatistics { + RetentionPolicyStatistics { + total_cleanups: self.cleanup_count, + total_objects_deleted: self.objects_deleted, + last_cleanup_time: self.last_cleanup_time, + config: self.config.clone(), + } + } +} + +/// Retention policy statistics +#[derive(Debug, Clone)] +pub struct RetentionPolicyStatistics { + pub total_cleanups: u64, + pub total_objects_deleted: u64, + pub last_cleanup_time: SystemTime, + pub config: RetentionPolicyConfig, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_retention_policy_creation() { + let config = RetentionPolicyConfig::default(); + let engine = RetentionPolicyEngine::new(config); + + assert_eq!(engine.config().default_retention_days, 30); + assert_eq!(engine.config().max_system_load, 0.6); + assert_eq!(engine.config().min_disk_space, 20.0); + } + + #[tokio::test] + async fn test_retention_policy_evaluation() { + let config = RetentionPolicyConfig::default(); + let engine = RetentionPolicyEngine::new(config); + + let context = PolicyContext { + system_load: 0.5, + disk_space_available: 80.0, + active_operations: 10, + current_time: SystemTime::now(), + health_issues: std::collections::HashMap::new(), + resource_usage: ResourceUsage::default(), + }; + + // Test object within retention period + let object_age = Duration::from_secs(7 * 24 * 3600); // 7 days + let result = engine.evaluate(object_age, &context).await; + assert!(!result.allowed); + assert!(result.reason.contains("Object within retention period")); + + // Test object exceeding retention period + let object_age = Duration::from_secs(40 * 24 * 3600); // 40 days + let result = engine.evaluate(object_age, &context).await; + assert!(result.allowed); + assert!(result.reason.contains("Object age exceeds retention period")); + } + + #[tokio::test] + async fn test_retention_policy_system_constraints() { + let config = RetentionPolicyConfig::default(); + let engine = RetentionPolicyEngine::new(config); + + let context = PolicyContext { + system_load: 0.7, // Above threshold + disk_space_available: 80.0, + active_operations: 10, + current_time: SystemTime::now(), + health_issues: std::collections::HashMap::new(), + resource_usage: ResourceUsage::default(), + }; + + let object_age = Duration::from_secs(40 * 24 * 3600); // 40 days + let result = engine.evaluate(object_age, &context).await; + assert!(!result.allowed); + assert!(result.reason.contains("System load too high")); + } + + #[tokio::test] + async fn test_retention_rules() { + let config = RetentionPolicyConfig::default(); + let engine = RetentionPolicyEngine::new(config); + + // Test default retention + assert_eq!(engine.get_retention_days_for_object("unknown.txt"), 30); + + // Test log file retention + assert_eq!(engine.get_retention_days_for_object("app.log"), 7); + + // Test temp file retention + assert_eq!(engine.get_retention_days_for_object("temp/file.txt"), 1); + + // Test cache file retention + assert_eq!(engine.get_retention_days_for_object("cache/data.bin"), 3); + } + + #[tokio::test] + async fn test_pattern_matching() { + let config = RetentionPolicyConfig::default(); + let engine = RetentionPolicyEngine::new(config); + + // Test wildcard matching + assert!(engine.matches_pattern("app.log", "*.log")); + assert!(engine.matches_pattern("error.log", "*.log")); + assert!(!engine.matches_pattern("app.txt", "*.log")); + + // Test exact matching + assert!(engine.matches_pattern("temp/file.txt", "temp/file.txt")); + assert!(!engine.matches_pattern("temp/file.txt", "temp/other.txt")); + } + + #[tokio::test] + async fn test_cleanup_evaluation() { + let config = RetentionPolicyConfig::default(); + let engine = RetentionPolicyEngine::new(config); + + let context = PolicyContext { + system_load: 0.5, + disk_space_available: 80.0, + active_operations: 10, + current_time: SystemTime::now(), + health_issues: std::collections::HashMap::new(), + resource_usage: ResourceUsage::default(), + }; + + let result = engine.evaluate_cleanup(&context).await; + // Should be allowed if enough time has passed since last cleanup + assert!(result.allowed || result.reason.contains("Cleanup interval not reached")); + } + + #[tokio::test] + async fn test_retention_statistics() { + let config = RetentionPolicyConfig::default(); + let mut engine = RetentionPolicyEngine::new(config); + + assert_eq!(engine.get_statistics().total_cleanups, 0); + assert_eq!(engine.get_statistics().total_objects_deleted, 0); + + engine.record_cleanup(50); + assert_eq!(engine.get_statistics().total_cleanups, 1); + assert_eq!(engine.get_statistics().total_objects_deleted, 50); + + engine.record_cleanup(30); + assert_eq!(engine.get_statistics().total_cleanups, 2); + assert_eq!(engine.get_statistics().total_objects_deleted, 80); + } + + #[tokio::test] + async fn test_retention_rule_management() { + let config = RetentionPolicyConfig::default(); + let mut engine = RetentionPolicyEngine::new(config); + + let initial_rules = engine.get_retention_rules().len(); + + // Add a new rule + let new_rule = RetentionRule { + pattern: "backup/*".to_string(), + retention_days: 90, + enabled: true, + priority: 4, + recursive: true, + }; + engine.add_retention_rule(new_rule); + + assert_eq!(engine.get_retention_rules().len(), initial_rules + 1); + + // Remove a rule + let removed = engine.remove_retention_rule("*.log"); + assert!(removed); + assert_eq!(engine.get_retention_rules().len(), initial_rules); + } +} \ No newline at end of file diff --git a/crates/ahm/src/policy/scan_policy.rs b/crates/ahm/src/policy/scan_policy.rs new file mode 100644 index 000000000..44e3fc14c --- /dev/null +++ b/crates/ahm/src/policy/scan_policy.rs @@ -0,0 +1,373 @@ +// Copyright 2024 RustFS Team + +use std::time::{Duration, SystemTime}; + +use crate::scanner::Severity; + +use super::{PolicyContext, PolicyResult, ResourceUsage}; + +/// Configuration for scan policies +#[derive(Debug, Clone)] +pub struct ScanPolicyConfig { + /// Maximum number of concurrent scans + pub max_concurrent_scans: usize, + /// Maximum scan duration per cycle + pub max_scan_duration: Duration, + /// Minimum interval between scans + pub min_scan_interval: Duration, + /// Maximum system load threshold for scanning + pub max_system_load: f64, + /// Minimum available disk space percentage for scanning + pub min_disk_space: f64, + /// Maximum number of active operations for scanning + pub max_active_operations: u64, + /// Whether to enable deep scanning + pub enable_deep_scan: bool, + /// Deep scan interval (how often to perform deep scans) + pub deep_scan_interval: Duration, + /// Bandwidth limit for scanning (bytes per second) + pub bandwidth_limit: Option, + /// Priority-based scanning configuration + pub priority_config: ScanPriorityConfig, +} + +/// Priority-based scanning configuration +#[derive(Debug, Clone)] +pub struct ScanPriorityConfig { + /// Whether to enable priority-based scanning + pub enabled: bool, + /// Critical issues scan interval + pub critical_interval: Duration, + /// High priority issues scan interval + pub high_interval: Duration, + /// Medium priority issues scan interval + pub medium_interval: Duration, + /// Low priority issues scan interval + pub low_interval: Duration, +} + +impl Default for ScanPolicyConfig { + fn default() -> Self { + Self { + max_concurrent_scans: 4, + max_scan_duration: Duration::from_secs(3600), // 1 hour + min_scan_interval: Duration::from_secs(300), // 5 minutes + max_system_load: 0.8, + min_disk_space: 10.0, // 10% minimum disk space + max_active_operations: 100, + enable_deep_scan: true, + deep_scan_interval: Duration::from_secs(86400), // 24 hours + bandwidth_limit: Some(100 * 1024 * 1024), // 100 MB/s + priority_config: ScanPriorityConfig::default(), + } + } +} + +impl Default for ScanPriorityConfig { + fn default() -> Self { + Self { + enabled: true, + critical_interval: Duration::from_secs(60), // 1 minute + high_interval: Duration::from_secs(300), // 5 minutes + medium_interval: Duration::from_secs(1800), // 30 minutes + low_interval: Duration::from_secs(3600), // 1 hour + } + } +} + +/// Scan policy engine +pub struct ScanPolicyEngine { + config: ScanPolicyConfig, + last_scan_time: SystemTime, + last_deep_scan_time: SystemTime, + scan_count: u64, +} + +impl ScanPolicyEngine { + /// Create a new scan policy engine + pub fn new(config: ScanPolicyConfig) -> Self { + Self { + config, + last_scan_time: SystemTime::now(), + last_deep_scan_time: SystemTime::now(), + scan_count: 0, + } + } + + /// Get the configuration + pub fn config(&self) -> &ScanPolicyConfig { + &self.config + } + + /// Evaluate scan policy + pub async fn evaluate(&self, context: &PolicyContext) -> PolicyResult { + let mut reasons = Vec::new(); + let mut allowed = true; + + // Check system load + if context.system_load > self.config.max_system_load { + allowed = false; + reasons.push(format!( + "System load too high: {:.2} > {:.2}", + context.system_load, self.config.max_system_load + )); + } + + // Check disk space + if context.disk_space_available < self.config.min_disk_space { + allowed = false; + reasons.push(format!( + "Disk space too low: {:.1}% < {:.1}%", + context.disk_space_available, self.config.min_disk_space + )); + } + + // Check active operations + if context.active_operations > self.config.max_active_operations { + allowed = false; + reasons.push(format!( + "Too many active operations: {} > {}", + context.active_operations, self.config.max_active_operations + )); + } + + // Check scan interval + let time_since_last_scan = context.current_time + .duration_since(self.last_scan_time) + .unwrap_or(Duration::ZERO); + + if time_since_last_scan < self.config.min_scan_interval { + allowed = false; + reasons.push(format!( + "Scan interval too short: {:?} < {:?}", + time_since_last_scan, self.config.min_scan_interval + )); + } + + // Check resource usage + if context.resource_usage.cpu_usage > 90.0 { + allowed = false; + reasons.push("CPU usage too high".to_string()); + } + + if context.resource_usage.memory_usage > 90.0 { + allowed = false; + reasons.push("Memory usage too high".to_string()); + } + + let reason = if reasons.is_empty() { + "Scan allowed".to_string() + } else { + reasons.join("; ") + }; + + PolicyResult { + allowed, + reason, + metadata: Some(serde_json::json!({ + "scan_count": self.scan_count, + "time_since_last_scan": time_since_last_scan.as_secs(), + "system_load": context.system_load, + "disk_space_available": context.disk_space_available, + "active_operations": context.active_operations, + })), + evaluated_at: context.current_time, + } + } + + /// Evaluate deep scan policy + pub async fn evaluate_deep_scan(&self, context: &PolicyContext) -> PolicyResult { + let mut base_result = self.evaluate(context).await; + + if !base_result.allowed { + return base_result; + } + + // Check deep scan interval + let time_since_last_deep_scan = context.current_time + .duration_since(self.last_deep_scan_time) + .unwrap_or(Duration::ZERO); + + if time_since_last_deep_scan < self.config.deep_scan_interval { + base_result.allowed = false; + base_result.reason = format!( + "Deep scan interval too short: {:?} < {:?}", + time_since_last_deep_scan, self.config.deep_scan_interval + ); + } else { + base_result.reason = "Deep scan allowed".to_string(); + } + + // Add deep scan metadata + if let Some(ref mut metadata) = base_result.metadata { + if let Some(obj) = metadata.as_object_mut() { + obj.insert( + "time_since_last_deep_scan".to_string(), + serde_json::Value::Number(serde_json::Number::from(time_since_last_deep_scan.as_secs())), + ); + obj.insert( + "deep_scan_enabled".to_string(), + serde_json::Value::Bool(self.config.enable_deep_scan), + ); + } + } + + base_result + } + + /// Get scan interval based on priority + pub fn get_priority_interval(&self, severity: Severity) -> Duration { + if !self.config.priority_config.enabled { + return self.config.min_scan_interval; + } + + match severity { + Severity::Critical => self.config.priority_config.critical_interval, + Severity::High => self.config.priority_config.high_interval, + Severity::Medium => self.config.priority_config.medium_interval, + Severity::Low => self.config.priority_config.low_interval, + } + } + + /// Update scan statistics + pub fn record_scan(&mut self) { + self.last_scan_time = SystemTime::now(); + self.scan_count += 1; + } + + /// Update deep scan statistics + pub fn record_deep_scan(&mut self) { + self.last_deep_scan_time = SystemTime::now(); + } + + /// Get scan statistics + pub fn get_statistics(&self) -> ScanPolicyStatistics { + ScanPolicyStatistics { + total_scans: self.scan_count, + last_scan_time: self.last_scan_time, + last_deep_scan_time: self.last_deep_scan_time, + config: self.config.clone(), + } + } +} + +/// Scan policy statistics +#[derive(Debug, Clone)] +pub struct ScanPolicyStatistics { + pub total_scans: u64, + pub last_scan_time: SystemTime, + pub last_deep_scan_time: SystemTime, + pub config: ScanPolicyConfig, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::scanner::Severity; + + #[tokio::test] + async fn test_scan_policy_creation() { + let config = ScanPolicyConfig::default(); + let engine = ScanPolicyEngine::new(config); + + assert_eq!(engine.config().max_concurrent_scans, 4); + assert_eq!(engine.config().max_system_load, 0.8); + assert_eq!(engine.config().min_disk_space, 10.0); + } + + #[tokio::test] + async fn test_scan_policy_evaluation() { + let config = ScanPolicyConfig::default(); + let engine = ScanPolicyEngine::new(config); + + let context = PolicyContext { + system_load: 0.5, + disk_space_available: 80.0, + active_operations: 10, + current_time: SystemTime::now(), + health_issues: std::collections::HashMap::new(), + resource_usage: ResourceUsage::default(), + }; + + let result = engine.evaluate(&context).await; + assert!(result.allowed); + assert!(result.reason.contains("Scan allowed")); + } + + #[tokio::test] + async fn test_scan_policy_system_load_limit() { + let config = ScanPolicyConfig::default(); + let engine = ScanPolicyEngine::new(config); + + let context = PolicyContext { + system_load: 0.9, // Above threshold + disk_space_available: 80.0, + active_operations: 10, + current_time: SystemTime::now(), + health_issues: std::collections::HashMap::new(), + resource_usage: ResourceUsage::default(), + }; + + let result = engine.evaluate(&context).await; + assert!(!result.allowed); + assert!(result.reason.contains("System load too high")); + } + + #[tokio::test] + async fn test_scan_policy_disk_space_limit() { + let config = ScanPolicyConfig::default(); + let engine = ScanPolicyEngine::new(config); + + let context = PolicyContext { + system_load: 0.5, + disk_space_available: 5.0, // Below threshold + active_operations: 10, + current_time: SystemTime::now(), + health_issues: std::collections::HashMap::new(), + resource_usage: ResourceUsage::default(), + }; + + let result = engine.evaluate(&context).await; + assert!(!result.allowed); + assert!(result.reason.contains("Disk space too low")); + } + + #[tokio::test] + async fn test_priority_intervals() { + let config = ScanPolicyConfig::default(); + let engine = ScanPolicyEngine::new(config); + + assert_eq!( + engine.get_priority_interval(Severity::Critical), + Duration::from_secs(60) + ); + assert_eq!( + engine.get_priority_interval(Severity::High), + Duration::from_secs(300) + ); + assert_eq!( + engine.get_priority_interval(Severity::Medium), + Duration::from_secs(1800) + ); + assert_eq!( + engine.get_priority_interval(Severity::Low), + Duration::from_secs(3600) + ); + } + + #[tokio::test] + async fn test_scan_statistics() { + let config = ScanPolicyConfig::default(); + let mut engine = ScanPolicyEngine::new(config); + + assert_eq!(engine.get_statistics().total_scans, 0); + + engine.record_scan(); + assert_eq!(engine.get_statistics().total_scans, 1); + + engine.record_deep_scan(); + let stats = engine.get_statistics(); + assert_eq!(stats.total_scans, 1); + assert!(stats.last_deep_scan_time > stats.last_scan_time); + } +} \ No newline at end of file diff --git a/crates/ahm/src/scanner/bandwidth_limiter.rs b/crates/ahm/src/scanner/bandwidth_limiter.rs new file mode 100644 index 000000000..cf0e6a8da --- /dev/null +++ b/crates/ahm/src/scanner/bandwidth_limiter.rs @@ -0,0 +1,353 @@ +// 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}, + Arc, + }, + time::{Duration, Instant}, +}; + +use tokio::{ + sync::RwLock, + time::{sleep, sleep_until, Instant as TokioInstant}, +}; +use tracing::{debug, info, warn}; + +use crate::error::Result; + +/// Configuration for bandwidth limiting +#[derive(Debug, Clone)] +pub struct BandwidthConfig { + /// Maximum bytes per second + pub bytes_per_second: u64, + /// Maximum operations per second + pub operations_per_second: u64, + /// Burst allowance multiplier + pub burst_multiplier: f64, + /// Whether to enable adaptive throttling + pub adaptive_throttling: bool, + /// Minimum sleep duration between operations + pub min_sleep_duration: Duration, + /// Maximum sleep duration between operations + pub max_sleep_duration: Duration, +} + +impl Default for BandwidthConfig { + fn default() -> Self { + Self { + bytes_per_second: 100 * 1024 * 1024, // 100 MB/s + operations_per_second: 1000, // 1000 ops/s + burst_multiplier: 2.0, + adaptive_throttling: true, + min_sleep_duration: Duration::from_micros(100), + max_sleep_duration: Duration::from_millis(100), + } + } +} + +/// Bandwidth limiter for controlling scan I/O rates +pub struct BandwidthLimiter { + config: BandwidthConfig, + bytes_this_second: Arc, + operations_this_second: Arc, + last_reset: Arc>, + adaptive_sleep_duration: Arc>, + total_bytes_processed: Arc, + total_operations_processed: Arc, + start_time: Instant, +} + +impl BandwidthLimiter { + /// Create a new bandwidth limiter + pub fn new(config: BandwidthConfig) -> Self { + let adaptive_sleep = if config.adaptive_throttling { + config.min_sleep_duration + } else { + Duration::from_micros(1000) // 1ms default + }; + + Self { + config, + bytes_this_second: Arc::new(AtomicU64::new(0)), + operations_this_second: Arc::new(AtomicU64::new(0)), + last_reset: Arc::new(RwLock::new(Instant::now())), + adaptive_sleep_duration: Arc::new(RwLock::new(adaptive_sleep)), + total_bytes_processed: Arc::new(AtomicU64::new(0)), + total_operations_processed: Arc::new(AtomicU64::new(0)), + start_time: Instant::now(), + } + } + + /// Wait for bandwidth allowance before processing bytes + pub async fn wait_for_bytes(&self, bytes: u64) -> Result<()> { + if self.config.bytes_per_second == 0 { + return Ok(()); + } + + let mut total_wait_time = Duration::ZERO; + let mut remaining_bytes = bytes; + + while remaining_bytes > 0 { + // Reset counters if a second has passed + self.reset_counters_if_needed().await; + + let current_bytes = self.bytes_this_second.load(Ordering::Relaxed); + let burst_limit = (self.config.bytes_per_second as f64 * self.config.burst_multiplier) as u64; + + if current_bytes >= burst_limit { + // We're over the burst limit, wait + let wait_time = self.calculate_wait_time(current_bytes, self.config.bytes_per_second).await; + sleep(wait_time).await; + total_wait_time += wait_time; + continue; + } + + let bytes_to_process = std::cmp::min(remaining_bytes, burst_limit - current_bytes); + self.bytes_this_second.fetch_add(bytes_to_process, Ordering::Relaxed); + self.total_bytes_processed.fetch_add(bytes_to_process, Ordering::Relaxed); + remaining_bytes -= bytes_to_process; + + // Adaptive throttling + if self.config.adaptive_throttling { + self.update_adaptive_sleep(bytes_to_process).await; + } + } + + if total_wait_time > Duration::ZERO { + debug!("Bandwidth limiter waited {:?} for {} bytes", total_wait_time, bytes); + } + + Ok(()) + } + + /// Wait for bandwidth allowance before processing an operation + pub async fn wait_for_operation(&self) -> Result<()> { + if self.config.operations_per_second == 0 { + return Ok(()); + } + + // Reset counters if a second has passed + self.reset_counters_if_needed().await; + + let current_ops = self.operations_this_second.load(Ordering::Relaxed); + let burst_limit = (self.config.operations_per_second as f64 * self.config.burst_multiplier) as u64; + + if current_ops >= burst_limit { + // We're over the burst limit, wait + let wait_time = self.calculate_wait_time(current_ops, self.config.operations_per_second).await; + sleep(wait_time).await; + debug!("Bandwidth limiter waited {:?} for operation", wait_time); + } + + self.operations_this_second.fetch_add(1, Ordering::Relaxed); + self.total_operations_processed.fetch_add(1, Ordering::Relaxed); + + Ok(()) + } + + /// Wait for bandwidth allowance before processing both bytes and operations + pub async fn wait_for_bytes_and_operation(&self, bytes: u64) -> Result<()> { + self.wait_for_bytes(bytes).await?; + self.wait_for_operation().await?; + Ok(()) + } + + /// Reset counters if a second has passed + async fn reset_counters_if_needed(&self) { + let mut last_reset = self.last_reset.write().await; + let now = Instant::now(); + + if now.duration_since(*last_reset) >= Duration::from_secs(1) { + self.bytes_this_second.store(0, Ordering::Relaxed); + self.operations_this_second.store(0, Ordering::Relaxed); + *last_reset = now; + } + } + + /// Calculate wait time based on current usage and limit + async fn calculate_wait_time(&self, current: u64, limit: u64) -> Duration { + if current == 0 || limit == 0 { + return self.config.min_sleep_duration; + } + + let utilization = current as f64 / limit as f64; + let base_sleep = self.config.min_sleep_duration.as_micros() as f64; + let max_sleep = self.config.max_sleep_duration.as_micros() as f64; + + // Exponential backoff based on utilization + let sleep_micros = base_sleep * (utilization * utilization); + let sleep_micros = sleep_micros.min(max_sleep).max(base_sleep); + + Duration::from_micros(sleep_micros as u64) + } + + /// Update adaptive sleep duration based on recent activity + async fn update_adaptive_sleep(&self, bytes_processed: u64) { + let mut sleep_duration = self.adaptive_sleep_duration.write().await; + + // Simple adaptive algorithm: increase sleep if we're processing too much + let current_rate = bytes_processed as f64 / sleep_duration.as_secs_f64(); + let target_rate = self.config.bytes_per_second as f64; + + if current_rate > target_rate * 1.1 { + // We're going too fast, increase sleep + *sleep_duration = Duration::from_micros( + (sleep_duration.as_micros() as f64 * 1.1) as u64 + ).min(self.config.max_sleep_duration); + } else if current_rate < target_rate * 0.9 { + // We're going too slow, decrease sleep + *sleep_duration = Duration::from_micros( + (sleep_duration.as_micros() as f64 * 0.9) as u64 + ).max(self.config.min_sleep_duration); + } + } + + /// Get current bandwidth statistics + pub async fn statistics(&self) -> BandwidthStatistics { + let elapsed = self.start_time.elapsed(); + let total_bytes = self.total_bytes_processed.load(Ordering::Relaxed); + let total_ops = self.total_operations_processed.load(Ordering::Relaxed); + let current_bytes = self.bytes_this_second.load(Ordering::Relaxed); + let current_ops = self.operations_this_second.load(Ordering::Relaxed); + let adaptive_sleep = *self.adaptive_sleep_duration.read().await; + + BandwidthStatistics { + total_bytes_processed: total_bytes, + total_operations_processed: total_ops, + current_bytes_per_second: current_bytes, + current_operations_per_second: current_ops, + average_bytes_per_second: if elapsed.as_secs() > 0 { + total_bytes / elapsed.as_secs() + } else { + 0 + }, + average_operations_per_second: if elapsed.as_secs() > 0 { + total_ops / elapsed.as_secs() + } else { + 0 + }, + adaptive_sleep_duration: adaptive_sleep, + uptime: elapsed, + } + } + + /// Reset all statistics + pub async fn reset_statistics(&self) { + self.total_bytes_processed.store(0, Ordering::Relaxed); + self.total_operations_processed.store(0, Ordering::Relaxed); + self.bytes_this_second.store(0, Ordering::Relaxed); + self.operations_this_second.store(0, Ordering::Relaxed); + *self.last_reset.write().await = Instant::now(); + *self.adaptive_sleep_duration.write().await = self.config.min_sleep_duration; + } + + /// Update configuration + pub async fn update_config(&self, new_config: BandwidthConfig) { + info!("Updating bandwidth limiter config: {:?}", new_config); + + // Reset adaptive sleep if adaptive throttling is disabled + if !new_config.adaptive_throttling { + *self.adaptive_sleep_duration.write().await = new_config.min_sleep_duration; + } + + // Note: We can't update the config struct itself since it's not wrapped in Arc + // In a real implementation, you might want to wrap the config in Arc as well + warn!("Config update not fully implemented - config struct is not mutable"); + } +} + +/// Statistics for bandwidth limiting +#[derive(Debug, Clone)] +pub struct BandwidthStatistics { + pub total_bytes_processed: u64, + pub total_operations_processed: u64, + pub current_bytes_per_second: u64, + pub current_operations_per_second: u64, + pub average_bytes_per_second: u64, + pub average_operations_per_second: u64, + pub adaptive_sleep_duration: Duration, + pub uptime: Duration, +} + +#[cfg(test)] +mod tests { + use super::*; + use tokio::time::Instant as TokioInstant; + + #[tokio::test] + async fn test_bandwidth_limiter_creation() { + let config = BandwidthConfig::default(); + let limiter = BandwidthLimiter::new(config); + let stats = limiter.statistics().await; + assert_eq!(stats.total_bytes_processed, 0); + assert_eq!(stats.total_operations_processed, 0); + } + + #[tokio::test] + async fn test_bytes_limiting() { + let config = BandwidthConfig { + bytes_per_second: 1000, // 1KB/s + operations_per_second: 1000, + ..Default::default() + }; + let limiter = BandwidthLimiter::new(config); + + let start = TokioInstant::now(); + + // Process 500 bytes (should not be limited) + limiter.wait_for_bytes(500).await.unwrap(); + + // Process another 600 bytes (should be limited) + limiter.wait_for_bytes(600).await.unwrap(); + + let elapsed = start.elapsed(); + assert!(elapsed >= Duration::from_millis(100)); // Should take some time due to limiting + } + + #[tokio::test] + async fn test_operation_limiting() { + let config = BandwidthConfig { + bytes_per_second: 1000000, // 1MB/s + operations_per_second: 10, // 10 ops/s + ..Default::default() + }; + let limiter = BandwidthLimiter::new(config); + + let start = TokioInstant::now(); + + // Process 15 operations (should be limited) + for _ in 0..15 { + limiter.wait_for_operation().await.unwrap(); + } + + let elapsed = start.elapsed(); + assert!(elapsed >= Duration::from_millis(500)); // Should take some time due to limiting + } + + #[tokio::test] + async fn test_statistics() { + let config = BandwidthConfig::default(); + let limiter = BandwidthLimiter::new(config); + + limiter.wait_for_bytes(1000).await.unwrap(); + limiter.wait_for_operation().await.unwrap(); + + let stats = limiter.statistics().await; + assert_eq!(stats.total_bytes_processed, 1000); + assert_eq!(stats.total_operations_processed, 1); + assert!(stats.uptime > Duration::ZERO); + } +} \ No newline at end of file diff --git a/crates/ahm/src/scanner/disk_scanner.rs b/crates/ahm/src/scanner/disk_scanner.rs new file mode 100644 index 000000000..8d636107c --- /dev/null +++ b/crates/ahm/src/scanner/disk_scanner.rs @@ -0,0 +1,591 @@ +// 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::{ + collections::HashMap, + path::Path, + sync::Arc, + time::{Duration, Instant, SystemTime}, +}; + +use tokio::sync::RwLock; +use tracing::{error, info}; +use anyhow; + +use crate::error::Result; +use super::{HealthIssue, HealthIssueType, Severity}; + +/// Configuration for disk scanning +#[derive(Debug, Clone)] +pub struct DiskScannerConfig { + /// Scan interval for disk health checks + pub scan_interval: Duration, + /// Minimum free space threshold (percentage) + pub min_free_space_percent: f64, + /// Maximum disk usage threshold (percentage) + pub max_disk_usage_percent: f64, + /// Minimum inode usage threshold (percentage) + pub min_inode_usage_percent: f64, + /// Maximum inode usage threshold (percentage) + pub max_inode_usage_percent: f64, + /// Whether to check disk I/O performance + pub check_io_performance: bool, + /// Whether to check disk temperature (if available) + pub check_temperature: bool, + /// Whether to check disk SMART status (if available) + pub check_smart_status: bool, + /// Timeout for individual disk operations + pub operation_timeout: Duration, + /// Maximum number of concurrent disk scans + pub max_concurrent_scans: usize, +} + +impl Default for DiskScannerConfig { + fn default() -> Self { + Self { + scan_interval: Duration::from_secs(300), // 5 minutes + min_free_space_percent: 10.0, // 10% minimum free space + max_disk_usage_percent: 90.0, // 90% maximum usage + min_inode_usage_percent: 5.0, // 5% minimum inode usage + max_inode_usage_percent: 95.0, // 95% maximum inode usage + check_io_performance: true, + check_temperature: false, // Disabled by default + check_smart_status: false, // Disabled by default + operation_timeout: Duration::from_secs(30), + max_concurrent_scans: 4, + } + } +} + +/// Disk information and health status +#[derive(Debug, Clone)] +pub struct DiskInfo { + pub device_path: String, + pub mount_point: String, + pub filesystem_type: String, + pub total_space: u64, + pub used_space: u64, + pub free_space: u64, + pub available_space: u64, + pub usage_percent: f64, + pub inode_total: Option, + pub inode_used: Option, + pub inode_free: Option, + pub inode_usage_percent: Option, + pub last_scan_time: SystemTime, + pub health_status: DiskHealthStatus, + pub performance_metrics: Option, + pub temperature: Option, + pub smart_status: Option, +} + +/// Disk health status +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DiskHealthStatus { + Healthy, + Warning, + Critical, + Unknown, +} + +/// Disk performance metrics +#[derive(Debug, Clone)] +pub struct DiskPerformanceMetrics { + pub read_bytes_per_sec: f64, + pub write_bytes_per_sec: f64, + pub read_operations_per_sec: f64, + pub write_operations_per_sec: f64, + pub average_response_time_ms: f64, + pub queue_depth: f64, + pub utilization_percent: f64, + pub last_updated: SystemTime, +} + +/// SMART status information +#[derive(Debug, Clone)] +pub struct SmartStatus { + pub overall_health: SmartHealthStatus, + pub temperature: Option, + pub power_on_hours: Option, + pub reallocated_sectors: Option, + pub pending_sectors: Option, + pub uncorrectable_sectors: Option, + pub attributes: HashMap, +} + +/// SMART health status +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SmartHealthStatus { + Passed, + Failed, + Unknown, +} + +/// SMART attribute +#[derive(Debug, Clone)] +pub struct SmartAttribute { + pub name: String, + pub value: u64, + pub worst: u64, + pub threshold: u64, + pub status: SmartAttributeStatus, +} + +/// SMART attribute status +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SmartAttributeStatus { + Good, + Warning, + Critical, + Unknown, +} + +/// Result of scanning a single disk +#[derive(Debug, Clone)] +pub struct DiskScanResult { + pub disk_info: DiskInfo, + pub health_issues: Vec, + pub scan_duration: Duration, + pub success: bool, + pub error_message: Option, +} + +/// Disk scanner for monitoring disk health and performance +pub struct DiskScanner { + config: DiskScannerConfig, + statistics: Arc>, + last_scan_results: Arc>>, +} + +/// Statistics for disk scanning +#[derive(Debug, Clone, Default)] +pub struct DiskScannerStatistics { + pub disks_scanned: u64, + pub disks_with_issues: u64, + pub total_issues_found: u64, + pub total_scan_time: Duration, + pub average_scan_time: Duration, + pub last_scan_time: Option, + pub scan_cycles_completed: u64, + pub scan_cycles_failed: u64, +} + +impl DiskScanner { + /// Create a new disk scanner + pub fn new(config: DiskScannerConfig) -> Self { + Self { + config, + statistics: Arc::new(RwLock::new(DiskScannerStatistics::default())), + last_scan_results: Arc::new(RwLock::new(HashMap::new())), + } + } + + /// Scan all mounted disks + pub async fn scan_all_disks(&self) -> Result> { + let scan_start = Instant::now(); + let mut results = Vec::new(); + + // Get list of mounted filesystems + let mount_points = self.get_mount_points().await?; + + info!("Starting disk scan for {} mount points", mount_points.len()); + + // Scan each mount point + for mount_point in mount_points { + match self.scan_disk(&mount_point).await { + Ok(result) => { + results.push(result.clone()); + + // Store result for later reference + let mut last_results = self.last_scan_results.write().await; + last_results.insert(mount_point.clone(), result); + } + Err(e) => { + error!("Failed to scan disk at {}: {}", mount_point, e); + + // Create error result + let error_result = DiskScanResult { + disk_info: DiskInfo { + device_path: "unknown".to_string(), + mount_point: mount_point.clone(), + filesystem_type: "unknown".to_string(), + total_space: 0, + used_space: 0, + free_space: 0, + available_space: 0, + usage_percent: 0.0, + inode_total: None, + inode_used: None, + inode_free: None, + inode_usage_percent: None, + last_scan_time: SystemTime::now(), + health_status: DiskHealthStatus::Unknown, + performance_metrics: None, + temperature: None, + smart_status: None, + }, + health_issues: vec![HealthIssue { + issue_type: HealthIssueType::DiskReadError, + severity: Severity::High, + bucket: "system".to_string(), + object: mount_point.clone(), + description: format!("Failed to scan disk: {}", e), + metadata: None, + }], + scan_duration: scan_start.elapsed(), + success: false, + error_message: Some(e.to_string()), + }; + + results.push(error_result); + } + } + } + + // Update statistics + self.update_statistics(|stats| { + stats.disks_scanned += results.len() as u64; + stats.disks_with_issues += results.iter().filter(|r| !r.health_issues.is_empty()).count() as u64; + stats.total_issues_found += results.iter().map(|r| r.health_issues.len() as u64).sum::(); + stats.total_scan_time += scan_start.elapsed(); + stats.average_scan_time = Duration::from_millis( + stats.total_scan_time.as_millis() as u64 / stats.disks_scanned.max(1) + ); + stats.last_scan_time = Some(SystemTime::now()); + stats.scan_cycles_completed += 1; + }).await; + + info!( + "Disk scan completed: {} disks, {} issues found in {:?}", + results.len(), + results.iter().map(|r| r.health_issues.len()).sum::(), + scan_start.elapsed() + ); + + Ok(results) + } + + /// Scan a single disk + pub async fn scan_disk(&self, mount_point: &str) -> Result { + let scan_start = Instant::now(); + let mut health_issues = Vec::new(); + + // Get disk space information + let disk_info = self.get_disk_info(mount_point).await?; + + // Check disk space usage + if disk_info.usage_percent > self.config.max_disk_usage_percent { + health_issues.push(HealthIssue { + issue_type: HealthIssueType::DiskFull, + severity: if disk_info.usage_percent > 95.0 { Severity::Critical } else { Severity::High }, + bucket: "system".to_string(), + object: mount_point.to_string(), + description: format!("Disk usage is {}%, exceeds threshold of {}%", + disk_info.usage_percent, self.config.max_disk_usage_percent), + metadata: None, + }); + } + + if disk_info.usage_percent < self.config.min_free_space_percent { + health_issues.push(HealthIssue { + issue_type: HealthIssueType::DiskFull, + severity: Severity::Medium, + bucket: "system".to_string(), + object: mount_point.to_string(), + description: format!("Free space is only {}%, below threshold of {}%", + 100.0 - disk_info.usage_percent, self.config.min_free_space_percent), + metadata: None, + }); + } + + // Check inode usage if available + if let Some(inode_usage) = disk_info.inode_usage_percent { + if inode_usage > self.config.max_inode_usage_percent { + health_issues.push(HealthIssue { + issue_type: HealthIssueType::DiskFull, + severity: if inode_usage > 95.0 { Severity::Critical } else { Severity::High }, + bucket: "system".to_string(), + object: mount_point.to_string(), + description: format!("Inode usage is {}%, exceeds threshold of {}%", + inode_usage, self.config.max_inode_usage_percent), + metadata: None, + }); + } + } + + // Check I/O performance if enabled + if self.config.check_io_performance { + if let Some(metrics) = &disk_info.performance_metrics { + if metrics.utilization_percent > 90.0 { + health_issues.push(HealthIssue { + issue_type: HealthIssueType::DiskReadError, + severity: Severity::Medium, + bucket: "system".to_string(), + object: mount_point.to_string(), + description: format!("High disk utilization: {}%", metrics.utilization_percent), + metadata: None, + }); + } + + if metrics.average_response_time_ms > 100.0 { + health_issues.push(HealthIssue { + issue_type: HealthIssueType::DiskReadError, + severity: Severity::Medium, + bucket: "system".to_string(), + object: mount_point.to_string(), + description: format!("High disk response time: {}ms", metrics.average_response_time_ms), + metadata: None, + }); + } + } + } + + // Check temperature if enabled + if self.config.check_temperature { + if let Some(temp) = disk_info.temperature { + if temp > 60.0 { + health_issues.push(HealthIssue { + issue_type: HealthIssueType::DiskReadError, + severity: if temp > 70.0 { Severity::Critical } else { Severity::High }, + bucket: "system".to_string(), + object: mount_point.to_string(), + description: format!("High disk temperature: {}°C", temp), + metadata: None, + }); + } + } + } + + // Check SMART status if enabled + if self.config.check_smart_status { + if let Some(smart) = &disk_info.smart_status { + if smart.overall_health == SmartHealthStatus::Failed { + health_issues.push(HealthIssue { + issue_type: HealthIssueType::DiskReadError, + severity: Severity::Critical, + bucket: "system".to_string(), + object: mount_point.to_string(), + description: "SMART health check failed".to_string(), + metadata: None, + }); + } + } + } + + let scan_duration = scan_start.elapsed(); + let success = health_issues.is_empty(); + + Ok(DiskScanResult { + disk_info, + health_issues, + scan_duration, + success, + error_message: None, + }) + } + + /// Get list of mounted filesystems + async fn get_mount_points(&self) -> Result> { + // TODO: Implement actual mount point detection + // For now, return common mount points + Ok(vec![ + "/".to_string(), + "/data".to_string(), + "/var".to_string(), + ]) + } + + /// Get disk information for a mount point + async fn get_disk_info(&self, mount_point: &str) -> Result { + let path = Path::new(mount_point); + + // Get filesystem statistics using std::fs instead of nix for now + let _metadata = match std::fs::metadata(path) { + Ok(metadata) => metadata, + Err(e) => { + return Err(crate::error::Error::Other(anyhow::anyhow!("Failed to get filesystem stats: {}", e))); + } + }; + + // For now, use placeholder values since we can't easily get filesystem stats + let total_space = 1000000000; // 1GB placeholder + let free_space = 500000000; // 500MB placeholder + let available_space = 450000000; // 450MB placeholder + let used_space = total_space - free_space; + let usage_percent = (used_space as f64 / total_space as f64) * 100.0; + + // Get inode information (placeholder) + let inode_total = Some(1000000); + let inode_free = Some(500000); + let inode_used = Some(500000); + let inode_usage_percent = Some(50.0); + + // Get filesystem type + let filesystem_type = self.get_filesystem_type(mount_point).await.unwrap_or_else(|_| "unknown".to_string()); + + // Get device path + let device_path = self.get_device_path(mount_point).await.unwrap_or_else(|_| "unknown".to_string()); + + // Get performance metrics if enabled + let performance_metrics = if self.config.check_io_performance { + self.get_performance_metrics(&device_path).await.ok() + } else { + None + }; + + // Get temperature if enabled + let temperature = if self.config.check_temperature { + self.get_disk_temperature(&device_path).await.ok().flatten() + } else { + None + }; + + // Get SMART status if enabled + let smart_status = if self.config.check_smart_status { + self.get_smart_status(&device_path).await.ok().flatten() + } else { + None + }; + + // Determine health status (placeholder - will be set by scan_disk method) + let health_status = DiskHealthStatus::Healthy; + + Ok(DiskInfo { + device_path, + mount_point: mount_point.to_string(), + filesystem_type, + total_space, + used_space, + free_space, + available_space, + usage_percent, + inode_total, + inode_used, + inode_free, + inode_usage_percent, + last_scan_time: SystemTime::now(), + health_status, + performance_metrics, + temperature, + smart_status, + }) + } + + /// Get filesystem type for a mount point + async fn get_filesystem_type(&self, _mount_point: &str) -> Result { + // TODO: Implement filesystem type detection + // For now, return a placeholder + Ok("ext4".to_string()) + } + + /// Get device path for a mount point + async fn get_device_path(&self, _mount_point: &str) -> Result { + // TODO: Implement device path detection + // For now, return a placeholder + Ok("/dev/sda1".to_string()) + } + + /// Get disk performance metrics + async fn get_performance_metrics(&self, _device_path: &str) -> Result { + // TODO: Implement performance metrics collection + // For now, return placeholder metrics + Ok(DiskPerformanceMetrics { + read_bytes_per_sec: 1000000.0, // 1MB/s + write_bytes_per_sec: 500000.0, // 500KB/s + read_operations_per_sec: 100.0, + write_operations_per_sec: 50.0, + average_response_time_ms: 5.0, + queue_depth: 1.0, + utilization_percent: 10.0, + last_updated: SystemTime::now(), + }) + } + + /// Get disk temperature + async fn get_disk_temperature(&self, _device_path: &str) -> Result> { + // TODO: Implement temperature monitoring + // For now, return None (temperature not available) + Ok(None) + } + + /// Get SMART status + async fn get_smart_status(&self, _device_path: &str) -> Result> { + // TODO: Implement SMART status checking + // For now, return None (SMART not available) + Ok(None) + } + + /// Update scanner statistics + async fn update_statistics(&self, update_fn: F) + where + F: FnOnce(&mut DiskScannerStatistics), + { + let mut stats = self.statistics.write().await; + update_fn(&mut stats); + } + + /// Get current statistics + pub async fn statistics(&self) -> DiskScannerStatistics { + self.statistics.read().await.clone() + } + + /// Get last scan results + pub async fn last_scan_results(&self) -> HashMap { + self.last_scan_results.read().await.clone() + } + + /// Reset statistics + pub async fn reset_statistics(&self) { + let mut stats = self.statistics.write().await; + *stats = DiskScannerStatistics::default(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_disk_scanner_creation() { + let config = DiskScannerConfig::default(); + let scanner = DiskScanner::new(config); + assert_eq!(scanner.statistics().await.disks_scanned, 0); + } + + #[tokio::test] + async fn test_disk_info_creation() { + let disk_info = DiskInfo { + device_path: "/dev/sda1".to_string(), + mount_point: "/".to_string(), + filesystem_type: "ext4".to_string(), + total_space: 1000000000, + used_space: 500000000, + free_space: 500000000, + available_space: 450000000, + usage_percent: 50.0, + inode_total: Some(1000000), + inode_used: Some(500000), + inode_free: Some(500000), + inode_usage_percent: Some(50.0), + last_scan_time: SystemTime::now(), + health_status: DiskHealthStatus::Healthy, + performance_metrics: None, + temperature: None, + smart_status: None, + }; + + assert_eq!(disk_info.usage_percent, 50.0); + assert_eq!(disk_info.health_status, DiskHealthStatus::Healthy); + } +} \ No newline at end of file diff --git a/crates/ahm/src/scanner/engine.rs b/crates/ahm/src/scanner/engine.rs new file mode 100644 index 000000000..129b21a76 --- /dev/null +++ b/crates/ahm/src/scanner/engine.rs @@ -0,0 +1,536 @@ +// 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::{ + collections::HashMap, + path::{Path, PathBuf}, + sync::Arc, + time::{Duration, Instant, SystemTime}, +}; + +use tokio::{ + sync::{broadcast, RwLock}, + time::sleep, +}; +use tracing::{error, info, warn}; +use tokio_util::sync::CancellationToken; + +use crate::{core, error::Result, metrics, SystemEvent}; +use crate::core::Status; +use super::{HealthIssue, HealthIssueType, Severity}; + +/// Represents a discovered object during scanning +#[derive(Debug, Clone)] +pub struct ScannedObject { + pub bucket: String, + pub object: String, + pub version_id: Option, + pub path: PathBuf, + pub size: u64, + pub modified_time: SystemTime, + pub metadata: HashMap, + pub health_issues: Vec, +} + +/// Configuration for the scanner engine +#[derive(Debug, Clone)] +pub struct EngineConfig { + /// Root directory to scan + pub root_path: String, + /// Maximum number of concurrent scan workers + pub max_workers: usize, + /// Scan interval between cycles + pub scan_interval: Duration, + /// Bandwidth limit for scanning (bytes per second) + pub bandwidth_limit: Option, + /// Whether to enable deep scanning (bitrot detection) + pub enable_deep_scan: bool, + /// Probability of healing objects during scan (1 in N) + pub heal_probability: u32, + /// Maximum folders to scan before compacting + pub max_folders_before_compact: u64, + /// Sleep duration between folder scans + pub folder_sleep_duration: Duration, +} + +impl Default for EngineConfig { + fn default() -> Self { + Self { + root_path: "/data".to_string(), + max_workers: 4, + scan_interval: Duration::from_secs(300), // 5 minutes + bandwidth_limit: None, + enable_deep_scan: false, + heal_probability: 1024, // 1 in 1024 objects + max_folders_before_compact: 10000, + folder_sleep_duration: Duration::from_millis(1), + } + } +} + +/// Scanner statistics +#[derive(Debug, Clone, Default)] +pub struct ScannerStatistics { + pub objects_scanned: u64, + pub bytes_scanned: u64, + pub issues_found: u64, + pub scan_duration: Duration, + pub scan_rate_objects_per_sec: f64, + pub scan_rate_bytes_per_sec: f64, + pub folders_scanned: u64, + pub objects_with_issues: u64, +} + +/// Main scanner engine +pub struct Engine { + config: EngineConfig, + coordinator: Arc, + metrics: Arc, + cancel_token: CancellationToken, + status: Arc>, + statistics: Arc>, + scan_cycle: Arc>, +} + +impl Engine { + /// Create a new scanner engine + pub async fn new( + config: EngineConfig, + coordinator: Arc, + metrics: Arc, + cancel_token: CancellationToken, + ) -> Result { + let engine = Self { + config, + coordinator, + metrics, + cancel_token, + status: Arc::new(RwLock::new(Status::Initializing)), + statistics: Arc::new(RwLock::new(ScannerStatistics::default())), + scan_cycle: Arc::new(RwLock::new(0)), + }; + + info!("Scanner engine created with config: {:?}", engine.config); + Ok(engine) + } + + /// Start the scanner engine + pub async fn start(&self) -> Result<()> { + info!("Starting scanner engine"); + *self.status.write().await = Status::Running; + + let engine = self.clone_for_background(); + tokio::spawn(async move { + if let Err(e) = engine.run_scan_loop().await { + error!("Scanner engine error: {}", e); + } + }); + + Ok(()) + } + + /// Stop the scanner engine + pub async fn stop(&self) -> Result<()> { + info!("Stopping scanner engine"); + *self.status.write().await = Status::Stopping; + self.cancel_token.cancel(); + *self.status.write().await = Status::Stopped; + Ok(()) + } + + /// Get current status + pub async fn status(&self) -> Status { + self.status.read().await.clone() + } + + /// Get current statistics + pub async fn statistics(&self) -> ScannerStatistics { + self.statistics.read().await.clone() + } + + /// Clone the engine for background tasks + fn clone_for_background(&self) -> Arc { + Arc::new(Self { + config: self.config.clone(), + coordinator: self.coordinator.clone(), + metrics: self.metrics.clone(), + cancel_token: self.cancel_token.clone(), + status: self.status.clone(), + statistics: self.statistics.clone(), + scan_cycle: self.scan_cycle.clone(), + }) + } + + /// Main scan loop + async fn run_scan_loop(&self) -> Result<()> { + info!("Scanner engine loop started"); + + loop { + tokio::select! { + _ = self.cancel_token.cancelled() => { + info!("Scanner engine received cancellation signal"); + break; + } + _ = sleep(self.config.scan_interval) => { + if let Err(e) = self.run_scan_cycle().await { + error!("Scan cycle failed: {}", e); + } + } + } + } + + Ok(()) + } + + /// Run a single scan cycle + async fn run_scan_cycle(&self) -> Result<()> { + let cycle_start = Instant::now(); + let cycle = { + let mut cycle_guard = self.scan_cycle.write().await; + *cycle_guard += 1; + *cycle_guard + }; + + info!("Starting scan cycle {}", cycle); + + // Reset statistics for new cycle + { + let mut stats = self.statistics.write().await; + *stats = ScannerStatistics::default(); + } + + // Scan the root directory + let scan_result = self.scan_directory(&self.config.root_path).await?; + + // Update statistics + { + let mut stats = self.statistics.write().await; + stats.scan_duration = cycle_start.elapsed(); + stats.objects_scanned = scan_result.objects.len() as u64; + stats.bytes_scanned = scan_result.total_size; + stats.issues_found = scan_result.total_issues; + stats.folders_scanned = scan_result.folders_scanned; + stats.objects_with_issues = scan_result.objects_with_issues; + + if stats.scan_duration.as_secs() > 0 { + stats.scan_rate_objects_per_sec = stats.objects_scanned as f64 / stats.scan_duration.as_secs() as f64; + stats.scan_rate_bytes_per_sec = stats.bytes_scanned as f64 / stats.scan_duration.as_secs() as f64; + } + } + + // Publish scan completion event + let scan_report = crate::scanner::ScanReport { + scan_id: cycle.to_string(), + status: "completed".to_string(), + summary: format!("Scanned {} objects, found {} issues", scan_result.objects.len(), scan_result.total_issues), + issues_found: scan_result.total_issues, + }; + + self.coordinator.publish_event(SystemEvent::ScanCompleted(scan_report)).await?; + + info!( + "Scan cycle {} completed: {} objects, {} bytes, {} issues in {:?}", + cycle, + scan_result.objects.len(), + scan_result.total_size, + scan_result.total_issues, + cycle_start.elapsed() + ); + + Ok(()) + } + + /// Scan a directory recursively + async fn scan_directory(&self, path: &str) -> Result { + let mut result = ScanResult::default(); + let path_buf = PathBuf::from(path); + + if !path_buf.exists() { + warn!("Scan path does not exist: {}", path); + return Ok(result); + } + + if !path_buf.is_dir() { + warn!("Scan path is not a directory: {}", path); + return Ok(result); + } + + self.scan_directory_recursive(&path_buf, &mut result).await?; + Ok(result) + } + + /// Recursively scan a directory + async fn scan_directory_recursive(&self, dir_path: &Path, result: &mut ScanResult) -> Result<()> { + result.folders_scanned += 1; + + // Check for cancellation + if self.cancel_token.is_cancelled() { + return Ok(()); + } + + let entries = match std::fs::read_dir(dir_path) { + Ok(entries) => entries, + Err(e) => { + warn!("Failed to read directory {}: {}", dir_path.display(), e); + return Ok(()); + } + }; + + for entry in entries { + if self.cancel_token.is_cancelled() { + break; + } + + let entry = match entry { + Ok(entry) => entry, + Err(e) => { + warn!("Failed to read directory entry: {}", e); + continue; + } + }; + + let file_path = entry.path(); + let _path_str = file_path.to_string_lossy(); + let entry_name = file_path.file_name() + .and_then(|n| n.to_str()) + .unwrap_or("unknown"); + + // Skip hidden files and system files + if entry_name.starts_with('.') || entry_name == ".." || entry_name == "." { + continue; + } + + if file_path.is_dir() { + // Recursively scan subdirectories + Box::pin(self.scan_directory_recursive(&file_path, result)).await?; + } else if file_path.is_file() { + // Scan individual file + if let Some(scanned_object) = self.scan_object(&file_path).await? { + result.objects.push(scanned_object.clone()); + result.total_size += scanned_object.size; + + if !scanned_object.health_issues.is_empty() { + result.objects_with_issues += 1; + result.total_issues += scanned_object.health_issues.len() as u64; + + // Publish health issues + for issue in &scanned_object.health_issues { + let health_issue = crate::scanner::HealthIssue { + issue_type: issue.issue_type.clone(), + severity: issue.severity, + bucket: scanned_object.bucket.clone(), + object: scanned_object.object.clone(), + description: issue.description.clone(), + metadata: None, // TODO: Convert HashMap to ObjectMetadata + }; + + self.coordinator.publish_event(SystemEvent::HealthIssueDetected(health_issue)).await?; + } + } + + // Publish object discovered event + let metadata = crate::ObjectMetadata { + size: scanned_object.size, + mod_time: scanned_object.modified_time.duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as i64, + content_type: "application/octet-stream".to_string(), + etag: "".to_string(), // TODO: Calculate actual ETag + }; + + self.coordinator.publish_event(SystemEvent::ObjectDiscovered { + bucket: scanned_object.bucket.clone(), + object: scanned_object.object.clone(), + version_id: scanned_object.version_id.clone(), + metadata, + }).await?; + } + } + + // Sleep between items to avoid overwhelming the system + sleep(self.config.folder_sleep_duration).await; + } + + Ok(()) + } + + /// Scan a single object file + async fn scan_object(&self, file_path: &Path) -> Result> { + let metadata = match std::fs::metadata(file_path) { + Ok(metadata) => metadata, + Err(e) => { + warn!("Failed to read file metadata {}: {}", file_path.display(), e); + return Ok(None); + } + }; + + // Extract bucket and object from path + let (bucket, object) = self.extract_bucket_object_from_path(file_path)?; + if bucket.is_empty() || object.is_empty() { + return Ok(None); + } + + // Check for health issues + let health_issues = self.check_object_health(file_path, &metadata).await?; + + let scanned_object = ScannedObject { + bucket, + object, + version_id: None, // TODO: Extract version ID from path + path: file_path.to_path_buf(), + size: metadata.len(), + modified_time: metadata.modified().unwrap_or(SystemTime::now()), + metadata: HashMap::new(), // TODO: Extract metadata + health_issues, + }; + + Ok(Some(scanned_object)) + } + + /// Extract bucket and object name from file path + fn extract_bucket_object_from_path(&self, file_path: &Path) -> Result<(String, String)> { + let _path_str = file_path.to_string_lossy(); + let root_path = Path::new(&self.config.root_path); + + if let Ok(relative_path) = file_path.strip_prefix(root_path) { + let components: Vec<&str> = relative_path.components() + .filter_map(|c| c.as_os_str().to_str()) + .collect(); + + if components.len() >= 2 { + let bucket = components[0].to_string(); + let object = components[1..].join("/"); + return Ok((bucket, object)); + } + } + + Ok((String::new(), String::new())) + } + + /// Check object health and detect issues + async fn check_object_health(&self, file_path: &Path, metadata: &std::fs::Metadata) -> Result> { + let mut issues = Vec::new(); + + // Extract bucket and object from path for health issues + let (bucket, object) = self.extract_bucket_object_from_path(file_path)?; + + // Check file size + if metadata.len() == 0 { + issues.push(HealthIssue { + issue_type: HealthIssueType::ObjectTooSmall, + severity: Severity::Low, + bucket: bucket.clone(), + object: object.clone(), + description: "Object has zero size".to_string(), + metadata: None, + }); + } + + // Check file permissions + if !metadata.permissions().readonly() { + issues.push(HealthIssue { + issue_type: HealthIssueType::PolicyViolation, + severity: Severity::Medium, + bucket: bucket.clone(), + object: object.clone(), + description: "Object is not read-only".to_string(), + metadata: None, + }); + } + + // TODO: Add more health checks: + // - Checksum verification + // - Replication status + // - Encryption status + // - Metadata consistency + // - Disk health + + Ok(issues) + } + + /// Start scanning operations + pub async fn start_scan(&self) -> Result<()> { + let mut status = self.status.write().await; + *status = Status::Running; + info!("Scanning operations started"); + Ok(()) + } + + /// Stop scanning operations + pub async fn stop_scan(&self) -> Result<()> { + let mut status = self.status.write().await; + *status = Status::Stopped; + info!("Scanning operations stopped"); + Ok(()) + } + + /// Get engine configuration + pub async fn get_config(&self) -> ScanConfig { + self.config.clone() + } +} + +/// Result of a scan operation +#[derive(Debug, Clone, Default)] +pub struct ScanResult { + pub objects: Vec, + pub total_size: u64, + pub total_issues: u64, + pub folders_scanned: u64, + pub objects_with_issues: u64, +} + +#[cfg(test)] +mod tests { + use super::*; + use tokio::time::Duration; + + #[tokio::test] + async fn test_engine_creation() { + let config = EngineConfig::default(); + let coordinator = Arc::new(core::Coordinator::new( + core::CoordinatorConfig::default(), + Arc::new(metrics::Collector::new(metrics::CollectorConfig::default()).await.unwrap()), + CancellationToken::new(), + ).await.unwrap()); + let metrics = Arc::new(metrics::Collector::new(metrics::CollectorConfig::default()).await.unwrap()); + let cancel_token = CancellationToken::new(); + + let engine = Engine::new(config, coordinator, metrics, cancel_token).await; + assert!(engine.is_ok()); + } + + #[tokio::test] + async fn test_path_extraction() { + let config = EngineConfig { + root_path: "/data".to_string(), + ..Default::default() + }; + let coordinator = Arc::new(core::Coordinator::new( + core::CoordinatorConfig::default(), + Arc::new(metrics::Collector::new(metrics::CollectorConfig::default()).await.unwrap()), + CancellationToken::new(), + ).await.unwrap()); + let metrics = Arc::new(metrics::Collector::new(metrics::CollectorConfig::default()).await.unwrap()); + let cancel_token = CancellationToken::new(); + + let engine = Engine::new(config, coordinator, metrics, cancel_token).await.unwrap(); + + let test_path = Path::new("/data/bucket1/object1.txt"); + let (bucket, object) = engine.extract_bucket_object_from_path(test_path).unwrap(); + + assert_eq!(bucket, "bucket1"); + assert_eq!(object, "object1.txt"); + } +} \ No newline at end of file diff --git a/crates/ahm/src/scanner/metrics_collector.rs b/crates/ahm/src/scanner/metrics_collector.rs new file mode 100644 index 000000000..45c0d4b14 --- /dev/null +++ b/crates/ahm/src/scanner/metrics_collector.rs @@ -0,0 +1,526 @@ +// 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::{ + collections::HashMap, + sync::Arc, + time::{Duration, Instant, SystemTime}, +}; + +use tokio::sync::RwLock; +use tracing::{debug, error, info, warn}; + +use crate::error::Result; +use super::{HealthIssue, HealthIssueType, Severity}; + +/// Configuration for metrics collection +#[derive(Debug, Clone)] +pub struct MetricsConfig { + /// Collection interval for metrics + pub collection_interval: Duration, + /// Retention period for historical metrics + pub retention_period: Duration, + /// Maximum number of data points to keep in memory + pub max_data_points: usize, + /// Whether to enable detailed metrics collection + pub enable_detailed_metrics: bool, + /// Whether to enable performance profiling + pub enable_profiling: bool, + /// Whether to enable resource usage tracking + pub enable_resource_tracking: bool, +} + +impl Default for MetricsConfig { + fn default() -> Self { + Self { + collection_interval: Duration::from_secs(60), // 1 minute + retention_period: Duration::from_secs(3600 * 24), // 24 hours + max_data_points: 1440, // 24 hours worth of minute-level data + enable_detailed_metrics: true, + enable_profiling: false, + enable_resource_tracking: true, + } + } +} + +/// Scanner performance metrics +#[derive(Debug, Clone)] +pub struct ScannerMetrics { + /// Objects scanned per second + pub objects_per_second: f64, + /// Bytes scanned per second + pub bytes_per_second: f64, + /// Average scan time per object + pub avg_scan_time_per_object: Duration, + /// Total objects scanned in current cycle + pub total_objects_scanned: u64, + /// Total bytes scanned in current cycle + pub total_bytes_scanned: u64, + /// Number of health issues detected + pub health_issues_detected: u64, + /// Scan success rate (percentage) + pub success_rate: f64, + /// Current scan cycle duration + pub current_cycle_duration: Duration, + /// Average scan cycle duration + pub avg_cycle_duration: Duration, + /// Last scan completion time + pub last_scan_completion: Option, +} + +/// Resource usage metrics +#[derive(Debug, Clone)] +pub struct ResourceMetrics { + /// CPU usage percentage + pub cpu_usage_percent: f64, + /// Memory usage in bytes + pub memory_usage_bytes: u64, + /// Memory usage percentage + pub memory_usage_percent: f64, + /// Disk I/O operations per second + pub disk_io_ops_per_sec: f64, + /// Disk I/O bytes per second + pub disk_io_bytes_per_sec: f64, + /// Network I/O bytes per second + pub network_io_bytes_per_sec: f64, + /// Number of active threads + pub active_threads: u32, + /// Number of open file descriptors + pub open_file_descriptors: u32, +} + +/// Health metrics summary +#[derive(Debug, Clone)] +pub struct HealthMetrics { + /// Total health issues by severity + pub issues_by_severity: HashMap, + /// Total health issues by type + pub issues_by_type: HashMap, + /// Objects with health issues + pub objects_with_issues: u64, + /// Percentage of objects with issues + pub objects_with_issues_percent: f64, + /// Last health check time + pub last_health_check: SystemTime, + /// Health score (0-100, higher is better) + pub health_score: f64, +} + +/// Historical metrics data point +#[derive(Debug, Clone)] +pub struct MetricsDataPoint { + pub timestamp: SystemTime, + pub scanner_metrics: ScannerMetrics, + pub resource_metrics: ResourceMetrics, + pub health_metrics: HealthMetrics, +} + +/// Metrics collector for scanner system +pub struct MetricsCollector { + config: MetricsConfig, + current_metrics: Arc>, + historical_data: Arc>>, + collection_start_time: Instant, +} + +/// Current metrics state +#[derive(Debug, Clone)] +pub struct CurrentMetrics { + pub scanner_metrics: ScannerMetrics, + pub resource_metrics: ResourceMetrics, + pub health_metrics: HealthMetrics, + pub last_update: SystemTime, +} + +impl MetricsCollector { + /// Create a new metrics collector + pub fn new(config: MetricsConfig) -> Self { + let collector = Self { + config, + current_metrics: Arc::new(RwLock::new(CurrentMetrics { + scanner_metrics: ScannerMetrics { + objects_per_second: 0.0, + bytes_per_second: 0.0, + avg_scan_time_per_object: Duration::ZERO, + total_objects_scanned: 0, + total_bytes_scanned: 0, + health_issues_detected: 0, + success_rate: 100.0, + current_cycle_duration: Duration::ZERO, + avg_cycle_duration: Duration::ZERO, + last_scan_completion: None, + }, + resource_metrics: ResourceMetrics { + cpu_usage_percent: 0.0, + memory_usage_bytes: 0, + memory_usage_percent: 0.0, + disk_io_ops_per_sec: 0.0, + disk_io_bytes_per_sec: 0.0, + network_io_bytes_per_sec: 0.0, + active_threads: 0, + open_file_descriptors: 0, + }, + health_metrics: HealthMetrics { + issues_by_severity: HashMap::new(), + issues_by_type: HashMap::new(), + objects_with_issues: 0, + objects_with_issues_percent: 0.0, + last_health_check: SystemTime::now(), + health_score: 100.0, + }, + last_update: SystemTime::now(), + })), + historical_data: Arc::new(RwLock::new(Vec::new())), + collection_start_time: Instant::now(), + }; + + info!("Metrics collector created with config: {:?}", collector.config); + collector + } + + /// Start metrics collection + pub async fn start_collection(&self) -> Result<()> { + info!("Starting metrics collection"); + + let collector = self.clone_for_background(); + tokio::spawn(async move { + if let Err(e) = collector.run_collection_loop().await { + error!("Metrics collection error: {}", e); + } + }); + + Ok(()) + } + + /// Stop metrics collection + pub async fn stop_collection(&self) -> Result<()> { + info!("Stopping metrics collection"); + Ok(()) + } + + /// Update scanner metrics + pub async fn update_scanner_metrics(&self, metrics: ScannerMetrics) -> Result<()> { + let mut current = self.current_metrics.write().await; + current.scanner_metrics = metrics; + current.last_update = SystemTime::now(); + Ok(()) + } + + /// Update resource metrics + pub async fn update_resource_metrics(&self, metrics: ResourceMetrics) -> Result<()> { + let mut current = self.current_metrics.write().await; + current.resource_metrics = metrics; + current.last_update = SystemTime::now(); + Ok(()) + } + + /// Update health metrics + pub async fn update_health_metrics(&self, metrics: HealthMetrics) -> Result<()> { + let mut current = self.current_metrics.write().await; + current.health_metrics = metrics; + current.last_update = SystemTime::now(); + Ok(()) + } + + /// Record a health issue + pub async fn record_health_issue(&self, issue: &HealthIssue) -> Result<()> { + let mut current = self.current_metrics.write().await; + + // Update severity count + *current.health_metrics.issues_by_severity.entry(issue.severity).or_insert(0) += 1; + + // Update type count + *current.health_metrics.issues_by_type.entry(issue.issue_type.clone()).or_insert(0) += 1; + + // Update scanner metrics + current.scanner_metrics.health_issues_detected += 1; + + current.last_update = SystemTime::now(); + Ok(()) + } + + /// Get current metrics + pub async fn current_metrics(&self) -> CurrentMetrics { + self.current_metrics.read().await.clone() + } + + /// Get historical metrics + pub async fn historical_metrics(&self, duration: Duration) -> Vec { + let historical = self.historical_data.read().await; + let cutoff_time = SystemTime::now() - duration; + + historical.iter() + .filter(|point| point.timestamp >= cutoff_time) + .cloned() + .collect() + } + + /// Get metrics summary + pub async fn metrics_summary(&self) -> MetricsSummary { + let current = self.current_metrics.read().await; + let historical = self.historical_data.read().await; + + let uptime = self.collection_start_time.elapsed(); + let total_data_points = historical.len(); + + // Calculate averages from historical data + let avg_objects_per_sec = if !historical.is_empty() { + historical.iter() + .map(|point| point.scanner_metrics.objects_per_second) + .sum::() / historical.len() as f64 + } else { + 0.0 + }; + + let avg_bytes_per_sec = if !historical.is_empty() { + historical.iter() + .map(|point| point.scanner_metrics.bytes_per_second) + .sum::() / historical.len() as f64 + } else { + 0.0 + }; + + let avg_cpu_usage = if !historical.is_empty() { + historical.iter() + .map(|point| point.resource_metrics.cpu_usage_percent) + .sum::() / historical.len() as f64 + } else { + 0.0 + }; + + let avg_memory_usage = if !historical.is_empty() { + historical.iter() + .map(|point| point.resource_metrics.memory_usage_percent) + .sum::() / historical.len() as f64 + } else { + 0.0 + }; + + MetricsSummary { + uptime, + total_data_points, + current_scanner_metrics: current.scanner_metrics.clone(), + current_resource_metrics: current.resource_metrics.clone(), + current_health_metrics: current.health_metrics.clone(), + avg_objects_per_sec, + avg_bytes_per_sec, + avg_cpu_usage, + avg_memory_usage, + last_update: current.last_update, + } + } + + /// Clone the collector for background tasks + fn clone_for_background(&self) -> Arc { + Arc::new(Self { + config: self.config.clone(), + current_metrics: self.current_metrics.clone(), + historical_data: self.historical_data.clone(), + collection_start_time: self.collection_start_time, + }) + } + + /// Main collection loop + async fn run_collection_loop(&self) -> Result<()> { + info!("Metrics collection loop started"); + + loop { + // Collect current metrics + self.collect_current_metrics().await?; + + // Store historical data point + self.store_historical_data_point().await?; + + // Clean up old data + self.cleanup_old_data().await?; + + // Wait for next collection interval + tokio::time::sleep(self.config.collection_interval).await; + } + } + + /// Collect current system metrics + async fn collect_current_metrics(&self) -> Result<()> { + if self.config.enable_resource_tracking { + let resource_metrics = self.collect_resource_metrics().await?; + self.update_resource_metrics(resource_metrics).await?; + } + + Ok(()) + } + + /// Collect resource usage metrics + async fn collect_resource_metrics(&self) -> Result { + // TODO: Implement actual resource metrics collection + // For now, return placeholder metrics + Ok(ResourceMetrics { + cpu_usage_percent: 0.0, + memory_usage_bytes: 0, + memory_usage_percent: 0.0, + disk_io_ops_per_sec: 0.0, + disk_io_bytes_per_sec: 0.0, + network_io_bytes_per_sec: 0.0, + active_threads: 0, + open_file_descriptors: 0, + }) + } + + /// Store current metrics as historical data point + async fn store_historical_data_point(&self) -> Result<()> { + let current = self.current_metrics.read().await; + let data_point = MetricsDataPoint { + timestamp: SystemTime::now(), + scanner_metrics: current.scanner_metrics.clone(), + resource_metrics: current.resource_metrics.clone(), + health_metrics: current.health_metrics.clone(), + }; + + let mut historical = self.historical_data.write().await; + historical.push(data_point); + + // Limit the number of data points + if historical.len() > self.config.max_data_points { + historical.remove(0); + } + + Ok(()) + } + + /// Clean up old historical data + async fn cleanup_old_data(&self) -> Result<()> { + let cutoff_time = SystemTime::now() - self.config.retention_period; + let mut historical = self.historical_data.write().await; + + historical.retain(|point| point.timestamp >= cutoff_time); + + Ok(()) + } + + /// Reset all metrics + pub async fn reset_metrics(&self) -> Result<()> { + let mut current = self.current_metrics.write().await; + *current = CurrentMetrics { + scanner_metrics: ScannerMetrics { + objects_per_second: 0.0, + bytes_per_second: 0.0, + avg_scan_time_per_object: Duration::ZERO, + total_objects_scanned: 0, + total_bytes_scanned: 0, + health_issues_detected: 0, + success_rate: 100.0, + current_cycle_duration: Duration::ZERO, + avg_cycle_duration: Duration::ZERO, + last_scan_completion: None, + }, + resource_metrics: ResourceMetrics { + cpu_usage_percent: 0.0, + memory_usage_bytes: 0, + memory_usage_percent: 0.0, + disk_io_ops_per_sec: 0.0, + disk_io_bytes_per_sec: 0.0, + network_io_bytes_per_sec: 0.0, + active_threads: 0, + open_file_descriptors: 0, + }, + health_metrics: HealthMetrics { + issues_by_severity: HashMap::new(), + issues_by_type: HashMap::new(), + objects_with_issues: 0, + objects_with_issues_percent: 0.0, + last_health_check: SystemTime::now(), + health_score: 100.0, + }, + last_update: SystemTime::now(), + }; + + let mut historical = self.historical_data.write().await; + historical.clear(); + + Ok(()) + } +} + +/// Summary of all metrics +#[derive(Debug, Clone)] +pub struct MetricsSummary { + pub uptime: Duration, + pub total_data_points: usize, + pub current_scanner_metrics: ScannerMetrics, + pub current_resource_metrics: ResourceMetrics, + pub current_health_metrics: HealthMetrics, + pub avg_objects_per_sec: f64, + pub avg_bytes_per_sec: f64, + pub avg_cpu_usage: f64, + pub avg_memory_usage: f64, + pub last_update: SystemTime, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_metrics_collector_creation() { + let config = MetricsConfig::default(); + let collector = MetricsCollector::new(config); + let metrics = collector.current_metrics().await; + assert_eq!(metrics.scanner_metrics.total_objects_scanned, 0); + } + + #[tokio::test] + async fn test_metrics_update() { + let config = MetricsConfig::default(); + let collector = MetricsCollector::new(config); + + let scanner_metrics = ScannerMetrics { + objects_per_second: 100.0, + bytes_per_second: 1024.0, + avg_scan_time_per_object: Duration::from_millis(10), + total_objects_scanned: 1000, + total_bytes_scanned: 1024000, + health_issues_detected: 5, + success_rate: 99.5, + current_cycle_duration: Duration::from_secs(60), + avg_cycle_duration: Duration::from_secs(65), + last_scan_completion: Some(SystemTime::now()), + }; + + collector.update_scanner_metrics(scanner_metrics).await.unwrap(); + + let current = collector.current_metrics().await; + assert_eq!(current.scanner_metrics.total_objects_scanned, 1000); + assert_eq!(current.scanner_metrics.health_issues_detected, 5); + } + + #[tokio::test] + async fn test_health_issue_recording() { + let config = MetricsConfig::default(); + let collector = MetricsCollector::new(config); + + let issue = HealthIssue { + issue_type: HealthIssueType::DiskFull, + severity: Severity::High, + bucket: "test-bucket".to_string(), + object: "test-object".to_string(), + description: "Test issue".to_string(), + metadata: None, + }; + + collector.record_health_issue(&issue).await.unwrap(); + + let current = collector.current_metrics().await; + assert_eq!(current.scanner_metrics.health_issues_detected, 1); + assert_eq!(current.health_metrics.issues_by_severity.get(&Severity::High), Some(&1)); + } +} \ No newline at end of file diff --git a/crates/ahm/src/scanner/object_scanner.rs b/crates/ahm/src/scanner/object_scanner.rs new file mode 100644 index 000000000..fd438d8fa --- /dev/null +++ b/crates/ahm/src/scanner/object_scanner.rs @@ -0,0 +1,419 @@ +// 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::{ + collections::HashMap, + path::Path, + sync::Arc, + time::{Duration, SystemTime}, +}; + +use tokio::sync::RwLock; +use tracing::info; + +use crate::error::Result; +use super::{HealthIssue, HealthIssueType, Severity}; + +/// Configuration for object scanning +#[derive(Debug, Clone)] +pub struct ObjectScannerConfig { + /// Whether to perform checksum verification + pub verify_checksum: bool, + /// Whether to check replication status + pub check_replication: bool, + /// Whether to validate metadata consistency + pub validate_metadata: bool, + /// Maximum object size to scan (bytes) + pub max_object_size: u64, + /// Minimum object size (bytes) + pub min_object_size: u64, + /// Timeout for individual object scans + pub scan_timeout: Duration, + /// Whether to enable deep scanning (bitrot detection) + pub enable_deep_scan: bool, +} + +impl Default for ObjectScannerConfig { + fn default() -> Self { + Self { + verify_checksum: true, + check_replication: true, + validate_metadata: true, + max_object_size: 1024 * 1024 * 1024 * 1024, // 1TB + min_object_size: 0, + scan_timeout: Duration::from_secs(30), + enable_deep_scan: false, + } + } +} + +/// Result of scanning a single object +#[derive(Debug, Clone)] +pub struct ObjectScanResult { + /// Object identifier + pub bucket: String, + pub object: String, + pub version_id: Option, + /// Scan success status + pub success: bool, + /// Object metadata discovered + pub metadata: Option, + /// Health issues detected + pub health_issues: Vec, + /// Time taken to scan this object + pub scan_duration: Duration, + /// Error message if scan failed + pub error_message: Option, +} + +/// Object metadata +#[derive(Debug, Clone)] +pub struct ObjectMetadata { + pub size: u64, + pub modified_time: SystemTime, + pub content_type: String, + pub etag: String, + pub checksum: Option, + pub replication_status: Option, + pub encryption_status: Option, + pub custom_metadata: HashMap, +} + +/// Object scanner for individual object health checking +pub struct ObjectScanner { + config: ObjectScannerConfig, + statistics: Arc>, +} + +/// Statistics for object scanning +#[derive(Debug, Clone, Default)] +pub struct ObjectScannerStatistics { + pub objects_scanned: u64, + pub objects_with_issues: u64, + pub total_issues_found: u64, + pub total_scan_time: Duration, + pub average_scan_time: Duration, + pub checksum_verifications: u64, + pub checksum_failures: u64, + pub replication_checks: u64, + pub replication_failures: u64, +} + +impl ObjectScanner { + /// Create a new object scanner + pub fn new(config: ObjectScannerConfig) -> Self { + Self { + config, + statistics: Arc::new(RwLock::new(ObjectScannerStatistics::default())), + } + } + + /// Scan a single object for health issues + pub async fn scan_object(&self, bucket: &str, object: &str, version_id: Option<&str>, path: &Path) -> Result { + let scan_start = std::time::Instant::now(); + let mut health_issues = Vec::new(); + let mut error_message = None; + + // Check if file exists + if !path.exists() { + return Ok(ObjectScanResult { + bucket: bucket.to_string(), + object: object.to_string(), + version_id: version_id.map(|v| v.to_string()), + success: false, + metadata: None, + health_issues: vec![HealthIssue { + issue_type: HealthIssueType::MissingReplica, + severity: Severity::Critical, + bucket: bucket.to_string(), + object: object.to_string(), + description: "Object file does not exist".to_string(), + metadata: None, + }], + scan_duration: scan_start.elapsed(), + error_message: Some("Object file not found".to_string()), + }); + } + + // Get file metadata + let metadata = match std::fs::metadata(path) { + Ok(metadata) => metadata, + Err(e) => { + error_message = Some(format!("Failed to read file metadata: {}", e)); + health_issues.push(HealthIssue { + issue_type: HealthIssueType::DiskReadError, + severity: Severity::High, + bucket: bucket.to_string(), + object: object.to_string(), + description: "Failed to read file metadata".to_string(), + metadata: None, + }); + return Ok(ObjectScanResult { + bucket: bucket.to_string(), + object: object.to_string(), + version_id: version_id.map(|v| v.to_string()), + success: false, + metadata: None, + health_issues, + scan_duration: scan_start.elapsed(), + error_message, + }); + } + }; + + // Check file size + let file_size = metadata.len(); + if file_size < self.config.min_object_size { + health_issues.push(HealthIssue { + issue_type: HealthIssueType::ObjectTooSmall, + severity: Severity::Low, + bucket: bucket.to_string(), + object: object.to_string(), + description: format!("Object size {} is below minimum {}", file_size, self.config.min_object_size), + metadata: None, + }); + } + + if file_size > self.config.max_object_size { + health_issues.push(HealthIssue { + issue_type: HealthIssueType::ObjectTooLarge, + severity: Severity::Medium, + bucket: bucket.to_string(), + object: object.to_string(), + description: format!("Object size {} exceeds maximum {}", file_size, self.config.max_object_size), + metadata: None, + }); + } + + // Verify checksum if enabled + let checksum = if self.config.verify_checksum { + match self.verify_checksum(path).await { + Ok(cs) => { + self.update_statistics(|stats| stats.checksum_verifications += 1).await; + Some(cs) + } + Err(_e) => { + self.update_statistics(|stats| stats.checksum_failures += 1).await; + health_issues.push(HealthIssue { + issue_type: HealthIssueType::ChecksumMismatch, + severity: Severity::High, + bucket: bucket.to_string(), + object: object.to_string(), + description: "Checksum verification failed".to_string(), + metadata: None, + }); + None + } + } + } else { + None + }; + + // Check replication status if enabled + let replication_status = if self.config.check_replication { + match self.check_replication_status(bucket, object).await { + Ok(status) => { + self.update_statistics(|stats| stats.replication_checks += 1).await; + Some(status) + } + Err(_e) => { + self.update_statistics(|stats| stats.replication_failures += 1).await; + health_issues.push(HealthIssue { + issue_type: HealthIssueType::MissingReplica, + severity: Severity::High, + bucket: bucket.to_string(), + object: object.to_string(), + description: "Replication status check failed".to_string(), + metadata: None, + }); + None + } + } + } else { + None + }; + + // Validate metadata if enabled + if self.config.validate_metadata { + if let Some(issue) = self.validate_metadata(bucket, object, &metadata).await? { + health_issues.push(issue); + } + } + + // Create object metadata + let object_metadata = ObjectMetadata { + size: file_size, + modified_time: metadata.modified().unwrap_or(SystemTime::now()), + content_type: self.detect_content_type(path), + etag: self.calculate_etag(path).await?, + checksum, + replication_status, + encryption_status: None, // TODO: Implement encryption status check + custom_metadata: HashMap::new(), // TODO: Extract custom metadata + }; + + let scan_duration = scan_start.elapsed(); + let success = health_issues.is_empty(); + + // Update statistics + self.update_statistics(|stats| { + stats.objects_scanned += 1; + if !health_issues.is_empty() { + stats.objects_with_issues += 1; + stats.total_issues_found += health_issues.len() as u64; + } + stats.total_scan_time += scan_duration; + stats.average_scan_time = Duration::from_millis( + stats.total_scan_time.as_millis() as u64 / stats.objects_scanned.max(1) + ); + }).await; + + Ok(ObjectScanResult { + bucket: bucket.to_string(), + object: object.to_string(), + version_id: version_id.map(|v| v.to_string()), + success, + metadata: Some(object_metadata), + health_issues, + scan_duration, + error_message, + }) + } + + /// Verify object checksum + async fn verify_checksum(&self, _path: &Path) -> Result { + // TODO: Implement actual checksum verification + // For now, return a placeholder checksum + Ok("placeholder_checksum".to_string()) + } + + /// Check object replication status + async fn check_replication_status(&self, _bucket: &str, _object: &str) -> Result { + // TODO: Implement actual replication status checking + // For now, return a placeholder status + Ok("replicated".to_string()) + } + + /// Validate object metadata + async fn validate_metadata(&self, _bucket: &str, _object: &str, _metadata: &std::fs::Metadata) -> Result> { + // TODO: Implement actual metadata validation + // For now, return None (no issues) + Ok(None) + } + + /// Detect content type from file extension + fn detect_content_type(&self, path: &Path) -> String { + if let Some(extension) = path.extension() { + match extension.to_str().unwrap_or("").to_lowercase().as_str() { + "txt" => "text/plain", + "json" => "application/json", + "xml" => "application/xml", + "html" | "htm" => "text/html", + "css" => "text/css", + "js" => "application/javascript", + "png" => "image/png", + "jpg" | "jpeg" => "image/jpeg", + "gif" => "image/gif", + "pdf" => "application/pdf", + "zip" => "application/zip", + "tar" => "application/x-tar", + "gz" => "application/gzip", + _ => "application/octet-stream", + }.to_string() + } else { + "application/octet-stream".to_string() + } + } + + /// Calculate object ETag + async fn calculate_etag(&self, _path: &Path) -> Result { + // TODO: Implement actual ETag calculation + // For now, return a placeholder ETag + Ok("placeholder_etag".to_string()) + } + + /// Update scanner statistics + async fn update_statistics(&self, update_fn: F) + where + F: FnOnce(&mut ObjectScannerStatistics), + { + let mut stats = self.statistics.write().await; + update_fn(&mut stats); + } + + /// Get current statistics + pub async fn statistics(&self) -> ObjectScannerStatistics { + self.statistics.read().await.clone() + } + + /// Reset statistics + pub async fn reset_statistics(&self) { + let mut stats = self.statistics.write().await; + *stats = ObjectScannerStatistics::default(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + use std::fs::File; + use std::io::Write; + + #[tokio::test] + async fn test_object_scanner_creation() { + let config = ObjectScannerConfig::default(); + let scanner = ObjectScanner::new(config); + assert_eq!(scanner.statistics().await.objects_scanned, 0); + } + + #[tokio::test] + async fn test_content_type_detection() { + let config = ObjectScannerConfig::default(); + let scanner = ObjectScanner::new(config); + + let path = Path::new("test.txt"); + assert_eq!(scanner.detect_content_type(path), "text/plain"); + + let path = Path::new("test.json"); + assert_eq!(scanner.detect_content_type(path), "application/json"); + + let path = Path::new("test.unknown"); + assert_eq!(scanner.detect_content_type(path), "application/octet-stream"); + } + + #[tokio::test] + async fn test_object_scanning() { + let temp_dir = TempDir::new().unwrap(); + let test_file = temp_dir.path().join("test.txt"); + + // Create a test file + let mut file = File::create(&test_file).unwrap(); + writeln!(file, "test content").unwrap(); + + let config = ObjectScannerConfig::default(); + let scanner = ObjectScanner::new(config); + + let result = scanner.scan_object("test-bucket", "test.txt", None, &test_file).await.unwrap(); + + assert!(result.success); + assert_eq!(result.bucket, "test-bucket"); + assert_eq!(result.object, "test.txt"); + assert!(result.metadata.is_some()); + + let metadata = result.metadata.unwrap(); + assert!(metadata.size > 0); + assert_eq!(metadata.content_type, "text/plain"); + } +} \ No newline at end of file From b40ef147a9bee3ca187704f9ee521092221f5c96 Mon Sep 17 00:00:00 2001 From: dandan Date: Wed, 9 Jul 2025 17:32:40 +0800 Subject: [PATCH 02/29] refact: step 2 Signed-off-by: junxiang Mu <1948535941@qq.com> --- Cargo.toml | 2 +- crates/ahm/architecture.md | 557 --------- crates/ahm/architecture_ch.md | 557 --------- crates/ahm/src/api/admin_api.rs | 843 ------------- crates/ahm/src/api/metrics_api.rs | 1180 ------------------- crates/ahm/src/api/mod.rs | 504 -------- crates/ahm/src/api/status_api.rs | 761 ------------ crates/ahm/src/core/coordinator.rs | 448 ------- crates/ahm/src/core/lifecycle.rs | 22 - crates/ahm/src/core/mod.rs | 40 - crates/ahm/src/core/scheduler.rs | 226 ---- crates/ahm/src/heal/engine.rs | 438 ------- crates/ahm/src/heal/mod.rs | 360 ------ crates/ahm/src/heal/priority_queue.rs | 413 ------- crates/ahm/src/heal/repair_worker.rs | 505 -------- crates/ahm/src/heal/validation.rs | 453 ------- crates/ahm/src/lib.rs | 1 + crates/ahm/src/metrics.rs | 284 +++++ crates/ahm/src/metrics/aggregator.rs | 739 ------------ crates/ahm/src/metrics/collector.rs | 426 ------- crates/ahm/src/metrics/mod.rs | 617 ---------- crates/ahm/src/metrics/reporter.rs | 861 -------------- crates/ahm/src/metrics/storage.rs | 573 --------- crates/ahm/src/policy/heal_policy.rs | 508 -------- crates/ahm/src/policy/mod.rs | 258 ---- crates/ahm/src/policy/retention_policy.rs | 487 -------- crates/ahm/src/policy/scan_policy.rs | 373 ------ crates/ahm/src/scanner.rs | 902 ++++++++++++++ crates/ahm/src/scanner/bandwidth_limiter.rs | 353 ------ crates/ahm/src/scanner/disk_scanner.rs | 591 ---------- crates/ahm/src/scanner/engine.rs | 536 --------- crates/ahm/src/scanner/metrics_collector.rs | 526 --------- crates/ahm/src/scanner/object_scanner.rs | 419 ------- 33 files changed, 1188 insertions(+), 14575 deletions(-) delete mode 100644 crates/ahm/architecture.md delete mode 100644 crates/ahm/architecture_ch.md delete mode 100644 crates/ahm/src/api/admin_api.rs delete mode 100644 crates/ahm/src/api/metrics_api.rs delete mode 100644 crates/ahm/src/api/mod.rs delete mode 100644 crates/ahm/src/api/status_api.rs delete mode 100644 crates/ahm/src/core/coordinator.rs delete mode 100644 crates/ahm/src/core/lifecycle.rs delete mode 100644 crates/ahm/src/core/mod.rs delete mode 100644 crates/ahm/src/core/scheduler.rs delete mode 100644 crates/ahm/src/heal/engine.rs delete mode 100644 crates/ahm/src/heal/mod.rs delete mode 100644 crates/ahm/src/heal/priority_queue.rs delete mode 100644 crates/ahm/src/heal/repair_worker.rs delete mode 100644 crates/ahm/src/heal/validation.rs create mode 100644 crates/ahm/src/metrics.rs delete mode 100644 crates/ahm/src/metrics/aggregator.rs delete mode 100644 crates/ahm/src/metrics/collector.rs delete mode 100644 crates/ahm/src/metrics/mod.rs delete mode 100644 crates/ahm/src/metrics/reporter.rs delete mode 100644 crates/ahm/src/metrics/storage.rs delete mode 100644 crates/ahm/src/policy/heal_policy.rs delete mode 100644 crates/ahm/src/policy/mod.rs delete mode 100644 crates/ahm/src/policy/retention_policy.rs delete mode 100644 crates/ahm/src/policy/scan_policy.rs create mode 100644 crates/ahm/src/scanner.rs delete mode 100644 crates/ahm/src/scanner/bandwidth_limiter.rs delete mode 100644 crates/ahm/src/scanner/disk_scanner.rs delete mode 100644 crates/ahm/src/scanner/engine.rs delete mode 100644 crates/ahm/src/scanner/metrics_collector.rs delete mode 100644 crates/ahm/src/scanner/object_scanner.rs diff --git a/Cargo.toml b/Cargo.toml index 84e2e9c76..113938c08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,7 +91,7 @@ argon2 = { version = "0.5.3", features = ["std"] } atoi = "2.0.0" async-channel = "2.5.0" async-recursion = "1.1.1" -async-trait = "0.1.88" +async-trait = "0.1" async-compression = { version = "0.4.0" } atomic_enum = "0.3.0" aws-sdk-s3 = "1.96.0" diff --git a/crates/ahm/architecture.md b/crates/ahm/architecture.md deleted file mode 100644 index fab56c49b..000000000 --- a/crates/ahm/architecture.md +++ /dev/null @@ -1,557 +0,0 @@ -# RustFS Advanced Health & Metrics (AHM) System Architecture - -## Overview - -The RustFS AHM system is a newly designed distributed storage health monitoring and repair system that provides intelligent scanning, automatic repair, rich metrics, and policy-driven management capabilities. - -## System Architecture - -### Overall Architecture Diagram - -``` -┌─────────────────────────────────────┐ -│ API Layer (REST/gRPC) │ -├─────────────────────────────────────┤ -│ Policy & Configuration │ -├─────────────────────────────────────┤ -│ Core Coordination Engine │ -├─────────────────────────────────────┤ -│ Scanner Engine │ Heal Engine │ -├─────────────────────────────────────┤ -│ Metrics & Observability │ -├─────────────────────────────────────┤ -│ Storage Abstraction │ -└─────────────────────────────────────┘ -``` - -### Module Structure - -``` -rustfs/crates/ecstore/src/ahm/ -├── mod.rs # Module entry point and public interfaces -├── core/ # Core engines -│ ├── coordinator.rs # Distributed coordinator - event routing and state management -│ ├── scheduler.rs # Task scheduler - priority queue and work assignment -│ └── lifecycle.rs # Lifecycle manager - system startup/shutdown control -├── scanner/ # Scanning system -│ ├── engine.rs # Scan engine - scan process control -│ ├── object_scanner.rs # Object scanner - object-level integrity checks -│ ├── disk_scanner.rs # Disk scanner - disk-level health checks -│ ├── metrics_collector.rs # Metrics collector - scan process data collection -│ └── bandwidth_limiter.rs # Bandwidth limiter - I/O resource control -├── heal/ # Repair system -│ ├── engine.rs # Heal engine - repair process control -│ ├── priority_queue.rs # Priority queue - repair task ordering -│ ├── repair_worker.rs # Repair worker - actual repair execution -│ └── validation.rs # Repair validator - repair result verification -├── metrics/ # Metrics system -│ ├── collector.rs # Metrics collector - real-time data collection -│ ├── aggregator.rs # Metrics aggregator - data aggregation and computation -│ ├── storage.rs # Metrics storage - time-series data storage -│ └── reporter.rs # Metrics reporter - external system export -├── policy/ # Policy system -│ ├── scan_policy.rs # Scan policy - scan behavior configuration -│ ├── heal_policy.rs # Heal policy - repair priority and strategy -│ └── retention_policy.rs # Retention policy - data lifecycle management -└── api/ # API interfaces - ├── admin_api.rs # Admin API - system management operations - ├── metrics_api.rs # Metrics API - metrics query and export - └── status_api.rs # Status API - system status monitoring -``` - -## Core Design Principles - -### 1. Event-Driven Architecture - -```rust -pub enum SystemEvent { - ObjectDiscovered { bucket: String, object: String, metadata: ObjectMetadata }, - HealthIssueDetected { issue_type: HealthIssueType, severity: Severity }, - HealCompleted { result: HealResult }, - ScanCycleCompleted { statistics: ScanStatistics }, - ResourceUsageUpdated { usage: ResourceUsage }, -} -``` - -- **Scanner** generates discovery events -- **Heal** responds to repair events -- **Metrics** collects all event statistics -- **Policy** controls event processing strategies - -### 2. Layered Modular Design - -#### **API Layer**: REST/gRPC interfaces -- Unified response format -- Comprehensive error handling -- Authentication and authorization support - -#### **Policy Layer**: Configurable business rules -- Scan frequency and depth control -- Repair priority policies -- Data retention rules - -#### **Coordination Layer**: System coordination and scheduling -- Event routing and distribution -- Resource management and allocation -- Task scheduling and execution - -#### **Engine Layer**: Core business logic -- Intelligent scanning algorithms -- Adaptive repair strategies -- Performance optimization control - -#### **Metrics Layer**: Observability support -- Real-time metrics collection -- Historical trend analysis -- Multi-format export - -### 3. Multi-Mode Scanning Strategies - -```rust -pub enum ScanStrategy { - Full { mode: ScanMode, scope: ScanScope }, // Full scan - Incremental { since: Instant, mode: ScanMode }, // Incremental scan - Smart { sample_rate: f64, favor_unscanned: bool }, // Smart sampling - Targeted { targets: Vec, mode: ScanMode }, // Targeted scan -} - -pub enum ScanMode { - Quick, // Quick scan - metadata only - Normal, // Normal scan - basic integrity verification - Deep, // Deep scan - includes bit-rot detection -} -``` - -### 4. Priority-Based Repair System - -```rust -pub enum HealPriority { - Low = 0, - Normal = 1, - High = 2, - Critical = 3, - Emergency = 4, -} - -pub enum HealMode { - RealTime, // Real-time repair - triggered on GET/PUT - Background, // Background repair - scheduled tasks - OnDemand, // On-demand repair - admin triggered - Emergency, // Emergency repair - critical issues -} -``` - -## API Usage Guide - -### 1. System Management API - -#### Start AHM System - -```http -POST /admin/system/start -Content-Type: application/json - -{ - "coordinator": { - "event_buffer_size": 10000, - "max_concurrent_operations": 1000 - }, - "scanner": { - "default_scan_mode": "Normal", - "scan_interval": "24h" - }, - "heal": { - "max_workers": 16, - "queue_capacity": 50000 - } -} -``` - -**Response Example:** -```json -{ - "success": true, - "data": { - "system_id": "ahm-001", - "status": "Running", - "started_at": "2024-01-15T10:30:00Z" - }, - "timestamp": "2024-01-15T10:30:00Z" -} -``` - -#### Get System Status - -```http -GET /status/health -``` - -**Response Example:** -```json -{ - "success": true, - "data": { - "status": "Running", - "version": "1.0.0", - "uptime_seconds": 3600, - "subsystems": { - "scanner": { - "status": "Scanning", - "last_check": "2024-01-15T10:29:00Z", - "error_message": null - }, - "heal": { - "status": "Idle", - "last_check": "2024-01-15T10:29:00Z", - "error_message": null - }, - "metrics": { - "status": "Running", - "last_check": "2024-01-15T10:29:00Z", - "error_message": null - } - } - }, - "timestamp": "2024-01-15T10:30:00Z" -} -``` - -### 2. Scan Management API - -#### Start Scan Task - -```http -POST /admin/scan/start -Content-Type: application/json - -{ - "strategy": { - "type": "Full", - "mode": "Normal", - "scope": { - "buckets": ["important-data", "user-uploads"], - "include_system_objects": false, - "max_objects": 1000000 - } - }, - "priority": "High" -} -``` - -**Response Example:** -```json -{ - "success": true, - "data": { - "scan_id": "scan-12345", - "status": "Started", - "estimated_duration": "2h30m", - "estimated_objects": 850000 - }, - "timestamp": "2024-01-15T10:30:00Z" -} -``` - -#### Query Scan Status - -```http -GET /admin/scan/{scan_id}/status -``` - -**Response Example:** -```json -{ - "success": true, - "data": { - "scan_id": "scan-12345", - "status": "Scanning", - "progress": { - "objects_scanned": 425000, - "bytes_scanned": 1073741824000, - "issues_detected": 23, - "completion_percentage": 50.0, - "scan_rate_ops": 117.5, - "scan_rate_bps": 268435456, - "elapsed_time": "1h15m", - "estimated_remaining": "1h15m" - }, - "issues": [ - { - "issue_type": "MissingShards", - "severity": "High", - "bucket": "user-uploads", - "object": "photos/IMG_001.jpg", - "description": "Missing 1 data shard", - "detected_at": "2024-01-15T11:15:00Z" - } - ] - }, - "timestamp": "2024-01-15T11:45:00Z" -} -``` - -### 3. Heal Management API - -#### Submit Heal Request - -```http -POST /admin/heal/request -Content-Type: application/json - -{ - "bucket": "user-uploads", - "object": "photos/IMG_001.jpg", - "version_id": null, - "priority": "High", - "mode": "OnDemand", - "max_retries": 3 -} -``` - -**Response Example:** -```json -{ - "success": true, - "data": { - "heal_request_id": "heal-67890", - "status": "Queued", - "priority": "High", - "estimated_start": "2024-01-15T11:50:00Z", - "queue_position": 5 - }, - "timestamp": "2024-01-15T11:45:00Z" -} -``` - -#### Query Heal Status - -```http -GET /admin/heal/{heal_request_id}/status -``` - -**Response Example:** -```json -{ - "success": true, - "data": { - "heal_request_id": "heal-67890", - "status": "Completed", - "result": { - "success": true, - "shards_repaired": 1, - "total_shards": 8, - "duration": "45s", - "strategy_used": "ParityShardRepair", - "validation_results": [ - { - "validation_type": "Checksum", - "passed": true, - "details": "Object checksum verified", - "duration": "2s" - }, - { - "validation_type": "ShardCount", - "passed": true, - "details": "All 8 shards present", - "duration": "1s" - } - ] - } - }, - "timestamp": "2024-01-15T11:46:00Z" -} -``` - -### 4. Metrics Query API - -#### Get System Metrics - -```http -GET /metrics/system?period=1h&metrics=objects_total,scan_rate,heal_success_rate -``` - -**Response Example:** -```json -{ - "success": true, - "data": { - "period": "1h", - "timestamp_range": { - "start": "2024-01-15T10:45:00Z", - "end": "2024-01-15T11:45:00Z" - }, - "metrics": { - "objects_total": { - "value": 2500000, - "unit": "count", - "labels": {} - }, - "scan_rate_objects_per_second": { - "value": 117.5, - "unit": "ops", - "labels": {} - }, - "heal_success_rate": { - "value": 0.98, - "unit": "ratio", - "labels": {} - } - } - }, - "timestamp": "2024-01-15T11:45:00Z" -} -``` - -#### Export Prometheus Format Metrics - -```http -GET /metrics/prometheus -``` - -**Response Example:** -``` -# HELP rustfs_objects_total Total number of objects in the system -# TYPE rustfs_objects_total gauge -rustfs_objects_total 2500000 - -# HELP rustfs_scan_rate_objects_per_second Object scanning rate -# TYPE rustfs_scan_rate_objects_per_second gauge -rustfs_scan_rate_objects_per_second 117.5 - -# HELP rustfs_heal_success_rate Healing operation success rate -# TYPE rustfs_heal_success_rate gauge -rustfs_heal_success_rate 0.98 - -# HELP rustfs_health_issues_total Total health issues detected -# TYPE rustfs_health_issues_total counter -rustfs_health_issues_total{severity="critical"} 0 -rustfs_health_issues_total{severity="high"} 3 -rustfs_health_issues_total{severity="medium"} 15 -rustfs_health_issues_total{severity="low"} 45 -``` - -### 5. Policy Configuration API - -#### Update Scan Policy - -```http -PUT /admin/policy/scan -Content-Type: application/json - -{ - "default_scan_interval": "12h", - "deep_scan_probability": 0.1, - "bandwidth_limit_mbps": 100, - "concurrent_scanners": 4, - "skip_system_objects": true, - "priority_buckets": ["critical-data", "user-data"] -} -``` - -#### Update Heal Policy - -```http -PUT /admin/policy/heal -Content-Type: application/json - -{ - "max_concurrent_heals": 8, - "emergency_heal_timeout": "5m", - "auto_heal_enabled": true, - "heal_verification_required": true, - "priority_mapping": { - "critical_buckets": "Emergency", - "important_buckets": "High", - "standard_buckets": "Normal" - } -} -``` - -## Usage Examples - -### Complete Monitoring and Repair Workflow - -```bash -# 1. Start AHM system -curl -X POST http://localhost:9000/admin/system/start \ - -H "Content-Type: application/json" \ - -d '{"scanner": {"default_scan_mode": "Normal"}}' - -# 2. Start full scan -SCAN_ID=$(curl -X POST http://localhost:9000/admin/scan/start \ - -H "Content-Type: application/json" \ - -d '{"strategy": {"type": "Full", "mode": "Normal"}}' | \ - jq -r '.data.scan_id') - -# 3. Monitor scan progress -watch "curl -s http://localhost:9000/admin/scan/$SCAN_ID/status | jq '.data.progress'" - -# 4. View discovered issues -curl -s http://localhost:9000/admin/scan/$SCAN_ID/status | \ - jq '.data.issues[]' - -# 5. Start repair for discovered issues -HEAL_ID=$(curl -X POST http://localhost:9000/admin/heal/request \ - -H "Content-Type: application/json" \ - -d '{ - "bucket": "user-uploads", - "object": "photos/IMG_001.jpg", - "priority": "High" - }' | jq -r '.data.heal_request_id') - -# 6. Monitor repair progress -watch "curl -s http://localhost:9000/admin/heal/$HEAL_ID/status | jq '.data'" - -# 7. View system metrics -curl -s http://localhost:9000/metrics/system?period=1h | jq '.data.metrics' - -# 8. Export Prometheus metrics -curl -s http://localhost:9000/metrics/prometheus -``` - -## Key Features - -### 1. Intelligent Scanning -- **Multi-level scan modes**: Quick/Normal/Deep three depths -- **Adaptive sampling**: Intelligent object selection based on historical data -- **Bandwidth control**: Configurable I/O resource limits -- **Incremental scanning**: Timestamp-based change detection - -### 2. Intelligent Repair -- **Priority queue**: Repair ordering based on business importance -- **Multiple repair strategies**: Data shard, parity shard, hybrid repair -- **Real-time validation**: Post-repair integrity verification -- **Retry mechanism**: Configurable failure retry policies - -### 3. Rich Metrics -- **Real-time statistics**: Object counts, storage usage, performance metrics -- **Historical trends**: Time-series data storage and analysis -- **Multi-format export**: Prometheus, JSON, CSV formats -- **Custom metrics**: Extensible metrics definition framework - -### 4. Policy-Driven -- **Configurable policies**: Independent configuration for scan, heal, retention policies -- **Dynamic adjustment**: Runtime policy updates without restart -- **Business alignment**: Differentiated handling based on business importance - -## Deployment Recommendations - -### 1. Resource Configuration -- **CPU**: Recommended 16+ cores for parallel scanning and repair -- **Memory**: Recommended 32GB+ for metrics cache and task queues -- **Network**: Recommended gigabit+ bandwidth for cross-node data sync -- **Storage**: Recommended SSD for metrics data storage - -### 2. Monitoring Integration -- **Prometheus**: Metrics collection and alerting -- **Grafana**: Visualization dashboards -- **ELK Stack**: Log aggregation and analysis -- **Jaeger**: Distributed tracing - -### 3. High Availability Deployment -- **Multi-instance deployment**: Avoid single points of failure -- **Load balancing**: API request distribution -- **Data backup**: Metrics and configuration data backup -- **Failover**: Automatic failure detection and switching - -This architecture design provides RustFS with modern, scalable, and highly observable health monitoring and repair capabilities that meet the operational requirements of enterprise-grade distributed storage systems. \ No newline at end of file diff --git a/crates/ahm/architecture_ch.md b/crates/ahm/architecture_ch.md deleted file mode 100644 index e349cf513..000000000 --- a/crates/ahm/architecture_ch.md +++ /dev/null @@ -1,557 +0,0 @@ -# RustFS Advanced Health & Metrics (AHM) 系统架构设计 - -## 概述 - -RustFS AHM 系统是一个全新设计的分布式存储健康监控和修复系统,提供智能扫描、自动修复、丰富指标和策略驱动的管理能力。 - -## 系统架构 - -### 整体架构图 - -``` -┌─────────────────────────────────────┐ -│ API Layer (REST/gRPC) │ -├─────────────────────────────────────┤ -│ Policy & Configuration │ -├─────────────────────────────────────┤ -│ Core Coordination Engine │ -├─────────────────────────────────────┤ -│ Scanner Engine │ Heal Engine │ -├─────────────────────────────────────┤ -│ Metrics & Observability │ -├─────────────────────────────────────┤ -│ Storage Abstraction │ -└─────────────────────────────────────┘ -``` - -### 模块结构 - -``` -rustfs/crates/ecstore/src/ahm/ -├── mod.rs # 模块入口和公共接口 -├── core/ # 核心引擎 -│ ├── coordinator.rs # 分布式协调器 - 事件路由和状态管理 -│ ├── scheduler.rs # 任务调度器 - 优先级队列和工作分配 -│ └── lifecycle.rs # 生命周期管理器 - 系统启停控制 -├── scanner/ # 扫描系统 -│ ├── engine.rs # 扫描引擎 - 扫描流程控制 -│ ├── object_scanner.rs # 对象扫描器 - 对象级完整性检查 -│ ├── disk_scanner.rs # 磁盘扫描器 - 磁盘级健康检查 -│ ├── metrics_collector.rs # 指标收集器 - 扫描过程数据收集 -│ └── bandwidth_limiter.rs # 带宽限制器 - I/O 资源控制 -├── heal/ # 修复系统 -│ ├── engine.rs # 修复引擎 - 修复流程控制 -│ ├── priority_queue.rs # 优先级队列 - 修复任务排序 -│ ├── repair_worker.rs # 修复工作器 - 实际修复执行 -│ └── validation.rs # 修复验证器 - 修复结果验证 -├── metrics/ # 指标系统 -│ ├── collector.rs # 指标收集器 - 实时数据收集 -│ ├── aggregator.rs # 指标聚合器 - 数据聚合计算 -│ ├── storage.rs # 指标存储器 - 时序数据存储 -│ └── reporter.rs # 指标报告器 - 外部系统导出 -├── policy/ # 策略系统 -│ ├── scan_policy.rs # 扫描策略 - 扫描行为配置 -│ ├── heal_policy.rs # 修复策略 - 修复优先级和策略 -│ └── retention_policy.rs # 保留策略 - 数据生命周期管理 -└── api/ # API接口 - ├── admin_api.rs # 管理API - 系统管理操作 - ├── metrics_api.rs # 指标API - 指标查询和导出 - └── status_api.rs # 状态API - 系统状态监控 -``` - -## 核心设计理念 - -### 1. 事件驱动架构 - -```rust -pub enum SystemEvent { - ObjectDiscovered { bucket: String, object: String, metadata: ObjectMetadata }, - HealthIssueDetected { issue_type: HealthIssueType, severity: Severity }, - HealCompleted { result: HealResult }, - ScanCycleCompleted { statistics: ScanStatistics }, - ResourceUsageUpdated { usage: ResourceUsage }, -} -``` - -- **Scanner** 产生发现事件 -- **Heal** 响应修复事件 -- **Metrics** 收集所有事件统计 -- **Policy** 控制事件处理策略 - -### 2. 分层模块化设计 - -#### **API层**: REST/gRPC接口 -- 统一的响应格式 -- 完整的错误处理 -- 认证和授权支持 - -#### **策略层**: 可配置的业务规则 -- 扫描频率和深度控制 -- 修复优先级策略 -- 数据保留规则 - -#### **协调层**: 系统协调和调度 -- 事件路由分发 -- 资源管理分配 -- 任务调度执行 - -#### **引擎层**: 核心业务逻辑 -- 智能扫描算法 -- 自适应修复策略 -- 性能优化控制 - -#### **指标层**: 可观测性支持 -- 实时指标收集 -- 历史趋势分析 -- 多格式导出 - -### 3. 多模式扫描策略 - -```rust -pub enum ScanStrategy { - Full { mode: ScanMode, scope: ScanScope }, // 全量扫描 - Incremental { since: Instant, mode: ScanMode }, // 增量扫描 - Smart { sample_rate: f64, favor_unscanned: bool }, // 智能采样 - Targeted { targets: Vec, mode: ScanMode }, // 定向扫描 -} - -pub enum ScanMode { - Quick, // 快速扫描 - 仅元数据检查 - Normal, // 标准扫描 - 基础完整性验证 - Deep, // 深度扫描 - 包含位腐蚀检测 -} -``` - -### 4. 优先级修复系统 - -```rust -pub enum HealPriority { - Low = 0, - Normal = 1, - High = 2, - Critical = 3, - Emergency = 4, -} - -pub enum HealMode { - RealTime, // 实时修复 - GET/PUT时触发 - Background, // 后台修复 - 计划任务 - OnDemand, // 按需修复 - 管理员触发 - Emergency, // 紧急修复 - 关键问题 -} -``` - -## API 使用指南 - -### 1. 系统管理 API - -#### 启动 AHM 系统 - -```http -POST /admin/system/start -Content-Type: application/json - -{ - "coordinator": { - "event_buffer_size": 10000, - "max_concurrent_operations": 1000 - }, - "scanner": { - "default_scan_mode": "Normal", - "scan_interval": "24h" - }, - "heal": { - "max_workers": 16, - "queue_capacity": 50000 - } -} -``` - -**响应示例:** -```json -{ - "success": true, - "data": { - "system_id": "ahm-001", - "status": "Running", - "started_at": "2024-01-15T10:30:00Z" - }, - "timestamp": "2024-01-15T10:30:00Z" -} -``` - -#### 获取系统状态 - -```http -GET /status/health -``` - -**响应示例:** -```json -{ - "success": true, - "data": { - "status": "Running", - "version": "1.0.0", - "uptime_seconds": 3600, - "subsystems": { - "scanner": { - "status": "Scanning", - "last_check": "2024-01-15T10:29:00Z", - "error_message": null - }, - "heal": { - "status": "Idle", - "last_check": "2024-01-15T10:29:00Z", - "error_message": null - }, - "metrics": { - "status": "Running", - "last_check": "2024-01-15T10:29:00Z", - "error_message": null - } - } - }, - "timestamp": "2024-01-15T10:30:00Z" -} -``` - -### 2. 扫描管理 API - -#### 启动扫描任务 - -```http -POST /admin/scan/start -Content-Type: application/json - -{ - "strategy": { - "type": "Full", - "mode": "Normal", - "scope": { - "buckets": ["important-data", "user-uploads"], - "include_system_objects": false, - "max_objects": 1000000 - } - }, - "priority": "High" -} -``` - -**响应示例:** -```json -{ - "success": true, - "data": { - "scan_id": "scan-12345", - "status": "Started", - "estimated_duration": "2h30m", - "estimated_objects": 850000 - }, - "timestamp": "2024-01-15T10:30:00Z" -} -``` - -#### 查询扫描状态 - -```http -GET /admin/scan/{scan_id}/status -``` - -**响应示例:** -```json -{ - "success": true, - "data": { - "scan_id": "scan-12345", - "status": "Scanning", - "progress": { - "objects_scanned": 425000, - "bytes_scanned": 1073741824000, - "issues_detected": 23, - "completion_percentage": 50.0, - "scan_rate_ops": 117.5, - "scan_rate_bps": 268435456, - "elapsed_time": "1h15m", - "estimated_remaining": "1h15m" - }, - "issues": [ - { - "issue_type": "MissingShards", - "severity": "High", - "bucket": "user-uploads", - "object": "photos/IMG_001.jpg", - "description": "Missing 1 data shard", - "detected_at": "2024-01-15T11:15:00Z" - } - ] - }, - "timestamp": "2024-01-15T11:45:00Z" -} -``` - -### 3. 修复管理 API - -#### 提交修复请求 - -```http -POST /admin/heal/request -Content-Type: application/json - -{ - "bucket": "user-uploads", - "object": "photos/IMG_001.jpg", - "version_id": null, - "priority": "High", - "mode": "OnDemand", - "max_retries": 3 -} -``` - -**响应示例:** -```json -{ - "success": true, - "data": { - "heal_request_id": "heal-67890", - "status": "Queued", - "priority": "High", - "estimated_start": "2024-01-15T11:50:00Z", - "queue_position": 5 - }, - "timestamp": "2024-01-15T11:45:00Z" -} -``` - -#### 查询修复状态 - -```http -GET /admin/heal/{heal_request_id}/status -``` - -**响应示例:** -```json -{ - "success": true, - "data": { - "heal_request_id": "heal-67890", - "status": "Completed", - "result": { - "success": true, - "shards_repaired": 1, - "total_shards": 8, - "duration": "45s", - "strategy_used": "ParityShardRepair", - "validation_results": [ - { - "validation_type": "Checksum", - "passed": true, - "details": "Object checksum verified", - "duration": "2s" - }, - { - "validation_type": "ShardCount", - "passed": true, - "details": "All 8 shards present", - "duration": "1s" - } - ] - } - }, - "timestamp": "2024-01-15T11:46:00Z" -} -``` - -### 4. 指标查询 API - -#### 获取系统指标 - -```http -GET /metrics/system?period=1h&metrics=objects_total,scan_rate,heal_success_rate -``` - -**响应示例:** -```json -{ - "success": true, - "data": { - "period": "1h", - "timestamp_range": { - "start": "2024-01-15T10:45:00Z", - "end": "2024-01-15T11:45:00Z" - }, - "metrics": { - "objects_total": { - "value": 2500000, - "unit": "count", - "labels": {} - }, - "scan_rate_objects_per_second": { - "value": 117.5, - "unit": "ops", - "labels": {} - }, - "heal_success_rate": { - "value": 0.98, - "unit": "ratio", - "labels": {} - } - } - }, - "timestamp": "2024-01-15T11:45:00Z" -} -``` - -#### 导出 Prometheus 格式指标 - -```http -GET /metrics/prometheus -``` - -**响应示例:** -``` -# HELP rustfs_objects_total Total number of objects in the system -# TYPE rustfs_objects_total gauge -rustfs_objects_total 2500000 - -# HELP rustfs_scan_rate_objects_per_second Object scanning rate -# TYPE rustfs_scan_rate_objects_per_second gauge -rustfs_scan_rate_objects_per_second 117.5 - -# HELP rustfs_heal_success_rate Healing operation success rate -# TYPE rustfs_heal_success_rate gauge -rustfs_heal_success_rate 0.98 - -# HELP rustfs_health_issues_total Total health issues detected -# TYPE rustfs_health_issues_total counter -rustfs_health_issues_total{severity="critical"} 0 -rustfs_health_issues_total{severity="high"} 3 -rustfs_health_issues_total{severity="medium"} 15 -rustfs_health_issues_total{severity="low"} 45 -``` - -### 5. 策略配置 API - -#### 更新扫描策略 - -```http -PUT /admin/policy/scan -Content-Type: application/json - -{ - "default_scan_interval": "12h", - "deep_scan_probability": 0.1, - "bandwidth_limit_mbps": 100, - "concurrent_scanners": 4, - "skip_system_objects": true, - "priority_buckets": ["critical-data", "user-data"] -} -``` - -#### 更新修复策略 - -```http -PUT /admin/policy/heal -Content-Type: application/json - -{ - "max_concurrent_heals": 8, - "emergency_heal_timeout": "5m", - "auto_heal_enabled": true, - "heal_verification_required": true, - "priority_mapping": { - "critical_buckets": "Emergency", - "important_buckets": "High", - "standard_buckets": "Normal" - } -} -``` - -## 使用示例 - -### 完整的监控和修复流程 - -```bash -# 1. 启动 AHM 系统 -curl -X POST http://localhost:9000/admin/system/start \ - -H "Content-Type: application/json" \ - -d '{"scanner": {"default_scan_mode": "Normal"}}' - -# 2. 启动全量扫描 -SCAN_ID=$(curl -X POST http://localhost:9000/admin/scan/start \ - -H "Content-Type: application/json" \ - -d '{"strategy": {"type": "Full", "mode": "Normal"}}' | \ - jq -r '.data.scan_id') - -# 3. 监控扫描进度 -watch "curl -s http://localhost:9000/admin/scan/$SCAN_ID/status | jq '.data.progress'" - -# 4. 查看发现的问题 -curl -s http://localhost:9000/admin/scan/$SCAN_ID/status | \ - jq '.data.issues[]' - -# 5. 针对发现的问题启动修复 -HEAL_ID=$(curl -X POST http://localhost:9000/admin/heal/request \ - -H "Content-Type: application/json" \ - -d '{ - "bucket": "user-uploads", - "object": "photos/IMG_001.jpg", - "priority": "High" - }' | jq -r '.data.heal_request_id') - -# 6. 监控修复进度 -watch "curl -s http://localhost:9000/admin/heal/$HEAL_ID/status | jq '.data'" - -# 7. 查看系统指标 -curl -s http://localhost:9000/metrics/system?period=1h | jq '.data.metrics' - -# 8. 导出 Prometheus 指标 -curl -s http://localhost:9000/metrics/prometheus -``` - -## 关键特性 - -### 1. 智能扫描 -- **多级扫描模式**: Quick/Normal/Deep 三种深度 -- **自适应采样**: 基于历史数据智能选择扫描对象 -- **带宽控制**: 可配置的 I/O 资源限制 -- **增量扫描**: 基于时间戳的变化检测 - -### 2. 智能修复 -- **优先级队列**: 基于业务重要性的修复排序 -- **多种修复策略**: 数据分片、奇偶校验、混合修复 -- **实时验证**: 修复后的完整性验证 -- **重试机制**: 可配置的失败重试策略 - -### 3. 丰富指标 -- **实时统计**: 对象数量、存储使用、性能指标 -- **历史趋势**: 时序数据存储和分析 -- **多格式导出**: Prometheus、JSON、CSV 等格式 -- **自定义指标**: 可扩展的指标定义框架 - -### 4. 策略驱动 -- **可配置策略**: 扫描、修复、保留策略独立配置 -- **动态调整**: 运行时策略更新,无需重启 -- **业务对齐**: 基于业务重要性的差异化处理 - -## 部署建议 - -### 1. 资源配置 -- **CPU**: 推荐 16+ 核心用于并行扫描和修复 -- **内存**: 推荐 32GB+ 用于指标缓存和任务队列 -- **网络**: 推荐千兆以上带宽用于跨节点数据同步 -- **存储**: 推荐 SSD 用于指标数据存储 - -### 2. 监控集成 -- **Prometheus**: 指标收集和告警 -- **Grafana**: 可视化仪表板 -- **ELK Stack**: 日志聚合和分析 -- **Jaeger**: 分布式链路追踪 - -### 3. 高可用部署 -- **多实例部署**: 避免单点故障 -- **负载均衡**: API 请求分发 -- **数据备份**: 指标和配置数据备份 -- **故障转移**: 自动故障检测和切换 - -这个架构设计为 RustFS 提供了现代化、可扩展、高可观测的健康监控和修复能力,能够满足企业级分布式存储系统的运维需求。 \ No newline at end of file diff --git a/crates/ahm/src/api/admin_api.rs b/crates/ahm/src/api/admin_api.rs deleted file mode 100644 index 19c1a4942..000000000 --- a/crates/ahm/src/api/admin_api.rs +++ /dev/null @@ -1,843 +0,0 @@ -// Copyright 2024 RustFS Team - -use std::sync::Arc; - -use tracing::{debug, error, info, warn}; - -use crate::{ - error::Result, - heal::HealEngine, - policy::{ScanPolicyEngine as PolicyEngine}, - scanner::{Engine as ScanEngine}, -}; - -use super::{HttpRequest, HttpResponse}; - -/// Configuration for the admin API -#[derive(Debug, Clone)] -pub struct AdminApiConfig { - /// Whether to enable admin API - pub enabled: bool, - /// Admin API prefix - pub prefix: String, - /// Authentication required - pub require_auth: bool, - /// Admin token - pub admin_token: Option, - /// Rate limiting for admin endpoints - pub rate_limit_requests_per_minute: u32, - /// Maximum request body size - pub max_request_size: usize, - /// Enable audit logging - pub enable_audit_logging: bool, - /// Audit log path - pub audit_log_path: Option, -} - -impl Default for AdminApiConfig { - fn default() -> Self { - Self { - enabled: true, - prefix: "/admin".to_string(), - require_auth: true, - admin_token: Some("admin-secret-token".to_string()), - rate_limit_requests_per_minute: 100, - max_request_size: 1024 * 1024, // 1 MB - enable_audit_logging: true, - audit_log_path: Some("/tmp/rustfs/admin-audit.log".to_string()), - } - } -} - -/// Admin API that provides administrative operations -pub struct AdminApi { - config: AdminApiConfig, - scan_engine: Arc, - heal_engine: Arc, - policy_engine: Arc, -} - -impl AdminApi { - /// Create a new admin API - pub async fn new( - config: AdminApiConfig, - scan_engine: Arc, - heal_engine: Arc, - policy_engine: Arc, - ) -> Result { - Ok(Self { - config, - scan_engine, - heal_engine, - policy_engine, - }) - } - - /// Get the configuration - pub fn config(&self) -> &AdminApiConfig { - &self.config - } - - /// Handle HTTP request - pub async fn handle_request(&self, request: HttpRequest) -> Result { - // Check authentication if required - if self.config.require_auth { - if !self.authenticate_request(&request).await? { - return Ok(HttpResponse { - status_code: 401, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Unauthorized", - "message": "Authentication required" - }).to_string(), - }); - } - } - - // Log audit if enabled - if self.config.enable_audit_logging { - self.log_audit(&request).await?; - } - - match request.path.as_str() { - // Scan operations - "/admin/scan/start" => self.start_scan(request).await, - "/admin/scan/stop" => self.stop_scan(request).await, - "/admin/scan/status" => self.get_scan_status(request).await, - "/admin/scan/config" => self.get_scan_config(request).await, - "/admin/scan/config" if request.method == "PUT" => self.update_scan_config(request).await, - - // Heal operations - "/admin/heal/start" => self.start_heal(request).await, - "/admin/heal/stop" => self.stop_heal(request).await, - "/admin/heal/status" => self.get_heal_status(request).await, - "/admin/heal/config" => self.get_heal_config(request).await, - "/admin/heal/config" if request.method == "PUT" => self.update_heal_config(request).await, - - // Policy operations - "/admin/policy/list" => self.list_policies(request).await, - "/admin/policy/get" => self.get_policy(request).await, - "/admin/policy/create" => self.create_policy(request).await, - "/admin/policy/update" => self.update_policy(request).await, - "/admin/policy/delete" => self.delete_policy(request).await, - - // System operations - "/admin/system/status" => self.get_system_status(request).await, - "/admin/system/config" => self.get_system_config(request).await, - "/admin/system/restart" => self.restart_system(request).await, - "/admin/system/shutdown" => self.shutdown_system(request).await, - - // Default 404 - _ => Ok(HttpResponse { - status_code: 404, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Not Found", - "message": "Admin endpoint not found" - }).to_string(), - }), - } - } - - /// Authenticate request - async fn authenticate_request(&self, request: &HttpRequest) -> Result { - if let Some(token) = &self.config.admin_token { - // Check for Authorization header - if let Some(auth_header) = request.headers.iter().find(|(k, _)| k.to_lowercase() == "authorization") { - if auth_header.1 == format!("Bearer {}", token) { - return Ok(true); - } - } - - // Check for token in query parameters - if let Some(token_param) = request.query_params.iter().find(|(k, _)| k == "token") { - if token_param.1 == *token { - return Ok(true); - } - } - } - - Ok(false) - } - - /// Log audit entry - async fn log_audit(&self, request: &HttpRequest) -> Result<()> { - let audit_entry = serde_json::json!({ - "timestamp": chrono::Utc::now().to_rfc3339(), - "method": request.method, - "path": request.path, - "ip": "127.0.0.1", // In real implementation, get from request - "user_agent": "admin-api", // In real implementation, get from headers - }); - - if let Some(log_path) = &self.config.audit_log_path { - // In a real implementation, this would write to the audit log file - debug!("Audit log entry: {}", audit_entry); - } - - Ok(()) - } - - /// Start scan operation - async fn start_scan(&self, _request: HttpRequest) -> Result { - match self.scan_engine.start_scan().await { - Ok(_) => { - info!("Scan started via admin API"); - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "message": "Scan started successfully", - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - Err(e) => { - error!("Failed to start scan: {}", e); - Ok(HttpResponse { - status_code: 500, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Internal Server Error", - "message": format!("Failed to start scan: {}", e) - }).to_string(), - }) - } - } - } - - /// Stop scan operation - async fn stop_scan(&self, _request: HttpRequest) -> Result { - match self.scan_engine.stop_scan().await { - Ok(_) => { - info!("Scan stopped via admin API"); - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "message": "Scan stopped successfully", - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - Err(e) => { - error!("Failed to stop scan: {}", e); - Ok(HttpResponse { - status_code: 500, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Internal Server Error", - "message": format!("Failed to stop scan: {}", e) - }).to_string(), - }) - } - } - } - - /// Get scan status - async fn get_scan_status(&self, _request: HttpRequest) -> Result { - let status = self.scan_engine.get_status().await; - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "scan_status": status, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - - /// Get scan configuration - async fn get_scan_config(&self, _request: HttpRequest) -> Result { - let config = self.scan_engine.get_config().await; - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "scan_config": config, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - - /// Update scan configuration - async fn update_scan_config(&self, request: HttpRequest) -> Result { - if let Some(body) = request.body { - match serde_json::from_str::(&body) { - Ok(config_json) => { - // In a real implementation, this would update the scan configuration - info!("Scan config updated via admin API: {:?}", config_json); - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "message": "Scan configuration updated successfully", - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - Err(e) => { - Ok(HttpResponse { - status_code: 400, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Bad Request", - "message": format!("Invalid JSON: {}", e) - }).to_string(), - }) - } - } - } else { - Ok(HttpResponse { - status_code: 400, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Bad Request", - "message": "Request body required" - }).to_string(), - }) - } - } - - /// Start heal operation - async fn start_heal(&self, _request: HttpRequest) -> Result { - match self.heal_engine.start_healing().await { - Ok(_) => { - info!("Healing started via admin API"); - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "message": "Healing started successfully", - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - Err(e) => { - error!("Failed to start healing: {}", e); - Ok(HttpResponse { - status_code: 500, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Internal Server Error", - "message": format!("Failed to start healing: {}", e) - }).to_string(), - }) - } - } - } - - /// Stop heal operation - async fn stop_heal(&self, _request: HttpRequest) -> Result { - match self.heal_engine.stop_healing().await { - Ok(_) => { - info!("Healing stopped via admin API"); - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "message": "Healing stopped successfully", - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - Err(e) => { - error!("Failed to stop healing: {}", e); - Ok(HttpResponse { - status_code: 500, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Internal Server Error", - "message": format!("Failed to stop healing: {}", e) - }).to_string(), - }) - } - } - } - - /// Get heal status - async fn get_heal_status(&self, _request: HttpRequest) -> Result { - let status = self.heal_engine.get_status().await; - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "heal_status": status, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - - /// Get heal configuration - async fn get_heal_config(&self, _request: HttpRequest) -> Result { - let config = self.heal_engine.get_config().await; - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "heal_config": config, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - - /// Update heal configuration - async fn update_heal_config(&self, request: HttpRequest) -> Result { - if let Some(body) = request.body { - match serde_json::from_str::(&body) { - Ok(config_json) => { - // In a real implementation, this would update the heal configuration - info!("Heal config updated via admin API: {:?}", config_json); - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "message": "Heal configuration updated successfully", - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - Err(e) => { - Ok(HttpResponse { - status_code: 400, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Bad Request", - "message": format!("Invalid JSON: {}", e) - }).to_string(), - }) - } - } - } else { - Ok(HttpResponse { - status_code: 400, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Bad Request", - "message": "Request body required" - }).to_string(), - }) - } - } - - /// List policies - async fn list_policies(&self, _request: HttpRequest) -> Result { - let policies = self.policy_engine.list_policies().await?; - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "policies": policies, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - - /// Get policy - async fn get_policy(&self, request: HttpRequest) -> Result { - if let Some(policy_name) = request.query_params.iter().find(|(k, _)| k == "name") { - match self.policy_engine.get_policy(&policy_name.1).await { - Ok(policy) => { - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "policy": policy, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - Err(e) => { - Ok(HttpResponse { - status_code: 404, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Not Found", - "message": format!("Policy not found: {}", e) - }).to_string(), - }) - } - } - } else { - Ok(HttpResponse { - status_code: 400, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Bad Request", - "message": "Policy name parameter required" - }).to_string(), - }) - } - } - - /// Create policy - async fn create_policy(&self, request: HttpRequest) -> Result { - if let Some(body) = request.body { - match serde_json::from_str::(&body) { - Ok(policy_json) => { - // In a real implementation, this would create the policy - info!("Policy created via admin API: {:?}", policy_json); - - Ok(HttpResponse { - status_code: 201, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "message": "Policy created successfully", - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - Err(e) => { - Ok(HttpResponse { - status_code: 400, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Bad Request", - "message": format!("Invalid JSON: {}", e) - }).to_string(), - }) - } - } - } else { - Ok(HttpResponse { - status_code: 400, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Bad Request", - "message": "Request body required" - }).to_string(), - }) - } - } - - /// Update policy - async fn update_policy(&self, request: HttpRequest) -> Result { - if let Some(body) = request.body { - match serde_json::from_str::(&body) { - Ok(policy_json) => { - // In a real implementation, this would update the policy - info!("Policy updated via admin API: {:?}", policy_json); - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "message": "Policy updated successfully", - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - Err(e) => { - Ok(HttpResponse { - status_code: 400, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Bad Request", - "message": format!("Invalid JSON: {}", e) - }).to_string(), - }) - } - } - } else { - Ok(HttpResponse { - status_code: 400, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Bad Request", - "message": "Request body required" - }).to_string(), - }) - } - } - - /// Delete policy - async fn delete_policy(&self, request: HttpRequest) -> Result { - if let Some(policy_name) = request.query_params.iter().find(|(k, _)| k == "name") { - // In a real implementation, this would delete the policy - info!("Policy deleted via admin API: {}", policy_name.1); - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "message": "Policy deleted successfully", - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } else { - Ok(HttpResponse { - status_code: 400, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Bad Request", - "message": "Policy name parameter required" - }).to_string(), - }) - } - } - - /// Get system status - async fn get_system_status(&self, _request: HttpRequest) -> Result { - let scan_status = self.scan_engine.get_status().await; - let heal_status = self.heal_engine.get_status().await; - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "system_status": { - "scan": scan_status, - "heal": heal_status, - "overall": "healthy" - }, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - - /// Get system configuration - async fn get_system_config(&self, _request: HttpRequest) -> Result { - let scan_config = self.scan_engine.get_config().await; - let heal_config = self.heal_engine.get_config().await; - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "system_config": { - "scan": scan_config, - "heal": heal_config - }, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - - /// Restart system - async fn restart_system(&self, _request: HttpRequest) -> Result { - // In a real implementation, this would restart the system - info!("System restart requested via admin API"); - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "message": "System restart initiated", - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - - /// Shutdown system - async fn shutdown_system(&self, _request: HttpRequest) -> Result { - // In a real implementation, this would shutdown the system - info!("System shutdown requested via admin API"); - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "message": "System shutdown initiated", - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - heal::HealEngineConfig, - policy::PolicyEngineConfig, - scanner::ScanEngineConfig, - }; - - #[tokio::test] - async fn test_admin_api_creation() { - let config = AdminApiConfig::default(); - let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); - let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); - let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); - - let admin_api = AdminApi::new(config, scan_engine, heal_engine, policy_engine).await.unwrap(); - - assert!(admin_api.config().enabled); - assert_eq!(admin_api.config().prefix, "/admin"); - } - - #[tokio::test] - async fn test_authentication() { - let config = AdminApiConfig { - admin_token: Some("test-token".to_string()), - ..Default::default() - }; - let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); - let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); - let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); - - let admin_api = AdminApi::new(config, scan_engine, heal_engine, policy_engine).await.unwrap(); - - // Test with valid token in header - let request = HttpRequest { - method: "GET".to_string(), - path: "/admin/scan/status".to_string(), - headers: vec![("Authorization".to_string(), "Bearer test-token".to_string())], - body: None, - query_params: vec![], - }; - - let response = admin_api.handle_request(request).await.unwrap(); - assert_eq!(response.status_code, 200); - - // Test with valid token in query - let request = HttpRequest { - method: "GET".to_string(), - path: "/admin/scan/status".to_string(), - headers: vec![], - body: None, - query_params: vec![("token".to_string(), "test-token".to_string())], - }; - - let response = admin_api.handle_request(request).await.unwrap(); - assert_eq!(response.status_code, 200); - - // Test with invalid token - let request = HttpRequest { - method: "GET".to_string(), - path: "/admin/scan/status".to_string(), - headers: vec![("Authorization".to_string(), "Bearer invalid-token".to_string())], - body: None, - query_params: vec![], - }; - - let response = admin_api.handle_request(request).await.unwrap(); - assert_eq!(response.status_code, 401); - } - - #[tokio::test] - async fn test_scan_operations() { - let config = AdminApiConfig { - require_auth: false, // Disable auth for testing - ..Default::default() - }; - let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); - let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); - let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); - - let admin_api = AdminApi::new(config, scan_engine, heal_engine, policy_engine).await.unwrap(); - - // Test start scan - let request = HttpRequest { - method: "POST".to_string(), - path: "/admin/scan/start".to_string(), - headers: vec![], - body: None, - query_params: vec![], - }; - - let response = admin_api.handle_request(request).await.unwrap(); - assert_eq!(response.status_code, 200); - - // Test get scan status - let request = HttpRequest { - method: "GET".to_string(), - path: "/admin/scan/status".to_string(), - headers: vec![], - body: None, - query_params: vec![], - }; - - let response = admin_api.handle_request(request).await.unwrap(); - assert_eq!(response.status_code, 200); - } - - #[tokio::test] - async fn test_heal_operations() { - let config = AdminApiConfig { - require_auth: false, // Disable auth for testing - ..Default::default() - }; - let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); - let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); - let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); - - let admin_api = AdminApi::new(config, scan_engine, heal_engine, policy_engine).await.unwrap(); - - // Test start heal - let request = HttpRequest { - method: "POST".to_string(), - path: "/admin/heal/start".to_string(), - headers: vec![], - body: None, - query_params: vec![], - }; - - let response = admin_api.handle_request(request).await.unwrap(); - assert_eq!(response.status_code, 200); - - // Test get heal status - let request = HttpRequest { - method: "GET".to_string(), - path: "/admin/heal/status".to_string(), - headers: vec![], - body: None, - query_params: vec![], - }; - - let response = admin_api.handle_request(request).await.unwrap(); - assert_eq!(response.status_code, 200); - } - - #[tokio::test] - async fn test_system_operations() { - let config = AdminApiConfig { - require_auth: false, // Disable auth for testing - ..Default::default() - }; - let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); - let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); - let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); - - let admin_api = AdminApi::new(config, scan_engine, heal_engine, policy_engine).await.unwrap(); - - // Test get system status - let request = HttpRequest { - method: "GET".to_string(), - path: "/admin/system/status".to_string(), - headers: vec![], - body: None, - query_params: vec![], - }; - - let response = admin_api.handle_request(request).await.unwrap(); - assert_eq!(response.status_code, 200); - assert!(response.body.contains("system_status")); - } -} \ No newline at end of file diff --git a/crates/ahm/src/api/metrics_api.rs b/crates/ahm/src/api/metrics_api.rs deleted file mode 100644 index 06f1efa50..000000000 --- a/crates/ahm/src/api/metrics_api.rs +++ /dev/null @@ -1,1180 +0,0 @@ -// Copyright 2024 RustFS Team - -use std::sync::Arc; -use std::time::{SystemTime, Duration}; - -use tracing::{debug, error, info, warn}; - -use crate::{ - error::Result, - metrics::{Collector, Reporter, Storage, MetricsQuery, MetricType}, -}; - -use super::{HttpRequest, HttpResponse}; - -/// Configuration for the metrics API -#[derive(Debug, Clone)] -pub struct MetricsApiConfig { - /// Whether to enable metrics API - pub enabled: bool, - /// Metrics API prefix - pub prefix: String, - /// Authentication required - pub require_auth: bool, - /// Metrics token - pub metrics_token: Option, - /// Rate limiting for metrics endpoints - pub rate_limit_requests_per_minute: u32, - /// Maximum request body size - pub max_request_size: usize, - /// Enable metrics caching - pub enable_caching: bool, - /// Cache TTL in seconds - pub cache_ttl_seconds: u64, - /// Enable metrics compression - pub enable_compression: bool, - /// Default metrics format - pub default_format: MetricsFormat, -} - -impl Default for MetricsApiConfig { - fn default() -> Self { - Self { - enabled: true, - prefix: "/metrics".to_string(), - require_auth: false, - metrics_token: None, - rate_limit_requests_per_minute: 1000, - max_request_size: 1024 * 1024, // 1 MB - enable_caching: true, - cache_ttl_seconds: 300, // 5 minutes - enable_compression: true, - default_format: MetricsFormat::Json, - } - } -} - -/// Metrics format -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum MetricsFormat { - Json, - Prometheus, - Csv, - Xml, -} - -/// Backup report -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BackupReport { - pub timestamp: SystemTime, - pub backup_id: String, - pub status: BackupStatus, - pub objects_backed_up: u64, - pub total_size: u64, - pub duration: Duration, - pub errors: Vec, -} - -/// Restore report -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RestoreReport { - pub timestamp: SystemTime, - pub restore_id: String, - pub status: RestoreStatus, - pub objects_restored: u64, - pub total_size: u64, - pub duration: Duration, - pub errors: Vec, -} - -/// Data integrity report -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DataIntegrityReport { - pub timestamp: SystemTime, - pub validation_id: String, - pub status: ValidationStatus, - pub objects_validated: u64, - pub corrupted_objects: u64, - pub duration: Duration, - pub details: Vec, -} - -/// Backup status -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum BackupStatus { - Pending, - InProgress, - Completed, - Failed, -} - -/// Restore status -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum RestoreStatus { - Pending, - InProgress, - Completed, - Failed, -} - -/// Validation status -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum ValidationStatus { - Pending, - InProgress, - Completed, - Failed, -} - -/// Validation detail -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ValidationDetail { - pub object_path: String, - pub status: ValidationResult, - pub error_message: Option, -} - -/// Validation result -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum ValidationResult { - Valid, - Corrupted, - Missing, - AccessDenied, -} - -/// Metrics API that provides metrics data and operations -pub struct MetricsApi { - config: MetricsApiConfig, - collector: Arc, - reporter: Arc, - storage: Arc, -} - -impl MetricsApi { - /// Create a new metrics API - pub async fn new( - config: MetricsApiConfig, - collector: Arc, - reporter: Arc, - storage: Arc, - ) -> Result { - Ok(Self { - config, - collector, - reporter, - storage, - }) - } - - /// Get the configuration - pub fn config(&self) -> &MetricsApiConfig { - &self.config - } - - /// Handle HTTP request - pub async fn handle_request(&self, request: HttpRequest) -> Result { - // Check authentication if required - if self.config.require_auth { - if !self.authenticate_request(&request).await? { - return Ok(HttpResponse { - status_code: 401, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Unauthorized", - "message": "Authentication required" - }).to_string(), - }) - } - } - - match request.path.as_str() { - // Current metrics - "/metrics/current" => self.get_current_metrics(request).await, - "/metrics/latest" => self.get_latest_metrics(request).await, - - // Historical metrics - "/metrics/history" => self.get_metrics_history(request).await, - "/metrics/range" => self.get_metrics_range(request).await, - - // Aggregated metrics - "/metrics/aggregated" => self.get_aggregated_metrics(request).await, - "/metrics/summary" => self.get_metrics_summary(request).await, - - // Specific metric types - "/metrics/system" => self.get_system_metrics(request).await, - "/metrics/scan" => self.get_scan_metrics(request).await, - "/metrics/heal" => self.get_heal_metrics(request).await, - "/metrics/policy" => self.get_policy_metrics(request).await, - "/metrics/network" => self.get_network_metrics(request).await, - "/metrics/disk" => self.get_disk_metrics(request).await, - - // Health issues - "/metrics/health-issues" => self.get_health_issues(request).await, - "/metrics/alerts" => self.get_alerts(request).await, - - // Reports - "/metrics/reports" => self.get_reports(request).await, - "/metrics/reports/comprehensive" => self.get_comprehensive_report(request).await, - - // Prometheus format - "/metrics/prometheus" => self.get_prometheus_metrics(request).await, - - // Storage operations - "/metrics/storage/backup" => self.backup_metrics(request).await, - "/metrics/storage/restore" => self.restore_metrics(request).await, - "/metrics/storage/validate" => self.validate_metrics(request).await, - - // Default 404 - _ => Ok(HttpResponse { - status_code: 404, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Not Found", - "message": "Metrics endpoint not found" - }).to_string(), - }), - } - } - - /// Authenticate request - async fn authenticate_request(&self, request: &HttpRequest) -> Result { - if let Some(token) = &self.config.metrics_token { - // Check for Authorization header - if let Some(auth_header) = request.headers.iter().find(|(k, _)| k.to_lowercase() == "authorization") { - if auth_header.1 == format!("Bearer {}", token) { - return Ok(true); - } - } - - // Check for token in query parameters - if let Some(token_param) = request.query_params.iter().find(|(k, _)| k == "token") { - if token_param.1 == *token { - return Ok(true); - } - } - } - - Ok(false) - } - - /// Get current metrics - async fn get_current_metrics(&self, _request: HttpRequest) -> Result { - match self.collector.collect_metrics().await { - Ok(metrics) => { - let format = self.get_request_format(&_request); - let body = self.format_metrics(&metrics, format.clone()).await?; - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), self.get_content_type(format))], - body, - }) - } - Err(e) => { - error!("Failed to collect current metrics: {}", e); - Ok(HttpResponse { - status_code: 500, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Internal Server Error", - "message": format!("Failed to collect metrics: {}", e) - }).to_string(), - }) - } - } - } - - /// Get latest metrics - async fn get_latest_metrics(&self, _request: HttpRequest) -> Result { - match self.collector.get_latest_metrics().await { - Ok(Some(metrics)) => { - let format = self.get_request_format(&_request); - let body = self.format_metrics(&metrics, format.clone()).await?; - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), self.get_content_type(format))], - body, - }) - } - Ok(None) => { - Ok(HttpResponse { - status_code: 404, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Not Found", - "message": "No metrics available" - }).to_string(), - }) - } - Err(e) => { - error!("Failed to get latest metrics: {}", e); - Ok(HttpResponse { - status_code: 500, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Internal Server Error", - "message": format!("Failed to get latest metrics: {}", e) - }).to_string(), - }) - } - } - } - - /// Get metrics history - async fn get_metrics_history(&self, request: HttpRequest) -> Result { - let hours = request.query_params - .iter() - .find(|(k, _)| k == "hours") - .and_then(|(_, v)| v.parse::().ok()) - .unwrap_or(24); - - let end_time = std::time::SystemTime::now(); - let start_time = end_time - std::time::Duration::from_secs(hours * 3600); - - let query = MetricsQuery { - start_time, - end_time, - interval: std::time::Duration::from_secs(300), // 5 minutes - metrics: vec![], - severity_filter: None, - limit: None, - }; - - match self.collector.query_metrics(query).await { - Ok(aggregated) => { - let format = self.get_request_format(&request); - let body = self.format_aggregated_metrics(&aggregated, format.clone()).await?; - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), self.get_content_type(format))], - body, - }) - } - Err(e) => { - error!("Failed to get metrics history: {}", e); - Ok(HttpResponse { - status_code: 500, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Internal Server Error", - "message": format!("Failed to get metrics history: {}", e) - }).to_string(), - }) - } - } - } - - /// Get metrics range - async fn get_metrics_range(&self, request: HttpRequest) -> Result { - let start_time = request.query_params - .iter() - .find(|(k, _)| k == "start") - .and_then(|(_, v)| v.parse::().ok()) - .map(|ts| std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(ts)) - .unwrap_or_else(|| std::time::SystemTime::now() - std::time::Duration::from_secs(3600)); - - let end_time = request.query_params - .iter() - .find(|(k, _)| k == "end") - .and_then(|(_, v)| v.parse::().ok()) - .map(|ts| std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(ts)) - .unwrap_or_else(std::time::SystemTime::now); - - let query = MetricsQuery { - start_time, - end_time, - interval: std::time::Duration::from_secs(300), // 5 minutes - metrics: vec![], - severity_filter: None, - limit: None, - }; - - match self.collector.query_metrics(query).await { - Ok(aggregated) => { - let format = self.get_request_format(&request); - let body = self.format_aggregated_metrics(&aggregated, format.clone()).await?; - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), self.get_content_type(format))], - body, - }) - } - Err(e) => { - error!("Failed to get metrics range: {}", e); - Ok(HttpResponse { - status_code: 500, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Internal Server Error", - "message": format!("Failed to get metrics range: {}", e) - }).to_string(), - }) - } - } - } - - /// Get aggregated metrics - async fn get_aggregated_metrics(&self, request: HttpRequest) -> Result { - let query = self.parse_metrics_query(&request)?; - - match self.collector.query_metrics(query).await { - Ok(aggregated) => { - let format = self.get_request_format(&request); - let body = self.format_aggregated_metrics(&aggregated, format.clone()).await?; - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), self.get_content_type(format))], - body, - }) - } - Err(e) => { - error!("Failed to get aggregated metrics: {}", e); - Ok(HttpResponse { - status_code: 500, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Internal Server Error", - "message": format!("Failed to get aggregated metrics: {}", e) - }).to_string(), - }) - } - } - } - - /// Get metrics summary - async fn get_metrics_summary(&self, request: HttpRequest) -> Result { - let hours = request.query_params - .iter() - .find(|(k, _)| k == "hours") - .and_then(|(_, v)| v.parse::().ok()) - .unwrap_or(24); - - let end_time = std::time::SystemTime::now(); - let start_time = end_time - std::time::Duration::from_secs(hours * 3600); - - let query = MetricsQuery { - start_time, - end_time, - interval: std::time::Duration::from_secs(3600), // 1 hour - metrics: vec![], - severity_filter: None, - limit: None, - }; - - match self.collector.query_metrics(query).await { - Ok(aggregated) => { - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "summary": aggregated.summary, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - Err(e) => { - error!("Failed to get metrics summary: {}", e); - Ok(HttpResponse { - status_code: 500, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Internal Server Error", - "message": format!("Failed to get metrics summary: {}", e) - }).to_string(), - }) - } - } - } - - /// Get system metrics - async fn get_system_metrics(&self, request: HttpRequest) -> Result { - match self.collector.collect_metrics().await { - Ok(metrics) => { - let system_data = serde_json::json!({ - "cpu_usage": metrics.cpu_usage, - "memory_usage": metrics.memory_usage, - "disk_usage": metrics.disk_usage, - "system_load": metrics.system_load, - "active_operations": metrics.active_operations, - "network_io": metrics.network_io, - "disk_io": metrics.disk_io, - }); - - let format = self.get_request_format(&request); - let body = self.format_json_data(&system_data, format.clone()).await?; - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), self.get_content_type(format))], - body, - }) - } - Err(e) => { - error!("Failed to get system metrics: {}", e); - Ok(HttpResponse { - status_code: 500, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Internal Server Error", - "message": format!("Failed to get system metrics: {}", e) - }).to_string(), - }) - } - } - } - - /// Get scan metrics - async fn get_scan_metrics(&self, request: HttpRequest) -> Result { - match self.collector.collect_metrics().await { - Ok(metrics) => { - let scan_data = serde_json::json!({ - "scan_metrics": metrics.scan_metrics, - }); - - let format = self.get_request_format(&request); - let body = self.format_json_data(&scan_data, format.clone()).await?; - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), self.get_content_type(format))], - body, - }) - } - Err(e) => { - error!("Failed to get scan metrics: {}", e); - Ok(HttpResponse { - status_code: 500, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Internal Server Error", - "message": format!("Failed to get scan metrics: {}", e) - }).to_string(), - }) - } - } - } - - /// Get heal metrics - async fn get_heal_metrics(&self, request: HttpRequest) -> Result { - match self.collector.collect_metrics().await { - Ok(metrics) => { - let heal_data = serde_json::json!({ - "heal_metrics": metrics.heal_metrics, - }); - - let format = self.get_request_format(&request); - let body = self.format_json_data(&heal_data, format.clone()).await?; - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), self.get_content_type(format))], - body, - }) - } - Err(e) => { - error!("Failed to get heal metrics: {}", e); - Ok(HttpResponse { - status_code: 500, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Internal Server Error", - "message": format!("Failed to get heal metrics: {}", e) - }).to_string(), - }) - } - } - } - - /// Get policy metrics - async fn get_policy_metrics(&self, request: HttpRequest) -> Result { - match self.collector.collect_metrics().await { - Ok(metrics) => { - let policy_data = serde_json::json!({ - "policy_metrics": metrics.policy_metrics, - }); - - let format = self.get_request_format(&request); - let body = self.format_json_data(&policy_data, format.clone()).await?; - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), self.get_content_type(format))], - body, - }) - } - Err(e) => { - error!("Failed to get policy metrics: {}", e); - Ok(HttpResponse { - status_code: 500, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Internal Server Error", - "message": format!("Failed to get policy metrics: {}", e) - }).to_string(), - }) - } - } - } - - /// Get network metrics - async fn get_network_metrics(&self, request: HttpRequest) -> Result { - match self.collector.collect_metrics().await { - Ok(metrics) => { - let network_data = serde_json::json!({ - "network_io": metrics.network_io, - }); - - let format = self.get_request_format(&request); - let body = self.format_json_data(&network_data, format.clone()).await?; - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), self.get_content_type(format))], - body, - }) - } - Err(e) => { - error!("Failed to get network metrics: {}", e); - Ok(HttpResponse { - status_code: 500, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Internal Server Error", - "message": format!("Failed to get network metrics: {}", e) - }).to_string(), - }) - } - } - } - - /// Get disk metrics - async fn get_disk_metrics(&self, request: HttpRequest) -> Result { - match self.collector.collect_metrics().await { - Ok(metrics) => { - let disk_data = serde_json::json!({ - "disk_io": metrics.disk_io, - "disk_usage": metrics.disk_usage, - }); - - let format = self.get_request_format(&request); - let body = self.format_json_data(&disk_data, format.clone()).await?; - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), self.get_content_type(format))], - body, - }) - } - Err(e) => { - error!("Failed to get disk metrics: {}", e); - Ok(HttpResponse { - status_code: 500, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Internal Server Error", - "message": format!("Failed to get disk metrics: {}", e) - }).to_string(), - }) - } - } - } - - /// Get health issues - async fn get_health_issues(&self, _request: HttpRequest) -> Result { - match self.collector.collect_metrics().await { - Ok(metrics) => { - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "health_issues": metrics.health_issues, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - Err(e) => { - error!("Failed to get health issues: {}", e); - Ok(HttpResponse { - status_code: 500, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Internal Server Error", - "message": format!("Failed to get health issues: {}", e) - }).to_string(), - }) - } - } - } - - /// Get alerts - async fn get_alerts(&self, request: HttpRequest) -> Result { - let hours = request.query_params - .iter() - .find(|(k, _)| k == "hours") - .and_then(|(_, v)| v.parse::().ok()) - .unwrap_or(24); - - match self.reporter.get_recent_alerts(hours).await { - Ok(alerts) => { - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "alerts": alerts, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - Err(e) => { - error!("Failed to get alerts: {}", e); - Ok(HttpResponse { - status_code: 500, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Internal Server Error", - "message": format!("Failed to get alerts: {}", e) - }).to_string(), - }) - } - } - } - - /// Get reports - async fn get_reports(&self, _request: HttpRequest) -> Result { - let stats = self.reporter.get_statistics().await; - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "reports": stats, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - - /// Get comprehensive report - async fn get_comprehensive_report(&self, request: HttpRequest) -> Result { - let query = self.parse_metrics_query(&request)?; - - match self.collector.query_metrics(query).await { - Ok(aggregated) => { - match self.reporter.generate_comprehensive_report(&aggregated).await { - Ok(report) => { - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "report": report, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - Err(e) => { - error!("Failed to generate comprehensive report: {}", e); - Ok(HttpResponse { - status_code: 500, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Internal Server Error", - "message": format!("Failed to generate report: {}", e) - }).to_string(), - }) - } - } - } - Err(e) => { - error!("Failed to get metrics for comprehensive report: {}", e); - Ok(HttpResponse { - status_code: 500, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Internal Server Error", - "message": format!("Failed to get metrics: {}", e) - }).to_string(), - }) - } - } - } - - /// Get Prometheus metrics - async fn get_prometheus_metrics(&self, _request: HttpRequest) -> Result { - match self.collector.collect_metrics().await { - Ok(metrics) => { - let prometheus_data = self.format_prometheus_metrics(&metrics).await?; - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "text/plain; version=0.0.4; charset=utf-8".to_string())], - body: prometheus_data, - }) - } - Err(e) => { - error!("Failed to get Prometheus metrics: {}", e); - Ok(HttpResponse { - status_code: 500, - headers: vec![("Content-Type".to_string(), "text/plain".to_string())], - body: format!("# ERROR: Failed to get metrics: {}", e), - }) - } - } - } - - /// Backup metrics - async fn backup_metrics(&self, request: HttpRequest) -> Result { - let backup_path = request.query_params - .iter() - .find(|(k, _)| k == "path") - .map(|(_, v)| std::path::PathBuf::from(v)) - .unwrap_or_else(|| std::path::PathBuf::from("/tmp/rustfs/metrics-backup")); - - match self.storage.backup_data(&backup_path).await { - Ok(report) => { - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "backup_report": report, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - Err(e) => { - error!("Failed to backup metrics: {}", e); - Ok(HttpResponse { - status_code: 500, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Internal Server Error", - "message": format!("Failed to backup metrics: {}", e) - }).to_string(), - }) - } - } - } - - /// Restore metrics - async fn restore_metrics(&self, request: HttpRequest) -> Result { - let backup_path = request.query_params - .iter() - .find(|(k, _)| k == "path") - .map(|(_, v)| std::path::PathBuf::from(v)) - .unwrap_or_else(|| std::path::PathBuf::from("/tmp/rustfs/metrics-backup")); - - match self.storage.restore_data(&backup_path).await { - Ok(report) => { - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "restore_report": report, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - Err(e) => { - error!("Failed to restore metrics: {}", e); - Ok(HttpResponse { - status_code: 500, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Internal Server Error", - "message": format!("Failed to restore metrics: {}", e) - }).to_string(), - }) - } - } - } - - /// Validate metrics - async fn validate_metrics(&self, _request: HttpRequest) -> Result { - match self.storage.validate_data_integrity().await { - Ok(report) => { - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "validation_report": report, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - Err(e) => { - error!("Failed to validate metrics: {}", e); - Ok(HttpResponse { - status_code: 500, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Internal Server Error", - "message": format!("Failed to validate metrics: {}", e) - }).to_string(), - }) - } - } - } - - /// Helper methods - fn get_request_format(&self, request: &HttpRequest) -> MetricsFormat { - request.query_params - .iter() - .find(|(k, _)| k == "format") - .and_then(|(_, v)| match v.as_str() { - "prometheus" => Some(MetricsFormat::Prometheus), - "csv" => Some(MetricsFormat::Csv), - "xml" => Some(MetricsFormat::Xml), - _ => Some(MetricsFormat::Json), - }) - .unwrap_or(self.config.default_format.clone()) - } - - fn get_content_type(&self, format: MetricsFormat) -> String { - match format { - MetricsFormat::Json => "application/json".to_string(), - MetricsFormat::Prometheus => "text/plain; version=0.0.4; charset=utf-8".to_string(), - MetricsFormat::Csv => "text/csv".to_string(), - MetricsFormat::Xml => "application/xml".to_string(), - } - } - - async fn format_metrics(&self, metrics: &crate::metrics::SystemMetrics, format: MetricsFormat) -> Result { - match format { - MetricsFormat::Json => Ok(serde_json::json!({ - "status": "success", - "metrics": metrics, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string()), - MetricsFormat::Prometheus => self.format_prometheus_metrics(metrics).await, - MetricsFormat::Csv => self.format_csv_metrics(metrics).await, - MetricsFormat::Xml => self.format_xml_metrics(metrics).await, - } - } - - async fn format_aggregated_metrics(&self, aggregated: &crate::metrics::AggregatedMetrics, format: MetricsFormat) -> Result { - match format { - MetricsFormat::Json => Ok(serde_json::json!({ - "status": "success", - "aggregated_metrics": aggregated, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string()), - MetricsFormat::Prometheus => self.format_prometheus_aggregated(aggregated).await, - MetricsFormat::Csv => self.format_csv_aggregated(aggregated).await, - MetricsFormat::Xml => self.format_xml_aggregated(aggregated).await, - } - } - - async fn format_json_data(&self, data: &serde_json::Value, format: MetricsFormat) -> Result { - match format { - MetricsFormat::Json => Ok(serde_json::json!({ - "status": "success", - "data": data, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string()), - _ => Ok(serde_json::json!(data).to_string()), - } - } - - async fn format_prometheus_metrics(&self, metrics: &crate::metrics::SystemMetrics) -> Result { - let mut prometheus_lines = Vec::new(); - - // System metrics - prometheus_lines.push(format!("rustfs_cpu_usage_percent {}", metrics.cpu_usage)); - prometheus_lines.push(format!("rustfs_memory_usage_percent {}", metrics.memory_usage)); - prometheus_lines.push(format!("rustfs_disk_usage_percent {}", metrics.disk_usage)); - prometheus_lines.push(format!("rustfs_system_load {}", metrics.system_load)); - prometheus_lines.push(format!("rustfs_active_operations {}", metrics.active_operations)); - - // Network metrics - prometheus_lines.push(format!("rustfs_network_bytes_received_per_sec {}", metrics.network_io.bytes_received_per_sec)); - prometheus_lines.push(format!("rustfs_network_bytes_sent_per_sec {}", metrics.network_io.bytes_sent_per_sec)); - - // Disk metrics - prometheus_lines.push(format!("rustfs_disk_bytes_read_per_sec {}", metrics.disk_io.bytes_read_per_sec)); - prometheus_lines.push(format!("rustfs_disk_bytes_written_per_sec {}", metrics.disk_io.bytes_written_per_sec)); - - // Scan metrics - prometheus_lines.push(format!("rustfs_scan_objects_scanned_total {}", metrics.scan_metrics.objects_scanned)); - prometheus_lines.push(format!("rustfs_scan_bytes_scanned_total {}", metrics.scan_metrics.bytes_scanned)); - - // Heal metrics - prometheus_lines.push(format!("rustfs_heal_total_repairs {}", metrics.heal_metrics.total_repairs)); - prometheus_lines.push(format!("rustfs_heal_successful_repairs {}", metrics.heal_metrics.successful_repairs)); - prometheus_lines.push(format!("rustfs_heal_failed_repairs {}", metrics.heal_metrics.failed_repairs)); - - Ok(prometheus_lines.join("\n")) - } - - async fn format_prometheus_aggregated(&self, _aggregated: &crate::metrics::AggregatedMetrics) -> Result { - // In a real implementation, this would format aggregated metrics for Prometheus - Ok("# Aggregated metrics not yet implemented for Prometheus format".to_string()) - } - - async fn format_csv_metrics(&self, _metrics: &crate::metrics::SystemMetrics) -> Result { - // In a real implementation, this would format metrics as CSV - Ok("timestamp,cpu_usage,memory_usage,disk_usage\n".to_string()) - } - - async fn format_csv_aggregated(&self, _aggregated: &crate::metrics::AggregatedMetrics) -> Result { - // In a real implementation, this would format aggregated metrics as CSV - Ok("timestamp,avg_cpu_usage,avg_memory_usage,avg_disk_usage\n".to_string()) - } - - async fn format_xml_metrics(&self, _metrics: &crate::metrics::SystemMetrics) -> Result { - // In a real implementation, this would format metrics as XML - Ok("success".to_string()) - } - - async fn format_xml_aggregated(&self, _aggregated: &crate::metrics::AggregatedMetrics) -> Result { - // In a real implementation, this would format aggregated metrics as XML - Ok("success".to_string()) - } - - fn parse_metrics_query(&self, request: &HttpRequest) -> Result { - let start_time = request.query_params - .iter() - .find(|(k, _)| k == "start") - .and_then(|(_, v)| v.parse::().ok()) - .map(|ts| std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(ts)) - .unwrap_or_else(|| std::time::SystemTime::now() - std::time::Duration::from_secs(3600)); - - let end_time = request.query_params - .iter() - .find(|(k, _)| k == "end") - .and_then(|(_, v)| v.parse::().ok()) - .map(|ts| std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(ts)) - .unwrap_or_else(std::time::SystemTime::now); - - let interval = request.query_params - .iter() - .find(|(k, _)| k == "interval") - .and_then(|(_, v)| v.parse::().ok()) - .map(|secs| std::time::Duration::from_secs(secs)) - .unwrap_or(std::time::Duration::from_secs(300)); - - let metrics = request.query_params - .iter() - .filter(|(k, _)| k == "metric") - .map(|(_, v)| match v.as_str() { - "system" => MetricType::System, - "network" => MetricType::Network, - "disk" => MetricType::DiskIo, - "scan" => MetricType::Scan, - "heal" => MetricType::Heal, - "policy" => MetricType::Policy, - "health" => MetricType::HealthIssues, - _ => MetricType::System, - }) - .collect(); - - let limit = request.query_params - .iter() - .find(|(k, _)| k == "limit") - .and_then(|(_, v)| v.parse::().ok()); - - Ok(MetricsQuery { - start_time, - end_time, - interval, - metrics, - severity_filter: None, - limit, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::metrics::{CollectorConfig, ReporterConfig, StorageConfig}; - - #[tokio::test] - async fn test_metrics_api_creation() { - let config = MetricsApiConfig::default(); - let collector = Arc::new(Collector::new(CollectorConfig::default()).await.unwrap()); - let reporter = Arc::new(Reporter::new(ReporterConfig::default()).await.unwrap()); - let storage = Arc::new(Storage::new(StorageConfig::default()).await.unwrap()); - - let metrics_api = MetricsApi::new(config, collector, reporter, storage).await.unwrap(); - - assert!(metrics_api.config().enabled); - assert_eq!(metrics_api.config().prefix, "/metrics"); - } - - #[tokio::test] - async fn test_current_metrics() { - let config = MetricsApiConfig::default(); - let collector = Arc::new(Collector::new(CollectorConfig::default()).await.unwrap()); - let reporter = Arc::new(Reporter::new(ReporterConfig::default()).await.unwrap()); - let storage = Arc::new(Storage::new(StorageConfig::default()).await.unwrap()); - - let metrics_api = MetricsApi::new(config, collector, reporter, storage).await.unwrap(); - - let request = HttpRequest { - method: "GET".to_string(), - path: "/metrics/current".to_string(), - headers: vec![], - body: None, - query_params: vec![], - }; - - let response = metrics_api.handle_request(request).await.unwrap(); - assert_eq!(response.status_code, 200); - assert!(response.body.contains("status")); - } - - #[tokio::test] - async fn test_prometheus_metrics() { - let config = MetricsApiConfig::default(); - let collector = Arc::new(Collector::new(CollectorConfig::default()).await.unwrap()); - let reporter = Arc::new(Reporter::new(ReporterConfig::default()).await.unwrap()); - let storage = Arc::new(Storage::new(StorageConfig::default()).await.unwrap()); - - let metrics_api = MetricsApi::new(config, collector, reporter, storage).await.unwrap(); - - let request = HttpRequest { - method: "GET".to_string(), - path: "/metrics/prometheus".to_string(), - headers: vec![], - body: None, - query_params: vec![], - }; - - let response = metrics_api.handle_request(request).await.unwrap(); - assert_eq!(response.status_code, 200); - assert!(response.body.contains("rustfs_cpu_usage_percent")); - } - - #[tokio::test] - async fn test_system_metrics() { - let config = MetricsApiConfig::default(); - let collector = Arc::new(Collector::new(CollectorConfig::default()).await.unwrap()); - let reporter = Arc::new(Reporter::new(ReporterConfig::default()).await.unwrap()); - let storage = Arc::new(Storage::new(StorageConfig::default()).await.unwrap()); - - let metrics_api = MetricsApi::new(config, collector, reporter, storage).await.unwrap(); - - let request = HttpRequest { - method: "GET".to_string(), - path: "/metrics/system".to_string(), - headers: vec![], - body: None, - query_params: vec![], - }; - - let response = metrics_api.handle_request(request).await.unwrap(); - assert_eq!(response.status_code, 200); - assert!(response.body.contains("cpu_usage")); - } -} \ No newline at end of file diff --git a/crates/ahm/src/api/mod.rs b/crates/ahm/src/api/mod.rs deleted file mode 100644 index 913fa4552..000000000 --- a/crates/ahm/src/api/mod.rs +++ /dev/null @@ -1,504 +0,0 @@ -// 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. - -//! API interfaces for the AHM system -//! -//! Provides REST and gRPC endpoints for: -//! - Administrative operations -//! - Metrics and monitoring -//! - System status and control - -pub mod admin_api; -pub mod metrics_api; -pub mod status_api; - -pub use admin_api::{AdminApi, AdminApiConfig}; -pub use metrics_api::{MetricsApi, MetricsApiConfig}; -pub use status_api::{StatusApi, StatusApiConfig}; - -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::sync::Arc; - -use crate::{ - error::Result, - heal::HealEngine, - metrics::{Collector, Reporter, Storage}, - policy::{ScanPolicyEngine as PolicyEngine}, - scanner::{Engine as ScanEngine}, -}; - -/// Configuration for the API server -#[derive(Debug, Clone)] -pub struct ApiConfig { - /// Admin API configuration - pub admin: AdminApiConfig, - /// Metrics API configuration - pub metrics: MetricsApiConfig, - /// Status API configuration - pub status: StatusApiConfig, - /// Server address - pub address: String, - /// Server port - pub port: u16, - /// Enable HTTPS - pub enable_https: bool, - /// SSL certificate path - pub ssl_cert_path: Option, - /// SSL key path - pub ssl_key_path: Option, - /// Request timeout - pub request_timeout: std::time::Duration, - /// Maximum request size - pub max_request_size: usize, - /// Enable CORS - pub enable_cors: bool, - /// CORS origins - pub cors_origins: Vec, - /// Enable rate limiting - pub enable_rate_limiting: bool, - /// Rate limit requests per minute - pub rate_limit_requests_per_minute: u32, -} - -impl Default for ApiConfig { - fn default() -> Self { - Self { - admin: AdminApiConfig::default(), - metrics: MetricsApiConfig::default(), - status: StatusApiConfig::default(), - address: "127.0.0.1".to_string(), - port: 8080, - enable_https: false, - ssl_cert_path: None, - ssl_key_path: None, - request_timeout: std::time::Duration::from_secs(30), - max_request_size: 1024 * 1024, // 1 MB - enable_cors: true, - cors_origins: vec!["*".to_string()], - enable_rate_limiting: true, - rate_limit_requests_per_minute: 1000, - } - } -} - -/// API server that provides HTTP endpoints for AHM functionality -pub struct ApiServer { - config: ApiConfig, - admin_api: Arc, - metrics_api: Arc, - status_api: Arc, - scan_engine: Arc, - heal_engine: Arc, - policy_engine: Arc, - metrics_collector: Arc, - metrics_reporter: Arc, - metrics_storage: Arc, -} - -impl ApiServer { - /// Create a new API server - pub async fn new( - config: ApiConfig, - scan_engine: Arc, - heal_engine: Arc, - policy_engine: Arc, - metrics_collector: Arc, - metrics_reporter: Arc, - metrics_storage: Arc, - ) -> Result { - let admin_api = Arc::new(AdminApi::new(config.admin.clone(), scan_engine.clone(), heal_engine.clone(), policy_engine.clone()).await?); - let metrics_api = Arc::new(MetricsApi::new(config.metrics.clone(), metrics_collector.clone(), metrics_reporter.clone(), metrics_storage.clone()).await?); - let status_api = Arc::new(StatusApi::new(config.status.clone(), scan_engine.clone(), heal_engine.clone(), policy_engine.clone()).await?); - - Ok(Self { - config, - admin_api, - metrics_api, - status_api, - scan_engine, - heal_engine, - policy_engine, - metrics_collector, - metrics_reporter, - metrics_storage, - }) - } - - /// Get the configuration - pub fn config(&self) -> &ApiConfig { - &self.config - } - - /// Start the API server - pub async fn start(&self) -> Result<()> { - // In a real implementation, this would start an HTTP server - // For now, we'll just simulate the server startup - tracing::info!("API server starting on {}:{}", self.config.address, self.config.port); - - if self.config.enable_https { - tracing::info!("HTTPS enabled"); - } - - if self.config.enable_cors { - tracing::info!("CORS enabled with origins: {:?}", self.config.cors_origins); - } - - if self.config.enable_rate_limiting { - tracing::info!("Rate limiting enabled: {} requests/minute", self.config.rate_limit_requests_per_minute); - } - - tracing::info!("API server started successfully"); - Ok(()) - } - - /// Stop the API server - pub async fn stop(&self) -> Result<()> { - tracing::info!("API server stopping"); - tracing::info!("API server stopped successfully"); - Ok(()) - } - - /// Get server status - pub async fn status(&self) -> ServerStatus { - ServerStatus { - address: self.config.address.clone(), - port: self.config.port, - https_enabled: self.config.enable_https, - cors_enabled: self.config.enable_cors, - rate_limiting_enabled: self.config.enable_rate_limiting, - admin_api_enabled: true, - metrics_api_enabled: true, - status_api_enabled: true, - } - } - - /// Get admin API - pub fn admin_api(&self) -> &Arc { - &self.admin_api - } - - /// Get metrics API - pub fn metrics_api(&self) -> &Arc { - &self.metrics_api - } - - /// Get status API - pub fn status_api(&self) -> &Arc { - &self.status_api - } - - /// Handle HTTP request - pub async fn handle_request(&self, request: HttpRequest) -> Result { - match request.path.as_str() { - // Admin API routes - path if path.starts_with("/admin") => { - self.admin_api.handle_request(request).await - } - // Metrics API routes - path if path.starts_with("/metrics") => { - self.metrics_api.handle_request(request).await - } - // Status API routes - path if path.starts_with("/status") => { - self.status_api.handle_request(request).await - } - // Health check - "/health" => { - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "healthy", - "timestamp": chrono::Utc::now().to_rfc3339(), - "version": env!("CARGO_PKG_VERSION") - }).to_string(), - }) - } - // Root endpoint - "/" => { - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "service": "RustFS AHM API", - "version": env!("CARGO_PKG_VERSION"), - "endpoints": { - "admin": "/admin", - "metrics": "/metrics", - "status": "/status", - "health": "/health" - } - }).to_string(), - }) - } - // 404 for unknown routes - _ => { - Ok(HttpResponse { - status_code: 404, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Not Found", - "message": "The requested endpoint does not exist" - }).to_string(), - }) - } - } - } -} - -/// HTTP request -#[derive(Debug, Clone)] -pub struct HttpRequest { - pub method: String, - pub path: String, - pub headers: Vec<(String, String)>, - pub body: Option, - pub query_params: Vec<(String, String)>, -} - -/// HTTP response -#[derive(Debug, Clone)] -pub struct HttpResponse { - pub status_code: u16, - pub headers: Vec<(String, String)>, - pub body: String, -} - -/// Server status -#[derive(Debug, Clone)] -pub struct ServerStatus { - pub address: String, - pub port: u16, - pub https_enabled: bool, - pub cors_enabled: bool, - pub rate_limiting_enabled: bool, - pub admin_api_enabled: bool, - pub metrics_api_enabled: bool, - pub status_api_enabled: bool, -} - -/// API endpoint information -#[derive(Debug, Clone)] -pub struct EndpointInfo { - pub path: String, - pub method: String, - pub description: String, - pub parameters: Vec, - pub response_type: String, -} - -/// Parameter information -#[derive(Debug, Clone)] -pub struct ParameterInfo { - pub name: String, - pub parameter_type: String, - pub required: bool, - pub description: String, -} - -/// API documentation -#[derive(Debug, Clone)] -pub struct ApiDocumentation { - pub title: String, - pub version: String, - pub description: String, - pub endpoints: Vec, -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - heal::HealEngineConfig, - metrics::{CollectorConfig, ReporterConfig, StorageConfig}, - policy::PolicyEngineConfig, - scanner::ScanEngineConfig, - }; - - #[tokio::test] - async fn test_api_server_creation() { - let config = ApiConfig::default(); - let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); - let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); - let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); - let metrics_collector = Arc::new(Collector::new(CollectorConfig::default()).await.unwrap()); - let metrics_reporter = Arc::new(Reporter::new(ReporterConfig::default()).await.unwrap()); - let metrics_storage = Arc::new(Storage::new(StorageConfig::default()).await.unwrap()); - - let server = ApiServer::new( - config, - scan_engine, - heal_engine, - policy_engine, - metrics_collector, - metrics_reporter, - metrics_storage, - ).await.unwrap(); - - assert_eq!(server.config().port, 8080); - assert_eq!(server.config().address, "127.0.0.1"); - } - - #[tokio::test] - async fn test_api_server_start_stop() { - let config = ApiConfig::default(); - let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); - let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); - let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); - let metrics_collector = Arc::new(Collector::new(CollectorConfig::default()).await.unwrap()); - let metrics_reporter = Arc::new(Reporter::new(ReporterConfig::default()).await.unwrap()); - let metrics_storage = Arc::new(Storage::new(StorageConfig::default()).await.unwrap()); - - let server = ApiServer::new( - config, - scan_engine, - heal_engine, - policy_engine, - metrics_collector, - metrics_reporter, - metrics_storage, - ).await.unwrap(); - - server.start().await.unwrap(); - server.stop().await.unwrap(); - } - - #[tokio::test] - async fn test_api_server_status() { - let config = ApiConfig::default(); - let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); - let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); - let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); - let metrics_collector = Arc::new(Collector::new(CollectorConfig::default()).await.unwrap()); - let metrics_reporter = Arc::new(Reporter::new(ReporterConfig::default()).await.unwrap()); - let metrics_storage = Arc::new(Storage::new(StorageConfig::default()).await.unwrap()); - - let server = ApiServer::new( - config, - scan_engine, - heal_engine, - policy_engine, - metrics_collector, - metrics_reporter, - metrics_storage, - ).await.unwrap(); - - let status = server.status().await; - assert_eq!(status.port, 8080); - assert_eq!(status.address, "127.0.0.1"); - assert!(status.admin_api_enabled); - assert!(status.metrics_api_enabled); - assert!(status.status_api_enabled); - } - - #[tokio::test] - async fn test_health_endpoint() { - let config = ApiConfig::default(); - let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); - let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); - let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); - let metrics_collector = Arc::new(Collector::new(CollectorConfig::default()).await.unwrap()); - let metrics_reporter = Arc::new(Reporter::new(ReporterConfig::default()).await.unwrap()); - let metrics_storage = Arc::new(Storage::new(StorageConfig::default()).await.unwrap()); - - let server = ApiServer::new( - config, - scan_engine, - heal_engine, - policy_engine, - metrics_collector, - metrics_reporter, - metrics_storage, - ).await.unwrap(); - - let request = HttpRequest { - method: "GET".to_string(), - path: "/health".to_string(), - headers: vec![], - body: None, - query_params: vec![], - }; - - let response = server.handle_request(request).await.unwrap(); - assert_eq!(response.status_code, 200); - assert!(response.body.contains("healthy")); - } - - #[tokio::test] - async fn test_root_endpoint() { - let config = ApiConfig::default(); - let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); - let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); - let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); - let metrics_collector = Arc::new(Collector::new(CollectorConfig::default()).await.unwrap()); - let metrics_reporter = Arc::new(Reporter::new(ReporterConfig::default()).await.unwrap()); - let metrics_storage = Arc::new(Storage::new(StorageConfig::default()).await.unwrap()); - - let server = ApiServer::new( - config, - scan_engine, - heal_engine, - policy_engine, - metrics_collector, - metrics_reporter, - metrics_storage, - ).await.unwrap(); - - let request = HttpRequest { - method: "GET".to_string(), - path: "/".to_string(), - headers: vec![], - body: None, - query_params: vec![], - }; - - let response = server.handle_request(request).await.unwrap(); - assert_eq!(response.status_code, 200); - assert!(response.body.contains("RustFS AHM API")); - } - - #[tokio::test] - async fn test_404_endpoint() { - let config = ApiConfig::default(); - let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); - let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); - let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); - let metrics_collector = Arc::new(Collector::new(CollectorConfig::default()).await.unwrap()); - let metrics_reporter = Arc::new(Reporter::new(ReporterConfig::default()).await.unwrap()); - let metrics_storage = Arc::new(Storage::new(StorageConfig::default()).await.unwrap()); - - let server = ApiServer::new( - config, - scan_engine, - heal_engine, - policy_engine, - metrics_collector, - metrics_reporter, - metrics_storage, - ).await.unwrap(); - - let request = HttpRequest { - method: "GET".to_string(), - path: "/unknown".to_string(), - headers: vec![], - body: None, - query_params: vec![], - }; - - let response = server.handle_request(request).await.unwrap(); - assert_eq!(response.status_code, 404); - assert!(response.body.contains("Not Found")); - } -} \ No newline at end of file diff --git a/crates/ahm/src/api/status_api.rs b/crates/ahm/src/api/status_api.rs deleted file mode 100644 index ca47b1213..000000000 --- a/crates/ahm/src/api/status_api.rs +++ /dev/null @@ -1,761 +0,0 @@ -// Copyright 2024 RustFS Team - -use std::sync::Arc; - -use tracing::{debug, error, info, warn}; - -use crate::{ - error::Result, - heal::HealEngine, - policy::{ScanPolicyEngine as PolicyEngine}, - scanner::{Engine as ScanEngine}, -}; - -use super::{HttpRequest, HttpResponse}; - -use serde::{Deserialize, Serialize}; - -/// Configuration for the status API -#[derive(Debug, Clone)] -pub struct StatusApiConfig { - /// Whether to enable status API - pub enabled: bool, - /// Status API prefix - pub prefix: String, - /// Authentication required - pub require_auth: bool, - /// Status token - pub status_token: Option, - /// Rate limiting for status endpoints - pub rate_limit_requests_per_minute: u32, - /// Maximum request body size - pub max_request_size: usize, - /// Enable detailed status information - pub enable_detailed_status: bool, - /// Status cache TTL in seconds - pub status_cache_ttl_seconds: u64, - /// Enable health checks - pub enable_health_checks: bool, - /// Health check timeout - pub health_check_timeout: std::time::Duration, -} - -impl Default for StatusApiConfig { - fn default() -> Self { - Self { - enabled: true, - prefix: "/status".to_string(), - require_auth: false, - status_token: None, - rate_limit_requests_per_minute: 1000, - max_request_size: 1024 * 1024, // 1 MB - enable_detailed_status: true, - status_cache_ttl_seconds: 30, // 30 seconds - enable_health_checks: true, - health_check_timeout: std::time::Duration::from_secs(5), - } - } -} - -/// Status API that provides system status and health information -pub struct StatusApi { - config: StatusApiConfig, - scan_engine: Arc, - heal_engine: Arc, - policy_engine: Arc, -} - -impl StatusApi { - /// Create a new status API - pub async fn new( - config: StatusApiConfig, - scan_engine: Arc, - heal_engine: Arc, - policy_engine: Arc, - ) -> Result { - Ok(Self { - config, - scan_engine, - heal_engine, - policy_engine, - }) - } - - /// Get the configuration - pub fn config(&self) -> &StatusApiConfig { - &self.config - } - - /// Handle HTTP request - pub async fn handle_request(&self, request: HttpRequest) -> Result { - // Check authentication if required - if self.config.require_auth { - if !self.authenticate_request(&request).await? { - return Ok(HttpResponse { - status_code: 401, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Unauthorized", - "message": "Authentication required" - }).to_string(), - }) - } - } - - match request.path.as_str() { - // Basic status - "/status" => self.get_status(request).await, - "/status/health" => self.get_health_status(request).await, - "/status/overview" => self.get_overview_status(request).await, - - // Component status - "/status/scan" => self.get_scan_status(request).await, - "/status/heal" => self.get_heal_status(request).await, - "/status/policy" => self.get_policy_status(request).await, - - // Detailed status - "/status/detailed" => self.get_detailed_status(request).await, - "/status/components" => self.get_components_status(request).await, - "/status/resources" => self.get_resources_status(request).await, - - // Health checks - "/status/health/check" => self.perform_health_check(request).await, - "/status/health/readiness" => self.get_readiness_status(request).await, - "/status/health/liveness" => self.get_liveness_status(request).await, - - // System information - "/status/info" => self.get_system_info(request).await, - "/status/version" => self.get_version_info(request).await, - "/status/uptime" => self.get_uptime_info(request).await, - - // Default 404 - _ => Ok(HttpResponse { - status_code: 404, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Not Found", - "message": "Status endpoint not found" - }).to_string(), - }), - } - } - - /// Authenticate request - async fn authenticate_request(&self, request: &HttpRequest) -> Result { - if let Some(token) = &self.config.status_token { - // Check for Authorization header - if let Some(auth_header) = request.headers.iter().find(|(k, _)| k.to_lowercase() == "authorization") { - if auth_header.1 == format!("Bearer {}", token) { - return Ok(true); - } - } - - // Check for token in query parameters - if let Some(token_param) = request.query_params.iter().find(|(k, _)| k == "token") { - if token_param.1 == *token { - return Ok(true); - } - } - } - - Ok(false) - } - - /// Get basic status - async fn get_status(&self, _request: HttpRequest) -> Result { - let scan_status = self.scan_engine.status().await; - let heal_status = self.heal_engine.get_status().await; - - let overall_status = if scan_status == crate::scanner::Status::Running && heal_status == crate::heal::Status::Running { - "healthy" - } else if scan_status == crate::scanner::Status::Stopped && heal_status == crate::heal::Status::Stopped { - "stopped" - } else { - "degraded" - }; - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "overall_status": overall_status, - "components": { - "scan": scan_status, - "heal": heal_status - }, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - - /// Get health status - async fn get_health_status(&self, _request: HttpRequest) -> Result { - let scan_status = self.scan_engine.status().await; - let heal_status = self.heal_engine.get_status().await; - - let is_healthy = scan_status == crate::scanner::Status::Running && heal_status == crate::heal::Status::Running; - let status_code = if is_healthy { 200 } else { 503 }; - - Ok(HttpResponse { - status_code, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": if is_healthy { "healthy" } else { "unhealthy" }, - "components": { - "scan": { - "status": scan_status, - "healthy": scan_status == crate::scanner::Status::Running - }, - "heal": { - "status": heal_status, - "healthy": heal_status == crate::heal::Status::Running - } - }, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - - /// Get overview status - async fn get_overview_status(&self, _request: HttpRequest) -> Result { - let scan_status = self.scan_engine.status().await; - let heal_status = self.heal_engine.get_status().await; - - let scan_config = self.scan_engine.get_config().await; - let heal_config = self.heal_engine.get_config().await; - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "overview": { - "scan": { - "status": scan_status, - "enabled": scan_config.enabled, - "scan_interval": scan_config.scan_interval.as_secs() - }, - "heal": { - "status": heal_status, - "enabled": heal_config.auto_heal_enabled, - "max_workers": heal_config.max_workers - } - }, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - - /// Get scan status - async fn get_scan_status(&self, _request: HttpRequest) -> Result { - let status = self.scan_engine.status().await; - let config = self.scan_engine.get_config().await; - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "scan": { - "status": status, - "enabled": config.enabled, - "scan_interval": config.scan_interval.as_secs(), - "max_concurrent_scans": config.max_concurrent_scans, - "scan_paths": config.scan_paths, - "bandwidth_limit": config.bandwidth_limit - }, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - - /// Get heal status - async fn get_heal_status(&self, _request: HttpRequest) -> Result { - let status = self.heal_engine.get_status().await; - let config = self.heal_engine.get_config().await; - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "heal": { - "status": status, - "enabled": config.auto_heal_enabled, - "max_workers": config.max_workers, - "repair_timeout": config.repair_timeout.as_secs(), - "retry_attempts": config.max_retry_attempts, - "priority_queue_size": config.max_queue_size - }, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - - /// Get policy status - async fn get_policy_status(&self, _request: HttpRequest) -> Result { - let policies = self.policy_engine.list_policies().await?; - let config = self.policy_engine.get_config().await; - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "policy": { - "enabled": config.enabled, - "total_policies": policies.len(), - "policies": policies, - "evaluation_timeout": config.evaluation_timeout.as_secs(), - "cache_enabled": config.cache_enabled - }, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - - /// Get detailed status - async fn get_detailed_status(&self, _request: HttpRequest) -> Result { - if !self.config.enable_detailed_status { - return Ok(HttpResponse { - status_code: 403, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "error": "Forbidden", - "message": "Detailed status is disabled" - }).to_string(), - }); - } - - let scan_status = self.scan_engine.status().await; - let heal_status = self.heal_engine.get_status().await; - let scan_config = self.scan_engine.get_config().await; - let heal_config = self.heal_engine.get_config().await; - let policy_config = self.policy_engine.get_config().await; - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "detailed_status": { - "scan": { - "status": scan_status, - "config": scan_config - }, - "heal": { - "status": heal_status, - "config": heal_config - }, - "policy": { - "config": policy_config - } - }, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - - /// Get components status - async fn get_components_status(&self, _request: HttpRequest) -> Result { - let scan_status = self.scan_engine.status().await; - let heal_status = self.heal_engine.get_status().await; - - let components = vec![ - serde_json::json!({ - "name": "scan_engine", - "status": scan_status, - "healthy": scan_status == crate::scanner::Status::Running, - "type": "scanner" - }), - serde_json::json!({ - "name": "heal_engine", - "status": heal_status, - "healthy": heal_status == crate::heal::Status::Running, - "type": "healer" - }), - serde_json::json!({ - "name": "policy_engine", - "status": "running", - "healthy": true, - "type": "policy" - }) - ]; - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "components": components, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - - /// Get resources status - async fn get_resources_status(&self, _request: HttpRequest) -> Result { - // In a real implementation, this would collect actual resource usage - // For now, we'll return simulated data - let resources = serde_json::json!({ - "cpu": { - "usage_percent": 25.5, - "cores": 8, - "load_average": 0.75 - }, - "memory": { - "usage_percent": 60.2, - "total_bytes": 8589934592, // 8 GB - "available_bytes": 3422552064 // ~3.2 GB - }, - "disk": { - "usage_percent": 45.8, - "total_bytes": 107374182400, // 100 GB - "available_bytes": 58133032960 // ~54 GB - }, - "network": { - "bytes_received_per_sec": 1048576, // 1 MB/s - "bytes_sent_per_sec": 524288 // 512 KB/s - } - }); - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "resources": resources, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - - /// Perform health checks - async fn perform_health_checks(&self) -> Result> { - let mut checks = Vec::new(); - let start_time = std::time::Instant::now(); - - // Check scan engine - let scan_start = std::time::Instant::now(); - let scan_status = self.scan_engine.status().await; - let scan_duration = scan_start.elapsed(); - checks.push(HealthCheckResult { - name: "scan_engine".to_string(), - healthy: scan_status == crate::scanner::Status::Running, - message: format!("Scan engine status: {:?}", scan_status), - duration_ms: scan_duration.as_millis() as u64, - }); - - // Check heal engine - let heal_start = std::time::Instant::now(); - let heal_status = self.heal_engine.get_status().await; - let heal_duration = heal_start.elapsed(); - checks.push(HealthCheckResult { - name: "heal_engine".to_string(), - healthy: heal_status == crate::heal::Status::Running, - message: format!("Heal engine status: {:?}", heal_status), - duration_ms: heal_duration.as_millis() as u64, - }); - - // Check policy engine - let policy_start = std::time::Instant::now(); - let policy_result = self.policy_engine.list_policies().await; - let policy_duration = policy_start.elapsed(); - checks.push(HealthCheckResult { - name: "policy_engine".to_string(), - healthy: policy_result.is_ok(), - message: if policy_result.is_ok() { - "Policy engine is responding".to_string() - } else { - format!("Policy engine error: {:?}", policy_result.unwrap_err()) - }, - duration_ms: policy_duration.as_millis() as u64, - }); - - let total_duration = start_time.elapsed(); - info!("Health checks completed in {:?}", total_duration); - - Ok(checks) - } - - /// Perform health check (alias for perform_health_checks) - async fn perform_health_check(&self, _request: HttpRequest) -> Result { - let checks = self.perform_health_checks().await?; - let all_healthy = checks.iter().all(|check| check.healthy); - let check_time = std::time::Instant::now().elapsed(); - - Ok(HttpResponse { - status_code: if all_healthy { 200 } else { 503 }, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": if all_healthy { "healthy" } else { "unhealthy" }, - "checks": checks, - "check_time_ms": check_time.as_millis(), - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - - /// Get readiness status - async fn get_readiness_status(&self, _request: HttpRequest) -> Result { - let scan_status = self.scan_engine.status().await; - let heal_status = self.heal_engine.get_status().await; - - let is_ready = scan_status == crate::scanner::Status::Running && heal_status == crate::heal::Status::Running; - let status_code = if is_ready { 200 } else { 503 }; - - Ok(HttpResponse { - status_code, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": if is_ready { "ready" } else { "not_ready" }, - "components": { - "scan_engine": scan_status == crate::scanner::Status::Running, - "heal_engine": heal_status == crate::heal::Status::Running - }, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - - /// Get liveness status - async fn get_liveness_status(&self, _request: HttpRequest) -> Result { - // Liveness check is simple - if we can respond, we're alive - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "alive", - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - - /// Get system information - async fn get_system_info(&self, _request: HttpRequest) -> Result { - let system_info = serde_json::json!({ - "service": "RustFS AHM", - "version": env!("CARGO_PKG_VERSION"), - "system_info": { - "rust_version": option_env!("RUST_VERSION").unwrap_or("unknown"), - "target_arch": option_env!("TARGET_ARCH").unwrap_or("unknown"), - "target_os": option_env!("TARGET_OS").unwrap_or("unknown"), - "build_time": option_env!("VERGEN_BUILD_TIMESTAMP").unwrap_or("unknown"), - "git_commit": option_env!("VERGEN_GIT_SHA").unwrap_or("unknown"), - "git_branch": option_env!("VERGEN_GIT_BRANCH").unwrap_or("unknown"), - }, - }); - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "system_info": system_info, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - - /// Get version information - async fn get_version_info(&self, _request: HttpRequest) -> Result { - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "version": env!("CARGO_PKG_VERSION"), - "build_time": option_env!("VERGEN_BUILD_TIMESTAMP").unwrap_or("unknown"), - "git_commit": option_env!("VERGEN_GIT_SHA").unwrap_or("unknown"), - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } - - /// Get uptime information - async fn get_uptime_info(&self, _request: HttpRequest) -> Result { - // In a real implementation, this would track actual uptime - // For now, we'll return simulated data - let uptime_seconds = 3600; // 1 hour - let uptime_duration = std::time::Duration::from_secs(uptime_seconds); - - Ok(HttpResponse { - status_code: 200, - headers: vec![("Content-Type".to_string(), "application/json".to_string())], - body: serde_json::json!({ - "status": "success", - "uptime": { - "seconds": uptime_seconds, - "duration": format!("{:?}", uptime_duration), - "start_time": chrono::Utc::now() - chrono::Duration::seconds(uptime_seconds as i64) - }, - "timestamp": chrono::Utc::now().to_rfc3339() - }).to_string(), - }) - } -} - -/// Health check result -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct HealthCheckResult { - pub name: String, - pub healthy: bool, - pub message: String, - pub duration_ms: u64, -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - heal::HealEngineConfig, - policy::PolicyEngineConfig, - scanner::ScanEngineConfig, - }; - - #[tokio::test] - async fn test_status_api_creation() { - let config = StatusApiConfig::default(); - let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); - let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); - let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); - - let status_api = StatusApi::new(config, scan_engine, heal_engine, policy_engine).await.unwrap(); - - assert!(status_api.config().enabled); - assert_eq!(status_api.config().prefix, "/status"); - } - - #[tokio::test] - async fn test_basic_status() { - let config = StatusApiConfig::default(); - let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); - let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); - let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); - - let status_api = StatusApi::new(config, scan_engine, heal_engine, policy_engine).await.unwrap(); - - let request = HttpRequest { - method: "GET".to_string(), - path: "/status".to_string(), - headers: vec![], - body: None, - query_params: vec![], - }; - - let response = status_api.handle_request(request).await.unwrap(); - assert_eq!(response.status_code, 200); - assert!(response.body.contains("overall_status")); - } - - #[tokio::test] - async fn test_health_status() { - let config = StatusApiConfig::default(); - let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); - let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); - let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); - - let status_api = StatusApi::new(config, scan_engine, heal_engine, policy_engine).await.unwrap(); - - let request = HttpRequest { - method: "GET".to_string(), - path: "/status/health".to_string(), - headers: vec![], - body: None, - query_params: vec![], - }; - - let response = status_api.handle_request(request).await.unwrap(); - assert_eq!(response.status_code, 200); - assert!(response.body.contains("status")); - } - - #[tokio::test] - async fn test_scan_status() { - let config = StatusApiConfig::default(); - let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); - let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); - let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); - - let status_api = StatusApi::new(config, scan_engine, heal_engine, policy_engine).await.unwrap(); - - let request = HttpRequest { - method: "GET".to_string(), - path: "/status/scan".to_string(), - headers: vec![], - body: None, - query_params: vec![], - }; - - let response = status_api.handle_request(request).await.unwrap(); - assert_eq!(response.status_code, 200); - assert!(response.body.contains("scan")); - } - - #[tokio::test] - async fn test_heal_status() { - let config = StatusApiConfig::default(); - let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); - let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); - let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); - - let status_api = StatusApi::new(config, scan_engine, heal_engine, policy_engine).await.unwrap(); - - let request = HttpRequest { - method: "GET".to_string(), - path: "/status/heal".to_string(), - headers: vec![], - body: None, - query_params: vec![], - }; - - let response = status_api.handle_request(request).await.unwrap(); - assert_eq!(response.status_code, 200); - assert!(response.body.contains("heal")); - } - - #[tokio::test] - async fn test_version_info() { - let config = StatusApiConfig::default(); - let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); - let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); - let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); - - let status_api = StatusApi::new(config, scan_engine, heal_engine, policy_engine).await.unwrap(); - - let request = HttpRequest { - method: "GET".to_string(), - path: "/status/version".to_string(), - headers: vec![], - body: None, - query_params: vec![], - }; - - let response = status_api.handle_request(request).await.unwrap(); - assert_eq!(response.status_code, 200); - assert!(response.body.contains("version")); - } - - #[tokio::test] - async fn test_liveness_status() { - let config = StatusApiConfig::default(); - let scan_engine = Arc::new(ScanEngine::new(ScanEngineConfig::default()).await.unwrap()); - let heal_engine = Arc::new(HealEngine::new(HealEngineConfig::default()).await.unwrap()); - let policy_engine = Arc::new(PolicyEngine::new(PolicyEngineConfig::default()).await.unwrap()); - - let status_api = StatusApi::new(config, scan_engine, heal_engine, policy_engine).await.unwrap(); - - let request = HttpRequest { - method: "GET".to_string(), - path: "/status/health/liveness".to_string(), - headers: vec![], - body: None, - query_params: vec![], - }; - - let response = status_api.handle_request(request).await.unwrap(); - assert_eq!(response.status_code, 200); - assert!(response.body.contains("alive")); - } -} \ No newline at end of file diff --git a/crates/ahm/src/core/coordinator.rs b/crates/ahm/src/core/coordinator.rs deleted file mode 100644 index 0c82a11ba..000000000 --- a/crates/ahm/src/core/coordinator.rs +++ /dev/null @@ -1,448 +0,0 @@ -// 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. - -//! Core coordinator for the AHM system -//! -//! The coordinator is responsible for: -//! - Event routing and distribution between subsystems -//! - Resource management and allocation -//! - Global state coordination -//! - Cross-system communication - -use std::{ - sync::{Arc, atomic::{AtomicU64, Ordering}}, - time::{Duration, Instant}, -}; - -use tokio::{ - sync::{broadcast, RwLock}, - task::JoinHandle, - time::interval, -}; -use tokio_util::sync::CancellationToken; -use tracing::{debug, info, warn}; - -use crate::{SystemEvent, metrics}; -use super::{Status, Scheduler, SchedulerConfig}; -use crate::scanner; -use crate::error::Result; -use crate::scanner::{HealthIssue, HealthIssueType, Severity}; - -/// Configuration for the coordinator -#[derive(Debug, Clone)] -pub struct CoordinatorConfig { - /// Event channel buffer size - pub event_buffer_size: usize, - /// Resource monitoring interval - pub resource_monitor_interval: Duration, - /// Maximum number of concurrent operations - pub max_concurrent_operations: usize, - /// Scheduler configuration - pub scheduler: SchedulerConfig, - /// Event channel capacity - pub event_channel_capacity: usize, - /// Health check interval - pub health_check_interval: Duration, - /// Metrics update interval - pub metrics_update_interval: Duration, -} - -impl Default for CoordinatorConfig { - fn default() -> Self { - Self { - event_buffer_size: 10000, - resource_monitor_interval: Duration::from_secs(30), - max_concurrent_operations: 100, - scheduler: SchedulerConfig::default(), - event_channel_capacity: 1024, - health_check_interval: Duration::from_secs(300), - metrics_update_interval: Duration::from_secs(60), - } - } -} - -/// Core coordinator for the AHM system -#[derive(Debug)] -pub struct Coordinator { - /// Configuration - config: CoordinatorConfig, - /// Current status - status: Arc>, - /// Event broadcaster - event_tx: broadcast::Sender, - /// Resource monitor handle - resource_monitor_handle: Arc>>>, - /// Event processor handle - event_processor_handle: Arc>>>, - /// Task scheduler - scheduler: Arc, - /// Metrics collector reference - metrics: Arc, - /// Active operations counter - active_operations: AtomicU64, - /// Cancellation token - cancel_token: CancellationToken, - /// Operation statistics - operation_stats: Arc>, -} - -impl Coordinator { - /// Create a new coordinator - pub async fn new( - config: CoordinatorConfig, - metrics: Arc, - cancel_token: CancellationToken, - ) -> Result { - let (event_tx, _) = broadcast::channel(config.event_buffer_size); - let scheduler = Arc::new(Scheduler::new(config.scheduler.clone()).await?); - - Ok(Self { - config, - status: Arc::new(RwLock::new(Status::Initializing)), - event_tx, - resource_monitor_handle: Arc::new(RwLock::new(None)), - event_processor_handle: Arc::new(RwLock::new(None)), - scheduler, - metrics, - active_operations: AtomicU64::new(0), - cancel_token, - operation_stats: Arc::new(RwLock::new(OperationStatistics::default())), - }) - } - - /// Start the coordinator - pub async fn start(&self) -> Result<()> { - info!("Starting AHM coordinator"); - - // Update status - *self.status.write().await = Status::Running; - - // Start resource monitor - self.start_resource_monitor().await?; - - // Start event processor - self.start_event_processor().await?; - - // Start scheduler - self.scheduler.start().await?; - - info!("AHM coordinator started successfully"); - Ok(()) - } - - /// Stop the coordinator - pub async fn stop(&self) -> Result<()> { - info!("Stopping AHM coordinator"); - - // Update status - *self.status.write().await = Status::Stopping; - - // Stop scheduler - self.scheduler.stop().await?; - - // Stop resource monitor - if let Some(handle) = self.resource_monitor_handle.write().await.take() { - handle.abort(); - } - - // Stop event processor - if let Some(handle) = self.event_processor_handle.write().await.take() { - handle.abort(); - } - - *self.status.write().await = Status::Stopped; - info!("AHM coordinator stopped"); - Ok(()) - } - - /// Get current status - pub async fn status(&self) -> Status { - self.status.read().await.clone() - } - - /// Subscribe to system events - pub fn subscribe_events(&self) -> broadcast::Receiver { - self.event_tx.subscribe() - } - - /// Publish a system event - pub async fn publish_event(&self, event: SystemEvent) -> Result<()> { - debug!("Publishing system event: {:?}", event); - - // Update operation statistics - self.update_operation_stats(&event).await; - - // Send to all subscribers - if let Err(e) = self.event_tx.send(event.clone()) { - warn!("Failed to publish event: {:?}", e); - } - - // Record the event in metrics - self.metrics.record_health_issue(&HealthIssue { - issue_type: HealthIssueType::Unknown, - severity: Severity::Low, - bucket: "system".to_string(), - object: "coordinator".to_string(), - description: format!("System event: {:?}", event), - metadata: None, - }).await?; - - Ok(()) - } - - /// Get system resource usage - pub async fn get_resource_usage(&self) -> metrics::ResourceUsage { - metrics::ResourceUsage { - disk_usage: metrics::DiskUsage { - total_bytes: 1_000_000_000, - used_bytes: 500_000_000, - available_bytes: 500_000_000, - usage_percentage: 50.0, - }, - memory_usage: metrics::MemoryUsage { - total_bytes: 16_000_000_000, - used_bytes: 4_000_000_000, - available_bytes: 12_000_000_000, - usage_percentage: 25.0, - }, - network_usage: metrics::NetworkUsage { - bytes_received: 1_000_000, - bytes_sent: 500_000, - packets_received: 1000, - packets_sent: 500, - }, - cpu_usage: metrics::CpuUsage { - usage_percentage: 0.25, - cores: 8, - load_average: 1.5, - }, - } - } - - /// Get operation statistics - pub async fn get_operation_statistics(&self) -> OperationStatistics { - self.operation_stats.read().await.clone() - } - - /// Get active operations count - pub fn get_active_operations_count(&self) -> u64 { - self.active_operations.load(Ordering::Relaxed) - } - - /// Register an active operation - pub fn register_operation(&self) -> OperationGuard { - let count = self.active_operations.fetch_add(1, Ordering::Relaxed); - debug!("Registered operation, active count: {}", count + 1); - OperationGuard::new(&self.active_operations) - } - - /// Start the resource monitor - async fn start_resource_monitor(&self) -> Result<()> { - let cancel_token = self.cancel_token.clone(); - let _event_tx = self.event_tx.clone(); - let interval_duration = self.config.resource_monitor_interval; - - let handle = tokio::spawn(async move { - let mut interval = interval(interval_duration); - - loop { - tokio::select! { - _ = cancel_token.cancelled() => { - debug!("Resource monitor cancelled"); - break; - } - _ = interval.tick() => { - // This would collect real resource metrics - // For now, we'll skip the actual collection - debug!("Resource monitor tick"); - } - } - } - }); - - *self.resource_monitor_handle.write().await = Some(handle); - Ok(()) - } - - /// Start the event processor - async fn start_event_processor(&self) -> Result<()> { - let mut event_rx = self.event_tx.subscribe(); - let cancel_token = self.cancel_token.clone(); - - let handle = tokio::spawn(async move { - loop { - tokio::select! { - _ = cancel_token.cancelled() => { - debug!("Event processor cancelled"); - break; - } - event = event_rx.recv() => { - match event { - Ok(event) => { - debug!("Processing system event: {:?}", event); - // Process the event (e.g., route to specific handlers) - } - Err(e) => { - warn!("Event processor error: {:?}", e); - } - } - } - } - } - }); - - *self.event_processor_handle.write().await = Some(handle); - Ok(()) - } - - /// Update operation statistics based on events - async fn update_operation_stats(&self, event: &SystemEvent) { - let mut stats = self.operation_stats.write().await; - - match event { - SystemEvent::ObjectDiscovered { .. } => { - stats.objects_discovered += 1; - } - SystemEvent::HealthIssueDetected(issue) => { - stats.health_issues_detected += 1; - match issue.severity { - scanner::Severity::Critical => stats.critical_issues += 1, - scanner::Severity::High => stats.high_priority_issues += 1, - scanner::Severity::Medium => stats.medium_priority_issues += 1, - scanner::Severity::Low => stats.low_priority_issues += 1, - } - } - SystemEvent::HealCompleted(result) => { - if result.success { - stats.heal_operations_succeeded += 1; - } else { - stats.heal_operations_failed += 1; - } - } - SystemEvent::ScanCompleted(_) => { - stats.scan_cycles_completed += 1; - } - SystemEvent::ResourceUsageUpdated { .. } => { - stats.resource_updates += 1; - } - } - - stats.last_updated = Instant::now(); - } -} - -/// RAII guard for tracking active operations -pub struct OperationGuard<'a> { - active_operations: &'a AtomicU64, -} - -impl<'a> OperationGuard<'a> { - pub fn new(active_operations: &'a AtomicU64) -> Self { - active_operations.fetch_add(1, Ordering::Relaxed); - Self { active_operations } - } -} - -impl Drop for OperationGuard<'_> { - fn drop(&mut self) { - self.active_operations.fetch_sub(1, Ordering::Relaxed); - } -} - -/// Operation statistics tracked by the coordinator -#[derive(Debug, Clone)] -pub struct OperationStatistics { - pub objects_discovered: u64, - pub health_issues_detected: u64, - pub heal_operations_succeeded: u64, - pub heal_operations_failed: u64, - pub scan_cycles_completed: u64, - pub resource_updates: u64, - pub critical_issues: u64, - pub high_priority_issues: u64, - pub medium_priority_issues: u64, - pub low_priority_issues: u64, - pub last_updated: Instant, -} - -impl Default for OperationStatistics { - fn default() -> Self { - Self { - objects_discovered: 0, - health_issues_detected: 0, - heal_operations_succeeded: 0, - heal_operations_failed: 0, - scan_cycles_completed: 0, - resource_updates: 0, - critical_issues: 0, - high_priority_issues: 0, - medium_priority_issues: 0, - low_priority_issues: 0, - last_updated: Instant::now(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::metrics::CollectorConfig; - - #[tokio::test] - async fn test_coordinator_lifecycle() { - let config = CoordinatorConfig::default(); - let metrics_config = CollectorConfig::default(); - let metrics = Arc::new(metrics::Collector::new(metrics_config).await.unwrap()); - let cancel_token = CancellationToken::new(); - - let coordinator = Coordinator::new(config, metrics, cancel_token).await.unwrap(); - - // Test initial status - assert_eq!(coordinator.status().await, Status::Initializing); - - // Start coordinator - coordinator.start().await.unwrap(); - assert_eq!(coordinator.status().await, Status::Running); - - // Stop coordinator - coordinator.stop().await.unwrap(); - assert_eq!(coordinator.status().await, Status::Stopped); - } - - #[tokio::test] - async fn test_operation_guard() { - let config = CoordinatorConfig::default(); - let metrics_config = CollectorConfig::default(); - let metrics = Arc::new(metrics::Collector::new(metrics_config).await.unwrap()); - let cancel_token = CancellationToken::new(); - - let coordinator = Coordinator::new(config, metrics, cancel_token).await.unwrap(); - - assert_eq!(coordinator.get_active_operations_count(), 0); - - { - let _guard1 = coordinator.register_operation(); - assert_eq!(coordinator.get_active_operations_count(), 1); - - { - let _guard2 = coordinator.register_operation(); - assert_eq!(coordinator.get_active_operations_count(), 2); - } - - assert_eq!(coordinator.get_active_operations_count(), 1); - } - - assert_eq!(coordinator.get_active_operations_count(), 0); - } -} \ No newline at end of file diff --git a/crates/ahm/src/core/lifecycle.rs b/crates/ahm/src/core/lifecycle.rs deleted file mode 100644 index ddb5d17a7..000000000 --- a/crates/ahm/src/core/lifecycle.rs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2024 RustFS Team - -use crate::error::Result; - -#[derive(Debug, Clone, Default)] -pub struct LifecycleConfig {} - -pub struct LifecycleManager {} - -impl LifecycleManager { - pub async fn new(_config: LifecycleConfig) -> Result { - Ok(Self {}) - } - - pub async fn start(&self) -> Result<()> { - Ok(()) - } - - pub async fn stop(&self) -> Result<()> { - Ok(()) - } -} \ No newline at end of file diff --git a/crates/ahm/src/core/mod.rs b/crates/ahm/src/core/mod.rs deleted file mode 100644 index 582162afb..000000000 --- a/crates/ahm/src/core/mod.rs +++ /dev/null @@ -1,40 +0,0 @@ -// 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. - -//! Core coordination and lifecycle management for the AHM system - -pub mod coordinator; -pub mod scheduler; -pub mod lifecycle; - -pub use coordinator::{Coordinator, CoordinatorConfig}; -pub use scheduler::{Scheduler, SchedulerConfig, Task, TaskPriority}; -pub use lifecycle::{LifecycleManager, LifecycleConfig}; - -/// Status of the core coordination system -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Status { - /// System is initializing - Initializing, - /// System is running normally - Running, - /// System is degraded but operational - Degraded, - /// System is shutting down - Stopping, - /// System has stopped - Stopped, - /// System encountered an error - Error(String), -} \ No newline at end of file diff --git a/crates/ahm/src/core/scheduler.rs b/crates/ahm/src/core/scheduler.rs deleted file mode 100644 index fa25fd27b..000000000 --- a/crates/ahm/src/core/scheduler.rs +++ /dev/null @@ -1,226 +0,0 @@ -// 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. - -//! Task scheduler for the AHM system - -use std::{ - collections::{BinaryHeap, HashMap}, - sync::{Arc, atomic::{AtomicU64, Ordering}}, - time::{Duration, Instant}, -}; - -use tokio::{ - sync::RwLock, - task::JoinHandle, -}; -use uuid::Uuid; - -use crate::error::Result; - -/// Task scheduler configuration -#[derive(Debug, Clone)] -pub struct SchedulerConfig { - /// Maximum number of concurrent tasks - pub max_concurrent_tasks: usize, - /// Default task timeout - pub default_timeout: Duration, - /// Queue capacity - pub queue_capacity: usize, - pub default_task_priority: TaskPriority, -} - -impl Default for SchedulerConfig { - fn default() -> Self { - Self { - max_concurrent_tasks: 10, - default_timeout: Duration::from_secs(300), // 5 minutes - queue_capacity: 1000, - default_task_priority: TaskPriority::Normal, - } - } -} - -/// Task priority levels -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum TaskPriority { - Low = 0, - Normal = 1, - High = 2, - Critical = 3, -} - -/// A scheduled task -#[derive(Debug, Clone)] -pub struct Task { - pub id: Uuid, - pub priority: TaskPriority, - pub scheduled_time: Instant, - pub timeout: Duration, - pub task_type: TaskType, - pub payload: TaskPayload, -} - -impl Task { - pub fn new(task_type: TaskType, payload: TaskPayload) -> Self { - Self { - id: Uuid::new_v4(), - priority: TaskPriority::Normal, - scheduled_time: Instant::now(), - timeout: Duration::from_secs(300), - task_type, - payload, - } - } - - pub fn with_priority(mut self, priority: TaskPriority) -> Self { - self.priority = priority; - self - } - - pub fn with_timeout(mut self, timeout: Duration) -> Self { - self.timeout = timeout; - self - } - - pub fn with_delay(mut self, delay: Duration) -> Self { - self.scheduled_time = Instant::now() + delay; - self - } -} - -/// Types of tasks that can be scheduled -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum TaskType { - Scan, - Heal, - Cleanup, - Maintenance, - Report, -} - -/// Task payload data -#[derive(Debug, Clone)] -pub enum TaskPayload { - Scan { - bucket: Option, - object_prefix: Option, - deep_scan: bool, - }, - Heal { - bucket: String, - object: String, - version_id: Option, - }, - Cleanup { - older_than: Duration, - }, - Maintenance { - operation: String, - }, - Report { - report_type: String, - }, -} - -/// Task scheduler -#[allow(dead_code)] -#[derive(Debug)] -pub struct Scheduler { - config: SchedulerConfig, - task_queue: Arc>>, - active_tasks: Arc>>>, - task_counter: AtomicU64, - worker_handles: Arc>>>, -} - -impl Scheduler { - pub async fn new(config: SchedulerConfig) -> Result { - Ok(Self { - config, - task_queue: Arc::new(RwLock::new(BinaryHeap::new())), - active_tasks: Arc::new(RwLock::new(HashMap::new())), - task_counter: AtomicU64::new(0), - worker_handles: Arc::new(RwLock::new(Vec::new())), - }) - } - - pub async fn start(&self) -> Result<()> { - // Start worker tasks - // Implementation would go here - Ok(()) - } - - pub async fn stop(&self) -> Result<()> { - // Stop all workers and drain queues - // Implementation would go here - Ok(()) - } - - pub async fn schedule_task(&self, task: Task) -> Result { - let task_id = task.id; - let prioritized_task = PrioritizedTask { - task, - sequence: self.task_counter.fetch_add(1, Ordering::Relaxed), - }; - - self.task_queue.write().await.push(prioritized_task); - Ok(task_id) - } - - pub async fn cancel_task(&self, task_id: Uuid) -> Result { - if let Some(handle) = self.active_tasks.write().await.remove(&task_id) { - handle.abort(); - Ok(true) - } else { - Ok(false) - } - } -} - -/// Task wrapper for priority queue ordering -#[derive(Debug)] -struct PrioritizedTask { - task: Task, - sequence: u64, -} - -impl PartialEq for PrioritizedTask { - fn eq(&self, other: &Self) -> bool { - self.task.priority == other.task.priority && self.sequence == other.sequence - } -} - -impl Eq for PrioritizedTask {} - -impl PartialOrd for PrioritizedTask { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for PrioritizedTask { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - // Higher priority first, then by sequence number for fairness - other.task.priority.cmp(&self.task.priority) - .then_with(|| self.sequence.cmp(&other.sequence)) - } -} - -#[derive(Debug, Clone)] -pub struct ScheduledTask { - pub id: Uuid, - pub task_type: TaskType, - pub priority: TaskPriority, - pub created_at: Instant, -} \ No newline at end of file diff --git a/crates/ahm/src/heal/engine.rs b/crates/ahm/src/heal/engine.rs deleted file mode 100644 index ba90108f6..000000000 --- a/crates/ahm/src/heal/engine.rs +++ /dev/null @@ -1,438 +0,0 @@ -// 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::{ - collections::HashMap, - sync::Arc, - time::{Duration, Instant, SystemTime}, -}; - -use tokio::{ - sync::{mpsc, RwLock}, - time::sleep, -}; -use tracing::{error, info, warn}; -use uuid::Uuid; - -use crate::error::Result; -use super::{HealConfig, HealPriority, HealResult, HealStatistics, HealTask, Status}; - -/// Main healing engine that coordinates repair operations -pub struct HealEngine { - config: HealConfig, - status: Arc>, - statistics: Arc>, - task_queue: Arc>>, - active_tasks: Arc>>, - completed_tasks: Arc>>, - shutdown_tx: Option>, -} - -impl HealEngine { - /// Create a new healing engine - pub fn new(config: HealConfig) -> Self { - Self { - config, - status: Arc::new(RwLock::new(Status::Initializing)), - statistics: Arc::new(RwLock::new(HealStatistics::default())), - task_queue: Arc::new(RwLock::new(Vec::new())), - active_tasks: Arc::new(RwLock::new(HashMap::new())), - completed_tasks: Arc::new(RwLock::new(Vec::new())), - shutdown_tx: None, - } - } - - /// Start the healing engine - pub async fn start(&mut self) -> Result<()> { - info!("Starting heal engine"); - - let (shutdown_tx, mut shutdown_rx) = mpsc::channel(1); - self.shutdown_tx = Some(shutdown_tx); - - // Update status - { - let mut status = self.status.write().await; - *status = Status::Idle; - } - - let config = self.config.clone(); - let status = Arc::clone(&self.status); - let statistics = Arc::clone(&self.statistics); - let task_queue = Arc::clone(&self.task_queue); - let active_tasks = Arc::clone(&self.active_tasks); - let completed_tasks = Arc::clone(&self.completed_tasks); - - // Start the main healing loop - tokio::spawn(async move { - let mut interval = tokio::time::interval(config.heal_interval); - - loop { - tokio::select! { - _ = interval.tick() => { - if let Err(e) = Self::process_healing_cycle( - &config, - &status, - &statistics, - &task_queue, - &active_tasks, - &completed_tasks, - ).await { - error!("Healing cycle failed: {}", e); - } - } - _ = shutdown_rx.recv() => { - info!("Shutdown signal received, stopping heal engine"); - break; - } - } - } - - // Update status to stopped - let mut status = status.write().await; - *status = Status::Stopped; - }); - - info!("Heal engine started successfully"); - Ok(()) - } - - /// Stop the healing engine - pub async fn stop(&mut self) -> Result<()> { - info!("Stopping heal engine"); - - // Update status - { - let mut status = self.status.write().await; - *status = Status::Stopping; - } - - // Send shutdown signal - if let Some(shutdown_tx) = &self.shutdown_tx { - let _ = shutdown_tx.send(()).await; - } - - // Wait for engine to stop - let mut attempts = 0; - while attempts < 10 { - let status = self.status.read().await; - if *status == Status::Stopped { - break; - } - drop(status); - sleep(Duration::from_millis(100)).await; - attempts += 1; - } - - info!("Heal engine stopped"); - Ok(()) - } - - /// Add a healing task to the queue - pub async fn add_task(&self, task: HealTask) -> Result<()> { - let task_id = task.id.clone(); - let queue = Arc::clone(&self.task_queue); - - // Add task to priority queue - queue.write().await.push(task); - - info!("Added healing task to queue: {}", task_id); - Ok(()) - } - - /// Get current engine status - pub async fn status(&self) -> Status { - self.status.read().await.clone() - } - - /// Get current engine status (alias for status) - pub async fn get_status(&self) -> Status { - self.status.read().await.clone() - } - - /// Get engine configuration - pub async fn get_config(&self) -> HealConfig { - self.config.clone() - } - - /// Get healing statistics - pub async fn statistics(&self) -> HealStatistics { - self.statistics.read().await.clone() - } - - /// Get completed healing results - pub async fn completed_results(&self) -> Vec { - self.completed_tasks.read().await.clone() - } - - /// Process a single healing cycle - async fn process_healing_cycle( - config: &HealConfig, - status: &Arc>, - statistics: &Arc>, - task_queue: &Arc>>, - active_tasks: &Arc>>, - completed_tasks: &Arc>>, - ) -> Result<()> { - // Update status to healing - { - let mut status = status.write().await; - *status = Status::Healing; - } - - // Get ready tasks from queue - let mut queue = task_queue.write().await; - let mut ready_tasks = Vec::new(); - let mut remaining_tasks = Vec::new(); - - for task in queue.drain(..) { - if task.is_ready() { - ready_tasks.push(task); - } else { - remaining_tasks.push(task); - } - } - - // Sort ready tasks by priority - ready_tasks.sort_by(|a, b| a.priority.cmp(&b.priority)); - - // Process ready tasks - let active_count = active_tasks.read().await.len(); - let max_concurrent = config.max_workers.saturating_sub(active_count); - - for task in ready_tasks.into_iter().take(max_concurrent) { - if let Err(e) = Self::process_task( - config, - statistics, - active_tasks, - completed_tasks, - task, - ).await { - error!("Failed to process healing task: {}", e); - } - } - - // Put remaining tasks back in queue - queue.extend(remaining_tasks); - - // Update statistics - { - let mut stats = statistics.write().await; - stats.queued_tasks = queue.len() as u64; - stats.active_workers = active_tasks.read().await.len() as u64; - } - - // Update status back to idle - { - let mut status = status.write().await; - *status = Status::Idle; - } - - Ok(()) - } - - /// Process a single healing task - async fn process_task( - config: &HealConfig, - statistics: &Arc>, - active_tasks: &Arc>>, - completed_tasks: &Arc>>, - task: HealTask, - ) -> Result<()> { - let task_id = task.id.clone(); - - // Add task to active tasks - { - let mut active = active_tasks.write().await; - active.insert(task_id.clone(), task.clone()); - } - - // Update statistics - { - let mut stats = statistics.write().await; - stats.total_repairs += 1; - stats.active_workers = active_tasks.read().await.len() as u64; - } - - info!("Processing healing task: {}", task_id); - - // Simulate healing operation - let start_time = Instant::now(); - let result = Self::perform_healing_operation(&task, config).await; - let duration = start_time.elapsed(); - - // Create heal result - let heal_result = HealResult { - success: result.is_ok(), - original_issue: task.issue.clone(), - repair_duration: duration, - retry_attempts: task.retry_count, - error_message: result.err().map(|e| e.to_string()), - metadata: None, - completed_at: SystemTime::now(), - }; - - // Update statistics - { - let mut stats = statistics.write().await; - if heal_result.success { - stats.successful_repairs += 1; - } else { - stats.failed_repairs += 1; - } - stats.total_repair_time += duration; - stats.average_repair_time = if stats.total_repairs > 0 { - Duration::from_secs_f64( - stats.total_repair_time.as_secs_f64() / stats.total_repairs as f64 - ) - } else { - Duration::ZERO - }; - stats.last_repair_time = Some(SystemTime::now()); - stats.total_retry_attempts += task.retry_count as u64; - } - - // Add result to completed tasks - { - let mut completed = completed_tasks.write().await; - completed.push(heal_result.clone()); - } - - // Remove task from active tasks - { - let mut active = active_tasks.write().await; - active.remove(&task_id); - } - - // Update statistics - { - let mut stats = statistics.write().await; - stats.active_workers = active_tasks.read().await.len() as u64; - } - - if heal_result.success { - info!("Healing task completed successfully: {}", task_id); - } else { - warn!("Healing task failed: {}", task_id); - } - - Ok(()) - } - - /// Perform the actual healing operation - async fn perform_healing_operation(task: &HealTask, _config: &HealConfig) -> Result<()> { - // Simulate healing operation based on issue type - match task.issue.issue_type { - crate::scanner::HealthIssueType::MissingReplica => { - // Simulate replica repair - sleep(Duration::from_millis(100)).await; - info!("Repaired missing replica for {}/{}", task.issue.bucket, task.issue.object); - } - crate::scanner::HealthIssueType::ChecksumMismatch => { - // Simulate checksum repair - sleep(Duration::from_millis(200)).await; - info!("Repaired checksum mismatch for {}/{}", task.issue.bucket, task.issue.object); - } - crate::scanner::HealthIssueType::DiskReadError => { - // Simulate disk error recovery - sleep(Duration::from_millis(300)).await; - info!("Recovered from disk read error for {}/{}", task.issue.bucket, task.issue.object); - } - _ => { - // Generic repair for other issue types - sleep(Duration::from_millis(150)).await; - info!("Performed generic repair for {}/{}", task.issue.bucket, task.issue.object); - } - } - - // Simulate occasional failures for testing - if task.retry_count > 0 && task.retry_count % 3 == 0 { - return Err(crate::error::Error::Other(anyhow::anyhow!("Simulated healing failure"))); - } - - Ok(()) - } - - /// Start healing operations - pub async fn start_healing(&self) -> Result<()> { - let mut status = self.status.write().await; - *status = Status::Running; - info!("Healing operations started"); - Ok(()) - } - - /// Stop healing operations - pub async fn stop_healing(&self) -> Result<()> { - let mut status = self.status.write().await; - *status = Status::Stopped; - info!("Healing operations stopped"); - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::scanner::{HealthIssue, HealthIssueType, Severity}; - - #[tokio::test] - async fn test_heal_engine_creation() { - let config = HealConfig::default(); - let engine = HealEngine::new(config); - - assert_eq!(engine.status().await, Status::Initializing); - } - - #[tokio::test] - async fn test_heal_engine_start_stop() { - let config = HealConfig::default(); - let mut engine = HealEngine::new(config); - - // Start engine - engine.start().await.unwrap(); - sleep(Duration::from_millis(100)).await; - - // Check status - let status = engine.status().await; - assert!(matches!(status, Status::Idle | Status::Healing)); - - // Stop engine - engine.stop().await.unwrap(); - sleep(Duration::from_millis(100)).await; - - // Check status - let status = engine.status().await; - assert_eq!(status, Status::Stopped); - } - - #[tokio::test] - async fn test_add_healing_task() { - let config = HealConfig::default(); - let engine = HealEngine::new(config); - - let issue = HealthIssue { - issue_type: HealthIssueType::MissingReplica, - severity: Severity::Critical, - bucket: "test-bucket".to_string(), - object: "test-object".to_string(), - description: "Test issue".to_string(), - metadata: None, - }; - - let task = HealTask::new(issue); - engine.add_task(task).await.unwrap(); - - let stats = engine.statistics().await; - assert_eq!(stats.queued_tasks, 1); - } -} \ No newline at end of file diff --git a/crates/ahm/src/heal/mod.rs b/crates/ahm/src/heal/mod.rs deleted file mode 100644 index 9e7847c95..000000000 --- a/crates/ahm/src/heal/mod.rs +++ /dev/null @@ -1,360 +0,0 @@ -// 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. - -//! Healing subsystem for the AHM system -//! -//! The heal subsystem provides intelligent repair capabilities: -//! - Priority-based healing queue -//! - Real-time and background healing modes -//! - Comprehensive repair validation -//! - Adaptive healing strategies - -pub mod engine; -pub mod priority_queue; -pub mod repair_worker; -pub mod validation; - -pub use engine::HealEngine; -pub use priority_queue::PriorityQueue; -pub use repair_worker::RepairWorker; -pub use validation::HealValidator; - -use std::time::{Duration, SystemTime}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; -use derive_builder::Builder; - -use crate::scanner::{HealthIssue, HealthIssueType, Severity}; - -/// Configuration for the healing system -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct HealConfig { - /// Maximum number of concurrent repair workers - pub max_workers: usize, - /// Maximum number of tasks in the priority queue - pub max_queue_size: usize, - /// Timeout for individual repair operations - pub repair_timeout: Duration, - /// Interval between healing cycles - pub heal_interval: Duration, - /// Whether to enable automatic healing - pub auto_heal_enabled: bool, - /// Maximum number of retry attempts for failed repairs - pub max_retry_attempts: u32, - /// Backoff delay between retry attempts - pub retry_backoff_delay: Duration, - /// Whether to validate repairs after completion - pub validate_after_repair: bool, -} - -impl Default for HealConfig { - fn default() -> Self { - Self { - max_workers: 4, - max_queue_size: 1000, - repair_timeout: Duration::from_secs(300), // 5 minutes - heal_interval: Duration::from_secs(60), // 1 minute - auto_heal_enabled: true, - max_retry_attempts: 3, - retry_backoff_delay: Duration::from_secs(30), - validate_after_repair: true, - } - } -} - -/// Result of a healing operation -#[derive(Debug, Clone)] -pub struct HealResult { - /// Whether the healing operation was successful - pub success: bool, - /// The original health issue that was addressed - pub original_issue: HealthIssue, - /// Time taken to complete the repair - pub repair_duration: Duration, - /// Number of retry attempts made - pub retry_attempts: u32, - /// Error message if repair failed - pub error_message: Option, - /// Additional metadata about the repair - pub metadata: Option, - /// Timestamp when the repair was completed - pub completed_at: SystemTime, -} - -/// Statistics for the healing system -#[derive(Debug, Clone, Default)] -pub struct HealStatistics { - /// Total number of repair tasks processed - pub total_repairs: u64, - /// Number of successful repairs - pub successful_repairs: u64, - /// Number of failed repairs - pub failed_repairs: u64, - /// Number of tasks currently in queue - pub queued_tasks: u64, - /// Number of active workers - pub active_workers: u64, - /// Total time spent on repairs - pub total_repair_time: Duration, - /// Average repair time - pub average_repair_time: Duration, - /// Last repair completion time - pub last_repair_time: Option, - /// Number of retry attempts made - pub total_retry_attempts: u64, -} - -/// Priority levels for healing tasks -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum HealPriority { - /// Critical issues that need immediate attention - Critical = 0, - /// High priority issues - High = 1, - /// Medium priority issues - Medium = 2, - /// Low priority issues - Low = 3, -} - -impl From for HealPriority { - fn from(severity: Severity) -> Self { - match severity { - Severity::Critical => HealPriority::Critical, - Severity::High => HealPriority::High, - Severity::Medium => HealPriority::Medium, - Severity::Low => HealPriority::Low, - } - } -} - -/// A healing task to be processed -#[derive(Debug, Clone)] -pub struct HealTask { - /// Unique identifier for the task - pub id: String, - /// The health issue to be repaired - pub issue: HealthIssue, - /// Priority level for this task - pub priority: HealPriority, - /// When the task was created - pub created_at: SystemTime, - /// When the task should be processed (for delayed tasks) - pub scheduled_at: Option, - /// Number of retry attempts made - pub retry_count: u32, - /// Maximum number of retry attempts allowed - pub max_retries: u32, - /// Additional context for the repair operation - pub context: Option, -} - -impl HealTask { - /// Create a new healing task - pub fn new(issue: HealthIssue) -> Self { - let priority = HealPriority::from(issue.severity); - Self { - id: uuid::Uuid::new_v4().to_string(), - issue, - priority, - created_at: SystemTime::now(), - scheduled_at: None, - retry_count: 0, - max_retries: 3, - context: None, - } - } - - /// Create a delayed healing task - pub fn delayed(issue: HealthIssue, delay: Duration) -> Self { - let mut task = Self::new(issue); - task.scheduled_at = Some(SystemTime::now() + delay); - task - } - - /// Check if the task is ready to be processed - pub fn is_ready(&self) -> bool { - if let Some(scheduled_at) = self.scheduled_at { - SystemTime::now() >= scheduled_at - } else { - true - } - } - - /// Check if the task can be retried - pub fn can_retry(&self) -> bool { - self.retry_count < self.max_retries - } - - /// Increment the retry count - pub fn increment_retry(&mut self) { - self.retry_count += 1; - } -} - -/// Heal engine status -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum Status { - /// Heal engine is initializing - Initializing, - /// Heal engine is idle - Idle, - /// Heal engine is running normally - Running, - /// Heal engine is actively healing - Healing, - /// Heal engine is paused - Paused, - /// Heal engine is stopping - Stopping, - /// Heal engine has stopped - Stopped, - /// Heal engine encountered an error - Error(String), -} - -/// Healing operation modes -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum HealMode { - /// Real-time healing during GET/PUT operations - RealTime, - /// Background healing during scheduled scans - Background, - /// On-demand healing triggered by admin - OnDemand, - /// Emergency healing for critical issues - Emergency, -} - -/// Validation result for a repaired object -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ValidationResult { - /// Type of validation performed - pub validation_type: ValidationType, - /// Whether validation passed - pub passed: bool, - /// Details about the validation - pub details: String, - /// Time taken for validation - pub duration: Duration, -} - -/// Types of validation that can be performed -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum ValidationType { - /// Checksum verification - Checksum, - /// Shard count verification - ShardCount, - /// Data integrity check - DataIntegrity, - /// Metadata consistency check - MetadataConsistency, - /// Cross-shard redundancy check - RedundancyCheck, -} - -/// Healing strategies for different scenarios -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum HealStrategy { - /// Repair using available data shards - DataShardRepair, - /// Repair using parity shards - ParityShardRepair, - /// Hybrid repair using both data and parity - HybridRepair, - /// Metadata-only repair - MetadataRepair, - /// Full object reconstruction - FullReconstruction, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_heal_priority_from_severity() { - assert_eq!(HealPriority::from(Severity::Critical), HealPriority::Critical); - assert_eq!(HealPriority::from(Severity::High), HealPriority::High); - assert_eq!(HealPriority::from(Severity::Medium), HealPriority::Medium); - assert_eq!(HealPriority::from(Severity::Low), HealPriority::Low); - } - - #[test] - fn test_heal_task_creation() { - let issue = HealthIssue { - issue_type: HealthIssueType::MissingReplica, - severity: Severity::Critical, - bucket: "test-bucket".to_string(), - object: "test-object".to_string(), - description: "Test issue".to_string(), - metadata: None, - }; - - let task = HealTask::new(issue.clone()); - assert_eq!(task.priority, HealPriority::Critical); - assert_eq!(task.issue.bucket, issue.bucket); - assert_eq!(task.issue.object, issue.object); - assert_eq!(task.retry_count, 0); - assert_eq!(task.max_retries, 3); - assert!(task.is_ready()); - } - - #[test] - fn test_delayed_heal_task() { - let issue = HealthIssue { - issue_type: HealthIssueType::MissingReplica, - severity: Severity::Medium, - bucket: "test-bucket".to_string(), - object: "test-object".to_string(), - description: "Test issue".to_string(), - metadata: None, - }; - - let delay = Duration::from_secs(1); - let task = HealTask::delayed(issue, delay); - - assert!(task.scheduled_at.is_some()); - assert!(!task.is_ready()); // Should not be ready immediately - - // Wait for the delay to pass - std::thread::sleep(delay + Duration::from_millis(100)); - assert!(task.is_ready()); - } - - #[test] - fn test_heal_task_retry_logic() { - let issue = HealthIssue { - issue_type: HealthIssueType::MissingReplica, - severity: Severity::Low, - bucket: "test-bucket".to_string(), - object: "test-object".to_string(), - description: "Test issue".to_string(), - metadata: None, - }; - - let mut task = HealTask::new(issue); - assert!(task.can_retry()); - - task.increment_retry(); - assert_eq!(task.retry_count, 1); - assert!(task.can_retry()); - - task.increment_retry(); - task.increment_retry(); - assert_eq!(task.retry_count, 3); - assert!(!task.can_retry()); - } -} \ No newline at end of file diff --git a/crates/ahm/src/heal/priority_queue.rs b/crates/ahm/src/heal/priority_queue.rs deleted file mode 100644 index 07a1b6001..000000000 --- a/crates/ahm/src/heal/priority_queue.rs +++ /dev/null @@ -1,413 +0,0 @@ -// 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::{ - collections::BinaryHeap, - sync::Arc, - time::{Duration, SystemTime}, -}; - -use tokio::sync::RwLock; -use tracing::{debug, info, warn}; - -use crate::error::Result; -use super::{HealPriority, HealTask}; - -/// Priority queue for healing tasks -pub struct PriorityQueue { - tasks: Arc>>, - max_size: usize, - statistics: Arc>, -} - -/// Statistics for the priority queue -#[derive(Debug, Clone, Default)] -pub struct QueueStatistics { - /// Total number of tasks added to the queue - pub total_tasks_added: u64, - /// Total number of tasks removed from the queue - pub total_tasks_removed: u64, - /// Current number of tasks in the queue - pub current_queue_size: u64, - /// Maximum queue size reached - pub max_queue_size_reached: u64, - /// Number of tasks rejected due to queue being full - pub tasks_rejected: u64, - /// Average time tasks spend in queue - pub average_queue_time: Duration, - /// Total time all tasks have spent in queue - pub total_queue_time: Duration, -} - -impl PriorityQueue { - /// Create a new priority queue - pub fn new(max_size: usize) -> Self { - Self { - tasks: Arc::new(RwLock::new(BinaryHeap::new())), - max_size, - statistics: Arc::new(RwLock::new(QueueStatistics::default())), - } - } - - /// Add a task to the queue - pub async fn push(&self, task: HealTask) -> Result<()> { - let mut tasks = self.tasks.write().await; - let mut stats = self.statistics.write().await; - - if tasks.len() >= self.max_size { - stats.tasks_rejected += 1; - warn!("Priority queue is full, rejecting task: {}", task.id); - return Err(crate::error::Error::Other(anyhow::anyhow!("Queue is full"))); - } - - let task_id = task.id.clone(); - let priority = task.priority.clone(); - tasks.push(task); - stats.total_tasks_added += 1; - stats.current_queue_size = tasks.len() as u64; - stats.max_queue_size_reached = stats.max_queue_size_reached.max(tasks.len() as u64); - - debug!("Added task to priority queue: {} (priority: {:?})", task_id, priority); - Ok(()) - } - - /// Remove and return the highest priority task - pub async fn pop(&self) -> Option { - let mut tasks = self.tasks.write().await; - let mut stats = self.statistics.write().await; - - if let Some(task) = tasks.pop() { - stats.total_tasks_removed += 1; - stats.current_queue_size = tasks.len() as u64; - - // Update queue time statistics - let queue_time = SystemTime::now().duration_since(task.created_at).unwrap_or(Duration::ZERO); - stats.total_queue_time += queue_time; - stats.average_queue_time = if stats.total_tasks_removed > 0 { - Duration::from_secs_f64( - stats.total_queue_time.as_secs_f64() / stats.total_tasks_removed as f64 - ) - } else { - Duration::ZERO - }; - - debug!("Removed task from priority queue: {} (priority: {:?})", task.id, task.priority); - Some(task) - } else { - None - } - } - - /// Peek at the highest priority task without removing it - pub async fn peek(&self) -> Option { - let tasks = self.tasks.read().await; - tasks.peek().cloned() - } - - /// Get the current size of the queue - pub async fn len(&self) -> usize { - self.tasks.read().await.len() - } - - /// Check if the queue is empty - pub async fn is_empty(&self) -> bool { - self.tasks.read().await.is_empty() - } - - /// Get queue statistics - pub async fn statistics(&self) -> QueueStatistics { - self.statistics.read().await.clone() - } - - /// Clear all tasks from the queue - pub async fn clear(&self) { - let mut tasks = self.tasks.write().await; - let mut stats = self.statistics.write().await; - - let cleared_count = tasks.len(); - tasks.clear(); - stats.current_queue_size = 0; - - info!("Cleared {} tasks from priority queue", cleared_count); - } - - /// Get all tasks that are ready to be processed - pub async fn get_ready_tasks(&self, max_count: usize) -> Vec { - let mut tasks = self.tasks.write().await; - let mut ready_tasks = Vec::new(); - let mut remaining_tasks = Vec::new(); - - while let Some(task) = tasks.pop() { - if task.is_ready() && ready_tasks.len() < max_count { - ready_tasks.push(task); - } else { - remaining_tasks.push(task); - } - } - - // Put remaining tasks back - for task in remaining_tasks { - tasks.push(task); - } - - ready_tasks - } - - /// Remove a specific task by ID - pub async fn remove_task(&self, task_id: &str) -> bool { - let mut tasks = self.tasks.write().await; - let mut stats = self.statistics.write().await; - - let mut temp_tasks = Vec::new(); - let mut found = false; - - while let Some(task) = tasks.pop() { - if task.id == task_id { - found = true; - stats.total_tasks_removed += 1; - debug!("Removed specific task from queue: {}", task_id); - } else { - temp_tasks.push(task); - } - } - - // Put remaining tasks back - for task in temp_tasks { - tasks.push(task); - } - - stats.current_queue_size = tasks.len() as u64; - found - } - - /// Get tasks by priority level - pub async fn get_tasks_by_priority(&self, priority: HealPriority) -> Vec { - let mut tasks = self.tasks.write().await; - let mut matching_tasks = Vec::new(); - let mut other_tasks = Vec::new(); - - while let Some(task) = tasks.pop() { - if task.priority == priority { - matching_tasks.push(task); - } else { - other_tasks.push(task); - } - } - - // Put other tasks back - for task in other_tasks { - tasks.push(task); - } - - matching_tasks - } - - /// Update task priority - pub async fn update_priority(&self, task_id: &str, new_priority: HealPriority) -> bool { - let mut tasks = self.tasks.write().await; - - let mut temp_tasks = Vec::new(); - let mut found = false; - - while let Some(mut task) = tasks.pop() { - if task.id == task_id { - task.priority = new_priority.clone(); - found = true; - debug!("Updated task priority: {} -> {:?}", task_id, new_priority); - } - temp_tasks.push(task); - } - - // Put all tasks back - for task in temp_tasks { - tasks.push(task); - } - - found - } -} - -// Implement Ord for HealTask to enable priority queue functionality -impl std::cmp::Ord for HealTask { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - // Higher priority (lower enum value) comes first - self.priority.cmp(&other.priority) - .then_with(|| self.created_at.cmp(&other.created_at)) - } -} - -impl std::cmp::PartialOrd for HealTask { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl std::cmp::PartialEq for HealTask { - fn eq(&self, other: &Self) -> bool { - self.id == other.id - } -} - -impl std::cmp::Eq for HealTask {} - -#[cfg(test)] -mod tests { - use super::*; - use crate::scanner::{HealthIssue, HealthIssueType, Severity}; - - #[tokio::test] - async fn test_priority_queue_creation() { - let queue = PriorityQueue::new(100); - assert_eq!(queue.len().await, 0); - assert!(queue.is_empty().await); - } - - #[tokio::test] - async fn test_priority_queue_push_pop() { - let queue = PriorityQueue::new(10); - - let issue1 = HealthIssue { - issue_type: HealthIssueType::MissingReplica, - severity: Severity::Low, - bucket: "bucket1".to_string(), - object: "object1".to_string(), - description: "Test issue 1".to_string(), - metadata: None, - }; - - let issue2 = HealthIssue { - issue_type: HealthIssueType::MissingReplica, - severity: Severity::Critical, - bucket: "bucket2".to_string(), - object: "object2".to_string(), - description: "Test issue 2".to_string(), - metadata: None, - }; - - let task1 = HealTask::new(issue1); - let task2 = HealTask::new(issue2); - - // Add tasks - queue.push(task1.clone()).await.unwrap(); - queue.push(task2.clone()).await.unwrap(); - - assert_eq!(queue.len().await, 2); - - // Critical task should come first - let first_task = queue.pop().await.unwrap(); - assert_eq!(first_task.priority, HealPriority::Critical); - assert_eq!(first_task.id, task2.id); - - let second_task = queue.pop().await.unwrap(); - assert_eq!(second_task.priority, HealPriority::Low); - assert_eq!(second_task.id, task1.id); - - assert!(queue.is_empty().await); - } - - #[tokio::test] - async fn test_priority_queue_full() { - let queue = PriorityQueue::new(1); - - let issue1 = HealthIssue { - issue_type: HealthIssueType::MissingReplica, - severity: Severity::Low, - bucket: "bucket1".to_string(), - object: "object1".to_string(), - description: "Test issue 1".to_string(), - metadata: None, - }; - - let issue2 = HealthIssue { - issue_type: HealthIssueType::MissingReplica, - severity: Severity::Critical, - bucket: "bucket2".to_string(), - object: "object2".to_string(), - description: "Test issue 2".to_string(), - metadata: None, - }; - - let task1 = HealTask::new(issue1); - let task2 = HealTask::new(issue2); - - // First task should succeed - queue.push(task1).await.unwrap(); - assert_eq!(queue.len().await, 1); - - // Second task should fail - let result = queue.push(task2).await; - assert!(result.is_err()); - assert_eq!(queue.len().await, 1); - - let stats = queue.statistics().await; - assert_eq!(stats.tasks_rejected, 1); - } - - #[tokio::test] - async fn test_priority_queue_remove_task() { - let queue = PriorityQueue::new(10); - - let issue = HealthIssue { - issue_type: HealthIssueType::MissingReplica, - severity: Severity::Medium, - bucket: "bucket1".to_string(), - object: "object1".to_string(), - description: "Test issue".to_string(), - metadata: None, - }; - - let task = HealTask::new(issue); - let task_id = task.id.clone(); - - queue.push(task).await.unwrap(); - assert_eq!(queue.len().await, 1); - - // Remove the task - let removed = queue.remove_task(&task_id).await; - assert!(removed); - assert_eq!(queue.len().await, 0); - - // Try to remove non-existent task - let removed = queue.remove_task("non-existent").await; - assert!(!removed); - } - - #[tokio::test] - async fn test_priority_queue_update_priority() { - let queue = PriorityQueue::new(10); - - let issue = HealthIssue { - issue_type: HealthIssueType::MissingReplica, - severity: Severity::Low, - bucket: "bucket1".to_string(), - object: "object1".to_string(), - description: "Test issue".to_string(), - metadata: None, - }; - - let task = HealTask::new(issue); - let task_id = task.id.clone(); - - queue.push(task).await.unwrap(); - - // Update priority - let updated = queue.update_priority(&task_id, HealPriority::Critical).await; - assert!(updated); - - // Check that the task now has higher priority - let popped_task = queue.pop().await.unwrap(); - assert_eq!(popped_task.priority, HealPriority::Critical); - assert_eq!(popped_task.id, task_id); - } -} \ No newline at end of file diff --git a/crates/ahm/src/heal/repair_worker.rs b/crates/ahm/src/heal/repair_worker.rs deleted file mode 100644 index 018e62a5c..000000000 --- a/crates/ahm/src/heal/repair_worker.rs +++ /dev/null @@ -1,505 +0,0 @@ -// 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, - time::{Duration, Instant, SystemTime}, -}; - -use tokio::{ - sync::{mpsc, RwLock}, - time::{sleep, timeout}, -}; -use tracing::{debug, error, info, warn}; - -use crate::error::Result; -use super::{HealConfig, HealResult, HealTask, Status}; - -/// Configuration for repair workers -#[derive(Debug, Clone)] -pub struct RepairWorkerConfig { - /// Worker ID - pub worker_id: String, - /// Maximum time to spend on a single repair operation - pub operation_timeout: Duration, - /// Whether to enable detailed logging - pub enable_detailed_logging: bool, - /// Maximum number of concurrent operations - pub max_concurrent_operations: usize, - /// Retry configuration - pub retry_config: RetryConfig, -} - -/// Retry configuration for repair operations -#[derive(Debug, Clone)] -pub struct RetryConfig { - /// Maximum number of retry attempts - pub max_attempts: u32, - /// Initial backoff delay - pub initial_backoff: Duration, - /// Maximum backoff delay - pub max_backoff: Duration, - /// Backoff multiplier - pub backoff_multiplier: f64, - /// Whether to use exponential backoff - pub exponential_backoff: bool, -} - -impl Default for RepairWorkerConfig { - fn default() -> Self { - Self { - worker_id: "worker-1".to_string(), - operation_timeout: Duration::from_secs(300), // 5 minutes - enable_detailed_logging: true, - max_concurrent_operations: 1, - retry_config: RetryConfig::default(), - } - } -} - -impl Default for RetryConfig { - fn default() -> Self { - Self { - max_attempts: 3, - initial_backoff: Duration::from_secs(1), - max_backoff: Duration::from_secs(60), - backoff_multiplier: 2.0, - exponential_backoff: true, - } - } -} - -/// Statistics for a repair worker -#[derive(Debug, Clone, Default)] -pub struct WorkerStatistics { - /// Total number of tasks processed - pub total_tasks_processed: u64, - /// Number of successful repairs - pub successful_repairs: u64, - /// Number of failed repairs - pub failed_repairs: u64, - /// Total time spent on repairs - pub total_repair_time: Duration, - /// Average repair time - pub average_repair_time: Duration, - /// Number of retry attempts made - pub total_retry_attempts: u64, - /// Current worker status - pub status: WorkerStatus, - /// Last task completion time - pub last_task_time: Option, -} - -/// Worker status -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum WorkerStatus { - /// Worker is idle - Idle, - /// Worker is processing a task - Processing, - /// Worker is retrying a failed task - Retrying, - /// Worker is stopping - Stopping, - /// Worker has stopped - Stopped, - /// Worker encountered an error - Error(String), -} - -impl Default for WorkerStatus { - fn default() -> Self { - WorkerStatus::Idle - } -} - -/// Repair worker that executes healing tasks -pub struct RepairWorker { - config: RepairWorkerConfig, - statistics: Arc>, - status: Arc>, - result_tx: mpsc::Sender, - shutdown_tx: Option>, -} - -impl RepairWorker { - /// Create a new repair worker - pub fn new( - config: RepairWorkerConfig, - result_tx: mpsc::Sender, - ) -> Self { - Self { - config, - statistics: Arc::new(RwLock::new(WorkerStatistics::default())), - status: Arc::new(RwLock::new(WorkerStatus::Idle)), - result_tx, - shutdown_tx: None, - } - } - - /// Start the repair worker - pub async fn start(&mut self) -> Result<()> { - info!("Starting repair worker: {}", self.config.worker_id); - - let (_task_tx, task_rx) = mpsc::channel(100); - let (shutdown_tx, mut shutdown_rx) = mpsc::channel(1); - - self.shutdown_tx = Some(shutdown_tx); - - // Update status - { - let mut status = self.status.write().await; - *status = WorkerStatus::Idle; - } - - let config = self.config.clone(); - let statistics = Arc::clone(&self.statistics); - let status = Arc::clone(&self.status); - let result_tx = self.result_tx.clone(); - - // Start the worker loop - tokio::spawn(async move { - let mut task_rx = task_rx; - - loop { - tokio::select! { - Some(task) = task_rx.recv() => { - if let Err(e) = Self::process_task( - &config, - &statistics, - &status, - &result_tx, - task, - ).await { - error!("Failed to process task: {}", e); - } - } - _ = shutdown_rx.recv() => { - info!("Shutdown signal received, stopping worker: {}", config.worker_id); - break; - } - } - } - - // Update status to stopped - let mut status = status.write().await; - *status = WorkerStatus::Stopped; - }); - - info!("Repair worker started: {}", self.config.worker_id); - Ok(()) - } - - /// Stop the repair worker - pub async fn stop(&mut self) -> Result<()> { - info!("Stopping repair worker: {}", self.config.worker_id); - - // Update status - { - let mut status = self.status.write().await; - *status = WorkerStatus::Stopping; - } - - // Send shutdown signal - if let Some(shutdown_tx) = &self.shutdown_tx { - let _ = shutdown_tx.send(()).await; - } - - // Wait for worker to stop - let mut attempts = 0; - while attempts < 10 { - let status = self.status.read().await; - if *status == WorkerStatus::Stopped { - break; - } - drop(status); - sleep(Duration::from_millis(100)).await; - attempts += 1; - } - - info!("Repair worker stopped: {}", self.config.worker_id); - Ok(()) - } - - /// Submit a task to the worker - pub async fn submit_task(&self, _task: HealTask) -> Result<()> { - // TODO: Implement task submission - Err(crate::error::Error::Other(anyhow::anyhow!("Task submission not implemented"))) - } - - /// Get worker statistics - pub async fn statistics(&self) -> WorkerStatistics { - self.statistics.read().await.clone() - } - - /// Get worker status - pub async fn status(&self) -> WorkerStatus { - self.status.read().await.clone() - } - - /// Process a single task - async fn process_task( - config: &RepairWorkerConfig, - statistics: &Arc>, - status: &Arc>, - result_tx: &mpsc::Sender, - task: HealTask, - ) -> Result<()> { - let task_id = task.id.clone(); - - // Update status to processing - { - let mut status = status.write().await; - *status = WorkerStatus::Processing; - } - - // Update statistics - { - let mut stats = statistics.write().await; - stats.total_tasks_processed += 1; - stats.status = WorkerStatus::Processing; - } - - info!("Processing repair task: {} (worker: {})", task_id, config.worker_id); - - let start_time = Instant::now(); - let mut attempt = 0; - let mut last_error = None; - - // Retry loop - while attempt < config.retry_config.max_attempts { - attempt += 1; - - if attempt > 1 { - // Update status to retrying - { - let mut status = status.write().await; - *status = WorkerStatus::Retrying; - } - - // Calculate backoff delay - let backoff_delay = if config.retry_config.exponential_backoff { - let delay = config.retry_config.initial_backoff * - (config.retry_config.backoff_multiplier.powi((attempt - 1) as i32)) as u32; - delay.min(config.retry_config.max_backoff) - } else { - config.retry_config.initial_backoff - }; - - warn!("Retrying task {} (attempt {}/{}), waiting {:?}", - task_id, attempt, config.retry_config.max_attempts, backoff_delay); - sleep(backoff_delay).await; - } - - // Attempt the repair operation - let result = timeout( - config.operation_timeout, - Self::perform_repair_operation(&task, config) - ).await; - - match result { - Ok(Ok(())) => { - // Success - let duration = start_time.elapsed(); - let heal_result = HealResult { - success: true, - original_issue: task.issue.clone(), - repair_duration: duration, - retry_attempts: attempt - 1, - error_message: None, - metadata: None, - completed_at: SystemTime::now(), - }; - - // Send result - if let Err(e) = result_tx.send(heal_result).await { - error!("Failed to send heal result: {}", e); - } - - // Update statistics - { - let mut stats = statistics.write().await; - stats.successful_repairs += 1; - stats.total_repair_time += duration; - stats.average_repair_time = if stats.total_tasks_processed > 0 { - Duration::from_secs_f64( - stats.total_repair_time.as_secs_f64() / stats.total_tasks_processed as f64 - ) - } else { - Duration::ZERO - }; - stats.total_retry_attempts += (attempt - 1) as u64; - stats.last_task_time = Some(SystemTime::now()); - stats.status = WorkerStatus::Idle; - } - - info!("Successfully completed repair task: {} (worker: {})", task_id, config.worker_id); - return Ok(()); - } - Ok(Err(e)) => { - // Operation failed - let error_msg = e.to_string(); - last_error = Some(e); - warn!("Repair operation failed for task {} (attempt {}/{}): {}", - task_id, attempt, config.retry_config.max_attempts, error_msg); - } - Err(_) => { - // Operation timed out - last_error = Some(crate::error::Error::Other(anyhow::anyhow!("Operation timed out"))); - warn!("Repair operation timed out for task {} (attempt {}/{})", - task_id, attempt, config.retry_config.max_attempts); - } - } - } - - // All attempts failed - let duration = start_time.elapsed(); - let heal_result = HealResult { - success: false, - original_issue: task.issue.clone(), - repair_duration: duration, - retry_attempts: attempt - 1, - error_message: last_error.map(|e| e.to_string()), - metadata: None, - completed_at: SystemTime::now(), - }; - - // Send result - if let Err(e) = result_tx.send(heal_result).await { - error!("Failed to send heal result: {}", e); - } - - // Update statistics - { - let mut stats = statistics.write().await; - stats.failed_repairs += 1; - stats.total_repair_time += duration; - stats.average_repair_time = if stats.total_tasks_processed > 0 { - Duration::from_secs_f64( - stats.total_repair_time.as_secs_f64() / stats.total_tasks_processed as f64 - ) - } else { - Duration::ZERO - }; - stats.total_retry_attempts += (attempt - 1) as u64; - stats.last_task_time = Some(SystemTime::now()); - stats.status = WorkerStatus::Idle; - } - - error!("Failed to complete repair task after {} attempts: {} (worker: {})", - attempt, task_id, config.worker_id); - Ok(()) - } - - /// Perform the actual repair operation - async fn perform_repair_operation(task: &HealTask, config: &RepairWorkerConfig) -> Result<()> { - if config.enable_detailed_logging { - debug!("Starting repair operation for task: {} (worker: {})", task.id, config.worker_id); - } - - // Simulate repair operation based on issue type - match task.issue.issue_type { - crate::scanner::HealthIssueType::MissingReplica => { - // Simulate replica repair - sleep(Duration::from_millis(100)).await; - if config.enable_detailed_logging { - debug!("Repaired missing replica for {}/{}", task.issue.bucket, task.issue.object); - } - } - crate::scanner::HealthIssueType::ChecksumMismatch => { - // Simulate checksum repair - sleep(Duration::from_millis(200)).await; - if config.enable_detailed_logging { - debug!("Repaired checksum mismatch for {}/{}", task.issue.bucket, task.issue.object); - } - } - crate::scanner::HealthIssueType::DiskReadError => { - // Simulate disk error recovery - sleep(Duration::from_millis(300)).await; - if config.enable_detailed_logging { - debug!("Recovered from disk read error for {}/{}", task.issue.bucket, task.issue.object); - } - } - _ => { - // Generic repair for other issue types - sleep(Duration::from_millis(150)).await; - if config.enable_detailed_logging { - debug!("Performed generic repair for {}/{}", task.issue.bucket, task.issue.object); - } - } - } - - // Simulate occasional failures for testing - if task.retry_count > 0 && task.retry_count % 3 == 0 { - return Err(crate::error::Error::Other(anyhow::anyhow!("Simulated repair failure"))); - } - - if config.enable_detailed_logging { - debug!("Completed repair operation for task: {} (worker: {})", task.id, config.worker_id); - } - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::scanner::{HealthIssue, HealthIssueType, Severity}; - - #[tokio::test] - async fn test_repair_worker_creation() { - let config = RepairWorkerConfig::default(); - let (result_tx, _result_rx) = mpsc::channel(100); - let worker = RepairWorker::new(config, result_tx); - - assert_eq!(worker.status().await, WorkerStatus::Idle); - } - - #[tokio::test] - async fn test_repair_worker_start_stop() { - let config = RepairWorkerConfig::default(); - let (result_tx, _result_rx) = mpsc::channel(100); - let mut worker = RepairWorker::new(config, result_tx); - - // Start worker - worker.start().await.unwrap(); - sleep(Duration::from_millis(100)).await; - - // Check status - let status = worker.status().await; - assert_eq!(status, WorkerStatus::Idle); - - // Stop worker - worker.stop().await.unwrap(); - sleep(Duration::from_millis(100)).await; - - // Check status - let status = worker.status().await; - assert_eq!(status, WorkerStatus::Stopped); - } - - #[tokio::test] - async fn test_repair_worker_statistics() { - let config = RepairWorkerConfig::default(); - let (result_tx, _result_rx) = mpsc::channel(100); - let worker = RepairWorker::new(config, result_tx); - - let stats = worker.statistics().await; - assert_eq!(stats.total_tasks_processed, 0); - assert_eq!(stats.successful_repairs, 0); - assert_eq!(stats.failed_repairs, 0); - assert_eq!(stats.status, WorkerStatus::Idle); - } -} \ No newline at end of file diff --git a/crates/ahm/src/heal/validation.rs b/crates/ahm/src/heal/validation.rs deleted file mode 100644 index 2bb481f03..000000000 --- a/crates/ahm/src/heal/validation.rs +++ /dev/null @@ -1,453 +0,0 @@ -// 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::{ - collections::HashMap, - sync::Arc, - time::{Duration, Instant, SystemTime}, -}; - -use tokio::sync::RwLock; -use tracing::{debug, error, info, warn}; - -use crate::error::Result; -use super::{HealResult, HealTask}; - -/// Configuration for validation operations -#[derive(Debug, Clone)] -pub struct ValidationConfig { - /// Whether to enable validation after repair - pub enable_post_repair_validation: bool, - /// Timeout for validation operations - pub validation_timeout: Duration, - /// Whether to enable detailed validation logging - pub enable_detailed_logging: bool, - /// Maximum number of validation retries - pub max_validation_retries: u32, - /// Validation retry delay - pub validation_retry_delay: Duration, -} - -impl Default for ValidationConfig { - fn default() -> Self { - Self { - enable_post_repair_validation: true, - validation_timeout: Duration::from_secs(60), // 1 minute - max_validation_retries: 3, - validation_retry_delay: Duration::from_secs(5), - enable_detailed_logging: true, - } - } -} - -/// Validation result for a repair operation -#[derive(Debug, Clone)] -pub struct ValidationResult { - /// Whether validation passed - pub passed: bool, - /// Validation type - pub validation_type: ValidationType, - /// Detailed validation message - pub message: String, - /// Time taken for validation - pub duration: Duration, - /// Validation timestamp - pub timestamp: SystemTime, - /// Additional validation metadata - pub metadata: Option, -} - -/// Types of validation that can be performed -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ValidationType { - /// Checksum validation - Checksum, - /// File existence validation - FileExistence, - /// File size validation - FileSize, - /// File permissions validation - FilePermissions, - /// Metadata consistency validation - MetadataConsistency, - /// Replication status validation - ReplicationStatus, - /// Data integrity validation - DataIntegrity, - /// Custom validation - Custom(String), -} - -/// Statistics for validation operations -#[derive(Debug, Clone, Default)] -pub struct ValidationStatistics { - /// Total number of validations performed - pub total_validations: u64, - /// Number of successful validations - pub successful_validations: u64, - /// Number of failed validations - pub failed_validations: u64, - /// Total time spent on validation - pub total_validation_time: Duration, - /// Average validation time - pub average_validation_time: Duration, - /// Number of validation retries - pub total_validation_retries: u64, - /// Last validation time - pub last_validation_time: Option, -} - -/// Validator for repair operations -pub struct HealValidator { - config: ValidationConfig, - statistics: Arc>, -} - -impl HealValidator { - /// Create a new validator - pub fn new(config: ValidationConfig) -> Self { - Self { - config, - statistics: Arc::new(RwLock::new(ValidationStatistics::default())), - } - } - - /// Validate a repair operation - pub async fn validate_repair(&self, task: &HealTask, result: &HealResult) -> Result> { - if !self.config.enable_post_repair_validation { - return Ok(Vec::new()); - } - - let start_time = Instant::now(); - let mut validation_results = Vec::new(); - - info!("Starting validation for repair task: {}", task.id); - - // Perform different types of validation based on the issue type - match task.issue.issue_type { - crate::scanner::HealthIssueType::MissingReplica => { - validation_results.extend(self.validate_replica_repair(task, result).await?); - } - crate::scanner::HealthIssueType::ChecksumMismatch => { - validation_results.extend(self.validate_checksum_repair(task, result).await?); - } - crate::scanner::HealthIssueType::DiskReadError => { - validation_results.extend(self.validate_disk_repair(task, result).await?); - } - _ => { - validation_results.extend(self.validate_generic_repair(task, result).await?); - } - } - - let duration = start_time.elapsed(); - - // Update statistics - { - let mut stats = self.statistics.write().await; - stats.total_validations += validation_results.len() as u64; - stats.total_validation_time += duration; - stats.average_validation_time = if stats.total_validations > 0 { - Duration::from_secs_f64( - stats.total_validation_time.as_secs_f64() / stats.total_validations as f64 - ) - } else { - Duration::ZERO - }; - stats.last_validation_time = Some(SystemTime::now()); - - let successful_count = validation_results.iter().filter(|r| r.passed).count(); - let failed_count = validation_results.len() - successful_count; - stats.successful_validations += successful_count as u64; - stats.failed_validations += failed_count as u64; - } - - if self.config.enable_detailed_logging { - debug!("Validation completed for task {}: {} passed, {} failed", - task.id, - validation_results.iter().filter(|r| r.passed).count(), - validation_results.iter().filter(|r| !r.passed).count() - ); - } - - Ok(validation_results) - } - - /// Validate replica repair - async fn validate_replica_repair(&self, task: &HealTask, _result: &HealResult) -> Result> { - let mut results = Vec::new(); - - // Validate file existence - let existence_result = self.validate_file_existence(&task.issue.bucket, &task.issue.object).await; - results.push(existence_result); - - // Validate replication status - let replication_result = self.validate_replication_status(&task.issue.bucket, &task.issue.object).await; - results.push(replication_result); - - Ok(results) - } - - /// Validate checksum repair - async fn validate_checksum_repair(&self, task: &HealTask, _result: &HealResult) -> Result> { - let mut results = Vec::new(); - - // Validate checksum - let checksum_result = self.validate_checksum(&task.issue.bucket, &task.issue.object).await; - results.push(checksum_result); - - // Validate data integrity - let integrity_result = self.validate_data_integrity(&task.issue.bucket, &task.issue.object).await; - results.push(integrity_result); - - Ok(results) - } - - /// Validate disk repair - async fn validate_disk_repair(&self, task: &HealTask, _result: &HealResult) -> Result> { - let mut results = Vec::new(); - - // Validate file existence - let existence_result = self.validate_file_existence(&task.issue.bucket, &task.issue.object).await; - results.push(existence_result); - - // Validate file permissions - let permissions_result = self.validate_file_permissions(&task.issue.bucket, &task.issue.object).await; - results.push(permissions_result); - - Ok(results) - } - - /// Validate generic repair - async fn validate_generic_repair(&self, task: &HealTask, _result: &HealResult) -> Result> { - let mut results = Vec::new(); - - // Validate file existence - let existence_result = self.validate_file_existence(&task.issue.bucket, &task.issue.object).await; - results.push(existence_result); - - // Validate metadata consistency - let metadata_result = self.validate_metadata_consistency(&task.issue.bucket, &task.issue.object).await; - results.push(metadata_result); - - Ok(results) - } - - /// Validate file existence - async fn validate_file_existence(&self, bucket: &str, object: &str) -> ValidationResult { - let start_time = Instant::now(); - - // Simulate file existence check - tokio::time::sleep(Duration::from_millis(10)).await; - - let duration = start_time.elapsed(); - let passed = true; // Simulate successful validation - - ValidationResult { - passed, - validation_type: ValidationType::FileExistence, - message: format!("File existence validation for {}/{}", bucket, object), - duration, - timestamp: SystemTime::now(), - metadata: None, - } - } - - /// Validate checksum - async fn validate_checksum(&self, bucket: &str, object: &str) -> ValidationResult { - let start_time = Instant::now(); - - // Simulate checksum validation - tokio::time::sleep(Duration::from_millis(20)).await; - - let duration = start_time.elapsed(); - let passed = true; // Simulate successful validation - - ValidationResult { - passed, - validation_type: ValidationType::Checksum, - message: format!("Checksum validation for {}/{}", bucket, object), - duration, - timestamp: SystemTime::now(), - metadata: None, - } - } - - /// Validate replication status - async fn validate_replication_status(&self, bucket: &str, object: &str) -> ValidationResult { - let start_time = Instant::now(); - - // Simulate replication status validation - tokio::time::sleep(Duration::from_millis(15)).await; - - let duration = start_time.elapsed(); - let passed = true; // Simulate successful validation - - ValidationResult { - passed, - validation_type: ValidationType::ReplicationStatus, - message: format!("Replication status validation for {}/{}", bucket, object), - duration, - timestamp: SystemTime::now(), - metadata: None, - } - } - - /// Validate file permissions - async fn validate_file_permissions(&self, bucket: &str, object: &str) -> ValidationResult { - let start_time = Instant::now(); - - // Simulate file permissions validation - tokio::time::sleep(Duration::from_millis(5)).await; - - let duration = start_time.elapsed(); - let passed = true; // Simulate successful validation - - ValidationResult { - passed, - validation_type: ValidationType::FilePermissions, - message: format!("File permissions validation for {}/{}", bucket, object), - duration, - timestamp: SystemTime::now(), - metadata: None, - } - } - - /// Validate metadata consistency - async fn validate_metadata_consistency(&self, bucket: &str, object: &str) -> ValidationResult { - let start_time = Instant::now(); - - // Simulate metadata consistency validation - tokio::time::sleep(Duration::from_millis(25)).await; - - let duration = start_time.elapsed(); - let passed = true; // Simulate successful validation - - ValidationResult { - passed, - validation_type: ValidationType::MetadataConsistency, - message: format!("Metadata consistency validation for {}/{}", bucket, object), - duration, - timestamp: SystemTime::now(), - metadata: None, - } - } - - /// Validate data integrity - async fn validate_data_integrity(&self, bucket: &str, object: &str) -> ValidationResult { - let start_time = Instant::now(); - - // Simulate data integrity validation - tokio::time::sleep(Duration::from_millis(30)).await; - - let duration = start_time.elapsed(); - let passed = true; // Simulate successful validation - - ValidationResult { - passed, - validation_type: ValidationType::DataIntegrity, - message: format!("Data integrity validation for {}/{}", bucket, object), - duration, - timestamp: SystemTime::now(), - metadata: None, - } - } - - /// Get validation statistics - pub async fn statistics(&self) -> ValidationStatistics { - self.statistics.read().await.clone() - } - - /// Reset validation statistics - pub async fn reset_statistics(&self) { - let mut stats = self.statistics.write().await; - *stats = ValidationStatistics::default(); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::scanner::{HealthIssue, HealthIssueType, Severity}; - - #[tokio::test] - async fn test_validator_creation() { - let config = ValidationConfig::default(); - let validator = HealValidator::new(config); - - let stats = validator.statistics().await; - assert_eq!(stats.total_validations, 0); - } - - #[tokio::test] - async fn test_validate_repair() { - let config = ValidationConfig::default(); - let validator = HealValidator::new(config); - - let issue = HealthIssue { - issue_type: HealthIssueType::MissingReplica, - severity: Severity::Critical, - bucket: "test-bucket".to_string(), - object: "test-object".to_string(), - description: "Test issue".to_string(), - metadata: None, - }; - - let task = super::HealTask::new(issue); - let result = super::HealResult { - success: true, - original_issue: task.issue.clone(), - repair_duration: Duration::from_secs(1), - retry_attempts: 0, - error_message: None, - metadata: None, - completed_at: SystemTime::now(), - }; - - let validation_results = validator.validate_repair(&task, &result).await.unwrap(); - assert!(!validation_results.is_empty()); - - let stats = validator.statistics().await; - assert_eq!(stats.total_validations, validation_results.len() as u64); - } - - #[tokio::test] - async fn test_validation_disabled() { - let mut config = ValidationConfig::default(); - config.enable_post_repair_validation = false; - let validator = HealValidator::new(config); - - let issue = HealthIssue { - issue_type: HealthIssueType::MissingReplica, - severity: Severity::Critical, - bucket: "test-bucket".to_string(), - object: "test-object".to_string(), - description: "Test issue".to_string(), - metadata: None, - }; - - let task = super::HealTask::new(issue); - let result = super::HealResult { - success: true, - original_issue: task.issue.clone(), - repair_duration: Duration::from_secs(1), - retry_attempts: 0, - error_message: None, - metadata: None, - completed_at: SystemTime::now(), - }; - - let validation_results = validator.validate_repair(&task, &result).await.unwrap(); - assert!(validation_results.is_empty()); - } -} \ No newline at end of file diff --git a/crates/ahm/src/lib.rs b/crates/ahm/src/lib.rs index d3d656194..d3f6e151b 100644 --- a/crates/ahm/src/lib.rs +++ b/crates/ahm/src/lib.rs @@ -17,6 +17,7 @@ use tokio_util::sync::CancellationToken; pub mod error; pub mod scanner; +pub mod metrics; pub use error::{Error, Result}; pub use scanner::{ diff --git a/crates/ahm/src/metrics.rs b/crates/ahm/src/metrics.rs new file mode 100644 index 000000000..b541b2fc3 --- /dev/null +++ b/crates/ahm/src/metrics.rs @@ -0,0 +1,284 @@ +// 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::{ + collections::HashMap, + sync::atomic::{AtomicU64, Ordering}, + time::{Duration, SystemTime}, +}; + +use serde::{Deserialize, Serialize}; +use tracing::info; + +/// Scanner metrics similar to MinIO's scanner metrics +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct ScannerMetrics { + /// Total objects scanned since server start + pub objects_scanned: u64, + /// Total object versions scanned since server start + pub versions_scanned: u64, + /// Total directories scanned since server start + pub directories_scanned: u64, + /// Total bucket scans started since server start + pub bucket_scans_started: u64, + /// Total bucket scans finished since server start + pub bucket_scans_finished: u64, + /// Total objects with health issues found + pub objects_with_issues: u64, + /// Total heal tasks queued + pub heal_tasks_queued: u64, + /// Total heal tasks completed + pub heal_tasks_completed: u64, + /// Total heal tasks failed + pub heal_tasks_failed: u64, + /// Last scan activity time + pub last_activity: Option, + /// Current scan cycle + pub current_cycle: u64, + /// Total scan cycles completed + pub total_cycles: u64, + /// Current scan duration + pub current_scan_duration: Option, + /// Average scan duration + pub avg_scan_duration: Duration, + /// Objects scanned per second + pub objects_per_second: f64, + /// Buckets scanned per second + pub buckets_per_second: f64, + /// Storage metrics by bucket + pub bucket_metrics: HashMap, + /// Disk metrics + pub disk_metrics: HashMap, +} + +/// Bucket-specific metrics +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct BucketMetrics { + /// Bucket name + pub bucket: String, + /// Total objects in bucket + pub total_objects: u64, + /// Total size of objects in bucket (bytes) + pub total_size: u64, + /// Objects with health issues + pub objects_with_issues: u64, + /// Last scan time + pub last_scan_time: Option, + /// Scan duration + pub scan_duration: Option, + /// Heal tasks queued for this bucket + pub heal_tasks_queued: u64, + /// Heal tasks completed for this bucket + pub heal_tasks_completed: u64, + /// Heal tasks failed for this bucket + pub heal_tasks_failed: u64, +} + +/// Disk-specific metrics +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct DiskMetrics { + /// Disk path + pub disk_path: String, + /// Total disk space (bytes) + pub total_space: u64, + /// Used disk space (bytes) + pub used_space: u64, + /// Free disk space (bytes) + pub free_space: u64, + /// Objects scanned on this disk + pub objects_scanned: u64, + /// Objects with issues on this disk + pub objects_with_issues: u64, + /// Last scan time + pub last_scan_time: Option, + /// Whether disk is online + pub is_online: bool, + /// Whether disk is being scanned + pub is_scanning: bool, +} + +/// Thread-safe metrics collector +pub struct MetricsCollector { + /// Atomic counters for real-time metrics + objects_scanned: AtomicU64, + versions_scanned: AtomicU64, + directories_scanned: AtomicU64, + bucket_scans_started: AtomicU64, + bucket_scans_finished: AtomicU64, + objects_with_issues: AtomicU64, + heal_tasks_queued: AtomicU64, + heal_tasks_completed: AtomicU64, + heal_tasks_failed: AtomicU64, + current_cycle: AtomicU64, + total_cycles: AtomicU64, +} + +impl MetricsCollector { + /// Create a new metrics collector + pub fn new() -> Self { + Self { + objects_scanned: AtomicU64::new(0), + versions_scanned: AtomicU64::new(0), + directories_scanned: AtomicU64::new(0), + bucket_scans_started: AtomicU64::new(0), + bucket_scans_finished: AtomicU64::new(0), + objects_with_issues: AtomicU64::new(0), + heal_tasks_queued: AtomicU64::new(0), + heal_tasks_completed: AtomicU64::new(0), + heal_tasks_failed: AtomicU64::new(0), + current_cycle: AtomicU64::new(0), + total_cycles: AtomicU64::new(0), + } + } + + /// Increment objects scanned count + pub fn increment_objects_scanned(&self, count: u64) { + self.objects_scanned.fetch_add(count, Ordering::Relaxed); + } + + /// Increment versions scanned count + pub fn increment_versions_scanned(&self, count: u64) { + self.versions_scanned.fetch_add(count, Ordering::Relaxed); + } + + /// Increment directories scanned count + pub fn increment_directories_scanned(&self, count: u64) { + self.directories_scanned.fetch_add(count, Ordering::Relaxed); + } + + /// Increment bucket scans started count + pub fn increment_bucket_scans_started(&self, count: u64) { + self.bucket_scans_started.fetch_add(count, Ordering::Relaxed); + } + + /// Increment bucket scans finished count + pub fn increment_bucket_scans_finished(&self, count: u64) { + self.bucket_scans_finished.fetch_add(count, Ordering::Relaxed); + } + + /// Increment objects with issues count + pub fn increment_objects_with_issues(&self, count: u64) { + self.objects_with_issues.fetch_add(count, Ordering::Relaxed); + } + + /// Increment heal tasks queued count + pub fn increment_heal_tasks_queued(&self, count: u64) { + self.heal_tasks_queued.fetch_add(count, Ordering::Relaxed); + } + + /// Increment heal tasks completed count + pub fn increment_heal_tasks_completed(&self, count: u64) { + self.heal_tasks_completed.fetch_add(count, Ordering::Relaxed); + } + + /// Increment heal tasks failed count + pub fn increment_heal_tasks_failed(&self, count: u64) { + self.heal_tasks_failed.fetch_add(count, Ordering::Relaxed); + } + + /// Set current cycle + pub fn set_current_cycle(&self, cycle: u64) { + self.current_cycle.store(cycle, Ordering::Relaxed); + } + + /// Increment total cycles + pub fn increment_total_cycles(&self) { + self.total_cycles.fetch_add(1, Ordering::Relaxed); + } + + /// Get current metrics snapshot + pub fn get_metrics(&self) -> ScannerMetrics { + ScannerMetrics { + objects_scanned: self.objects_scanned.load(Ordering::Relaxed), + versions_scanned: self.versions_scanned.load(Ordering::Relaxed), + directories_scanned: self.directories_scanned.load(Ordering::Relaxed), + bucket_scans_started: self.bucket_scans_started.load(Ordering::Relaxed), + bucket_scans_finished: self.bucket_scans_finished.load(Ordering::Relaxed), + objects_with_issues: self.objects_with_issues.load(Ordering::Relaxed), + heal_tasks_queued: self.heal_tasks_queued.load(Ordering::Relaxed), + heal_tasks_completed: self.heal_tasks_completed.load(Ordering::Relaxed), + heal_tasks_failed: self.heal_tasks_failed.load(Ordering::Relaxed), + last_activity: Some(SystemTime::now()), + current_cycle: self.current_cycle.load(Ordering::Relaxed), + total_cycles: self.total_cycles.load(Ordering::Relaxed), + current_scan_duration: None, // Will be set by scanner + avg_scan_duration: Duration::ZERO, // Will be calculated + objects_per_second: 0.0, // Will be calculated + buckets_per_second: 0.0, // Will be calculated + bucket_metrics: HashMap::new(), // Will be populated by scanner + disk_metrics: HashMap::new(), // Will be populated by scanner + } + } + + /// Reset all metrics + pub fn reset(&self) { + self.objects_scanned.store(0, Ordering::Relaxed); + self.versions_scanned.store(0, Ordering::Relaxed); + self.directories_scanned.store(0, Ordering::Relaxed); + self.bucket_scans_started.store(0, Ordering::Relaxed); + self.bucket_scans_finished.store(0, Ordering::Relaxed); + self.objects_with_issues.store(0, Ordering::Relaxed); + self.heal_tasks_queued.store(0, Ordering::Relaxed); + self.heal_tasks_completed.store(0, Ordering::Relaxed); + self.heal_tasks_failed.store(0, Ordering::Relaxed); + self.current_cycle.store(0, Ordering::Relaxed); + self.total_cycles.store(0, Ordering::Relaxed); + + info!("Scanner metrics reset"); + } +} + +impl Default for MetricsCollector { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_metrics_collector_creation() { + let collector = MetricsCollector::new(); + let metrics = collector.get_metrics(); + assert_eq!(metrics.objects_scanned, 0); + assert_eq!(metrics.versions_scanned, 0); + } + + #[test] + fn test_metrics_increment() { + let collector = MetricsCollector::new(); + + collector.increment_objects_scanned(10); + collector.increment_versions_scanned(5); + collector.increment_objects_with_issues(2); + + let metrics = collector.get_metrics(); + assert_eq!(metrics.objects_scanned, 10); + assert_eq!(metrics.versions_scanned, 5); + assert_eq!(metrics.objects_with_issues, 2); + } + + #[test] + fn test_metrics_reset() { + let collector = MetricsCollector::new(); + + collector.increment_objects_scanned(10); + collector.reset(); + + let metrics = collector.get_metrics(); + assert_eq!(metrics.objects_scanned, 0); + } +} \ No newline at end of file diff --git a/crates/ahm/src/metrics/aggregator.rs b/crates/ahm/src/metrics/aggregator.rs deleted file mode 100644 index 961a13440..000000000 --- a/crates/ahm/src/metrics/aggregator.rs +++ /dev/null @@ -1,739 +0,0 @@ -// Copyright 2024 RustFS Team - -use std::{ - collections::HashMap, - time::{Duration, SystemTime}, -}; - -use tracing::{debug, error, info, warn}; - -use crate::error::Result; - -use super::{ - AggregatedMetrics, DiskMetrics, HealMetrics, MetricsDataPoint, MetricsQuery, MetricsSummary, - NetworkMetrics, PolicyMetrics, ScanMetrics, SystemMetrics, -}; - -/// Configuration for the metrics aggregator -#[derive(Debug, Clone)] -pub struct AggregatorConfig { - /// Default aggregation interval - pub default_interval: Duration, - /// Maximum number of data points to keep in memory - pub max_data_points: usize, - /// Whether to enable automatic aggregation - pub enable_auto_aggregation: bool, - /// Aggregation window size - pub aggregation_window: Duration, - /// Whether to enable data compression - pub enable_compression: bool, - /// Compression threshold (number of points before compression) - pub compression_threshold: usize, - /// Whether to enable outlier detection - pub enable_outlier_detection: bool, - /// Outlier detection threshold (standard deviations) - pub outlier_threshold: f64, -} - -impl Default for AggregatorConfig { - fn default() -> Self { - Self { - default_interval: Duration::from_secs(300), // 5 minutes - max_data_points: 10000, - enable_auto_aggregation: true, - aggregation_window: Duration::from_secs(3600), // 1 hour - enable_compression: true, - compression_threshold: 1000, - enable_outlier_detection: true, - outlier_threshold: 2.0, // 2 standard deviations - } - } -} - -/// Metrics aggregator that processes and aggregates metrics data -#[derive(Debug, Clone)] -pub struct Aggregator { - config: AggregatorConfig, - data_points: Vec, - aggregation_cache: HashMap, - last_aggregation_time: SystemTime, - aggregation_count: u64, -} - -impl Aggregator { - /// Create a new metrics aggregator - pub async fn new(interval: Duration) -> Result { - let config = AggregatorConfig { - default_interval: interval, - ..Default::default() - }; - - Ok(Self { - config, - data_points: Vec::new(), - aggregation_cache: HashMap::new(), - last_aggregation_time: SystemTime::now(), - aggregation_count: 0, - }) - } - - /// Get the configuration - pub fn config(&self) -> &AggregatorConfig { - &self.config - } - - /// Add metrics data point - pub async fn add_data_point(&mut self, data_point: MetricsDataPoint) -> Result<()> { - self.data_points.push(data_point); - - // Trim old data points if we exceed the limit - if self.data_points.len() > self.config.max_data_points { - let excess = self.data_points.len() - self.config.max_data_points; - self.data_points.drain(0..excess); - } - - // Auto-aggregate if enabled - if self.config.enable_auto_aggregation { - self.auto_aggregate().await?; - } - - Ok(()) - } - - /// Aggregate metrics based on query - pub async fn aggregate_metrics(&mut self, query: MetricsQuery) -> Result { - let start_time = SystemTime::now(); - - // Check cache first - let cache_key = self.generate_cache_key(&query); - if let Some(cached) = self.aggregation_cache.get(&cache_key) { - debug!("Returning cached aggregation result"); - return Ok(cached.clone()); - } - - // Filter data points by time range - let filtered_points: Vec<&MetricsDataPoint> = self - .data_points - .iter() - .filter(|point| { - point.timestamp >= query.start_time && point.timestamp <= query.end_time - }) - .collect(); - - if filtered_points.is_empty() { - warn!("No data points found for the specified time range"); - return Ok(AggregatedMetrics { - query, - data_points: Vec::new(), - summary: MetricsSummary::default(), - }); - } - - // Aggregate data points - let aggregated_points = self.aggregate_data_points(&filtered_points, &query).await?; - - // Generate summary - let summary = self.generate_summary(&aggregated_points, &query).await?; - - let result = AggregatedMetrics { - query, - data_points: aggregated_points, - summary, - }; - - // Cache the result - self.aggregation_cache.insert(cache_key, result.clone()); - - let aggregation_time = start_time.elapsed(); - debug!("Metrics aggregation completed in {:?}", aggregation_time); - - Ok(result) - } - - /// Auto-aggregate data points - async fn auto_aggregate(&mut self) -> Result<()> { - let now = SystemTime::now(); - - // Check if it's time to aggregate - if now.duration_since(self.last_aggregation_time).unwrap() < self.config.aggregation_window { - return Ok(()); - } - - // Perform aggregation - let window_start = now - self.config.aggregation_window; - let query = MetricsQuery { - start_time: window_start, - end_time: now, - interval: self.config.default_interval, - metrics: vec![], // All metrics - severity_filter: None, - limit: None, - }; - - let _aggregated = self.aggregate_metrics(query).await?; - - self.last_aggregation_time = now; - self.aggregation_count += 1; - - info!("Auto-aggregation completed, count: {}", self.aggregation_count); - - Ok(()) - } - - /// Aggregate data points based on interval - async fn aggregate_data_points( - &self, - points: &[&MetricsDataPoint], - query: &MetricsQuery, - ) -> Result> { - if points.is_empty() { - return Ok(Vec::new()); - } - - let mut aggregated_points = Vec::new(); - let mut current_bucket_start = query.start_time; - let mut current_bucket_points = Vec::new(); - - for point in points { - if point.timestamp >= current_bucket_start + query.interval { - // Process current bucket - if !current_bucket_points.is_empty() { - let aggregated = self.aggregate_bucket(¤t_bucket_points, current_bucket_start).await?; - aggregated_points.push(aggregated); - } - - // Start new bucket - current_bucket_start = current_bucket_start + query.interval; - current_bucket_points.clear(); - } - - current_bucket_points.push(*point); - } - - // Process last bucket - if !current_bucket_points.is_empty() { - let aggregated = self.aggregate_bucket(¤t_bucket_points, current_bucket_start).await?; - aggregated_points.push(aggregated); - } - - Ok(aggregated_points) - } - - /// Aggregate a bucket of data points - async fn aggregate_bucket( - &self, - points: &[&MetricsDataPoint], - bucket_start: SystemTime, - ) -> Result { - let mut aggregated = MetricsDataPoint { - timestamp: bucket_start, - system: None, - network: None, - disk_io: None, - scan: None, - heal: None, - policy: None, - }; - - // Aggregate system metrics - let system_metrics: Vec<&SystemMetrics> = points - .iter() - .filter_map(|p| p.system.as_ref()) - .collect(); - - if !system_metrics.is_empty() { - aggregated.system = Some(self.aggregate_system_metrics(&system_metrics).await?); - } - - // Aggregate network metrics - let network_metrics: Vec<&NetworkMetrics> = points - .iter() - .filter_map(|p| p.network.as_ref()) - .collect(); - - if !network_metrics.is_empty() { - aggregated.network = Some(self.aggregate_network_metrics(&network_metrics).await?); - } - - // Aggregate disk I/O metrics - let disk_metrics: Vec<&DiskMetrics> = points - .iter() - .filter_map(|p| p.disk_io.as_ref()) - .collect(); - - if !disk_metrics.is_empty() { - aggregated.disk_io = Some(self.aggregate_disk_metrics(&disk_metrics).await?); - } - - // Aggregate scan metrics - let scan_metrics: Vec<&ScanMetrics> = points - .iter() - .filter_map(|p| p.scan.as_ref()) - .collect(); - - if !scan_metrics.is_empty() { - aggregated.scan = Some(self.aggregate_scan_metrics(&scan_metrics).await?); - } - - // Aggregate heal metrics - let heal_metrics: Vec<&HealMetrics> = points - .iter() - .filter_map(|p| p.heal.as_ref()) - .collect(); - - if !heal_metrics.is_empty() { - aggregated.heal = Some(self.aggregate_heal_metrics(&heal_metrics).await?); - } - - // Aggregate policy metrics - let policy_metrics: Vec<&PolicyMetrics> = points - .iter() - .filter_map(|p| p.policy.as_ref()) - .collect(); - - if !policy_metrics.is_empty() { - aggregated.policy = Some(self.aggregate_policy_metrics(&policy_metrics).await?); - } - - Ok(aggregated) - } - - /// Aggregate system metrics - async fn aggregate_system_metrics(&self, metrics: &[&SystemMetrics]) -> Result { - if metrics.is_empty() { - return Ok(SystemMetrics::default()); - } - - let cpu_usage: f64 = metrics.iter().map(|m| m.cpu_usage).sum::() / metrics.len() as f64; - let memory_usage: f64 = metrics.iter().map(|m| m.memory_usage).sum::() / metrics.len() as f64; - let disk_usage: f64 = metrics.iter().map(|m| m.disk_usage).sum::() / metrics.len() as f64; - let system_load: f64 = metrics.iter().map(|m| m.system_load).sum::() / metrics.len() as f64; - let active_operations: u64 = metrics.iter().map(|m| m.active_operations).sum::() / metrics.len() as u64; - - // Aggregate health issues - let mut health_issues = HashMap::new(); - for metric in metrics { - for (severity, count) in &metric.health_issues { - *health_issues.entry(*severity).or_insert(0) += count; - } - } - - Ok(SystemMetrics { - timestamp: SystemTime::now(), - cpu_usage, - memory_usage, - disk_usage, - network_io: NetworkMetrics::default(), // Will be aggregated separately - disk_io: DiskMetrics::default(), // Will be aggregated separately - active_operations, - system_load, - health_issues, - scan_metrics: ScanMetrics::default(), // Will be aggregated separately - heal_metrics: HealMetrics::default(), // Will be aggregated separately - policy_metrics: PolicyMetrics::default(), // Will be aggregated separately - }) - } - - /// Aggregate network metrics - async fn aggregate_network_metrics(&self, metrics: &[&NetworkMetrics]) -> Result { - if metrics.is_empty() { - return Ok(NetworkMetrics::default()); - } - - let bytes_received_per_sec: u64 = metrics.iter().map(|m| m.bytes_received_per_sec).sum::() / metrics.len() as u64; - let bytes_sent_per_sec: u64 = metrics.iter().map(|m| m.bytes_sent_per_sec).sum::() / metrics.len() as u64; - let packets_received_per_sec: u64 = metrics.iter().map(|m| m.packets_received_per_sec).sum::() / metrics.len() as u64; - let packets_sent_per_sec: u64 = metrics.iter().map(|m| m.packets_sent_per_sec).sum::() / metrics.len() as u64; - - Ok(NetworkMetrics { - bytes_received_per_sec, - bytes_sent_per_sec, - packets_received_per_sec, - packets_sent_per_sec, - }) - } - - /// Aggregate disk metrics - async fn aggregate_disk_metrics(&self, metrics: &[&DiskMetrics]) -> Result { - if metrics.is_empty() { - return Ok(DiskMetrics::default()); - } - - let bytes_read_per_sec: u64 = metrics.iter().map(|m| m.bytes_read_per_sec).sum::() / metrics.len() as u64; - let bytes_written_per_sec: u64 = metrics.iter().map(|m| m.bytes_written_per_sec).sum::() / metrics.len() as u64; - let read_ops_per_sec: u64 = metrics.iter().map(|m| m.read_ops_per_sec).sum::() / metrics.len() as u64; - let write_ops_per_sec: u64 = metrics.iter().map(|m| m.write_ops_per_sec).sum::() / metrics.len() as u64; - let avg_read_latency_ms: f64 = metrics.iter().map(|m| m.avg_read_latency_ms).sum::() / metrics.len() as f64; - let avg_write_latency_ms: f64 = metrics.iter().map(|m| m.avg_write_latency_ms).sum::() / metrics.len() as f64; - - Ok(DiskMetrics { - bytes_read_per_sec, - bytes_written_per_sec, - read_ops_per_sec, - write_ops_per_sec, - avg_read_latency_ms, - avg_write_latency_ms, - }) - } - - /// Aggregate scan metrics - async fn aggregate_scan_metrics(&self, metrics: &[&ScanMetrics]) -> Result { - if metrics.is_empty() { - return Ok(ScanMetrics::default()); - } - - let objects_scanned: u64 = metrics.iter().map(|m| m.objects_scanned).sum(); - let bytes_scanned: u64 = metrics.iter().map(|m| m.bytes_scanned).sum(); - let scan_duration: Duration = metrics.iter().map(|m| m.scan_duration).sum(); - let health_issues_found: u64 = metrics.iter().map(|m| m.health_issues_found).sum(); - let scan_cycles_completed: u64 = metrics.iter().map(|m| m.scan_cycles_completed).sum(); - - // Calculate rates - let total_duration_secs = scan_duration.as_secs_f64(); - let scan_rate_objects_per_sec = if total_duration_secs > 0.0 { - objects_scanned as f64 / total_duration_secs - } else { - 0.0 - }; - - let scan_rate_bytes_per_sec = if total_duration_secs > 0.0 { - bytes_scanned as f64 / total_duration_secs - } else { - 0.0 - }; - - Ok(ScanMetrics { - objects_scanned, - bytes_scanned, - scan_duration, - scan_rate_objects_per_sec, - scan_rate_bytes_per_sec, - health_issues_found, - scan_cycles_completed, - last_scan_time: metrics.last().and_then(|m| m.last_scan_time), - }) - } - - /// Aggregate heal metrics - async fn aggregate_heal_metrics(&self, metrics: &[&HealMetrics]) -> Result { - if metrics.is_empty() { - return Ok(HealMetrics::default()); - } - - let total_repairs: u64 = metrics.iter().map(|m| m.total_repairs).sum(); - let successful_repairs: u64 = metrics.iter().map(|m| m.successful_repairs).sum(); - let failed_repairs: u64 = metrics.iter().map(|m| m.failed_repairs).sum(); - let total_repair_time: Duration = metrics.iter().map(|m| m.total_repair_time).sum(); - let total_retry_attempts: u64 = metrics.iter().map(|m| m.total_retry_attempts).sum(); - - // Calculate average repair time - let average_repair_time = if total_repairs > 0 { - let total_ms = total_repair_time.as_millis() as u64; - Duration::from_millis(total_ms / total_repairs) - } else { - Duration::ZERO - }; - - // Get latest values for current state - let active_repair_workers = metrics.last().map(|m| m.active_repair_workers).unwrap_or(0); - let queued_repair_tasks = metrics.last().map(|m| m.queued_repair_tasks).unwrap_or(0); - let last_repair_time = metrics.last().and_then(|m| m.last_repair_time); - - Ok(HealMetrics { - total_repairs, - successful_repairs, - failed_repairs, - total_repair_time, - average_repair_time, - active_repair_workers, - queued_repair_tasks, - last_repair_time, - total_retry_attempts, - }) - } - - /// Aggregate policy metrics - async fn aggregate_policy_metrics(&self, metrics: &[&PolicyMetrics]) -> Result { - if metrics.is_empty() { - return Ok(PolicyMetrics::default()); - } - - let total_evaluations: u64 = metrics.iter().map(|m| m.total_evaluations).sum(); - let allowed_operations: u64 = metrics.iter().map(|m| m.allowed_operations).sum(); - let denied_operations: u64 = metrics.iter().map(|m| m.denied_operations).sum(); - let scan_policy_evaluations: u64 = metrics.iter().map(|m| m.scan_policy_evaluations).sum(); - let heal_policy_evaluations: u64 = metrics.iter().map(|m| m.heal_policy_evaluations).sum(); - let retention_policy_evaluations: u64 = metrics.iter().map(|m| m.retention_policy_evaluations).sum(); - let total_evaluation_time: Duration = metrics.iter().map(|m| m.average_evaluation_time).sum(); - - // Calculate average evaluation time - let average_evaluation_time = if total_evaluations > 0 { - let total_ms = total_evaluation_time.as_millis() as u64; - Duration::from_millis(total_ms / total_evaluations) - } else { - Duration::ZERO - }; - - Ok(PolicyMetrics { - total_evaluations, - allowed_operations, - denied_operations, - scan_policy_evaluations, - heal_policy_evaluations, - retention_policy_evaluations, - average_evaluation_time, - }) - } - - /// Generate summary statistics - async fn generate_summary( - &self, - data_points: &[MetricsDataPoint], - query: &MetricsQuery, - ) -> Result { - let total_points = data_points.len() as u64; - let time_range = query.end_time.duration_since(query.start_time).unwrap_or(Duration::ZERO); - - // Calculate averages from system metrics - let system_metrics: Vec<&SystemMetrics> = data_points - .iter() - .filter_map(|p| p.system.as_ref()) - .collect(); - - let avg_cpu_usage = if !system_metrics.is_empty() { - system_metrics.iter().map(|m| m.cpu_usage).sum::() / system_metrics.len() as f64 - } else { - 0.0 - }; - - let avg_memory_usage = if !system_metrics.is_empty() { - system_metrics.iter().map(|m| m.memory_usage).sum::() / system_metrics.len() as f64 - } else { - 0.0 - }; - - let avg_disk_usage = if !system_metrics.is_empty() { - system_metrics.iter().map(|m| m.disk_usage).sum::() / system_metrics.len() as f64 - } else { - 0.0 - }; - - // Calculate totals from scan and heal metrics - let scan_metrics: Vec<&ScanMetrics> = data_points - .iter() - .filter_map(|p| p.scan.as_ref()) - .collect(); - - let total_objects_scanned = scan_metrics.iter().map(|m| m.objects_scanned).sum(); - let total_health_issues = scan_metrics.iter().map(|m| m.health_issues_found).sum(); - - let heal_metrics: Vec<&HealMetrics> = data_points - .iter() - .filter_map(|p| p.heal.as_ref()) - .collect(); - - let total_repairs = heal_metrics.iter().map(|m| m.total_repairs).sum(); - let successful_repairs: u64 = heal_metrics.iter().map(|m| m.successful_repairs).sum(); - let repair_success_rate = if total_repairs > 0 { - successful_repairs as f64 / total_repairs as f64 - } else { - 0.0 - }; - - Ok(MetricsSummary { - total_points, - time_range, - avg_cpu_usage, - avg_memory_usage, - avg_disk_usage, - total_objects_scanned, - total_repairs, - repair_success_rate, - total_health_issues, - }) - } - - /// Generate cache key for query - fn generate_cache_key(&self, query: &MetricsQuery) -> String { - format!( - "{:?}_{:?}_{:?}_{:?}", - query.start_time, query.end_time, query.interval, query.metrics - ) - } - - /// Clear old cache entries - pub async fn clear_old_cache(&mut self) -> Result<()> { - let now = SystemTime::now(); - let retention_period = Duration::from_secs(3600); // 1 hour - - self.aggregation_cache.retain(|_key, value| { - if let Some(latest_point) = value.data_points.last() { - now.duration_since(latest_point.timestamp).unwrap_or(Duration::ZERO) < retention_period - } else { - false - } - }); - - info!("Cleared old cache entries, remaining: {}", self.aggregation_cache.len()); - Ok(()) - } - - /// Get aggregation statistics - pub fn get_statistics(&self) -> AggregatorStatistics { - AggregatorStatistics { - total_data_points: self.data_points.len(), - total_aggregations: self.aggregation_count, - cache_size: self.aggregation_cache.len(), - last_aggregation_time: self.last_aggregation_time, - config: self.config.clone(), - } - } -} - -/// Aggregator statistics -#[derive(Debug, Clone)] -pub struct AggregatorStatistics { - pub total_data_points: usize, - pub total_aggregations: u64, - pub cache_size: usize, - pub last_aggregation_time: SystemTime, - pub config: AggregatorConfig, -} - -impl Default for MetricsSummary { - fn default() -> Self { - Self { - total_points: 0, - time_range: Duration::ZERO, - avg_cpu_usage: 0.0, - avg_memory_usage: 0.0, - avg_disk_usage: 0.0, - total_objects_scanned: 0, - total_repairs: 0, - repair_success_rate: 0.0, - total_health_issues: 0, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::scanner::Severity; - - #[tokio::test] - async fn test_aggregator_creation() { - let aggregator = Aggregator::new(Duration::from_secs(300)).await.unwrap(); - - assert_eq!(aggregator.config().default_interval, Duration::from_secs(300)); - assert!(aggregator.config().enable_auto_aggregation); - } - - #[tokio::test] - async fn test_data_point_addition() { - let mut aggregator = Aggregator::new(Duration::from_secs(300)).await.unwrap(); - - let data_point = MetricsDataPoint { - timestamp: SystemTime::now(), - system: Some(SystemMetrics::default()), - network: None, - disk_io: None, - scan: None, - heal: None, - policy: None, - }; - - aggregator.add_data_point(data_point).await.unwrap(); - - let stats = aggregator.get_statistics(); - assert_eq!(stats.total_data_points, 1); - } - - #[tokio::test] - async fn test_metrics_aggregation() { - let mut aggregator = Aggregator::new(Duration::from_secs(300)).await.unwrap(); - - // Add some test data points - for i in 0..5 { - let mut system_metrics = SystemMetrics::default(); - system_metrics.cpu_usage = i as f64 * 10.0; - system_metrics.memory_usage = i as f64 * 20.0; - - let data_point = MetricsDataPoint { - timestamp: SystemTime::now() + Duration::from_secs(i * 60), - system: Some(system_metrics), - network: None, - disk_io: None, - scan: None, - heal: None, - policy: None, - }; - - aggregator.add_data_point(data_point).await.unwrap(); - } - - let query = MetricsQuery { - start_time: SystemTime::now(), - end_time: SystemTime::now() + Duration::from_secs(300), - interval: Duration::from_secs(60), - metrics: vec![MetricType::System], - severity_filter: None, - limit: None, - }; - - let result = aggregator.aggregate_metrics(query).await.unwrap(); - assert_eq!(result.data_points.len(), 5); - assert_eq!(result.summary.total_points, 5); - } - - #[tokio::test] - async fn test_system_metrics_aggregation() { - let mut aggregator = Aggregator::new(Duration::from_secs(300)).await.unwrap(); - - let metrics = vec![ - SystemMetrics { - cpu_usage: 10.0, - memory_usage: 20.0, - disk_usage: 30.0, - ..Default::default() - }, - SystemMetrics { - cpu_usage: 20.0, - memory_usage: 40.0, - disk_usage: 60.0, - ..Default::default() - }, - ]; - - let aggregated = aggregator.aggregate_system_metrics(&metrics.iter().collect::>()).await.unwrap(); - - assert_eq!(aggregated.cpu_usage, 15.0); - assert_eq!(aggregated.memory_usage, 30.0); - assert_eq!(aggregated.disk_usage, 45.0); - } - - #[tokio::test] - async fn test_cache_clearing() { - let mut aggregator = Aggregator::new(Duration::from_secs(300)).await.unwrap(); - - // Add some cached data - let query = MetricsQuery { - start_time: SystemTime::now() - Duration::from_secs(3600), - end_time: SystemTime::now() - Duration::from_secs(3000), - interval: Duration::from_secs(60), - metrics: vec![], - severity_filter: None, - limit: None, - }; - - let _result = aggregator.aggregate_metrics(query).await.unwrap(); - - let stats_before = aggregator.get_statistics(); - assert_eq!(stats_before.cache_size, 1); - - aggregator.clear_old_cache().await.unwrap(); - - let stats_after = aggregator.get_statistics(); - assert_eq!(stats_after.cache_size, 0); - } -} \ No newline at end of file diff --git a/crates/ahm/src/metrics/collector.rs b/crates/ahm/src/metrics/collector.rs deleted file mode 100644 index 3932b94e5..000000000 --- a/crates/ahm/src/metrics/collector.rs +++ /dev/null @@ -1,426 +0,0 @@ -// Copyright 2024 RustFS Team - -use std::{ - sync::Arc, - time::{Duration, Instant, SystemTime}, -}; - -use tokio::sync::RwLock; -use tracing::{debug, error, info, warn}; - -use crate::{ - error::Result, - scanner::{HealthIssue, Severity}, -}; - -use super::{ - AggregatedMetrics, Aggregator, DiskMetrics, HealMetrics, MetricsQuery, MetricType, - NetworkMetrics, PolicyMetrics, ScanMetrics, SystemMetrics, -}; - -/// Configuration for the metrics collector -#[derive(Debug, Clone)] -pub struct CollectorConfig { - /// Collection interval - pub collection_interval: Duration, - /// Whether to enable detailed metrics collection - pub enable_detailed_metrics: bool, - /// Maximum number of metrics to keep in memory - pub max_metrics_in_memory: usize, - /// Whether to enable automatic aggregation - pub enable_auto_aggregation: bool, - /// Aggregation interval - pub aggregation_interval: Duration, - /// Whether to enable resource monitoring - pub enable_resource_monitoring: bool, - /// Resource monitoring interval - pub resource_monitoring_interval: Duration, - /// Whether to enable health issue tracking - pub enable_health_issue_tracking: bool, - /// Metrics retention period - pub metrics_retention_period: Duration, -} - -impl Default for CollectorConfig { - fn default() -> Self { - Self { - collection_interval: Duration::from_secs(30), // 30 seconds - enable_detailed_metrics: true, - max_metrics_in_memory: 10000, - enable_auto_aggregation: true, - aggregation_interval: Duration::from_secs(300), // 5 minutes - enable_resource_monitoring: true, - resource_monitoring_interval: Duration::from_secs(10), // 10 seconds - enable_health_issue_tracking: true, - metrics_retention_period: Duration::from_secs(86400 * 7), // 7 days - } - } -} - -/// Metrics collector that gathers system metrics -#[derive(Debug)] -pub struct Collector { - config: CollectorConfig, - metrics: Arc>>, - aggregator: Arc, - last_collection_time: Arc>, - collection_count: Arc>, - health_issues: Arc>>, -} - -impl Collector { - /// Create a new metrics collector - pub async fn new(config: CollectorConfig) -> Result { - let aggregator = Arc::new(Aggregator::new(config.aggregation_interval).await?); - - Ok(Self { - config, - metrics: Arc::new(RwLock::new(Vec::new())), - aggregator, - last_collection_time: Arc::new(RwLock::new(SystemTime::now())), - collection_count: Arc::new(RwLock::new(0)), - health_issues: Arc::new(RwLock::new(std::collections::HashMap::new())), - }) - } - - /// Get the configuration - pub fn config(&self) -> &CollectorConfig { - &self.config - } - - /// Collect current system metrics - pub async fn collect_metrics(&self) -> Result { - let start_time = Instant::now(); - - let mut metrics = SystemMetrics::default(); - metrics.timestamp = SystemTime::now(); - - // Collect system resource metrics - if self.config.enable_resource_monitoring { - self.collect_system_resources(&mut metrics).await?; - } - - // Collect scan metrics - self.collect_scan_metrics(&mut metrics).await?; - - // Collect heal metrics - self.collect_heal_metrics(&mut metrics).await?; - - // Collect policy metrics - self.collect_policy_metrics(&mut metrics).await?; - - // Collect health issues - if self.config.enable_health_issue_tracking { - self.collect_health_issues(&mut metrics).await?; - } - - // Store metrics - { - let mut metrics_store = self.metrics.write().await; - metrics_store.push(metrics.clone()); - - // Trim old metrics if we exceed the limit - if metrics_store.len() > self.config.max_metrics_in_memory { - let excess = metrics_store.len() - self.config.max_metrics_in_memory; - metrics_store.drain(0..excess); - } - } - - // Update collection statistics - { - let mut last_time = self.last_collection_time.write().await; - *last_time = metrics.timestamp; - - let mut count = self.collection_count.write().await; - *count += 1; - } - - let collection_time = start_time.elapsed(); - debug!("Metrics collection completed in {:?}", collection_time); - - Ok(metrics) - } - - /// Collect system resource metrics - async fn collect_system_resources(&self, metrics: &mut SystemMetrics) -> Result<()> { - // Simulate system resource collection - // In a real implementation, this would use system APIs - - metrics.cpu_usage = self.get_cpu_usage().await?; - metrics.memory_usage = self.get_memory_usage().await?; - metrics.disk_usage = self.get_disk_usage().await?; - metrics.system_load = self.get_system_load().await?; - metrics.active_operations = self.get_active_operations().await?; - - // Collect network metrics - metrics.network_io = self.get_network_metrics().await?; - - // Collect disk I/O metrics - metrics.disk_io = self.get_disk_io_metrics().await?; - - Ok(()) - } - - /// Collect scan metrics - async fn collect_scan_metrics(&self, metrics: &mut SystemMetrics) -> Result<()> { - // In a real implementation, this would get data from the scanner - metrics.scan_metrics = ScanMetrics::default(); - - // Simulate some scan metrics - metrics.scan_metrics.objects_scanned = 1000; - metrics.scan_metrics.bytes_scanned = 1024 * 1024 * 100; // 100 MB - metrics.scan_metrics.scan_duration = Duration::from_secs(60); - metrics.scan_metrics.scan_rate_objects_per_sec = 16.67; // 1000 / 60 - metrics.scan_metrics.scan_rate_bytes_per_sec = 1_747_200.0; // 100MB / 60s - metrics.scan_metrics.health_issues_found = 5; - metrics.scan_metrics.scan_cycles_completed = 1; - metrics.scan_metrics.last_scan_time = Some(SystemTime::now()); - - Ok(()) - } - - /// Collect heal metrics - async fn collect_heal_metrics(&self, metrics: &mut SystemMetrics) -> Result<()> { - // In a real implementation, this would get data from the heal system - metrics.heal_metrics = HealMetrics::default(); - - // Simulate some heal metrics - metrics.heal_metrics.total_repairs = 10; - metrics.heal_metrics.successful_repairs = 8; - metrics.heal_metrics.failed_repairs = 2; - metrics.heal_metrics.total_repair_time = Duration::from_secs(300); - metrics.heal_metrics.average_repair_time = Duration::from_secs(30); - metrics.heal_metrics.active_repair_workers = 2; - metrics.heal_metrics.queued_repair_tasks = 5; - metrics.heal_metrics.last_repair_time = Some(SystemTime::now()); - metrics.heal_metrics.total_retry_attempts = 3; - - Ok(()) - } - - /// Collect policy metrics - async fn collect_policy_metrics(&self, metrics: &mut SystemMetrics) -> Result<()> { - // In a real implementation, this would get data from the policy system - metrics.policy_metrics = PolicyMetrics::default(); - - // Simulate some policy metrics - metrics.policy_metrics.total_evaluations = 50; - metrics.policy_metrics.allowed_operations = 45; - metrics.policy_metrics.denied_operations = 5; - metrics.policy_metrics.scan_policy_evaluations = 20; - metrics.policy_metrics.heal_policy_evaluations = 20; - metrics.policy_metrics.retention_policy_evaluations = 10; - metrics.policy_metrics.average_evaluation_time = Duration::from_millis(10); - - Ok(()) - } - - /// Collect health issues - async fn collect_health_issues(&self, metrics: &mut SystemMetrics) -> Result<()> { - let health_issues = self.health_issues.read().await; - metrics.health_issues = health_issues.clone(); - Ok(()) - } - - /// Record a health issue - pub async fn record_health_issue(&self, issue: &HealthIssue) -> Result<()> { - let mut issues = self.health_issues.write().await; - let count = issues.entry(issue.severity).or_insert(0); - *count += 1; - - info!("Recorded health issue: {:?} - {}", issue.severity, issue.description); - Ok(()) - } - - /// Record an event (alias for record_health_issue) - pub async fn record_event(&self, issue: &HealthIssue) -> Result<()> { - self.record_health_issue(issue).await - } - - /// Clear health issues - pub async fn clear_health_issues(&self) -> Result<()> { - let mut health_issues = self.health_issues.write().await; - health_issues.clear(); - - info!("Cleared all health issues"); - Ok(()) - } - - /// Query metrics with aggregation - pub async fn query_metrics(&self, query: MetricsQuery) -> Result { - // In a real implementation, this would query the aggregator - // For now, we'll return a simple aggregated result - let aggregator = self.aggregator.as_ref(); - let mut aggregator_guard = aggregator.write().await; - aggregator_guard.aggregate_metrics(query).await - } - - /// Get metrics for a specific time range - pub async fn get_metrics_range(&self, start_time: SystemTime, end_time: SystemTime) -> Result> { - let metrics = self.metrics.read().await; - let filtered_metrics: Vec = metrics - .iter() - .filter(|m| m.timestamp >= start_time && m.timestamp <= end_time) - .cloned() - .collect(); - - Ok(filtered_metrics) - } - - /// Get latest metrics - pub async fn get_latest_metrics(&self) -> Result> { - let metrics = self.metrics.read().await; - Ok(metrics.last().cloned()) - } - - /// Get collection statistics - pub async fn get_collection_statistics(&self) -> CollectionStatistics { - let collection_count = *self.collection_count.read().await; - let last_collection_time = *self.last_collection_time.read().await; - let metrics_count = self.metrics.read().await.len(); - - CollectionStatistics { - total_collections: collection_count, - last_collection_time, - metrics_in_memory: metrics_count, - config: self.config.clone(), - } - } - - /// Simulated system resource collection methods - async fn get_cpu_usage(&self) -> Result { - // Simulate CPU usage collection - Ok(25.5) // 25.5% - } - - async fn get_memory_usage(&self) -> Result { - // Simulate memory usage collection - Ok(60.2) // 60.2% - } - - async fn get_disk_usage(&self) -> Result { - // Simulate disk usage collection - Ok(45.8) // 45.8% - } - - async fn get_system_load(&self) -> Result { - // Simulate system load collection - Ok(0.75) // 0.75 - } - - async fn get_active_operations(&self) -> Result { - // Simulate active operations count - Ok(15) - } - - async fn get_network_metrics(&self) -> Result { - // Simulate network metrics collection - Ok(NetworkMetrics { - bytes_received_per_sec: 1024 * 1024, // 1 MB/s - bytes_sent_per_sec: 512 * 1024, // 512 KB/s - packets_received_per_sec: 1000, - packets_sent_per_sec: 500, - }) - } - - async fn get_disk_io_metrics(&self) -> Result { - // Simulate disk I/O metrics collection - Ok(DiskMetrics { - bytes_read_per_sec: 2 * 1024 * 1024, // 2 MB/s - bytes_written_per_sec: 1 * 1024 * 1024, // 1 MB/s - read_ops_per_sec: 200, - write_ops_per_sec: 100, - avg_read_latency_ms: 5.0, - avg_write_latency_ms: 8.0, - }) - } -} - -/// Collection statistics -#[derive(Debug, Clone)] -pub struct CollectionStatistics { - pub total_collections: u64, - pub last_collection_time: SystemTime, - pub metrics_in_memory: usize, - pub config: CollectorConfig, -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::scanner::{HealthIssue, HealthIssueType}; - - #[tokio::test] - async fn test_collector_creation() { - let config = CollectorConfig::default(); - let collector = Collector::new(config).await.unwrap(); - - assert_eq!(collector.config().collection_interval, Duration::from_secs(30)); - assert!(collector.config().enable_detailed_metrics); - } - - #[tokio::test] - async fn test_metrics_collection() { - let config = CollectorConfig::default(); - let collector = Collector::new(config).await.unwrap(); - - let metrics = collector.collect_metrics().await.unwrap(); - assert_eq!(metrics.cpu_usage, 25.5); - assert_eq!(metrics.memory_usage, 60.2); - assert_eq!(metrics.disk_usage, 45.8); - } - - #[tokio::test] - async fn test_health_issue_recording() { - let config = CollectorConfig::default(); - let collector = Collector::new(config).await.unwrap(); - - let issue = HealthIssue { - issue_type: HealthIssueType::MissingReplica, - severity: Severity::Critical, - bucket: "test-bucket".to_string(), - object: "test-object".to_string(), - description: "Test issue".to_string(), - metadata: None, - }; - - collector.record_health_issue(&issue).await.unwrap(); - - let stats = collector.get_collection_statistics().await; - assert_eq!(stats.total_collections, 0); // No collection yet - } - - #[tokio::test] - async fn test_latest_metrics() { - let config = CollectorConfig::default(); - let collector = Collector::new(config).await.unwrap(); - - // Initially no metrics - let latest = collector.get_latest_metrics().await.unwrap(); - assert!(latest.is_none()); - - // Collect metrics - collector.collect_metrics().await.unwrap(); - - // Now should have metrics - let latest = collector.get_latest_metrics().await.unwrap(); - assert!(latest.is_some()); - } - - #[tokio::test] - async fn test_collection_statistics() { - let config = CollectorConfig::default(); - let collector = Collector::new(config).await.unwrap(); - - let stats = collector.get_collection_statistics().await; - assert_eq!(stats.total_collections, 0); - assert_eq!(stats.metrics_in_memory, 0); - - // Collect metrics - collector.collect_metrics().await.unwrap(); - - let stats = collector.get_collection_statistics().await; - assert_eq!(stats.total_collections, 1); - assert_eq!(stats.metrics_in_memory, 1); - } -} \ No newline at end of file diff --git a/crates/ahm/src/metrics/mod.rs b/crates/ahm/src/metrics/mod.rs deleted file mode 100644 index f5f6ae859..000000000 --- a/crates/ahm/src/metrics/mod.rs +++ /dev/null @@ -1,617 +0,0 @@ -// 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. - -//! Metrics collection and aggregation system -//! -//! The metrics subsystem provides comprehensive data collection and analysis: -//! - Real-time metrics collection from all subsystems -//! - Time-series data storage and aggregation -//! - Export capabilities for external monitoring systems -//! - Performance analytics and trend analysis - -pub mod collector; -pub mod aggregator; -pub mod storage; -pub mod reporter; - -pub use collector::{Collector, CollectorConfig}; -pub use aggregator::{Aggregator, AggregatorConfig}; -pub use storage::{Storage, StorageConfig}; -pub use reporter::{Reporter, ReporterConfig}; - -use std::time::{Duration, SystemTime}; -use std::collections::HashMap; -use serde::{Deserialize, Serialize}; - -use crate::scanner::{HealthIssue, Severity}; - -/// Metrics subsystem status -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Status { - /// Metrics system is initializing - Initializing, - /// Metrics system is running normally - Running, - /// Metrics system is degraded (some exporters failing) - Degraded, - /// Metrics system is stopping - Stopping, - /// Metrics system has stopped - Stopped, - /// Metrics system encountered an error - Error(String), -} - -/// Metric data point with timestamp and value -#[derive(Debug, Clone)] -pub struct MetricPoint { - /// Metric name - pub name: String, - /// Metric value - pub value: MetricValue, - /// Timestamp when metric was collected - pub timestamp: SystemTime, - /// Additional labels/tags - pub labels: HashMap, -} - -/// Different types of metric values -#[derive(Debug, Clone)] -pub enum MetricValue { - /// Counter that only increases - Counter(u64), - /// Gauge that can go up or down - Gauge(f64), - /// Histogram with buckets - Histogram { - count: u64, - sum: f64, - buckets: Vec, - }, - /// Summary with quantiles - Summary { - count: u64, - sum: f64, - quantiles: Vec, - }, -} - -/// Histogram bucket -#[derive(Debug, Clone)] -pub struct HistogramBucket { - /// Upper bound of the bucket - pub le: f64, - /// Count of observations in this bucket - pub count: u64, -} - -/// Summary quantile -#[derive(Debug, Clone)] -pub struct Quantile { - /// Quantile value (e.g., 0.5 for median) - pub quantile: f64, - /// Value at this quantile - pub value: f64, -} - -/// Aggregation functions for metrics -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum AggregationFunction { - Sum, - Average, - Min, - Max, - Count, - Rate, - Percentile(u8), -} - -/// Time window for aggregation -#[derive(Debug, Clone)] -pub struct TimeWindow { - /// Duration of the window - pub duration: Duration, - /// How often to create new windows - pub step: Duration, -} - -/// Metric export configuration -#[derive(Debug, Clone)] -pub struct ExportConfig { - /// Export format - pub format: ExportFormat, - /// Export destination - pub destination: ExportDestination, - /// Export interval - pub interval: Duration, - /// Metric filters - pub filters: Vec, -} - -/// Supported export formats -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ExportFormat { - /// Prometheus format - Prometheus, - /// JSON format - Json, - /// CSV format - Csv, - /// Custom format - Custom(String), -} - -/// Export destinations -#[derive(Debug, Clone)] -pub enum ExportDestination { - /// HTTP endpoint - Http { url: String, headers: HashMap }, - /// File system - File { path: String }, - /// Standard output - Stdout, - /// Custom destination - Custom(String), -} - -/// Metric filtering rules -#[derive(Debug, Clone)] -pub struct MetricFilter { - /// Metric name pattern (regex) - pub name_pattern: String, - /// Label filters - pub label_filters: HashMap, - /// Include or exclude matching metrics - pub include: bool, -} - -/// System-wide metrics that are automatically collected -pub mod system_metrics { - /// Object-related metrics - pub const OBJECTS_TOTAL: &str = "rustfs_objects_total"; - pub const OBJECTS_SIZE_BYTES: &str = "rustfs_objects_size_bytes"; - pub const OBJECTS_SCANNED_TOTAL: &str = "rustfs_objects_scanned_total"; - pub const OBJECTS_HEAL_OPERATIONS_TOTAL: &str = "rustfs_objects_heal_operations_total"; - - /// Scanner metrics - pub const SCAN_CYCLES_TOTAL: &str = "rustfs_scan_cycles_total"; - pub const SCAN_DURATION_SECONDS: &str = "rustfs_scan_duration_seconds"; - pub const SCAN_RATE_OBJECTS_PER_SECOND: &str = "rustfs_scan_rate_objects_per_second"; - pub const SCAN_RATE_BYTES_PER_SECOND: &str = "rustfs_scan_rate_bytes_per_second"; - - /// Health metrics - pub const HEALTH_ISSUES_TOTAL: &str = "rustfs_health_issues_total"; - pub const HEALTH_ISSUES_BY_SEVERITY: &str = "rustfs_health_issues_by_severity"; - pub const HEAL_SUCCESS_RATE: &str = "rustfs_heal_success_rate"; - - /// System resource metrics - pub const DISK_USAGE_BYTES: &str = "rustfs_disk_usage_bytes"; - pub const DISK_IOPS: &str = "rustfs_disk_iops"; - pub const MEMORY_USAGE_BYTES: &str = "rustfs_memory_usage_bytes"; - pub const CPU_USAGE_PERCENT: &str = "rustfs_cpu_usage_percent"; - - /// Performance metrics - pub const OPERATION_DURATION_SECONDS: &str = "rustfs_operation_duration_seconds"; - pub const ACTIVE_OPERATIONS: &str = "rustfs_active_operations"; - pub const THROUGHPUT_BYTES_PER_SECOND: &str = "rustfs_throughput_bytes_per_second"; -} - -/// System metrics collected by AHM -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SystemMetrics { - /// Timestamp when metrics were collected - pub timestamp: SystemTime, - /// CPU usage percentage - pub cpu_usage: f64, - /// Memory usage percentage - pub memory_usage: f64, - /// Disk usage percentage - pub disk_usage: f64, - /// Network I/O bytes per second - pub network_io: NetworkMetrics, - /// Disk I/O bytes per second - pub disk_io: DiskMetrics, - /// Active operations count - pub active_operations: u64, - /// System load average - pub system_load: f64, - /// Health issues count by severity - pub health_issues: std::collections::HashMap, - /// Scan metrics - pub scan_metrics: ScanMetrics, - /// Heal metrics - pub heal_metrics: HealMetrics, - /// Policy metrics - pub policy_metrics: PolicyMetrics, -} - -/// Network I/O metrics -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct NetworkMetrics { - /// Bytes received per second - pub bytes_received_per_sec: u64, - /// Bytes sent per second - pub bytes_sent_per_sec: u64, - /// Packets received per second - pub packets_received_per_sec: u64, - /// Packets sent per second - pub packets_sent_per_sec: u64, -} - -/// Disk I/O metrics -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DiskMetrics { - /// Bytes read per second - pub bytes_read_per_sec: u64, - /// Bytes written per second - pub bytes_written_per_sec: u64, - /// Read operations per second - pub read_ops_per_sec: u64, - /// Write operations per second - pub write_ops_per_sec: u64, - /// Average read latency in milliseconds - pub avg_read_latency_ms: f64, - /// Average write latency in milliseconds - pub avg_write_latency_ms: f64, -} - -/// Scan operation metrics -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ScanMetrics { - /// Total objects scanned - pub objects_scanned: u64, - /// Total bytes scanned - pub bytes_scanned: u64, - /// Scan duration - pub scan_duration: Duration, - /// Scan rate (objects per second) - pub scan_rate_objects_per_sec: f64, - /// Scan rate (bytes per second) - pub scan_rate_bytes_per_sec: f64, - /// Health issues found - pub health_issues_found: u64, - /// Scan cycles completed - pub scan_cycles_completed: u64, - /// Last scan time - pub last_scan_time: Option, -} - -/// Heal operation metrics -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct HealMetrics { - /// Total repair operations - pub total_repairs: u64, - /// Successful repairs - pub successful_repairs: u64, - /// Failed repairs - pub failed_repairs: u64, - /// Total repair time - pub total_repair_time: Duration, - /// Average repair time - pub average_repair_time: Duration, - /// Active repair workers - pub active_repair_workers: u64, - /// Queued repair tasks - pub queued_repair_tasks: u64, - /// Last repair time - pub last_repair_time: Option, - /// Retry attempts - pub total_retry_attempts: u64, -} - -/// Policy evaluation metrics -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PolicyMetrics { - /// Total policy evaluations - pub total_evaluations: u64, - /// Allowed operations - pub allowed_operations: u64, - /// Denied operations - pub denied_operations: u64, - /// Scan policy evaluations - pub scan_policy_evaluations: u64, - /// Heal policy evaluations - pub heal_policy_evaluations: u64, - /// Retention policy evaluations - pub retention_policy_evaluations: u64, - /// Average evaluation time - pub average_evaluation_time: Duration, -} - -impl Default for SystemMetrics { - fn default() -> Self { - Self { - timestamp: SystemTime::now(), - cpu_usage: 0.0, - memory_usage: 0.0, - disk_usage: 0.0, - network_io: NetworkMetrics::default(), - disk_io: DiskMetrics::default(), - active_operations: 0, - system_load: 0.0, - health_issues: std::collections::HashMap::new(), - scan_metrics: ScanMetrics::default(), - heal_metrics: HealMetrics::default(), - policy_metrics: PolicyMetrics::default(), - } - } -} - -impl Default for NetworkMetrics { - fn default() -> Self { - Self { - bytes_received_per_sec: 0, - bytes_sent_per_sec: 0, - packets_received_per_sec: 0, - packets_sent_per_sec: 0, - } - } -} - -impl Default for DiskMetrics { - fn default() -> Self { - Self { - bytes_read_per_sec: 0, - bytes_written_per_sec: 0, - read_ops_per_sec: 0, - write_ops_per_sec: 0, - avg_read_latency_ms: 0.0, - avg_write_latency_ms: 0.0, - } - } -} - -impl Default for ScanMetrics { - fn default() -> Self { - Self { - objects_scanned: 0, - bytes_scanned: 0, - scan_duration: Duration::ZERO, - scan_rate_objects_per_sec: 0.0, - scan_rate_bytes_per_sec: 0.0, - health_issues_found: 0, - scan_cycles_completed: 0, - last_scan_time: None, - } - } -} - -impl Default for HealMetrics { - fn default() -> Self { - Self { - total_repairs: 0, - successful_repairs: 0, - failed_repairs: 0, - total_repair_time: Duration::ZERO, - average_repair_time: Duration::ZERO, - active_repair_workers: 0, - queued_repair_tasks: 0, - last_repair_time: None, - total_retry_attempts: 0, - } - } -} - -impl Default for PolicyMetrics { - fn default() -> Self { - Self { - total_evaluations: 0, - allowed_operations: 0, - denied_operations: 0, - scan_policy_evaluations: 0, - heal_policy_evaluations: 0, - retention_policy_evaluations: 0, - average_evaluation_time: Duration::ZERO, - } - } -} - -/// Metrics query parameters -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MetricsQuery { - /// Start time for the query - pub start_time: SystemTime, - /// End time for the query - pub end_time: SystemTime, - /// Metrics aggregation interval - pub interval: Duration, - /// Metrics to include in the query - pub metrics: Vec, - /// Filter by severity - pub severity_filter: Option, - /// Limit number of results - pub limit: Option, -} - -/// Types of metrics that can be queried -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum MetricType { - /// System metrics (CPU, memory, disk) - System, - /// Network metrics - Network, - /// Disk I/O metrics - DiskIo, - /// Scan metrics - Scan, - /// Heal metrics - Heal, - /// Policy metrics - Policy, - /// Health issues - HealthIssues, -} - -/// Aggregated metrics data -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AggregatedMetrics { - /// Query parameters used - pub query: MetricsQuery, - /// Aggregated data points - pub data_points: Vec, - /// Summary statistics - pub summary: MetricsSummary, -} - -/// Individual metrics data point -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MetricsDataPoint { - /// Timestamp for this data point - pub timestamp: SystemTime, - /// System metrics - pub system: Option, - /// Network metrics - pub network: Option, - /// Disk I/O metrics - pub disk_io: Option, - /// Scan metrics - pub scan: Option, - /// Heal metrics - pub heal: Option, - /// Policy metrics - pub policy: Option, -} - -/// Summary statistics for aggregated metrics -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MetricsSummary { - /// Total data points - pub total_points: u64, - /// Time range covered - pub time_range: Duration, - /// Average CPU usage - pub avg_cpu_usage: f64, - /// Average memory usage - pub avg_memory_usage: f64, - /// Average disk usage - pub avg_disk_usage: f64, - /// Total objects scanned - pub total_objects_scanned: u64, - /// Total repairs performed - pub total_repairs: u64, - /// Success rate for repairs - pub repair_success_rate: f64, - /// Total health issues - pub total_health_issues: u64, -} - -/// Resource usage information -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ResourceUsage { - /// Disk usage information - pub disk_usage: DiskUsage, - /// Memory usage information - pub memory_usage: MemoryUsage, - /// Network usage information - pub network_usage: NetworkUsage, - /// CPU usage information - pub cpu_usage: CpuUsage, -} - -/// Disk usage information -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DiskUsage { - /// Total disk space in bytes - pub total_bytes: u64, - /// Used disk space in bytes - pub used_bytes: u64, - /// Available disk space in bytes - pub available_bytes: u64, - /// Usage percentage - pub usage_percentage: f64, -} - -/// Memory usage information -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MemoryUsage { - /// Total memory in bytes - pub total_bytes: u64, - /// Used memory in bytes - pub used_bytes: u64, - /// Available memory in bytes - pub available_bytes: u64, - /// Usage percentage - pub usage_percentage: f64, -} - -/// Network usage information -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct NetworkUsage { - /// Bytes received - pub bytes_received: u64, - /// Bytes sent - pub bytes_sent: u64, - /// Packets received - pub packets_received: u64, - /// Packets sent - pub packets_sent: u64, -} - -/// CPU usage information -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CpuUsage { - /// CPU usage percentage - pub usage_percentage: f64, - /// Number of CPU cores - pub cores: u32, - /// Load average - pub load_average: f64, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_system_metrics_creation() { - let metrics = SystemMetrics::default(); - assert_eq!(metrics.cpu_usage, 0.0); - assert_eq!(metrics.memory_usage, 0.0); - assert_eq!(metrics.active_operations, 0); - } - - #[test] - fn test_scan_metrics_creation() { - let metrics = ScanMetrics::default(); - assert_eq!(metrics.objects_scanned, 0); - assert_eq!(metrics.bytes_scanned, 0); - assert_eq!(metrics.scan_cycles_completed, 0); - } - - #[test] - fn test_heal_metrics_creation() { - let metrics = HealMetrics::default(); - assert_eq!(metrics.total_repairs, 0); - assert_eq!(metrics.successful_repairs, 0); - assert_eq!(metrics.failed_repairs, 0); - } - - #[test] - fn test_metrics_query_creation() { - let start_time = SystemTime::now(); - let end_time = start_time + Duration::from_secs(3600); - let query = MetricsQuery { - start_time, - end_time, - interval: Duration::from_secs(60), - metrics: vec![MetricType::System, MetricType::Scan], - severity_filter: Some(Severity::Critical), - limit: Some(100), - }; - - assert_eq!(query.metrics.len(), 2); - assert_eq!(query.interval, Duration::from_secs(60)); - assert_eq!(query.limit, Some(100)); - } -} \ No newline at end of file diff --git a/crates/ahm/src/metrics/reporter.rs b/crates/ahm/src/metrics/reporter.rs deleted file mode 100644 index 45aacc031..000000000 --- a/crates/ahm/src/metrics/reporter.rs +++ /dev/null @@ -1,861 +0,0 @@ -// Copyright 2024 RustFS Team - -use std::{ - collections::HashMap, - fmt, - sync::Arc, - time::{Duration, SystemTime}, -}; - -use tokio::sync::RwLock; -use tracing::{debug, error, info, warn}; -use serde::{Serialize, Deserialize}; - -use crate::error::Result; - -use super::{ - AggregatedMetrics, MetricsQuery, MetricsSummary, SystemMetrics, -}; - -/// Configuration for the metrics reporter -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ReporterConfig { - /// Whether to enable reporting - pub enabled: bool, - /// Report generation interval - pub report_interval: Duration, - /// Maximum number of reports to keep in memory - pub max_reports_in_memory: usize, - /// Alert thresholds - pub alert_thresholds: AlertThresholds, - /// Report output format - pub default_format: ReportFormat, - /// Whether to enable alerting - pub enable_alerts: bool, - /// Maximum number of alerts to keep in memory - pub max_alerts_in_memory: usize, - /// Report output directory - pub output_directory: Option, - /// Whether to enable HTTP reporting - pub enable_http_reporting: bool, - /// HTTP reporting endpoint - pub http_endpoint: Option, -} - -impl Default for ReporterConfig { - fn default() -> Self { - Self { - enabled: true, - report_interval: Duration::from_secs(60), // 1 minute - max_reports_in_memory: 1000, - alert_thresholds: AlertThresholds::default(), - default_format: ReportFormat::Json, - enable_alerts: true, - max_alerts_in_memory: 1000, - output_directory: None, - enable_http_reporting: false, - http_endpoint: None, - } - } -} - -/// Alert thresholds for metrics reporting -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AlertThresholds { - /// CPU usage threshold (percentage) - pub cpu_usage_threshold: f64, - /// Memory usage threshold (percentage) - pub memory_usage_threshold: f64, - /// Disk usage threshold (percentage) - pub disk_usage_threshold: f64, - /// System load threshold - pub system_load_threshold: f64, - /// Repair failure rate threshold (percentage) - pub repair_failure_rate_threshold: f64, - /// Health issues threshold (count) - pub health_issues_threshold: u64, -} - -impl Default for AlertThresholds { - fn default() -> Self { - Self { - cpu_usage_threshold: 80.0, - memory_usage_threshold: 85.0, - disk_usage_threshold: 90.0, - system_load_threshold: 5.0, - repair_failure_rate_threshold: 20.0, - health_issues_threshold: 10, - } - } -} - -/// Metrics reporter that generates and outputs metrics reports -pub struct Reporter { - config: ReporterConfig, - reports: Arc>>, - alerts: Arc>>, - last_report_time: Arc>, - report_count: Arc>, - alert_count: Arc>, -} - -impl Reporter { - /// Create a new metrics reporter - pub async fn new(config: ReporterConfig) -> Result { - Ok(Self { - config, - reports: Arc::new(RwLock::new(Vec::new())), - alerts: Arc::new(RwLock::new(Vec::new())), - last_report_time: Arc::new(RwLock::new(SystemTime::now())), - report_count: Arc::new(RwLock::new(0)), - alert_count: Arc::new(RwLock::new(0)), - }) - } - - /// Get the configuration - pub fn config(&self) -> &ReporterConfig { - &self.config - } - - /// Generate a metrics report - pub async fn generate_report(&self, metrics: &SystemMetrics) -> Result { - let start_time = SystemTime::now(); - - let report = MetricsReport { - timestamp: start_time, - metrics: metrics.clone(), - alerts: self.check_alerts(metrics).await?, - summary: self.generate_summary(metrics).await?, - format: self.config.default_format, - }; - - // Store report - { - let mut reports = self.reports.write().await; - reports.push(report.clone()); - - // Trim old reports if we exceed the limit - if reports.len() > self.config.max_reports_in_memory { - let excess = reports.len() - self.config.max_reports_in_memory; - reports.drain(0..excess); - } - } - - // Update statistics - { - let mut last_time = self.last_report_time.write().await; - *last_time = start_time; - - let mut count = self.report_count.write().await; - *count += 1; - } - - info!("Generated metrics report #{}", *self.report_count.read().await); - Ok(report) - } - - /// Generate a comprehensive report from aggregated metrics - pub async fn generate_comprehensive_report(&self, aggregated: &AggregatedMetrics) -> Result { - let start_time = SystemTime::now(); - - let report = ComprehensiveReport { - timestamp: start_time, - query: aggregated.query.clone(), - data_points: aggregated.data_points.len(), - summary: aggregated.summary.clone(), - alerts: self.check_aggregated_alerts(aggregated).await?, - trends: self.analyze_trends(aggregated).await?, - recommendations: self.generate_recommendations(aggregated).await?, - }; - - info!("Generated comprehensive report with {} data points", report.data_points); - Ok(report) - } - - /// Output a report in the specified format - pub async fn output_report(&self, report: &MetricsReport, format: ReportFormat) -> Result<()> { - match format { - ReportFormat::Console => self.output_to_console(report).await?, - ReportFormat::File => self.output_to_file(report).await?, - ReportFormat::Http => self.output_to_http(report).await?, - ReportFormat::Prometheus => self.output_prometheus(report).await?, - ReportFormat::Json => self.output_json(report).await?, - ReportFormat::Csv => self.output_csv(report).await?, - } - - Ok(()) - } - - /// Check for alerts based on metrics - async fn check_alerts(&self, metrics: &SystemMetrics) -> Result> { - let mut alerts = Vec::new(); - - // Check CPU usage - if metrics.cpu_usage > self.config.alert_thresholds.cpu_usage_threshold { - alerts.push(Alert { - timestamp: SystemTime::now(), - severity: AlertSeverity::Warning, - category: AlertCategory::System, - message: format!("High CPU usage: {:.1}%", metrics.cpu_usage), - metric_value: metrics.cpu_usage, - threshold: self.config.alert_thresholds.cpu_usage_threshold, - }); - } - - // Check memory usage - if metrics.memory_usage > self.config.alert_thresholds.memory_usage_threshold { - alerts.push(Alert { - timestamp: SystemTime::now(), - severity: AlertSeverity::Warning, - category: AlertCategory::System, - message: format!("High memory usage: {:.1}%", metrics.memory_usage), - metric_value: metrics.memory_usage, - threshold: self.config.alert_thresholds.memory_usage_threshold, - }); - } - - // Check disk usage - if metrics.disk_usage > self.config.alert_thresholds.disk_usage_threshold { - alerts.push(Alert { - timestamp: SystemTime::now(), - severity: AlertSeverity::Critical, - category: AlertCategory::System, - message: format!("High disk usage: {:.1}%", metrics.disk_usage), - metric_value: metrics.disk_usage, - threshold: self.config.alert_thresholds.disk_usage_threshold, - }); - } - - // Check system load - if metrics.system_load > self.config.alert_thresholds.system_load_threshold { - alerts.push(Alert { - timestamp: SystemTime::now(), - severity: AlertSeverity::Warning, - category: AlertCategory::System, - message: format!("High system load: {:.2}", metrics.system_load), - metric_value: metrics.system_load, - threshold: self.config.alert_thresholds.system_load_threshold, - }); - } - - // Check repair failure rate - if metrics.heal_metrics.total_repairs > 0 { - let failure_rate = (metrics.heal_metrics.failed_repairs as f64 / metrics.heal_metrics.total_repairs as f64) * 100.0; - if failure_rate > self.config.alert_thresholds.repair_failure_rate_threshold { - alerts.push(Alert { - timestamp: SystemTime::now(), - severity: AlertSeverity::Critical, - category: AlertCategory::Heal, - message: format!("High repair failure rate: {:.1}%", failure_rate), - metric_value: failure_rate, - threshold: self.config.alert_thresholds.repair_failure_rate_threshold, - }); - } - } - - // Check health issues - let total_health_issues: u64 = metrics.health_issues.values().sum(); - if total_health_issues > self.config.alert_thresholds.health_issues_threshold { - alerts.push(Alert { - timestamp: SystemTime::now(), - severity: AlertSeverity::Warning, - category: AlertCategory::Health, - message: format!("High number of health issues: {}", total_health_issues), - metric_value: total_health_issues as f64, - threshold: self.config.alert_thresholds.health_issues_threshold as f64, - }); - } - - // Store alerts - if !alerts.is_empty() { - let mut alert_store = self.alerts.write().await; - alert_store.extend(alerts.clone()); - - let mut count = self.alert_count.write().await; - *count += alerts.len() as u64; - } - - Ok(alerts) - } - - /// Check for alerts based on aggregated metrics - async fn check_aggregated_alerts(&self, aggregated: &AggregatedMetrics) -> Result> { - let mut alerts = Vec::new(); - - // Check summary statistics - if aggregated.summary.avg_cpu_usage > self.config.alert_thresholds.cpu_usage_threshold { - alerts.push(Alert { - timestamp: SystemTime::now(), - severity: AlertSeverity::Warning, - category: AlertCategory::System, - message: format!("High average CPU usage: {:.1}%", aggregated.summary.avg_cpu_usage), - metric_value: aggregated.summary.avg_cpu_usage, - threshold: self.config.alert_thresholds.cpu_usage_threshold, - }); - } - - if aggregated.summary.repair_success_rate < (100.0 - self.config.alert_thresholds.repair_failure_rate_threshold) { - alerts.push(Alert { - timestamp: SystemTime::now(), - severity: AlertSeverity::Critical, - category: AlertCategory::Heal, - message: format!("Low repair success rate: {:.1}%", aggregated.summary.repair_success_rate * 100.0), - metric_value: aggregated.summary.repair_success_rate * 100.0, - threshold: 100.0 - self.config.alert_thresholds.repair_failure_rate_threshold, - }); - } - - Ok(alerts) - } - - /// Generate summary for metrics - async fn generate_summary(&self, metrics: &SystemMetrics) -> Result { - Ok(ReportSummary { - system_health: self.calculate_system_health(metrics), - performance_score: self.calculate_performance_score(metrics), - resource_utilization: self.calculate_resource_utilization(metrics), - operational_status: self.determine_operational_status(metrics), - key_metrics: self.extract_key_metrics(metrics), - }) - } - - /// Analyze trends in aggregated data - async fn analyze_trends(&self, aggregated: &AggregatedMetrics) -> Result> { - let mut trends = Vec::new(); - - if aggregated.data_points.len() < 2 { - return Ok(trends); - } - - // Analyze CPU usage trend - let cpu_values: Vec = aggregated - .data_points - .iter() - .filter_map(|p| p.system.as_ref().map(|s| s.cpu_usage)) - .collect(); - - if cpu_values.len() >= 2 { - let trend = self.calculate_trend(&cpu_values, "CPU Usage"); - trends.push(trend); - } - - // Analyze memory usage trend - let memory_values: Vec = aggregated - .data_points - .iter() - .filter_map(|p| p.system.as_ref().map(|s| s.memory_usage)) - .collect(); - - if memory_values.len() >= 2 { - let trend = self.calculate_trend(&memory_values, "Memory Usage"); - trends.push(trend); - } - - Ok(trends) - } - - /// Generate recommendations based on metrics - async fn generate_recommendations(&self, aggregated: &AggregatedMetrics) -> Result> { - let mut recommendations = Vec::new(); - - // Check for high resource usage - if aggregated.summary.avg_cpu_usage > 70.0 { - recommendations.push(Recommendation { - priority: RecommendationPriority::High, - category: RecommendationCategory::Performance, - title: "High CPU Usage".to_string(), - description: "Consider scaling up CPU resources or optimizing workload distribution".to_string(), - action: "Monitor CPU usage patterns and consider resource allocation adjustments".to_string(), - }); - } - - if aggregated.summary.avg_memory_usage > 80.0 { - recommendations.push(Recommendation { - priority: RecommendationPriority::High, - category: RecommendationCategory::Performance, - title: "High Memory Usage".to_string(), - description: "Memory usage is approaching critical levels".to_string(), - action: "Consider increasing memory allocation or optimizing memory usage".to_string(), - }); - } - - // Check for repair issues - if aggregated.summary.repair_success_rate < 0.8 { - recommendations.push(Recommendation { - priority: RecommendationPriority::Critical, - category: RecommendationCategory::Reliability, - title: "Low Repair Success Rate".to_string(), - description: "Data repair operations are failing frequently".to_string(), - action: "Investigate repair failures and check system health".to_string(), - }); - } - - Ok(recommendations) - } - - /// Calculate trend for a series of values - fn calculate_trend(&self, values: &[f64], metric_name: &str) -> TrendAnalysis { - if values.len() < 2 { - return TrendAnalysis { - metric_name: metric_name.to_string(), - trend_direction: TrendDirection::Stable, - change_rate: 0.0, - confidence: 0.0, - }; - } - - let first = values[0]; - let last = values[values.len() - 1]; - let change_rate = ((last - first) / first) * 100.0; - - let trend_direction = if change_rate > 5.0 { - TrendDirection::Increasing - } else if change_rate < -5.0 { - TrendDirection::Decreasing - } else { - TrendDirection::Stable - }; - - // Simple confidence calculation based on data points - let confidence = (values.len() as f64 / 10.0).min(1.0); - - TrendAnalysis { - metric_name: metric_name.to_string(), - trend_direction, - change_rate, - confidence, - } - } - - /// Calculate system health score - fn calculate_system_health(&self, metrics: &SystemMetrics) -> f64 { - let mut score = 100.0; - - // Deduct points for high resource usage - if metrics.cpu_usage > 80.0 { - score -= (metrics.cpu_usage - 80.0) * 0.5; - } - if metrics.memory_usage > 85.0 { - score -= (metrics.memory_usage - 85.0) * 0.5; - } - if metrics.disk_usage > 90.0 { - score -= (metrics.disk_usage - 90.0) * 1.0; - } - - // Deduct points for health issues - let total_health_issues: u64 = metrics.health_issues.values().sum(); - score -= total_health_issues as f64 * 5.0; - - // Deduct points for repair failures - if metrics.heal_metrics.total_repairs > 0 { - let failure_rate = metrics.heal_metrics.failed_repairs as f64 / metrics.heal_metrics.total_repairs as f64; - score -= failure_rate * 20.0; - } - - score.max(0.0) - } - - /// Calculate performance score - fn calculate_performance_score(&self, metrics: &SystemMetrics) -> f64 { - let mut score = 100.0; - - // Base score on resource efficiency - score -= metrics.cpu_usage * 0.3; - score -= metrics.memory_usage * 0.3; - score -= metrics.disk_usage * 0.2; - score -= metrics.system_load * 10.0; - - score.max(0.0) - } - - /// Calculate resource utilization - fn calculate_resource_utilization(&self, metrics: &SystemMetrics) -> f64 { - (metrics.cpu_usage + metrics.memory_usage + metrics.disk_usage) / 3.0 - } - - /// Determine operational status - fn determine_operational_status(&self, metrics: &SystemMetrics) -> OperationalStatus { - let health_score = self.calculate_system_health(metrics); - - if health_score >= 90.0 { - OperationalStatus::Excellent - } else if health_score >= 75.0 { - OperationalStatus::Good - } else if health_score >= 50.0 { - OperationalStatus::Fair - } else { - OperationalStatus::Poor - } - } - - /// Extract key metrics - fn extract_key_metrics(&self, metrics: &SystemMetrics) -> HashMap { - let mut key_metrics = HashMap::new(); - key_metrics.insert("cpu_usage".to_string(), metrics.cpu_usage); - key_metrics.insert("memory_usage".to_string(), metrics.memory_usage); - key_metrics.insert("disk_usage".to_string(), metrics.disk_usage); - key_metrics.insert("system_load".to_string(), metrics.system_load); - key_metrics.insert("active_operations".to_string(), metrics.active_operations as f64); - key_metrics.insert("objects_scanned".to_string(), metrics.scan_metrics.objects_scanned as f64); - key_metrics.insert("total_repairs".to_string(), metrics.heal_metrics.total_repairs as f64); - key_metrics.insert("successful_repairs".to_string(), metrics.heal_metrics.successful_repairs as f64); - - key_metrics - } - - /// Output methods (simulated) - async fn output_to_console(&self, report: &MetricsReport) -> Result<()> { - if self.config.enabled { - info!("=== Metrics Report ==="); - info!("Timestamp: {:?}", report.timestamp); - info!("System Health: {:.1}%", report.summary.system_health); - info!("Performance Score: {:.1}%", report.summary.performance_score); - info!("Operational Status: {:?}", report.summary.operational_status); - - if !report.alerts.is_empty() { - info!("=== Alerts ==="); - for alert in &report.alerts { - info!("[{}] {}: {}", alert.severity, alert.category, alert.message); - } - } - } - Ok(()) - } - - async fn output_to_file(&self, _report: &MetricsReport) -> Result<()> { - if self.config.enabled { - // In a real implementation, this would write to a file - debug!("Would write report to file: {}", self.config.output_directory.as_ref().unwrap_or(&String::new())); - } - Ok(()) - } - - async fn output_to_http(&self, _report: &MetricsReport) -> Result<()> { - if self.config.enable_http_reporting { - // In a real implementation, this would serve via HTTP - debug!("Would serve report via HTTP on endpoint: {}", self.config.http_endpoint.as_ref().unwrap_or(&String::new())); - } - Ok(()) - } - - async fn output_prometheus(&self, _report: &MetricsReport) -> Result<()> { - if self.config.enabled { - // In a real implementation, this would output Prometheus format - debug!("Would output Prometheus format"); - } - Ok(()) - } - - async fn output_json(&self, _report: &MetricsReport) -> Result<()> { - if self.config.enabled { - // In a real implementation, this would output JSON format - debug!("Would output JSON format"); - } - Ok(()) - } - - async fn output_csv(&self, _report: &MetricsReport) -> Result<()> { - if self.config.enabled { - // In a real implementation, this would output CSV format - debug!("Would output CSV format"); - } - Ok(()) - } - - /// Get reporting statistics - pub async fn get_statistics(&self) -> ReporterStatistics { - let report_count = *self.report_count.read().await; - let alert_count = *self.alert_count.read().await; - let last_report_time = *self.last_report_time.read().await; - let reports_count = self.reports.read().await.len(); - let alerts_count = self.alerts.read().await.len(); - - ReporterStatistics { - total_reports: report_count, - total_alerts: alert_count, - reports_in_memory: reports_count, - alerts_in_memory: alerts_count, - last_report_time, - config: self.config.clone(), - } - } - - /// Get recent alerts - pub async fn get_recent_alerts(&self, hours: u64) -> Result> { - let cutoff_time = SystemTime::now() - Duration::from_secs(hours * 3600); - let alerts = self.alerts.read().await; - - let recent_alerts: Vec = alerts - .iter() - .filter(|alert| alert.timestamp >= cutoff_time) - .cloned() - .collect(); - - Ok(recent_alerts) - } - - /// Clear old alerts - pub async fn clear_old_alerts(&self, hours: u64) -> Result<()> { - let cutoff_time = SystemTime::now() - Duration::from_secs(hours * 3600); - let mut alerts = self.alerts.write().await; - alerts.retain(|alert| alert.timestamp >= cutoff_time); - - info!("Cleared alerts older than {} hours", hours); - Ok(()) - } -} - -/// Metrics report -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MetricsReport { - pub timestamp: SystemTime, - pub metrics: SystemMetrics, - pub alerts: Vec, - pub summary: ReportSummary, - pub format: ReportFormat, -} - -/// Comprehensive report -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ComprehensiveReport { - pub timestamp: SystemTime, - pub query: MetricsQuery, - pub data_points: usize, - pub summary: MetricsSummary, - pub alerts: Vec, - pub trends: Vec, - pub recommendations: Vec, -} - -/// Report summary -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ReportSummary { - pub system_health: f64, - pub performance_score: f64, - pub resource_utilization: f64, - pub operational_status: OperationalStatus, - pub key_metrics: HashMap, -} - -/// Alert -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Alert { - pub timestamp: SystemTime, - pub severity: AlertSeverity, - pub category: AlertCategory, - pub message: String, - pub metric_value: f64, - pub threshold: f64, -} - -/// Alert severity -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum AlertSeverity { - Info, - Warning, - Critical, -} - -impl fmt::Display for AlertSeverity { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - AlertSeverity::Info => write!(f, "INFO"), - AlertSeverity::Warning => write!(f, "WARNING"), - AlertSeverity::Critical => write!(f, "CRITICAL"), - } - } -} - -/// Alert category -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum AlertCategory { - System, - Performance, - Health, - Heal, - Security, -} - -impl fmt::Display for AlertCategory { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - AlertCategory::System => write!(f, "SYSTEM"), - AlertCategory::Performance => write!(f, "PERFORMANCE"), - AlertCategory::Health => write!(f, "HEALTH"), - AlertCategory::Heal => write!(f, "HEAL"), - AlertCategory::Security => write!(f, "SECURITY"), - } - } -} - -/// Report format -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum ReportFormat { - Console, - File, - Http, - Prometheus, - Json, - Csv, -} - -/// Operational status -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum OperationalStatus { - Excellent, - Good, - Fair, - Poor, -} - -/// Trend analysis -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TrendAnalysis { - pub metric_name: String, - pub trend_direction: TrendDirection, - pub change_rate: f64, - pub confidence: f64, -} - -/// Trend direction -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum TrendDirection { - Increasing, - Decreasing, - Stable, -} - -/// Recommendation -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Recommendation { - pub priority: RecommendationPriority, - pub category: RecommendationCategory, - pub title: String, - pub description: String, - pub action: String, -} - -/// Recommendation priority -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum RecommendationPriority { - Low, - Medium, - High, - Critical, -} - -/// Recommendation category -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum RecommendationCategory { - Performance, - Reliability, - Security, - Maintenance, -} - -/// Reporter statistics -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ReporterStatistics { - pub total_reports: u64, - pub total_alerts: u64, - pub reports_in_memory: usize, - pub alerts_in_memory: usize, - pub last_report_time: SystemTime, - pub config: ReporterConfig, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_reporter_creation() { - let config = ReporterConfig::default(); - let reporter = Reporter::new(config).await.unwrap(); - - assert_eq!(reporter.config().report_interval, Duration::from_secs(60)); - assert!(reporter.config().enabled); - } - - #[tokio::test] - async fn test_report_generation() { - let config = ReporterConfig::default(); - let reporter = Reporter::new(config).await.unwrap(); - - let metrics = SystemMetrics::default(); - let report = reporter.generate_report(&metrics).await.unwrap(); - - assert_eq!(report.metrics.cpu_usage, 0.0); - assert_eq!(report.alerts.len(), 0); - } - - #[tokio::test] - async fn test_alert_generation() { - let config = ReporterConfig { - alert_thresholds: AlertThresholds { - cpu_usage_threshold: 50.0, - ..Default::default() - }, - ..Default::default() - }; - let reporter = Reporter::new(config).await.unwrap(); - - let mut metrics = SystemMetrics::default(); - metrics.cpu_usage = 75.0; // Above threshold - - let report = reporter.generate_report(&metrics).await.unwrap(); - assert!(!report.alerts.is_empty()); - assert_eq!(report.alerts[0].severity, AlertSeverity::Warning); - } - - #[tokio::test] - async fn test_comprehensive_report() { - let config = ReporterConfig::default(); - let reporter = Reporter::new(config).await.unwrap(); - - let aggregated = AggregatedMetrics { - query: MetricsQuery { - start_time: SystemTime::now(), - end_time: SystemTime::now() + Duration::from_secs(3600), - interval: Duration::from_secs(60), - metrics: vec![], - severity_filter: None, - limit: None, - }, - data_points: vec![], - summary: MetricsSummary::default(), - }; - - let report = reporter.generate_comprehensive_report(&aggregated).await.unwrap(); - assert_eq!(report.data_points, 0); - assert!(report.recommendations.is_empty()); - } - - #[tokio::test] - async fn test_reporter_statistics() { - let config = ReporterConfig::default(); - let reporter = Reporter::new(config).await.unwrap(); - - let stats = reporter.get_statistics().await; - assert_eq!(stats.total_reports, 0); - assert_eq!(stats.total_alerts, 0); - } - - #[tokio::test] - async fn test_alert_clearing() { - let config = ReporterConfig::default(); - let reporter = Reporter::new(config).await.unwrap(); - - // Generate some alerts - let mut metrics = SystemMetrics::default(); - metrics.cpu_usage = 90.0; // Above threshold - - let _report = reporter.generate_report(&metrics).await.unwrap(); - - // Clear old alerts - reporter.clear_old_alerts(1).await.unwrap(); - - let stats = reporter.get_statistics().await; - assert_eq!(stats.alerts_in_memory, 0); - } -} \ No newline at end of file diff --git a/crates/ahm/src/metrics/storage.rs b/crates/ahm/src/metrics/storage.rs deleted file mode 100644 index 66520ff7c..000000000 --- a/crates/ahm/src/metrics/storage.rs +++ /dev/null @@ -1,573 +0,0 @@ -// Copyright 2024 RustFS Team - -use std::{ - collections::HashMap, - sync::Arc, - path::PathBuf, - time::{Duration, Instant, SystemTime}, -}; - -use tokio::sync::RwLock; -use tracing::{debug, error, info, warn}; - -use crate::error::Result; - -use super::{ - AggregatedMetrics, MetricsDataPoint, MetricsQuery, MetricsSummary, SystemMetrics, -}; - -/// Configuration for metrics storage -#[derive(Debug, Clone)] -pub struct StorageConfig { - /// Storage directory path - pub storage_path: PathBuf, - /// Maximum file size for metrics files - pub max_file_size: u64, - /// Compression enabled - pub compression_enabled: bool, - /// Retention period for metrics data - pub retention_period: Duration, - /// Batch size for writes - pub batch_size: usize, - /// Flush interval - pub flush_interval: Duration, - /// Whether to enable data validation - pub enable_validation: bool, - /// Whether to enable data encryption - pub enable_encryption: bool, - /// Encryption key (if enabled) - pub encryption_key: Option, -} - -impl Default for StorageConfig { - fn default() -> Self { - Self { - storage_path: PathBuf::from("/tmp/rustfs/metrics"), - max_file_size: 100 * 1024 * 1024, // 100 MB - compression_enabled: true, - retention_period: Duration::from_secs(86400 * 30), // 30 days - batch_size: 1000, - flush_interval: Duration::from_secs(60), // 1 minute - enable_validation: true, - enable_encryption: false, - encryption_key: None, - } - } -} - -/// Metrics storage that persists metrics data to disk -pub struct Storage { - config: StorageConfig, - metrics_buffer: Arc>>, - aggregated_buffer: Arc>>, - file_handles: Arc>>, - last_flush_time: Arc>, - total_writes: Arc>, - total_reads: Arc>, -} - -impl Storage { - /// Create a new metrics storage - pub async fn new(config: StorageConfig) -> Result { - // Create storage directory if it doesn't exist - tokio::fs::create_dir_all(&config.storage_path).await?; - - Ok(Self { - config, - metrics_buffer: Arc::new(RwLock::new(Vec::new())), - aggregated_buffer: Arc::new(RwLock::new(Vec::new())), - file_handles: Arc::new(RwLock::new(HashMap::new())), - last_flush_time: Arc::new(RwLock::new(SystemTime::now())), - total_writes: Arc::new(RwLock::new(0)), - total_reads: Arc::new(RwLock::new(0)), - }) - } - - /// Get the configuration - pub fn config(&self) -> &StorageConfig { - &self.config - } - - /// Store system metrics - pub async fn store_metrics(&self, metrics: SystemMetrics) -> Result<()> { - let mut buffer = self.metrics_buffer.write().await; - buffer.push(metrics); - - // Flush if buffer is full - if buffer.len() >= self.config.batch_size { - self.flush_metrics_buffer().await?; - } - - // Update write count - { - let mut writes = self.total_writes.write().await; - *writes += 1; - } - - Ok(()) - } - - /// Store aggregated metrics - pub async fn store_aggregated_metrics(&self, aggregated: AggregatedMetrics) -> Result<()> { - let mut buffer = self.aggregated_buffer.write().await; - buffer.push(aggregated); - - // Flush if buffer is full - if buffer.len() >= self.config.batch_size { - self.flush_aggregated_buffer().await?; - } - - Ok(()) - } - - /// Retrieve metrics for a time range - pub async fn retrieve_metrics(&self, query: &MetricsQuery) -> Result> { - let start_time = Instant::now(); - - // Update read count - { - let mut reads = self.total_reads.write().await; - *reads += 1; - } - - // In a real implementation, this would read from disk files - // For now, we'll return data from the buffer - let buffer = self.metrics_buffer.read().await; - let filtered_metrics: Vec = buffer - .iter() - .filter(|m| m.timestamp >= query.start_time && m.timestamp <= query.end_time) - .cloned() - .collect(); - - let retrieval_time = start_time.elapsed(); - debug!("Metrics retrieval completed in {:?}", retrieval_time); - - Ok(filtered_metrics) - } - - /// Retrieve aggregated metrics - pub async fn retrieve_aggregated_metrics(&self, query: &MetricsQuery) -> Result> { - let buffer = self.aggregated_buffer.read().await; - let filtered_metrics: Vec = buffer - .iter() - .filter(|m| { - if let Some(first_point) = m.data_points.first() { - first_point.timestamp >= query.start_time - } else { - false - } - }) - .filter(|m| { - if let Some(last_point) = m.data_points.last() { - last_point.timestamp <= query.end_time - } else { - false - } - }) - .cloned() - .collect(); - - Ok(filtered_metrics) - } - - /// Flush metrics buffer to disk - async fn flush_metrics_buffer(&self) -> Result<()> { - let mut buffer = self.metrics_buffer.write().await; - if buffer.is_empty() { - return Ok(()); - } - - let metrics_to_write = buffer.drain(..).collect::>(); - drop(buffer); // Release lock - - // Write to file - self.write_metrics_to_file(&metrics_to_write).await?; - - // Update flush time - { - let mut last_flush = self.last_flush_time.write().await; - *last_flush = SystemTime::now(); - } - - info!("Flushed {} metrics to disk", metrics_to_write.len()); - Ok(()) - } - - /// Flush aggregated buffer to disk - async fn flush_aggregated_buffer(&self) -> Result<()> { - let mut buffer = self.aggregated_buffer.write().await; - if buffer.is_empty() { - return Ok(()); - } - - let aggregated_to_write = buffer.drain(..).collect::>(); - drop(buffer); // Release lock - - // Write to file - self.write_aggregated_to_file(&aggregated_to_write).await?; - - info!("Flushed {} aggregated metrics to disk", aggregated_to_write.len()); - Ok(()) - } - - /// Write metrics to file - async fn write_metrics_to_file(&self, metrics: &[SystemMetrics]) -> Result<()> { - let filename = format!("metrics_{}.json", SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs()); - let filepath = self.config.storage_path.join(filename); - - // In a real implementation, this would write to a file - // For now, we'll just simulate the write - debug!("Would write {} metrics to {}", metrics.len(), filepath.display()); - - Ok(()) - } - - /// Write aggregated metrics to file - async fn write_aggregated_to_file(&self, aggregated: &[AggregatedMetrics]) -> Result<()> { - let filename = format!("aggregated_{}.json", SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs()); - let filepath = self.config.storage_path.join(filename); - - // In a real implementation, this would write to a file - // For now, we'll just simulate the write - debug!("Would write {} aggregated metrics to {}", aggregated.len(), filepath.display()); - - Ok(()) - } - - /// Force flush all buffers - pub async fn force_flush(&self) -> Result<()> { - self.flush_metrics_buffer().await?; - self.flush_aggregated_buffer().await?; - - info!("Force flush completed"); - Ok(()) - } - - /// Clean up old data based on retention policy - pub async fn cleanup_old_data(&self) -> Result<()> { - let cutoff_time = SystemTime::now() - self.config.retention_period; - - // Clean up metrics buffer - { - let mut buffer = self.metrics_buffer.write().await; - buffer.retain(|m| m.timestamp >= cutoff_time); - } - - // Clean up aggregated buffer - { - let mut buffer = self.aggregated_buffer.write().await; - buffer.retain(|m| { - if let Some(first_point) = m.data_points.first() { - first_point.timestamp >= cutoff_time - } else { - false - } - }); - } - - // In a real implementation, this would also clean up old files - info!("Cleanup completed, removed data older than {:?}", cutoff_time); - Ok(()) - } - - /// Get storage statistics - pub async fn get_statistics(&self) -> StorageStatistics { - let metrics_count = self.metrics_buffer.read().await.len(); - let aggregated_count = self.aggregated_buffer.read().await.len(); - let total_writes = *self.total_writes.read().await; - let total_reads = *self.total_reads.read().await; - let last_flush_time = *self.last_flush_time.read().await; - - StorageStatistics { - metrics_in_buffer: metrics_count, - aggregated_in_buffer: aggregated_count, - total_writes, - total_reads, - last_flush_time, - config: self.config.clone(), - } - } - - /// Validate stored data integrity - pub async fn validate_data_integrity(&self) -> Result { - if !self.config.enable_validation { - return Ok(DataIntegrityReport { - is_valid: true, - total_records: 0, - corrupted_records: 0, - validation_time: Duration::ZERO, - errors: Vec::new(), - }); - } - - let start_time = Instant::now(); - let mut errors = Vec::new(); - let mut corrupted_records = 0; - - // Validate metrics buffer - { - let buffer = self.metrics_buffer.read().await; - for (i, metric) in buffer.iter().enumerate() { - if !self.validate_metric(metric) { - errors.push(format!("Invalid metric at index {}: {:?}", i, metric)); - corrupted_records += 1; - } - } - } - - // Validate aggregated buffer - { - let buffer = self.aggregated_buffer.read().await; - for (i, aggregated) in buffer.iter().enumerate() { - if !self.validate_aggregated(aggregated) { - errors.push(format!("Invalid aggregated metrics at index {}: {:?}", i, aggregated)); - corrupted_records += 1; - } - } - } - - let validation_time = start_time.elapsed(); - let total_records = { - let metrics_count = self.metrics_buffer.read().await.len(); - let aggregated_count = self.aggregated_buffer.read().await.len(); - metrics_count + aggregated_count - }; - - let is_valid = corrupted_records == 0; - - Ok(DataIntegrityReport { - is_valid, - total_records, - corrupted_records, - validation_time, - errors, - }) - } - - /// Validate a single metric - fn validate_metric(&self, metric: &SystemMetrics) -> bool { - // Basic validation checks - metric.cpu_usage >= 0.0 && metric.cpu_usage <= 100.0 - && metric.memory_usage >= 0.0 && metric.memory_usage <= 100.0 - && metric.disk_usage >= 0.0 && metric.disk_usage <= 100.0 - && metric.system_load >= 0.0 - } - - /// Validate aggregated metrics - fn validate_aggregated(&self, aggregated: &AggregatedMetrics) -> bool { - // Basic validation checks - !aggregated.data_points.is_empty() - && aggregated.query.start_time <= aggregated.query.end_time - && aggregated.summary.total_points > 0 - } - - /// Backup metrics data - pub async fn backup_data(&self, backup_path: &PathBuf) -> Result { - let start_time = Instant::now(); - - // Create backup directory - tokio::fs::create_dir_all(backup_path).await?; - - // In a real implementation, this would copy files to backup location - // For now, we'll just simulate the backup - let metrics_count = self.metrics_buffer.read().await.len(); - let aggregated_count = self.aggregated_buffer.read().await.len(); - - let backup_time = start_time.elapsed(); - - Ok(BackupReport { - backup_path: backup_path.clone(), - metrics_backed_up: metrics_count, - aggregated_backed_up: aggregated_count, - backup_time, - success: true, - }) - } - - /// Restore metrics data from backup - pub async fn restore_data(&self, backup_path: &PathBuf) -> Result { - let start_time = Instant::now(); - - // In a real implementation, this would restore from backup files - // For now, we'll just simulate the restore - debug!("Would restore data from {}", backup_path.display()); - - let restore_time = start_time.elapsed(); - - Ok(RestoreReport { - backup_path: backup_path.clone(), - metrics_restored: 0, - aggregated_restored: 0, - restore_time, - success: true, - }) - } -} - -/// Storage statistics -#[derive(Debug, Clone)] -pub struct StorageStatistics { - pub metrics_in_buffer: usize, - pub aggregated_in_buffer: usize, - pub total_writes: u64, - pub total_reads: u64, - pub last_flush_time: SystemTime, - pub config: StorageConfig, -} - -/// Data integrity validation report -#[derive(Debug, Clone)] -pub struct DataIntegrityReport { - pub is_valid: bool, - pub total_records: usize, - pub corrupted_records: usize, - pub validation_time: Duration, - pub errors: Vec, -} - -/// Backup report -#[derive(Debug, Clone)] -pub struct BackupReport { - pub backup_path: PathBuf, - pub metrics_backed_up: usize, - pub aggregated_backed_up: usize, - pub backup_time: Duration, - pub success: bool, -} - -/// Restore report -#[derive(Debug, Clone)] -pub struct RestoreReport { - pub backup_path: PathBuf, - pub metrics_restored: usize, - pub aggregated_restored: usize, - pub restore_time: Duration, - pub success: bool, -} - -#[cfg(test)] -mod tests { - use super::*; - use std::time::Instant; - - #[tokio::test] - async fn test_storage_creation() { - let config = StorageConfig::default(); - let storage = Storage::new(config).await.unwrap(); - - assert_eq!(storage.config().batch_size, 1000); - assert!(storage.config().compression_enabled); - } - - #[tokio::test] - async fn test_metrics_storage() { - let config = StorageConfig::default(); - let storage = Storage::new(config).await.unwrap(); - - let metrics = SystemMetrics::default(); - storage.store_metrics(metrics).await.unwrap(); - - let stats = storage.get_statistics().await; - assert_eq!(stats.metrics_in_buffer, 1); - assert_eq!(stats.total_writes, 1); - } - - #[tokio::test] - async fn test_aggregated_storage() { - let config = StorageConfig::default(); - let storage = Storage::new(config).await.unwrap(); - - let aggregated = AggregatedMetrics { - query: MetricsQuery { - start_time: SystemTime::now(), - end_time: SystemTime::now() + Duration::from_secs(3600), - interval: Duration::from_secs(60), - metrics: vec![], - severity_filter: None, - limit: None, - }, - data_points: vec![], - summary: MetricsSummary::default(), - }; - - storage.store_aggregated_metrics(aggregated).await.unwrap(); - - let stats = storage.get_statistics().await; - assert_eq!(stats.aggregated_in_buffer, 1); - } - - #[tokio::test] - async fn test_metrics_retrieval() { - let config = StorageConfig::default(); - let storage = Storage::new(config).await.unwrap(); - - // Store some metrics - for i in 0..5 { - let mut metrics = SystemMetrics::default(); - metrics.timestamp = SystemTime::now() + Duration::from_secs(i * 60); - storage.store_metrics(metrics).await.unwrap(); - } - - let query = MetricsQuery { - start_time: SystemTime::now(), - end_time: SystemTime::now() + Duration::from_secs(300), - interval: Duration::from_secs(60), - metrics: vec![], - severity_filter: None, - limit: None, - }; - - let retrieved = storage.retrieve_metrics(&query).await.unwrap(); - assert_eq!(retrieved.len(), 5); - } - - #[tokio::test] - async fn test_data_integrity_validation() { - let config = StorageConfig { - enable_validation: true, - ..Default::default() - }; - let storage = Storage::new(config).await.unwrap(); - - let report = storage.validate_data_integrity().await.unwrap(); - assert!(report.is_valid); - assert_eq!(report.corrupted_records, 0); - } - - #[tokio::test] - async fn test_force_flush() { - let config = StorageConfig::default(); - let storage = Storage::new(config).await.unwrap(); - - // Add some data - storage.store_metrics(SystemMetrics::default()).await.unwrap(); - - // Force flush - storage.force_flush().await.unwrap(); - - let stats = storage.get_statistics().await; - assert_eq!(stats.metrics_in_buffer, 0); - } - - #[tokio::test] - async fn test_cleanup_old_data() { - let config = StorageConfig::default(); - let storage = Storage::new(config).await.unwrap(); - - // Add some old data - let mut old_metrics = SystemMetrics::default(); - old_metrics.timestamp = SystemTime::now() - Duration::from_secs(86400 * 31); // 31 days old - storage.store_metrics(old_metrics).await.unwrap(); - - // Add some recent data - let mut recent_metrics = SystemMetrics::default(); - recent_metrics.timestamp = SystemTime::now(); - storage.store_metrics(recent_metrics).await.unwrap(); - - // Cleanup - storage.cleanup_old_data().await.unwrap(); - - let stats = storage.get_statistics().await; - assert_eq!(stats.metrics_in_buffer, 1); // Only recent data should remain - } -} \ No newline at end of file diff --git a/crates/ahm/src/policy/heal_policy.rs b/crates/ahm/src/policy/heal_policy.rs deleted file mode 100644 index 8342f089b..000000000 --- a/crates/ahm/src/policy/heal_policy.rs +++ /dev/null @@ -1,508 +0,0 @@ -// Copyright 2024 RustFS Team - -use std::time::{Duration, SystemTime}; - -use crate::scanner::{HealthIssue, Severity}; - -use super::{PolicyContext, PolicyResult, ResourceUsage}; - -/// Configuration for heal policies -#[derive(Debug, Clone)] -pub struct HealPolicyConfig { - /// Maximum number of concurrent repairs - pub max_concurrent_repairs: usize, - /// Maximum repair duration per operation - pub max_repair_duration: Duration, - /// Minimum interval between repairs - pub min_repair_interval: Duration, - /// Maximum system load threshold for healing - pub max_system_load: f64, - /// Minimum available disk space percentage for healing - pub min_disk_space: f64, - /// Maximum number of active operations for healing - pub max_active_operations: u64, - /// Whether to enable automatic healing - pub auto_heal_enabled: bool, - /// Priority-based healing configuration - pub priority_config: HealPriorityConfig, - /// Resource-based healing configuration - pub resource_config: HealResourceConfig, - /// Retry configuration - pub retry_config: HealRetryConfig, -} - -/// Priority-based healing configuration -#[derive(Debug, Clone)] -pub struct HealPriorityConfig { - /// Whether to enable priority-based healing - pub enabled: bool, - /// Critical issues heal immediately - pub critical_immediate: bool, - /// High priority issues heal within - pub high_timeout: Duration, - /// Medium priority issues heal within - pub medium_timeout: Duration, - /// Low priority issues heal within - pub low_timeout: Duration, -} - -/// Resource-based healing configuration -#[derive(Debug, Clone)] -pub struct HealResourceConfig { - /// Maximum CPU usage for healing - pub max_cpu_usage: f64, - /// Maximum memory usage for healing - pub max_memory_usage: f64, - /// Maximum disk I/O usage for healing - pub max_disk_io_usage: f64, - /// Maximum network I/O usage for healing - pub max_network_io_usage: f64, - /// Whether to enable resource-based throttling - pub enable_throttling: bool, -} - -/// Retry configuration for healing -#[derive(Debug, Clone)] -pub struct HealRetryConfig { - /// Maximum number of retry attempts - pub max_retry_attempts: u32, - /// Initial backoff delay - pub initial_backoff: Duration, - /// Maximum backoff delay - pub max_backoff: Duration, - /// Backoff multiplier - pub backoff_multiplier: f64, - /// Whether to use exponential backoff - pub exponential_backoff: bool, -} - -impl Default for HealPolicyConfig { - fn default() -> Self { - Self { - max_concurrent_repairs: 4, - max_repair_duration: Duration::from_secs(1800), // 30 minutes - min_repair_interval: Duration::from_secs(60), // 1 minute - max_system_load: 0.7, - min_disk_space: 15.0, // 15% minimum disk space - max_active_operations: 50, - auto_heal_enabled: true, - priority_config: HealPriorityConfig::default(), - resource_config: HealResourceConfig::default(), - retry_config: HealRetryConfig::default(), - } - } -} - -impl Default for HealPriorityConfig { - fn default() -> Self { - Self { - enabled: true, - critical_immediate: true, - high_timeout: Duration::from_secs(300), // 5 minutes - medium_timeout: Duration::from_secs(1800), // 30 minutes - low_timeout: Duration::from_secs(3600), // 1 hour - } - } -} - -impl Default for HealResourceConfig { - fn default() -> Self { - Self { - max_cpu_usage: 80.0, - max_memory_usage: 80.0, - max_disk_io_usage: 70.0, - max_network_io_usage: 70.0, - enable_throttling: true, - } - } -} - -impl Default for HealRetryConfig { - fn default() -> Self { - Self { - max_retry_attempts: 3, - initial_backoff: Duration::from_secs(30), - max_backoff: Duration::from_secs(300), - backoff_multiplier: 2.0, - exponential_backoff: true, - } - } -} - -/// Heal policy engine -pub struct HealPolicyEngine { - config: HealPolicyConfig, - last_repair_time: SystemTime, - repair_count: u64, - active_repairs: u64, -} - -impl HealPolicyEngine { - /// Create a new heal policy engine - pub fn new(config: HealPolicyConfig) -> Self { - Self { - config, - last_repair_time: SystemTime::now(), - repair_count: 0, - active_repairs: 0, - } - } - - /// Get the configuration - pub fn config(&self) -> &HealPolicyConfig { - &self.config - } - - /// Evaluate heal policy - pub async fn evaluate(&self, issue: &HealthIssue, context: &PolicyContext) -> PolicyResult { - let mut reasons = Vec::new(); - let mut allowed = true; - - // Check if auto-heal is enabled - if !self.config.auto_heal_enabled { - allowed = false; - reasons.push("Auto-heal is disabled".to_string()); - } - - // Check system load - if context.system_load > self.config.max_system_load { - allowed = false; - reasons.push(format!( - "System load too high: {:.2} > {:.2}", - context.system_load, self.config.max_system_load - )); - } - - // Check disk space - if context.disk_space_available < self.config.min_disk_space { - allowed = false; - reasons.push(format!( - "Disk space too low: {:.1}% < {:.1}%", - context.disk_space_available, self.config.min_disk_space - )); - } - - // Check active operations - if context.active_operations > self.config.max_active_operations { - allowed = false; - reasons.push(format!( - "Too many active operations: {} > {}", - context.active_operations, self.config.max_active_operations - )); - } - - // Check repair interval - let time_since_last_repair = context.current_time - .duration_since(self.last_repair_time) - .unwrap_or(Duration::ZERO); - - if time_since_last_repair < self.config.min_repair_interval { - allowed = false; - reasons.push(format!( - "Repair interval too short: {:?} < {:?}", - time_since_last_repair, self.config.min_repair_interval - )); - } - - // Check resource usage - if self.config.resource_config.enable_throttling { - if context.resource_usage.cpu_usage > self.config.resource_config.max_cpu_usage { - allowed = false; - reasons.push(format!( - "CPU usage too high: {:.1}% > {:.1}%", - context.resource_usage.cpu_usage, self.config.resource_config.max_cpu_usage - )); - } - - if context.resource_usage.memory_usage > self.config.resource_config.max_memory_usage { - allowed = false; - reasons.push(format!( - "Memory usage too high: {:.1}% > {:.1}%", - context.resource_usage.memory_usage, self.config.resource_config.max_memory_usage - )); - } - - if context.resource_usage.disk_io_usage > self.config.resource_config.max_disk_io_usage { - allowed = false; - reasons.push(format!( - "Disk I/O usage too high: {:.1}% > {:.1}%", - context.resource_usage.disk_io_usage, self.config.resource_config.max_disk_io_usage - )); - } - - if context.resource_usage.network_io_usage > self.config.resource_config.max_network_io_usage { - allowed = false; - reasons.push(format!( - "Network I/O usage too high: {:.1}% > {:.1}%", - context.resource_usage.network_io_usage, self.config.resource_config.max_network_io_usage - )); - } - } - - // Check priority-based policies - if self.config.priority_config.enabled { - match issue.severity { - Severity::Critical => { - if self.config.priority_config.critical_immediate { - // Critical issues should always be allowed unless resource constraints prevent it - if allowed { - reasons.clear(); - reasons.push("Critical issue - immediate repair allowed".to_string()); - } - } - } - Severity::High => { - // Check if we're within the high priority timeout - if time_since_last_repair > self.config.priority_config.high_timeout { - allowed = false; - reasons.push(format!( - "High priority issue timeout exceeded: {:?} > {:?}", - time_since_last_repair, self.config.priority_config.high_timeout - )); - } - } - Severity::Medium => { - // Check if we're within the medium priority timeout - if time_since_last_repair > self.config.priority_config.medium_timeout { - allowed = false; - reasons.push(format!( - "Medium priority issue timeout exceeded: {:?} > {:?}", - time_since_last_repair, self.config.priority_config.medium_timeout - )); - } - } - Severity::Low => { - // Check if we're within the low priority timeout - if time_since_last_repair > self.config.priority_config.low_timeout { - allowed = false; - reasons.push(format!( - "Low priority issue timeout exceeded: {:?} > {:?}", - time_since_last_repair, self.config.priority_config.low_timeout - )); - } - } - } - } - - let reason = if reasons.is_empty() { - "Heal allowed".to_string() - } else { - reasons.join("; ") - }; - - PolicyResult { - allowed, - reason, - metadata: Some(serde_json::json!({ - "repair_count": self.repair_count, - "active_repairs": self.active_repairs, - "time_since_last_repair": time_since_last_repair.as_secs(), - "issue_severity": format!("{:?}", issue.severity), - "issue_type": format!("{:?}", issue.issue_type), - "system_load": context.system_load, - "disk_space_available": context.disk_space_available, - "active_operations": context.active_operations, - })), - evaluated_at: context.current_time, - } - } - - /// Get repair timeout based on priority - pub fn get_repair_timeout(&self, severity: Severity) -> Duration { - if !self.config.priority_config.enabled { - return self.config.max_repair_duration; - } - - match severity { - Severity::Critical => Duration::from_secs(300), // 5 minutes for critical - Severity::High => self.config.priority_config.high_timeout, - Severity::Medium => self.config.priority_config.medium_timeout, - Severity::Low => self.config.priority_config.low_timeout, - } - } - - /// Get retry configuration - pub fn get_retry_config(&self) -> &HealRetryConfig { - &self.config.retry_config - } - - /// Update repair statistics - pub fn record_repair(&mut self) { - self.last_repair_time = SystemTime::now(); - self.repair_count += 1; - } - - /// Increment active repairs - pub fn increment_active_repairs(&mut self) { - self.active_repairs += 1; - } - - /// Decrement active repairs - pub fn decrement_active_repairs(&mut self) { - if self.active_repairs > 0 { - self.active_repairs -= 1; - } - } - - /// Get heal statistics - pub fn get_statistics(&self) -> HealPolicyStatistics { - HealPolicyStatistics { - total_repairs: self.repair_count, - active_repairs: self.active_repairs, - last_repair_time: self.last_repair_time, - config: self.config.clone(), - } - } -} - -/// Heal policy statistics -#[derive(Debug, Clone)] -pub struct HealPolicyStatistics { - pub total_repairs: u64, - pub active_repairs: u64, - pub last_repair_time: SystemTime, - pub config: HealPolicyConfig, -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::scanner::{HealthIssue, HealthIssueType, Severity}; - - #[tokio::test] - async fn test_heal_policy_creation() { - let config = HealPolicyConfig::default(); - let engine = HealPolicyEngine::new(config); - - assert_eq!(engine.config().max_concurrent_repairs, 4); - assert_eq!(engine.config().max_system_load, 0.7); - assert_eq!(engine.config().min_disk_space, 15.0); - } - - #[tokio::test] - async fn test_heal_policy_evaluation() { - let config = HealPolicyConfig::default(); - let engine = HealPolicyEngine::new(config); - - let issue = HealthIssue { - issue_type: HealthIssueType::MissingReplica, - severity: Severity::Medium, - bucket: "test-bucket".to_string(), - object: "test-object".to_string(), - description: "Test issue".to_string(), - metadata: None, - }; - - let context = PolicyContext { - system_load: 0.5, - disk_space_available: 80.0, - active_operations: 10, - current_time: SystemTime::now(), - health_issues: std::collections::HashMap::new(), - resource_usage: ResourceUsage::default(), - }; - - let result = engine.evaluate(&issue, &context).await; - assert!(result.allowed); - assert!(result.reason.contains("Heal allowed")); - } - - #[tokio::test] - async fn test_heal_policy_critical_immediate() { - let config = HealPolicyConfig::default(); - let engine = HealPolicyEngine::new(config); - - let issue = HealthIssue { - issue_type: HealthIssueType::MissingReplica, - severity: Severity::Critical, - bucket: "test-bucket".to_string(), - object: "test-object".to_string(), - description: "Test issue".to_string(), - metadata: None, - }; - - let context = PolicyContext { - system_load: 0.5, - disk_space_available: 80.0, - active_operations: 10, - current_time: SystemTime::now(), - health_issues: std::collections::HashMap::new(), - resource_usage: ResourceUsage::default(), - }; - - let result = engine.evaluate(&issue, &context).await; - assert!(result.allowed); - assert!(result.reason.contains("Critical issue - immediate repair allowed")); - } - - #[tokio::test] - async fn test_heal_policy_system_load_limit() { - let config = HealPolicyConfig::default(); - let engine = HealPolicyEngine::new(config); - - let issue = HealthIssue { - issue_type: HealthIssueType::MissingReplica, - severity: Severity::Medium, - bucket: "test-bucket".to_string(), - object: "test-object".to_string(), - description: "Test issue".to_string(), - metadata: None, - }; - - let context = PolicyContext { - system_load: 0.8, // Above threshold - disk_space_available: 80.0, - active_operations: 10, - current_time: SystemTime::now(), - health_issues: std::collections::HashMap::new(), - resource_usage: ResourceUsage::default(), - }; - - let result = engine.evaluate(&issue, &context).await; - assert!(!result.allowed); - assert!(result.reason.contains("System load too high")); - } - - #[tokio::test] - async fn test_repair_timeouts() { - let config = HealPolicyConfig::default(); - let engine = HealPolicyEngine::new(config); - - assert_eq!( - engine.get_repair_timeout(Severity::Critical), - Duration::from_secs(300) - ); - assert_eq!( - engine.get_repair_timeout(Severity::High), - Duration::from_secs(300) - ); - assert_eq!( - engine.get_repair_timeout(Severity::Medium), - Duration::from_secs(1800) - ); - assert_eq!( - engine.get_repair_timeout(Severity::Low), - Duration::from_secs(3600) - ); - } - - #[tokio::test] - async fn test_heal_statistics() { - let config = HealPolicyConfig::default(); - let mut engine = HealPolicyEngine::new(config); - - assert_eq!(engine.get_statistics().total_repairs, 0); - assert_eq!(engine.get_statistics().active_repairs, 0); - - engine.record_repair(); - engine.increment_active_repairs(); - engine.increment_active_repairs(); - - let stats = engine.get_statistics(); - assert_eq!(stats.total_repairs, 1); - assert_eq!(stats.active_repairs, 2); - - engine.decrement_active_repairs(); - assert_eq!(engine.get_statistics().active_repairs, 1); - } -} \ No newline at end of file diff --git a/crates/ahm/src/policy/mod.rs b/crates/ahm/src/policy/mod.rs deleted file mode 100644 index 507113ccd..000000000 --- a/crates/ahm/src/policy/mod.rs +++ /dev/null @@ -1,258 +0,0 @@ -// 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. - -//! Policy system for AHM operations -//! -//! Defines configurable policies for: -//! - Scanning behavior and frequency -//! - Healing priorities and strategies -//! - Data retention and lifecycle management - -pub mod scan_policy; -pub mod heal_policy; -pub mod retention_policy; - -pub use scan_policy::{ScanPolicyConfig, ScanPolicyEngine}; -pub use heal_policy::{HealPolicyConfig, HealPolicyEngine}; -pub use retention_policy::{RetentionPolicyConfig, RetentionPolicyEngine}; - -use std::time::{Duration, SystemTime}; -use serde::{Deserialize, Serialize}; - -use crate::scanner::{HealthIssue, Severity}; - -/// Policy evaluation result -#[derive(Debug, Clone)] -pub struct PolicyResult { - /// Whether the policy allows the action - pub allowed: bool, - /// Reason for the decision - pub reason: String, - /// Additional metadata - pub metadata: Option, - /// When the policy was evaluated - pub evaluated_at: SystemTime, -} - -/// Policy evaluation context -#[derive(Debug, Clone)] -pub struct PolicyContext { - /// Current system load - pub system_load: f64, - /// Available disk space percentage - pub disk_space_available: f64, - /// Number of active operations - pub active_operations: u64, - /// Current time - pub current_time: SystemTime, - /// Health issues count by severity - pub health_issues: std::collections::HashMap, - /// Resource usage metrics - pub resource_usage: ResourceUsage, -} - -/// Resource usage information -#[derive(Debug, Clone)] -pub struct ResourceUsage { - /// CPU usage percentage - pub cpu_usage: f64, - /// Memory usage percentage - pub memory_usage: f64, - /// Disk I/O usage percentage - pub disk_io_usage: f64, - /// Network I/O usage percentage - pub network_io_usage: f64, -} - -impl Default for ResourceUsage { - fn default() -> Self { - Self { - cpu_usage: 0.0, - memory_usage: 0.0, - disk_io_usage: 0.0, - network_io_usage: 0.0, - } - } -} - -/// Policy manager that coordinates all policies -pub struct PolicyManager { - scan_policy: ScanPolicyEngine, - heal_policy: HealPolicyEngine, - retention_policy: RetentionPolicyEngine, -} - -impl PolicyManager { - /// Create a new policy manager - pub fn new( - scan_config: ScanPolicyConfig, - heal_config: HealPolicyConfig, - retention_config: RetentionPolicyConfig, - ) -> Self { - Self { - scan_policy: ScanPolicyEngine::new(scan_config), - heal_policy: HealPolicyEngine::new(heal_config), - retention_policy: RetentionPolicyEngine::new(retention_config), - } - } - - /// Evaluate scan policy - pub async fn evaluate_scan_policy(&self, context: &PolicyContext) -> PolicyResult { - self.scan_policy.evaluate(context).await - } - - /// Evaluate heal policy - pub async fn evaluate_heal_policy(&self, issue: &HealthIssue, context: &PolicyContext) -> PolicyResult { - self.heal_policy.evaluate(issue, context).await - } - - /// Evaluate retention policy - pub async fn evaluate_retention_policy(&self, object_age: Duration, context: &PolicyContext) -> PolicyResult { - self.retention_policy.evaluate(object_age, context).await - } - - /// Get scan policy engine - pub fn scan_policy(&self) -> &ScanPolicyEngine { - &self.scan_policy - } - - /// Get heal policy engine - pub fn heal_policy(&self) -> &HealPolicyEngine { - &self.heal_policy - } - - /// Get retention policy engine - pub fn retention_policy(&self) -> &RetentionPolicyEngine { - &self.retention_policy - } - - /// Update scan policy configuration - pub async fn update_scan_policy(&mut self, config: ScanPolicyConfig) { - self.scan_policy = ScanPolicyEngine::new(config); - } - - /// Update heal policy configuration - pub async fn update_heal_policy(&mut self, config: HealPolicyConfig) { - self.heal_policy = HealPolicyEngine::new(config); - } - - /// Update retention policy configuration - pub async fn update_retention_policy(&mut self, config: RetentionPolicyConfig) { - self.retention_policy = RetentionPolicyEngine::new(config); - } - - /// List all policies - pub async fn list_policies(&self) -> crate::error::Result> { - // In a real implementation, this would return actual policy names - Ok(vec![ - "scan_policy".to_string(), - "heal_policy".to_string(), - "retention_policy".to_string(), - ]) - } - - /// Get a specific policy - pub async fn get_policy(&self, name: &str) -> crate::error::Result { - // In a real implementation, this would return the actual policy - Ok(format!("Policy configuration for: {}", name)) - } - - /// Get engine configuration - pub async fn get_config(&self) -> PolicyConfig { - PolicyConfig::default() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::scanner::{HealthIssue, HealthIssueType}; - - #[tokio::test] - async fn test_policy_manager_creation() { - let scan_config = ScanPolicyConfig::default(); - let heal_config = HealPolicyConfig::default(); - let retention_config = RetentionPolicyConfig::default(); - - let manager = PolicyManager::new(scan_config, heal_config, retention_config); - - // Test that all policy engines are available - assert!(manager.scan_policy().config().max_concurrent_scans > 0); - assert!(manager.heal_policy().config().max_concurrent_repairs > 0); - assert!(manager.retention_policy().config().default_retention_days > 0); - } - - #[tokio::test] - async fn test_policy_evaluation() { - let scan_config = ScanPolicyConfig::default(); - let heal_config = HealPolicyConfig::default(); - let retention_config = RetentionPolicyConfig::default(); - - let manager = PolicyManager::new(scan_config, heal_config, retention_config); - - let context = PolicyContext { - system_load: 0.5, - disk_space_available: 80.0, - active_operations: 10, - current_time: SystemTime::now(), - health_issues: std::collections::HashMap::new(), - resource_usage: ResourceUsage::default(), - }; - - // Test scan policy evaluation - let scan_result = manager.evaluate_scan_policy(&context).await; - assert!(scan_result.allowed); - - // Test heal policy evaluation - let issue = HealthIssue { - issue_type: HealthIssueType::MissingReplica, - severity: Severity::Critical, - bucket: "test-bucket".to_string(), - object: "test-object".to_string(), - description: "Test issue".to_string(), - metadata: None, - }; - - let heal_result = manager.evaluate_heal_policy(&issue, &context).await; - assert!(heal_result.allowed); - - // Test retention policy evaluation - let retention_result = manager.evaluate_retention_policy(Duration::from_secs(86400), &context).await; - assert!(retention_result.allowed); - } -} - -/// Master policy configuration -#[derive(Debug, Clone)] -pub struct PolicyConfig { - pub scan: ScanPolicyConfig, - pub heal: HealPolicyConfig, - pub retention: RetentionPolicyConfig, -} - -impl Default for PolicyConfig { - fn default() -> Self { - Self { - scan: ScanPolicyConfig::default(), - heal: HealPolicyConfig::default(), - retention: RetentionPolicyConfig::default(), - } - } -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct PolicyManagerConfig { - #[serde(default)] - pub default_scan_interval: Duration, -} \ No newline at end of file diff --git a/crates/ahm/src/policy/retention_policy.rs b/crates/ahm/src/policy/retention_policy.rs deleted file mode 100644 index f54591fac..000000000 --- a/crates/ahm/src/policy/retention_policy.rs +++ /dev/null @@ -1,487 +0,0 @@ -// Copyright 2024 RustFS Team - -use std::time::{Duration, SystemTime}; - -use super::{PolicyContext, PolicyResult, ResourceUsage}; - -/// Configuration for retention policies -#[derive(Debug, Clone)] -pub struct RetentionPolicyConfig { - /// Default retention period in days - pub default_retention_days: u32, - /// Whether to enable retention policies - pub enabled: bool, - /// Maximum system load threshold for retention operations - pub max_system_load: f64, - /// Minimum available disk space percentage for retention operations - pub min_disk_space: f64, - /// Maximum number of active operations for retention - pub max_active_operations: u64, - /// Retention rules by object type - pub retention_rules: Vec, - /// Whether to enable automatic cleanup - pub auto_cleanup_enabled: bool, - /// Cleanup interval - pub cleanup_interval: Duration, - /// Maximum objects to delete per cleanup cycle - pub max_objects_per_cleanup: u64, -} - -/// Retention rule for specific object types -#[derive(Debug, Clone)] -pub struct RetentionRule { - /// Object type pattern (e.g., "*.log", "temp/*") - pub pattern: String, - /// Retention period in days - pub retention_days: u32, - /// Whether this rule is enabled - pub enabled: bool, - /// Priority of this rule (higher = more important) - pub priority: u32, - /// Whether to apply this rule recursively - pub recursive: bool, -} - -impl Default for RetentionPolicyConfig { - fn default() -> Self { - Self { - default_retention_days: 30, - enabled: true, - max_system_load: 0.6, - min_disk_space: 20.0, // 20% minimum disk space - max_active_operations: 20, - retention_rules: vec![ - RetentionRule { - pattern: "*.log".to_string(), - retention_days: 7, - enabled: true, - priority: 1, - recursive: false, - }, - RetentionRule { - pattern: "temp/*".to_string(), - retention_days: 1, - enabled: true, - priority: 2, - recursive: true, - }, - RetentionRule { - pattern: "cache/*".to_string(), - retention_days: 3, - enabled: true, - priority: 3, - recursive: true, - }, - ], - auto_cleanup_enabled: true, - cleanup_interval: Duration::from_secs(3600), // 1 hour - max_objects_per_cleanup: 1000, - } - } -} - -/// Retention policy engine -pub struct RetentionPolicyEngine { - config: RetentionPolicyConfig, - last_cleanup_time: SystemTime, - cleanup_count: u64, - objects_deleted: u64, -} - -impl RetentionPolicyEngine { - /// Create a new retention policy engine - pub fn new(config: RetentionPolicyConfig) -> Self { - Self { - config, - last_cleanup_time: SystemTime::now(), - cleanup_count: 0, - objects_deleted: 0, - } - } - - /// Get the configuration - pub fn config(&self) -> &RetentionPolicyConfig { - &self.config - } - - /// Evaluate retention policy - pub async fn evaluate(&self, object_age: Duration, context: &PolicyContext) -> PolicyResult { - let mut reasons = Vec::new(); - let mut allowed = false; - - // Check if retention policies are enabled - if !self.config.enabled { - allowed = false; - reasons.push("Retention policies are disabled".to_string()); - } else { - // Check if object should be retained based on age - let retention_days = self.get_retention_days_for_object("default"); - let retention_duration = Duration::from_secs(retention_days as u64 * 24 * 3600); - - if object_age > retention_duration { - allowed = true; - reasons.push(format!( - "Object age exceeds retention period: {:?} > {:?}", - object_age, retention_duration - )); - } else { - allowed = false; - reasons.push(format!( - "Object within retention period: {:?} <= {:?}", - object_age, retention_duration - )); - } - } - - // Check system constraints - if context.system_load > self.config.max_system_load { - allowed = false; - reasons.push(format!( - "System load too high: {:.2} > {:.2}", - context.system_load, self.config.max_system_load - )); - } - - if context.disk_space_available < self.config.min_disk_space { - allowed = false; - reasons.push(format!( - "Disk space too low: {:.1}% < {:.1}%", - context.disk_space_available, self.config.min_disk_space - )); - } - - if context.active_operations > self.config.max_active_operations { - allowed = false; - reasons.push(format!( - "Too many active operations: {} > {}", - context.active_operations, self.config.max_active_operations - )); - } - - let reason = if reasons.is_empty() { - "Retention evaluation completed".to_string() - } else { - reasons.join("; ") - }; - - PolicyResult { - allowed, - reason, - metadata: Some(serde_json::json!({ - "object_age_seconds": object_age.as_secs(), - "cleanup_count": self.cleanup_count, - "objects_deleted": self.objects_deleted, - "system_load": context.system_load, - "disk_space_available": context.disk_space_available, - "active_operations": context.active_operations, - })), - evaluated_at: context.current_time, - } - } - - /// Evaluate cleanup policy - pub async fn evaluate_cleanup(&self, context: &PolicyContext) -> PolicyResult { - let mut reasons = Vec::new(); - let mut allowed = false; - - // Check if auto-cleanup is enabled - if !self.config.auto_cleanup_enabled { - allowed = false; - reasons.push("Auto-cleanup is disabled".to_string()); - } else { - // Check cleanup interval - let time_since_last_cleanup = context.current_time - .duration_since(self.last_cleanup_time) - .unwrap_or(Duration::ZERO); - - if time_since_last_cleanup >= self.config.cleanup_interval { - allowed = true; - reasons.push("Cleanup interval reached".to_string()); - } else { - allowed = false; - reasons.push(format!( - "Cleanup interval not reached: {:?} < {:?}", - time_since_last_cleanup, self.config.cleanup_interval - )); - } - } - - // Check system constraints - if context.system_load > self.config.max_system_load { - allowed = false; - reasons.push(format!( - "System load too high: {:.2} > {:.2}", - context.system_load, self.config.max_system_load - )); - } - - if context.disk_space_available < self.config.min_disk_space { - allowed = false; - reasons.push(format!( - "Disk space too low: {:.1}% < {:.1}%", - context.disk_space_available, self.config.min_disk_space - )); - } - - let reason = if reasons.is_empty() { - "Cleanup evaluation completed".to_string() - } else { - reasons.join("; ") - }; - - PolicyResult { - allowed, - reason, - metadata: Some(serde_json::json!({ - "cleanup_count": self.cleanup_count, - "objects_deleted": self.objects_deleted, - "max_objects_per_cleanup": self.config.max_objects_per_cleanup, - "system_load": context.system_load, - "disk_space_available": context.disk_space_available, - })), - evaluated_at: context.current_time, - } - } - - /// Get retention days for a specific object - pub fn get_retention_days_for_object(&self, object_path: &str) -> u32 { - // Find the highest priority matching rule - let mut best_rule: Option<&RetentionRule> = None; - let mut best_priority = 0; - - for rule in &self.config.retention_rules { - if !rule.enabled { - continue; - } - - if self.matches_pattern(object_path, &rule.pattern) { - if rule.priority > best_priority { - best_rule = Some(rule); - best_priority = rule.priority; - } - } - } - - best_rule - .map(|rule| rule.retention_days) - .unwrap_or(self.config.default_retention_days) - } - - /// Check if an object path matches a pattern - fn matches_pattern(&self, object_path: &str, pattern: &str) -> bool { - // Simple pattern matching - can be enhanced with regex - if pattern.contains('*') { - // Wildcard matching - let pattern_parts: Vec<&str> = pattern.split('*').collect(); - if pattern_parts.len() == 2 { - let prefix = pattern_parts[0]; - let suffix = pattern_parts[1]; - object_path.starts_with(prefix) && object_path.ends_with(suffix) - } else { - false - } - } else { - // Exact match - object_path == pattern - } - } - - /// Get all retention rules - pub fn get_retention_rules(&self) -> &[RetentionRule] { - &self.config.retention_rules - } - - /// Add a new retention rule - pub fn add_retention_rule(&mut self, rule: RetentionRule) { - self.config.retention_rules.push(rule); - } - - /// Remove a retention rule by pattern - pub fn remove_retention_rule(&mut self, pattern: &str) -> bool { - let initial_len = self.config.retention_rules.len(); - self.config.retention_rules.retain(|rule| rule.pattern != pattern); - self.config.retention_rules.len() < initial_len - } - - /// Update cleanup statistics - pub fn record_cleanup(&mut self, objects_deleted: u64) { - self.last_cleanup_time = SystemTime::now(); - self.cleanup_count += 1; - self.objects_deleted += objects_deleted; - } - - /// Get retention statistics - pub fn get_statistics(&self) -> RetentionPolicyStatistics { - RetentionPolicyStatistics { - total_cleanups: self.cleanup_count, - total_objects_deleted: self.objects_deleted, - last_cleanup_time: self.last_cleanup_time, - config: self.config.clone(), - } - } -} - -/// Retention policy statistics -#[derive(Debug, Clone)] -pub struct RetentionPolicyStatistics { - pub total_cleanups: u64, - pub total_objects_deleted: u64, - pub last_cleanup_time: SystemTime, - pub config: RetentionPolicyConfig, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_retention_policy_creation() { - let config = RetentionPolicyConfig::default(); - let engine = RetentionPolicyEngine::new(config); - - assert_eq!(engine.config().default_retention_days, 30); - assert_eq!(engine.config().max_system_load, 0.6); - assert_eq!(engine.config().min_disk_space, 20.0); - } - - #[tokio::test] - async fn test_retention_policy_evaluation() { - let config = RetentionPolicyConfig::default(); - let engine = RetentionPolicyEngine::new(config); - - let context = PolicyContext { - system_load: 0.5, - disk_space_available: 80.0, - active_operations: 10, - current_time: SystemTime::now(), - health_issues: std::collections::HashMap::new(), - resource_usage: ResourceUsage::default(), - }; - - // Test object within retention period - let object_age = Duration::from_secs(7 * 24 * 3600); // 7 days - let result = engine.evaluate(object_age, &context).await; - assert!(!result.allowed); - assert!(result.reason.contains("Object within retention period")); - - // Test object exceeding retention period - let object_age = Duration::from_secs(40 * 24 * 3600); // 40 days - let result = engine.evaluate(object_age, &context).await; - assert!(result.allowed); - assert!(result.reason.contains("Object age exceeds retention period")); - } - - #[tokio::test] - async fn test_retention_policy_system_constraints() { - let config = RetentionPolicyConfig::default(); - let engine = RetentionPolicyEngine::new(config); - - let context = PolicyContext { - system_load: 0.7, // Above threshold - disk_space_available: 80.0, - active_operations: 10, - current_time: SystemTime::now(), - health_issues: std::collections::HashMap::new(), - resource_usage: ResourceUsage::default(), - }; - - let object_age = Duration::from_secs(40 * 24 * 3600); // 40 days - let result = engine.evaluate(object_age, &context).await; - assert!(!result.allowed); - assert!(result.reason.contains("System load too high")); - } - - #[tokio::test] - async fn test_retention_rules() { - let config = RetentionPolicyConfig::default(); - let engine = RetentionPolicyEngine::new(config); - - // Test default retention - assert_eq!(engine.get_retention_days_for_object("unknown.txt"), 30); - - // Test log file retention - assert_eq!(engine.get_retention_days_for_object("app.log"), 7); - - // Test temp file retention - assert_eq!(engine.get_retention_days_for_object("temp/file.txt"), 1); - - // Test cache file retention - assert_eq!(engine.get_retention_days_for_object("cache/data.bin"), 3); - } - - #[tokio::test] - async fn test_pattern_matching() { - let config = RetentionPolicyConfig::default(); - let engine = RetentionPolicyEngine::new(config); - - // Test wildcard matching - assert!(engine.matches_pattern("app.log", "*.log")); - assert!(engine.matches_pattern("error.log", "*.log")); - assert!(!engine.matches_pattern("app.txt", "*.log")); - - // Test exact matching - assert!(engine.matches_pattern("temp/file.txt", "temp/file.txt")); - assert!(!engine.matches_pattern("temp/file.txt", "temp/other.txt")); - } - - #[tokio::test] - async fn test_cleanup_evaluation() { - let config = RetentionPolicyConfig::default(); - let engine = RetentionPolicyEngine::new(config); - - let context = PolicyContext { - system_load: 0.5, - disk_space_available: 80.0, - active_operations: 10, - current_time: SystemTime::now(), - health_issues: std::collections::HashMap::new(), - resource_usage: ResourceUsage::default(), - }; - - let result = engine.evaluate_cleanup(&context).await; - // Should be allowed if enough time has passed since last cleanup - assert!(result.allowed || result.reason.contains("Cleanup interval not reached")); - } - - #[tokio::test] - async fn test_retention_statistics() { - let config = RetentionPolicyConfig::default(); - let mut engine = RetentionPolicyEngine::new(config); - - assert_eq!(engine.get_statistics().total_cleanups, 0); - assert_eq!(engine.get_statistics().total_objects_deleted, 0); - - engine.record_cleanup(50); - assert_eq!(engine.get_statistics().total_cleanups, 1); - assert_eq!(engine.get_statistics().total_objects_deleted, 50); - - engine.record_cleanup(30); - assert_eq!(engine.get_statistics().total_cleanups, 2); - assert_eq!(engine.get_statistics().total_objects_deleted, 80); - } - - #[tokio::test] - async fn test_retention_rule_management() { - let config = RetentionPolicyConfig::default(); - let mut engine = RetentionPolicyEngine::new(config); - - let initial_rules = engine.get_retention_rules().len(); - - // Add a new rule - let new_rule = RetentionRule { - pattern: "backup/*".to_string(), - retention_days: 90, - enabled: true, - priority: 4, - recursive: true, - }; - engine.add_retention_rule(new_rule); - - assert_eq!(engine.get_retention_rules().len(), initial_rules + 1); - - // Remove a rule - let removed = engine.remove_retention_rule("*.log"); - assert!(removed); - assert_eq!(engine.get_retention_rules().len(), initial_rules); - } -} \ No newline at end of file diff --git a/crates/ahm/src/policy/scan_policy.rs b/crates/ahm/src/policy/scan_policy.rs deleted file mode 100644 index 44e3fc14c..000000000 --- a/crates/ahm/src/policy/scan_policy.rs +++ /dev/null @@ -1,373 +0,0 @@ -// Copyright 2024 RustFS Team - -use std::time::{Duration, SystemTime}; - -use crate::scanner::Severity; - -use super::{PolicyContext, PolicyResult, ResourceUsage}; - -/// Configuration for scan policies -#[derive(Debug, Clone)] -pub struct ScanPolicyConfig { - /// Maximum number of concurrent scans - pub max_concurrent_scans: usize, - /// Maximum scan duration per cycle - pub max_scan_duration: Duration, - /// Minimum interval between scans - pub min_scan_interval: Duration, - /// Maximum system load threshold for scanning - pub max_system_load: f64, - /// Minimum available disk space percentage for scanning - pub min_disk_space: f64, - /// Maximum number of active operations for scanning - pub max_active_operations: u64, - /// Whether to enable deep scanning - pub enable_deep_scan: bool, - /// Deep scan interval (how often to perform deep scans) - pub deep_scan_interval: Duration, - /// Bandwidth limit for scanning (bytes per second) - pub bandwidth_limit: Option, - /// Priority-based scanning configuration - pub priority_config: ScanPriorityConfig, -} - -/// Priority-based scanning configuration -#[derive(Debug, Clone)] -pub struct ScanPriorityConfig { - /// Whether to enable priority-based scanning - pub enabled: bool, - /// Critical issues scan interval - pub critical_interval: Duration, - /// High priority issues scan interval - pub high_interval: Duration, - /// Medium priority issues scan interval - pub medium_interval: Duration, - /// Low priority issues scan interval - pub low_interval: Duration, -} - -impl Default for ScanPolicyConfig { - fn default() -> Self { - Self { - max_concurrent_scans: 4, - max_scan_duration: Duration::from_secs(3600), // 1 hour - min_scan_interval: Duration::from_secs(300), // 5 minutes - max_system_load: 0.8, - min_disk_space: 10.0, // 10% minimum disk space - max_active_operations: 100, - enable_deep_scan: true, - deep_scan_interval: Duration::from_secs(86400), // 24 hours - bandwidth_limit: Some(100 * 1024 * 1024), // 100 MB/s - priority_config: ScanPriorityConfig::default(), - } - } -} - -impl Default for ScanPriorityConfig { - fn default() -> Self { - Self { - enabled: true, - critical_interval: Duration::from_secs(60), // 1 minute - high_interval: Duration::from_secs(300), // 5 minutes - medium_interval: Duration::from_secs(1800), // 30 minutes - low_interval: Duration::from_secs(3600), // 1 hour - } - } -} - -/// Scan policy engine -pub struct ScanPolicyEngine { - config: ScanPolicyConfig, - last_scan_time: SystemTime, - last_deep_scan_time: SystemTime, - scan_count: u64, -} - -impl ScanPolicyEngine { - /// Create a new scan policy engine - pub fn new(config: ScanPolicyConfig) -> Self { - Self { - config, - last_scan_time: SystemTime::now(), - last_deep_scan_time: SystemTime::now(), - scan_count: 0, - } - } - - /// Get the configuration - pub fn config(&self) -> &ScanPolicyConfig { - &self.config - } - - /// Evaluate scan policy - pub async fn evaluate(&self, context: &PolicyContext) -> PolicyResult { - let mut reasons = Vec::new(); - let mut allowed = true; - - // Check system load - if context.system_load > self.config.max_system_load { - allowed = false; - reasons.push(format!( - "System load too high: {:.2} > {:.2}", - context.system_load, self.config.max_system_load - )); - } - - // Check disk space - if context.disk_space_available < self.config.min_disk_space { - allowed = false; - reasons.push(format!( - "Disk space too low: {:.1}% < {:.1}%", - context.disk_space_available, self.config.min_disk_space - )); - } - - // Check active operations - if context.active_operations > self.config.max_active_operations { - allowed = false; - reasons.push(format!( - "Too many active operations: {} > {}", - context.active_operations, self.config.max_active_operations - )); - } - - // Check scan interval - let time_since_last_scan = context.current_time - .duration_since(self.last_scan_time) - .unwrap_or(Duration::ZERO); - - if time_since_last_scan < self.config.min_scan_interval { - allowed = false; - reasons.push(format!( - "Scan interval too short: {:?} < {:?}", - time_since_last_scan, self.config.min_scan_interval - )); - } - - // Check resource usage - if context.resource_usage.cpu_usage > 90.0 { - allowed = false; - reasons.push("CPU usage too high".to_string()); - } - - if context.resource_usage.memory_usage > 90.0 { - allowed = false; - reasons.push("Memory usage too high".to_string()); - } - - let reason = if reasons.is_empty() { - "Scan allowed".to_string() - } else { - reasons.join("; ") - }; - - PolicyResult { - allowed, - reason, - metadata: Some(serde_json::json!({ - "scan_count": self.scan_count, - "time_since_last_scan": time_since_last_scan.as_secs(), - "system_load": context.system_load, - "disk_space_available": context.disk_space_available, - "active_operations": context.active_operations, - })), - evaluated_at: context.current_time, - } - } - - /// Evaluate deep scan policy - pub async fn evaluate_deep_scan(&self, context: &PolicyContext) -> PolicyResult { - let mut base_result = self.evaluate(context).await; - - if !base_result.allowed { - return base_result; - } - - // Check deep scan interval - let time_since_last_deep_scan = context.current_time - .duration_since(self.last_deep_scan_time) - .unwrap_or(Duration::ZERO); - - if time_since_last_deep_scan < self.config.deep_scan_interval { - base_result.allowed = false; - base_result.reason = format!( - "Deep scan interval too short: {:?} < {:?}", - time_since_last_deep_scan, self.config.deep_scan_interval - ); - } else { - base_result.reason = "Deep scan allowed".to_string(); - } - - // Add deep scan metadata - if let Some(ref mut metadata) = base_result.metadata { - if let Some(obj) = metadata.as_object_mut() { - obj.insert( - "time_since_last_deep_scan".to_string(), - serde_json::Value::Number(serde_json::Number::from(time_since_last_deep_scan.as_secs())), - ); - obj.insert( - "deep_scan_enabled".to_string(), - serde_json::Value::Bool(self.config.enable_deep_scan), - ); - } - } - - base_result - } - - /// Get scan interval based on priority - pub fn get_priority_interval(&self, severity: Severity) -> Duration { - if !self.config.priority_config.enabled { - return self.config.min_scan_interval; - } - - match severity { - Severity::Critical => self.config.priority_config.critical_interval, - Severity::High => self.config.priority_config.high_interval, - Severity::Medium => self.config.priority_config.medium_interval, - Severity::Low => self.config.priority_config.low_interval, - } - } - - /// Update scan statistics - pub fn record_scan(&mut self) { - self.last_scan_time = SystemTime::now(); - self.scan_count += 1; - } - - /// Update deep scan statistics - pub fn record_deep_scan(&mut self) { - self.last_deep_scan_time = SystemTime::now(); - } - - /// Get scan statistics - pub fn get_statistics(&self) -> ScanPolicyStatistics { - ScanPolicyStatistics { - total_scans: self.scan_count, - last_scan_time: self.last_scan_time, - last_deep_scan_time: self.last_deep_scan_time, - config: self.config.clone(), - } - } -} - -/// Scan policy statistics -#[derive(Debug, Clone)] -pub struct ScanPolicyStatistics { - pub total_scans: u64, - pub last_scan_time: SystemTime, - pub last_deep_scan_time: SystemTime, - pub config: ScanPolicyConfig, -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::scanner::Severity; - - #[tokio::test] - async fn test_scan_policy_creation() { - let config = ScanPolicyConfig::default(); - let engine = ScanPolicyEngine::new(config); - - assert_eq!(engine.config().max_concurrent_scans, 4); - assert_eq!(engine.config().max_system_load, 0.8); - assert_eq!(engine.config().min_disk_space, 10.0); - } - - #[tokio::test] - async fn test_scan_policy_evaluation() { - let config = ScanPolicyConfig::default(); - let engine = ScanPolicyEngine::new(config); - - let context = PolicyContext { - system_load: 0.5, - disk_space_available: 80.0, - active_operations: 10, - current_time: SystemTime::now(), - health_issues: std::collections::HashMap::new(), - resource_usage: ResourceUsage::default(), - }; - - let result = engine.evaluate(&context).await; - assert!(result.allowed); - assert!(result.reason.contains("Scan allowed")); - } - - #[tokio::test] - async fn test_scan_policy_system_load_limit() { - let config = ScanPolicyConfig::default(); - let engine = ScanPolicyEngine::new(config); - - let context = PolicyContext { - system_load: 0.9, // Above threshold - disk_space_available: 80.0, - active_operations: 10, - current_time: SystemTime::now(), - health_issues: std::collections::HashMap::new(), - resource_usage: ResourceUsage::default(), - }; - - let result = engine.evaluate(&context).await; - assert!(!result.allowed); - assert!(result.reason.contains("System load too high")); - } - - #[tokio::test] - async fn test_scan_policy_disk_space_limit() { - let config = ScanPolicyConfig::default(); - let engine = ScanPolicyEngine::new(config); - - let context = PolicyContext { - system_load: 0.5, - disk_space_available: 5.0, // Below threshold - active_operations: 10, - current_time: SystemTime::now(), - health_issues: std::collections::HashMap::new(), - resource_usage: ResourceUsage::default(), - }; - - let result = engine.evaluate(&context).await; - assert!(!result.allowed); - assert!(result.reason.contains("Disk space too low")); - } - - #[tokio::test] - async fn test_priority_intervals() { - let config = ScanPolicyConfig::default(); - let engine = ScanPolicyEngine::new(config); - - assert_eq!( - engine.get_priority_interval(Severity::Critical), - Duration::from_secs(60) - ); - assert_eq!( - engine.get_priority_interval(Severity::High), - Duration::from_secs(300) - ); - assert_eq!( - engine.get_priority_interval(Severity::Medium), - Duration::from_secs(1800) - ); - assert_eq!( - engine.get_priority_interval(Severity::Low), - Duration::from_secs(3600) - ); - } - - #[tokio::test] - async fn test_scan_statistics() { - let config = ScanPolicyConfig::default(); - let mut engine = ScanPolicyEngine::new(config); - - assert_eq!(engine.get_statistics().total_scans, 0); - - engine.record_scan(); - assert_eq!(engine.get_statistics().total_scans, 1); - - engine.record_deep_scan(); - let stats = engine.get_statistics(); - assert_eq!(stats.total_scans, 1); - assert!(stats.last_deep_scan_time > stats.last_scan_time); - } -} \ No newline at end of file diff --git a/crates/ahm/src/scanner.rs b/crates/ahm/src/scanner.rs new file mode 100644 index 000000000..52f92bba3 --- /dev/null +++ b/crates/ahm/src/scanner.rs @@ -0,0 +1,902 @@ +// 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::{ + collections::HashMap, + sync::Arc, + time::{Duration, SystemTime}, +}; + +use rustfs_ecstore as ecstore; +use ecstore::{ + disk::{DiskAPI, DiskStore, WalkDirOptions}, + set_disk::SetDisks, +}; +use rustfs_filemeta::MetacacheReader; +use tokio::sync::RwLock; +use tokio_util::sync::CancellationToken; +use tracing::{debug, error, info, warn}; + +use crate::{ + error::{Error, Result}, + metrics::{BucketMetrics, DiskMetrics, MetricsCollector, ScannerMetrics}, +}; + +/// Custom scan mode enum for AHM scanner +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ScanMode { + /// Normal scan - basic object discovery and metadata collection + Normal, + /// Deep scan - includes EC verification and integrity checks + Deep, +} + +impl Default for ScanMode { + fn default() -> Self { + ScanMode::Normal + } +} + +/// Scanner configuration +#[derive(Debug, Clone)] +pub struct ScannerConfig { + /// Scan interval between cycles + pub scan_interval: Duration, + /// Deep scan interval (how often to perform deep scan) + pub deep_scan_interval: Duration, + /// Maximum concurrent scans + pub max_concurrent_scans: usize, + /// Whether to enable healing + pub enable_healing: bool, + /// Whether to enable metrics collection + pub enable_metrics: bool, + /// Current scan mode (normal, deep) + pub scan_mode: ScanMode, +} + +impl Default for ScannerConfig { + fn default() -> Self { + Self { + scan_interval: Duration::from_secs(3600), // 1 hour + deep_scan_interval: Duration::from_secs(3600), // 1 hour + max_concurrent_scans: 20, + enable_healing: true, + enable_metrics: true, + scan_mode: ScanMode::Normal, + } + } +} + +/// Scanner state +#[derive(Debug)] +pub struct ScannerState { + /// Whether scanner is running + pub is_running: bool, + /// Current scan cycle + pub current_cycle: u64, + /// Last scan start time + pub last_scan_start: Option, + /// Last scan end time + pub last_scan_end: Option, + /// Current scan duration + pub current_scan_duration: Option, + /// Last deep scan time + pub last_deep_scan_time: Option, + /// Buckets being scanned + pub scanning_buckets: Vec, + /// Disks being scanned + pub scanning_disks: Vec, +} + +impl Default for ScannerState { + fn default() -> Self { + Self { + is_running: false, + current_cycle: 0, + last_scan_start: None, + last_scan_end: None, + current_scan_duration: None, + last_deep_scan_time: None, + scanning_buckets: Vec::new(), + scanning_disks: Vec::new(), + } + } +} + +/// AHM Scanner - Automatic Health Management Scanner +/// +/// This scanner monitors the health of objects in the RustFS storage system. +/// It integrates with ECStore's SetDisks to perform real data scanning and +/// collects metrics similar to MinIO's scanner. +/// +/// The scanner operates on EC (Erasure Coding) sets, where each set contains +/// multiple disks that store the same objects with different shards. +pub struct Scanner { + /// Scanner configuration + config: Arc>, + /// Scanner state + state: Arc>, + /// Metrics collector + metrics: Arc, + /// Bucket metrics cache + bucket_metrics: Arc>>, + /// Disk metrics cache + disk_metrics: Arc>>, + /// EC Set disks - represents a complete erasure coding set + set_disks: Arc, + /// Cancellation token for graceful shutdown + cancel_token: CancellationToken, +} + +impl Scanner { + /// Create a new scanner + pub fn new(set_disks: Arc, config: Option) -> Self { + let config = config.unwrap_or_default(); + let cancel_token = CancellationToken::new(); + + info!("Creating AHM scanner for EC set with {} disks", set_disks.set_drive_count); + + Self { + config: Arc::new(RwLock::new(config)), + state: Arc::new(RwLock::new(ScannerState::default())), + metrics: Arc::new(MetricsCollector::new()), + bucket_metrics: Arc::new(RwLock::new(HashMap::new())), + disk_metrics: Arc::new(RwLock::new(HashMap::new())), + set_disks, + cancel_token, + } + } + + /// Start the scanner + pub async fn start(&self) -> Result<()> { + let mut state = self.state.write().await; + + if state.is_running { + warn!("Scanner is already running"); + return Ok(()); + } + + state.is_running = true; + state.last_scan_start = Some(SystemTime::now()); + + info!("Starting AHM scanner"); + + // Start background scan loop + let scanner = self.clone_for_background(); + tokio::spawn(async move { + if let Err(e) = scanner.scan_loop().await { + error!("Scanner loop failed: {}", e); + } + }); + + Ok(()) + } + + /// Stop the scanner gracefully + pub async fn stop(&self) -> Result<()> { + let mut state = self.state.write().await; + + if !state.is_running { + warn!("Scanner is not running"); + return Ok(()); + } + + info!("Stopping AHM scanner gracefully..."); + + // Trigger cancellation + self.cancel_token.cancel(); + + state.is_running = false; + state.last_scan_end = Some(SystemTime::now()); + + if let Some(start_time) = state.last_scan_start { + state.current_scan_duration = Some( + SystemTime::now() + .duration_since(start_time) + .unwrap_or(Duration::ZERO) + ); + } + + info!("AHM scanner stopped"); + Ok(()) + } + + /// Get a clone of the cancellation token (for external graceful shutdown) + pub fn cancellation_token(&self) -> CancellationToken { + self.cancel_token.clone() + } + + /// Get current scanner metrics + pub async fn get_metrics(&self) -> ScannerMetrics { + let mut metrics = self.metrics.get_metrics(); + + // Add bucket metrics + let bucket_metrics = self.bucket_metrics.read().await; + metrics.bucket_metrics = bucket_metrics.clone(); + + // Add disk metrics + let disk_metrics = self.disk_metrics.read().await; + metrics.disk_metrics = disk_metrics.clone(); + + // Add current scan duration + let state = self.state.read().await; + metrics.current_scan_duration = state.current_scan_duration; + + metrics + } + + /// Perform a single scan cycle + pub async fn scan_cycle(&self) -> Result<()> { + let start_time = SystemTime::now(); + + info!("Starting scan cycle {}", self.metrics.get_metrics().current_cycle + 1); + + // Update state + { + let mut state = self.state.write().await; + state.current_cycle += 1; + state.last_scan_start = Some(start_time); + state.scanning_buckets.clear(); + state.scanning_disks.clear(); + } + + self.metrics.set_current_cycle(self.state.read().await.current_cycle); + self.metrics.increment_total_cycles(); + + // Get online disks from the EC set + let (disks, _) = self.set_disks.get_online_disks_with_healing(false).await; + + if disks.is_empty() { + warn!("No online disks available for scanning"); + return Ok(()); + } + + info!("Scanning {} online disks", disks.len()); + + // Phase 1: Scan all disks concurrently to collect object metadata + let config = self.config.read().await; + let semaphore = Arc::new(tokio::sync::Semaphore::new(config.max_concurrent_scans)); + drop(config); + let mut scan_futures = Vec::new(); + + for disk in disks.clone() { + let semaphore = semaphore.clone(); + let scanner = self.clone_for_background(); + + let future = async move { + let _permit = semaphore.acquire().await.unwrap(); + scanner.scan_disk(&disk).await + }; + + scan_futures.push(future); + } + + // Wait for all scans to complete + let mut results = Vec::new(); + for future in scan_futures { + results.push(future.await); + } + + // Check results and collect object metadata + let mut successful_scans = 0; + let mut failed_scans = 0; + let mut all_disk_objects = Vec::new(); + + for result in results { + match result { + Ok(disk_objects) => { + successful_scans += 1; + all_disk_objects.push(disk_objects); + } + Err(e) => { + failed_scans += 1; + error!("Disk scan failed: {}", e); + // Add empty map for failed disk + all_disk_objects.push(HashMap::new()); + } + } + + } + + // Phase 2: Analyze object distribution and perform EC verification + if successful_scans > 0 { + if let Err(e) = self.analyze_object_distribution(&all_disk_objects, &disks).await { + error!("Object distribution analysis failed: {}", e); + } + } + + // Update scan duration + let scan_duration = SystemTime::now() + .duration_since(start_time) + .unwrap_or(Duration::ZERO); + + { + let mut state = self.state.write().await; + state.last_scan_end = Some(SystemTime::now()); + state.current_scan_duration = Some(scan_duration); + } + + info!("Completed scan cycle in {:?} ({} successful, {} failed)", + scan_duration, successful_scans, failed_scans); + Ok(()) + } + + /// Scan a single disk + async fn scan_disk(&self, disk: &DiskStore) -> Result>> { + let disk_path = disk.path().to_string_lossy().to_string(); + + info!("Scanning disk: {}", disk_path); + + // Update disk metrics + { + let mut disk_metrics = self.disk_metrics.write().await; + let metrics = disk_metrics.entry(disk_path.clone()).or_insert_with(|| DiskMetrics { + disk_path: disk_path.clone(), + ..Default::default() + }); + + metrics.is_scanning = true; + metrics.last_scan_time = Some(SystemTime::now()); + + // Get disk info + if let Ok(disk_info) = disk.disk_info(&ecstore::disk::DiskInfoOptions { + disk_id: disk_path.clone(), + metrics: true, + noop: false, + }).await { + metrics.total_space = disk_info.total; + metrics.used_space = disk_info.used; + metrics.free_space = disk_info.free; + metrics.is_online = disk.is_online().await; + } + } + + // Update state + { + let mut state = self.state.write().await; + state.scanning_disks.push(disk_path.clone()); + } + + // List volumes (buckets) on this disk + let volumes = match disk.list_volumes().await { + Ok(volumes) => volumes, + Err(e) => { + error!("Failed to list volumes on disk {}: {}", disk_path, e); + return Err(Error::Storage(e.into())); + } + }; + + // Scan each volume and collect object metadata + let mut disk_objects = HashMap::new(); + for volume in volumes { + // 检查取消信号 + if self.cancel_token.is_cancelled() { + info!("Cancellation requested, stopping disk scan"); + break; + } + + match self.scan_volume(disk, &volume.name).await { + Ok(object_metadata) => { + disk_objects.insert(volume.name, object_metadata); + } + Err(e) => { + error!("Failed to scan volume {} on disk {}: {}", volume.name, disk_path, e); + continue; + } + } + } + + // Update disk metrics after scan + { + let mut disk_metrics = self.disk_metrics.write().await; + if let Some(metrics) = disk_metrics.get_mut(&disk_path) { + metrics.is_scanning = false; + } + } + + // Update state + { + let mut state = self.state.write().await; + state.scanning_disks.retain(|d| d != &disk_path); + } + + Ok(disk_objects) + } + + /// Scan a single volume (bucket) and collect object information + /// + /// This method collects all objects from a disk for a specific bucket. + /// It returns a map of object names to their metadata for later analysis. + async fn scan_volume(&self, disk: &DiskStore, bucket: &str) -> Result> { + info!("Scanning bucket: {} on disk: {}", bucket, disk.to_string()); + + // Update bucket metrics + { + let mut bucket_metrics = self.bucket_metrics.write().await; + let metrics = bucket_metrics.entry(bucket.to_string()).or_insert_with(|| BucketMetrics { + bucket: bucket.to_string(), + ..Default::default() + }); + + metrics.last_scan_time = Some(SystemTime::now()); + } + + // Update state + { + let mut state = self.state.write().await; + state.scanning_buckets.push(bucket.to_string()); + } + + self.metrics.increment_bucket_scans_started(1); + + let scan_start = SystemTime::now(); + + // Walk through all objects in the bucket + let walk_opts = WalkDirOptions { + bucket: bucket.to_string(), + base_dir: String::new(), + recursive: true, + report_notfound: false, + filter_prefix: None, + forward_to: None, + limit: 0, + disk_id: String::new(), + }; + + // Use a buffer to collect scan results for processing + let mut scan_buffer = Vec::new(); + + if let Err(e) = disk.walk_dir(walk_opts, &mut scan_buffer).await { + error!("Failed to walk directory for bucket {}: {}", bucket, e); + return Err(Error::Storage(e.into())); + } + + // Process the scan results using MetacacheReader + let mut reader = MetacacheReader::new(std::io::Cursor::new(scan_buffer)); + let mut objects_scanned = 0u64; + let mut objects_with_issues = 0u64; + let mut object_metadata = HashMap::new(); + + // Process each object entry + while let Ok(Some(mut entry)) = reader.peek().await { + objects_scanned += 1; + // Check if this is an actual object (not just a directory) + if entry.is_object() { + debug!("Scanned object: {}", entry.name); + + // Parse object metadata + if let Ok(file_meta) = entry.xl_meta() { + if file_meta.versions.is_empty() { + objects_with_issues += 1; + warn!("Object {} has no versions", entry.name); + } else { + // Store object metadata for later analysis + object_metadata.insert(entry.name.clone(), file_meta); + } + } else { + objects_with_issues += 1; + warn!("Failed to parse metadata for object {}", entry.name); + } + } + } + + // Update metrics + self.metrics.increment_objects_scanned(objects_scanned); + self.metrics.increment_objects_with_issues(objects_with_issues); + self.metrics.increment_bucket_scans_finished(1); + + // Update bucket metrics + { + let mut bucket_metrics = self.bucket_metrics.write().await; + if let Some(metrics) = bucket_metrics.get_mut(bucket) { + metrics.total_objects = objects_scanned; + metrics.objects_with_issues = objects_with_issues; + metrics.scan_duration = Some( + SystemTime::now() + .duration_since(scan_start) + .unwrap_or(Duration::ZERO) + ); + } + } + + // Update state + { + let mut state = self.state.write().await; + state.scanning_buckets.retain(|b| b != bucket); + } + + debug!("Completed scanning bucket: {} on disk {} ({} objects, {} issues)", + bucket, disk.to_string(), objects_scanned, objects_with_issues); + + Ok(object_metadata) + } + + /// Analyze object distribution across all disks and perform EC verification + /// + /// This method takes the collected object metadata from all disks and: + /// 1. Creates a union of all objects across all disks + /// 2. Identifies missing objects on each disk (for healing) + /// 3. Performs EC decode verification for deep scan mode + async fn analyze_object_distribution( + &self, + all_disk_objects: &[HashMap>], + disks: &[DiskStore], + ) -> Result<()> { + info!("Analyzing object distribution across {} disks", disks.len()); + + // Step 1: Create union of all objects across all disks + let mut all_objects = HashMap::new(); // bucket -> Set + let mut object_locations = HashMap::new(); // (bucket, object) -> Vec + + for (disk_idx, disk_objects) in all_disk_objects.iter().enumerate() { + for (bucket, objects) in disk_objects { + // Add bucket to all_objects + let bucket_objects = all_objects.entry(bucket.clone()).or_insert_with(|| std::collections::HashSet::new()); + + for (object_name, _file_meta) in objects { + bucket_objects.insert(object_name.clone()); + + // Record which disk has this object + let key = (bucket.clone(), object_name.clone()); + let locations = object_locations.entry(key).or_insert_with(|| Vec::new()); + locations.push(disk_idx); + } + } + } + + info!("Found {} buckets with {} total objects", all_objects.len(), + all_objects.values().map(|s| s.len()).sum::()); + + // Step 2: Identify missing objects and perform EC verification + let mut objects_needing_heal = 0u64; + let mut objects_with_ec_issues = 0u64; + + for (bucket, objects) in &all_objects { + for object_name in objects { + let key = (bucket.clone(), object_name.clone()); + let empty_vec = Vec::new(); + let locations = object_locations.get(&key).unwrap_or(&empty_vec); + + // Check if object is missing from some disks + if locations.len() < disks.len() { + objects_needing_heal += 1; + let missing_disks: Vec = (0..disks.len()) + .filter(|&i| !locations.contains(&i)) + .collect(); + warn!("Object {}/{} missing from disks: {:?}", bucket, object_name, missing_disks); + println!("Object {}/{} missing from disks: {:?}", bucket, object_name, missing_disks); + // TODO: Trigger heal for this object + } + + // Step 3: Deep scan EC verification + let config = self.config.read().await; + if config.scan_mode == ScanMode::Deep { + // Find the first disk that has this object to get metadata + if let Some(&first_disk_idx) = locations.first() { + if let Some(file_meta) = all_disk_objects[first_disk_idx] + .get(bucket) + .and_then(|objects| objects.get(object_name)) + { + if let Err(e) = self.verify_ec_decode_with_locations( + bucket, object_name, file_meta, locations, disks + ).await { + objects_with_ec_issues += 1; + warn!("EC decode verification failed for object {}/{}: {}", bucket, object_name, e); + } + } + } + } + } + } + + info!("Analysis complete: {} objects need healing, {} objects have EC issues", + objects_needing_heal, objects_with_ec_issues); + + Ok(()) + } + + /// Verify EC decode capability for an object using known disk locations + /// + /// This method is optimized to use the known locations of object copies + /// instead of scanning all disks. + async fn verify_ec_decode_with_locations( + &self, + bucket: &str, + object: &str, + file_meta: &rustfs_filemeta::FileMeta, + locations: &[usize], + all_disks: &[DiskStore], + ) -> Result<()> { + // Get EC parameters from the latest version + let (data_blocks, _parity_blocks) = if let Some(latest_version) = file_meta.versions.last() { + if let Ok(version) = rustfs_filemeta::FileMetaVersion::try_from(latest_version.clone()) { + if let Some(obj) = version.object { + (obj.erasure_m, obj.erasure_n) + } else { + // Not an object version, skip EC verification + return Ok(()); + } + } else { + // Cannot parse version, skip EC verification + return Ok(()); + } + } else { + // No versions, skip EC verification + return Ok(()); + }; + + let read_quorum = data_blocks; // Need at least data_blocks to decode + + if locations.len() < read_quorum { + return Err(Error::Scanner(format!( + "Insufficient object copies for EC decode: need {}, have {}", + read_quorum, locations.len() + ))); + } + + // Try to read object metadata from the known locations + let mut successful_reads = 0; + let mut errors = Vec::new(); + + for &disk_idx in locations { + if successful_reads >= read_quorum { + break; // We have enough copies for EC decode + } + + let disk = &all_disks[disk_idx]; + match disk.read_xl(bucket, object, false).await { + Ok(_) => { + successful_reads += 1; + debug!("Successfully read object {}/{} from disk {} (index: {})", + bucket, object, disk.to_string(), disk_idx); + } + Err(e) => { + let error_msg = format!("{}", e); + errors.push(error_msg); + debug!("Failed to read object {}/{} from disk {} (index: {}): {}", + bucket, object, disk.to_string(), disk_idx, e); + } + } + } + + if successful_reads >= read_quorum { + debug!("EC decode verification passed for object {}/{} ({} successful reads from {} locations)", + bucket, object, successful_reads, locations.len()); + Ok(()) + } else { + Err(Error::Scanner(format!( + "EC decode verification failed for object {}/{}: need {} reads, got {} (errors: {:?})", + bucket, object, read_quorum, successful_reads, errors + ))) + } + } + + /// Background scan loop with graceful shutdown + async fn scan_loop(self) -> Result<()> { + let config = self.config.read().await; + let mut interval = tokio::time::interval(config.scan_interval); + let deep_scan_interval = config.deep_scan_interval; + drop(config); + let cancel_token = self.cancel_token.clone(); + + loop { + tokio::select! { + _ = interval.tick() => { + // Check if scanner should still be running + if !self.state.read().await.is_running { + break; + } + + // 检查取消信号 + if cancel_token.is_cancelled() { + info!("Cancellation requested, exiting scanner loop"); + break; + } + + // Determine if it's time for a deep scan + let current_time = SystemTime::now(); + let last_deep_scan_time = self.state.read().await.last_deep_scan_time.unwrap_or(SystemTime::UNIX_EPOCH); + + if current_time.duration_since(last_deep_scan_time).unwrap_or(Duration::ZERO) >= deep_scan_interval { + info!("Deep scan interval reached, switching to deep scan mode"); + self.config.write().await.scan_mode = ScanMode::Deep; + self.state.write().await.last_deep_scan_time = Some(current_time); + } + + // Perform scan cycle + if let Err(e) = self.scan_cycle().await { + error!("Scan cycle failed: {}", e); + } + } + _ = cancel_token.cancelled() => { + info!("Received cancellation, stopping scanner loop"); + break; + } + } + } + + info!("Scanner loop stopped"); + Ok(()) + } + + /// Clone scanner for background tasks + fn clone_for_background(&self) -> Self { + Self { + config: self.config.clone(), + state: Arc::clone(&self.state), + metrics: Arc::clone(&self.metrics), + bucket_metrics: Arc::clone(&self.bucket_metrics), + disk_metrics: Arc::clone(&self.disk_metrics), + set_disks: Arc::clone(&self.set_disks), + cancel_token: self.cancel_token.clone(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rustfs_ecstore::store::ECStore; + use rustfs_ecstore::disk::endpoint::Endpoint; + use rustfs_ecstore::endpoints::{EndpointServerPools, PoolEndpoints, Endpoints}; + use rustfs_ecstore::{StorageAPI, store_api::{ObjectIO, MakeBucketOptions, PutObjReader}}; + use std::fs; + use std::net::SocketAddr; + + #[tokio::test(flavor = "multi_thread")] + async fn test_scanner_basic_functionality() { + // create temp dir as 4 disks + let temp_dir = std::path::PathBuf::from("/tmp/rustfs_ahm_test"); + if temp_dir.exists() { + fs::remove_dir_all(&temp_dir).unwrap(); + } + fs::create_dir_all(&temp_dir).unwrap(); + + // create 4 disk dirs + let disk_paths = vec![ + temp_dir.join("disk1"), + temp_dir.join("disk2"), + temp_dir.join("disk3"), + temp_dir.join("disk4"), + ]; + + for disk_path in &disk_paths { + fs::create_dir_all(disk_path).unwrap(); + } + + // create EndpointServerPools + let mut endpoints = Vec::new(); + for (i, disk_path) in disk_paths.iter().enumerate() { + let mut endpoint = Endpoint::try_from(disk_path.to_str().unwrap()).unwrap(); + // set correct index + endpoint.set_pool_index(0); + endpoint.set_set_index(0); + endpoint.set_disk_index(i); + endpoints.push(endpoint); + } + + let pool_endpoints = PoolEndpoints { + legacy: false, + set_count: 1, + drives_per_set: 4, + endpoints: Endpoints::from(endpoints), + cmd_line: "test".to_string(), + platform: format!("OS: {} | Arch: {}", std::env::consts::OS, std::env::consts::ARCH), + }; + + let endpoint_pools = EndpointServerPools(vec![pool_endpoints]); + + // format disks + rustfs_ecstore::store::init_local_disks(endpoint_pools.clone()).await.unwrap(); + + // create ECStore + let server_addr: SocketAddr = "127.0.0.1:9000".parse().unwrap(); + let ecstore = ECStore::new(server_addr, endpoint_pools).await.unwrap(); + + // init bucket metadata system + let buckets_list = ecstore + .list_bucket(&rustfs_ecstore::store_api::BucketOptions { + no_metadata: true, + ..Default::default() + }) + .await + .unwrap(); + let buckets = buckets_list.into_iter().map(|v| v.name).collect(); + rustfs_ecstore::bucket::metadata_sys::init_bucket_metadata_sys(ecstore.clone(), buckets).await; + + // get first SetDisks + let set_disks = ecstore.pools[0].get_disks(0); + + // create some test data + let bucket_name = "test-bucket"; + let object_name = "test-object"; + let test_data = b"Hello, RustFS!"; + + // create bucket and verify + let bucket_opts = MakeBucketOptions::default(); + ecstore.make_bucket(bucket_name, &bucket_opts).await.expect("make_bucket failed"); + + // check bucket really exists + let buckets = ecstore.list_bucket(&rustfs_ecstore::store_api::BucketOptions::default()).await.unwrap(); + assert!(buckets.iter().any(|b| b.name == bucket_name), "bucket not found after creation"); + + // write object + let mut put_reader = PutObjReader::from_vec(test_data.to_vec()); + let object_opts = rustfs_ecstore::store_api::ObjectOptions::default(); + ecstore.put_object(bucket_name, object_name, &mut put_reader, &object_opts).await.expect("put_object failed"); + + // create Scanner and test basic functionality + let scanner = Scanner::new(set_disks, None); + + // Test 1: Normal scan - verify object is found + println!("=== Test 1: Normal scan ==="); + let scan_result = scanner.scan_cycle().await; + assert!(scan_result.is_ok(), "Normal scan should succeed"); + let metrics = scanner.get_metrics().await; + assert!(metrics.objects_scanned > 0, "Objects scanned should be positive"); + println!("Normal scan completed successfully"); + + // Test 2: Simulate disk corruption - delete object data from disk1 + println!("=== Test 2: Simulate disk corruption ==="); + let disk1_bucket_path = disk_paths[0].join(bucket_name); + let disk1_object_path = disk1_bucket_path.join(object_name); + + // Try to delete the object file from disk1 (simulate corruption) + // Note: This might fail if ECStore is actively using the file + match fs::remove_dir_all(&disk1_object_path) { + Ok(_) => { + println!("Successfully deleted object from disk1: {:?}", disk1_object_path); + + // Verify deletion by checking if the directory still exists + if disk1_object_path.exists() { + println!("WARNING: Directory still exists after deletion: {:?}", disk1_object_path); + } else { + println!("Confirmed: Directory was successfully deleted"); + } + } + Err(e) => { + println!("Could not delete object from disk1 (file may be in use): {:?} - {}", disk1_object_path, e); + // This is expected behavior - ECStore might be holding file handles + } + } + + // Scan again - should still complete (even with missing data) + let scan_result_after_corruption = scanner.scan_cycle().await; + println!("Scan after corruption result: {:?}", scan_result_after_corruption); + + // Scanner should handle missing data gracefully + assert!(scan_result_after_corruption.is_ok(), "Scanner should handle missing data gracefully"); + + // Test 3: Verify EC decode capability + println!("=== Test 3: Verify EC decode ==="); + // Note: EC decode verification is done internally during scan_cycle + // We can verify that the scanner handles missing data gracefully + println!("EC decode verification is handled internally during scan cycles"); + + // Test 4: Test metrics collection + println!("=== Test 4: Metrics collection ==="); + let final_metrics = scanner.get_metrics().await; + println!("Final metrics: {:?}", final_metrics); + + // Verify metrics are reasonable + assert!(final_metrics.total_cycles > 0, "Should have completed scan cycles"); + assert!(final_metrics.last_activity.is_some(), "Should have scan activity"); + + // clean up temp dir + // if let Err(e) = fs::remove_dir_all(&temp_dir) { + // eprintln!("Warning: Failed to clean up temp directory {:?}: {}", temp_dir, e); + // } + } +} + diff --git a/crates/ahm/src/scanner/bandwidth_limiter.rs b/crates/ahm/src/scanner/bandwidth_limiter.rs deleted file mode 100644 index cf0e6a8da..000000000 --- a/crates/ahm/src/scanner/bandwidth_limiter.rs +++ /dev/null @@ -1,353 +0,0 @@ -// 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}, - Arc, - }, - time::{Duration, Instant}, -}; - -use tokio::{ - sync::RwLock, - time::{sleep, sleep_until, Instant as TokioInstant}, -}; -use tracing::{debug, info, warn}; - -use crate::error::Result; - -/// Configuration for bandwidth limiting -#[derive(Debug, Clone)] -pub struct BandwidthConfig { - /// Maximum bytes per second - pub bytes_per_second: u64, - /// Maximum operations per second - pub operations_per_second: u64, - /// Burst allowance multiplier - pub burst_multiplier: f64, - /// Whether to enable adaptive throttling - pub adaptive_throttling: bool, - /// Minimum sleep duration between operations - pub min_sleep_duration: Duration, - /// Maximum sleep duration between operations - pub max_sleep_duration: Duration, -} - -impl Default for BandwidthConfig { - fn default() -> Self { - Self { - bytes_per_second: 100 * 1024 * 1024, // 100 MB/s - operations_per_second: 1000, // 1000 ops/s - burst_multiplier: 2.0, - adaptive_throttling: true, - min_sleep_duration: Duration::from_micros(100), - max_sleep_duration: Duration::from_millis(100), - } - } -} - -/// Bandwidth limiter for controlling scan I/O rates -pub struct BandwidthLimiter { - config: BandwidthConfig, - bytes_this_second: Arc, - operations_this_second: Arc, - last_reset: Arc>, - adaptive_sleep_duration: Arc>, - total_bytes_processed: Arc, - total_operations_processed: Arc, - start_time: Instant, -} - -impl BandwidthLimiter { - /// Create a new bandwidth limiter - pub fn new(config: BandwidthConfig) -> Self { - let adaptive_sleep = if config.adaptive_throttling { - config.min_sleep_duration - } else { - Duration::from_micros(1000) // 1ms default - }; - - Self { - config, - bytes_this_second: Arc::new(AtomicU64::new(0)), - operations_this_second: Arc::new(AtomicU64::new(0)), - last_reset: Arc::new(RwLock::new(Instant::now())), - adaptive_sleep_duration: Arc::new(RwLock::new(adaptive_sleep)), - total_bytes_processed: Arc::new(AtomicU64::new(0)), - total_operations_processed: Arc::new(AtomicU64::new(0)), - start_time: Instant::now(), - } - } - - /// Wait for bandwidth allowance before processing bytes - pub async fn wait_for_bytes(&self, bytes: u64) -> Result<()> { - if self.config.bytes_per_second == 0 { - return Ok(()); - } - - let mut total_wait_time = Duration::ZERO; - let mut remaining_bytes = bytes; - - while remaining_bytes > 0 { - // Reset counters if a second has passed - self.reset_counters_if_needed().await; - - let current_bytes = self.bytes_this_second.load(Ordering::Relaxed); - let burst_limit = (self.config.bytes_per_second as f64 * self.config.burst_multiplier) as u64; - - if current_bytes >= burst_limit { - // We're over the burst limit, wait - let wait_time = self.calculate_wait_time(current_bytes, self.config.bytes_per_second).await; - sleep(wait_time).await; - total_wait_time += wait_time; - continue; - } - - let bytes_to_process = std::cmp::min(remaining_bytes, burst_limit - current_bytes); - self.bytes_this_second.fetch_add(bytes_to_process, Ordering::Relaxed); - self.total_bytes_processed.fetch_add(bytes_to_process, Ordering::Relaxed); - remaining_bytes -= bytes_to_process; - - // Adaptive throttling - if self.config.adaptive_throttling { - self.update_adaptive_sleep(bytes_to_process).await; - } - } - - if total_wait_time > Duration::ZERO { - debug!("Bandwidth limiter waited {:?} for {} bytes", total_wait_time, bytes); - } - - Ok(()) - } - - /// Wait for bandwidth allowance before processing an operation - pub async fn wait_for_operation(&self) -> Result<()> { - if self.config.operations_per_second == 0 { - return Ok(()); - } - - // Reset counters if a second has passed - self.reset_counters_if_needed().await; - - let current_ops = self.operations_this_second.load(Ordering::Relaxed); - let burst_limit = (self.config.operations_per_second as f64 * self.config.burst_multiplier) as u64; - - if current_ops >= burst_limit { - // We're over the burst limit, wait - let wait_time = self.calculate_wait_time(current_ops, self.config.operations_per_second).await; - sleep(wait_time).await; - debug!("Bandwidth limiter waited {:?} for operation", wait_time); - } - - self.operations_this_second.fetch_add(1, Ordering::Relaxed); - self.total_operations_processed.fetch_add(1, Ordering::Relaxed); - - Ok(()) - } - - /// Wait for bandwidth allowance before processing both bytes and operations - pub async fn wait_for_bytes_and_operation(&self, bytes: u64) -> Result<()> { - self.wait_for_bytes(bytes).await?; - self.wait_for_operation().await?; - Ok(()) - } - - /// Reset counters if a second has passed - async fn reset_counters_if_needed(&self) { - let mut last_reset = self.last_reset.write().await; - let now = Instant::now(); - - if now.duration_since(*last_reset) >= Duration::from_secs(1) { - self.bytes_this_second.store(0, Ordering::Relaxed); - self.operations_this_second.store(0, Ordering::Relaxed); - *last_reset = now; - } - } - - /// Calculate wait time based on current usage and limit - async fn calculate_wait_time(&self, current: u64, limit: u64) -> Duration { - if current == 0 || limit == 0 { - return self.config.min_sleep_duration; - } - - let utilization = current as f64 / limit as f64; - let base_sleep = self.config.min_sleep_duration.as_micros() as f64; - let max_sleep = self.config.max_sleep_duration.as_micros() as f64; - - // Exponential backoff based on utilization - let sleep_micros = base_sleep * (utilization * utilization); - let sleep_micros = sleep_micros.min(max_sleep).max(base_sleep); - - Duration::from_micros(sleep_micros as u64) - } - - /// Update adaptive sleep duration based on recent activity - async fn update_adaptive_sleep(&self, bytes_processed: u64) { - let mut sleep_duration = self.adaptive_sleep_duration.write().await; - - // Simple adaptive algorithm: increase sleep if we're processing too much - let current_rate = bytes_processed as f64 / sleep_duration.as_secs_f64(); - let target_rate = self.config.bytes_per_second as f64; - - if current_rate > target_rate * 1.1 { - // We're going too fast, increase sleep - *sleep_duration = Duration::from_micros( - (sleep_duration.as_micros() as f64 * 1.1) as u64 - ).min(self.config.max_sleep_duration); - } else if current_rate < target_rate * 0.9 { - // We're going too slow, decrease sleep - *sleep_duration = Duration::from_micros( - (sleep_duration.as_micros() as f64 * 0.9) as u64 - ).max(self.config.min_sleep_duration); - } - } - - /// Get current bandwidth statistics - pub async fn statistics(&self) -> BandwidthStatistics { - let elapsed = self.start_time.elapsed(); - let total_bytes = self.total_bytes_processed.load(Ordering::Relaxed); - let total_ops = self.total_operations_processed.load(Ordering::Relaxed); - let current_bytes = self.bytes_this_second.load(Ordering::Relaxed); - let current_ops = self.operations_this_second.load(Ordering::Relaxed); - let adaptive_sleep = *self.adaptive_sleep_duration.read().await; - - BandwidthStatistics { - total_bytes_processed: total_bytes, - total_operations_processed: total_ops, - current_bytes_per_second: current_bytes, - current_operations_per_second: current_ops, - average_bytes_per_second: if elapsed.as_secs() > 0 { - total_bytes / elapsed.as_secs() - } else { - 0 - }, - average_operations_per_second: if elapsed.as_secs() > 0 { - total_ops / elapsed.as_secs() - } else { - 0 - }, - adaptive_sleep_duration: adaptive_sleep, - uptime: elapsed, - } - } - - /// Reset all statistics - pub async fn reset_statistics(&self) { - self.total_bytes_processed.store(0, Ordering::Relaxed); - self.total_operations_processed.store(0, Ordering::Relaxed); - self.bytes_this_second.store(0, Ordering::Relaxed); - self.operations_this_second.store(0, Ordering::Relaxed); - *self.last_reset.write().await = Instant::now(); - *self.adaptive_sleep_duration.write().await = self.config.min_sleep_duration; - } - - /// Update configuration - pub async fn update_config(&self, new_config: BandwidthConfig) { - info!("Updating bandwidth limiter config: {:?}", new_config); - - // Reset adaptive sleep if adaptive throttling is disabled - if !new_config.adaptive_throttling { - *self.adaptive_sleep_duration.write().await = new_config.min_sleep_duration; - } - - // Note: We can't update the config struct itself since it's not wrapped in Arc - // In a real implementation, you might want to wrap the config in Arc as well - warn!("Config update not fully implemented - config struct is not mutable"); - } -} - -/// Statistics for bandwidth limiting -#[derive(Debug, Clone)] -pub struct BandwidthStatistics { - pub total_bytes_processed: u64, - pub total_operations_processed: u64, - pub current_bytes_per_second: u64, - pub current_operations_per_second: u64, - pub average_bytes_per_second: u64, - pub average_operations_per_second: u64, - pub adaptive_sleep_duration: Duration, - pub uptime: Duration, -} - -#[cfg(test)] -mod tests { - use super::*; - use tokio::time::Instant as TokioInstant; - - #[tokio::test] - async fn test_bandwidth_limiter_creation() { - let config = BandwidthConfig::default(); - let limiter = BandwidthLimiter::new(config); - let stats = limiter.statistics().await; - assert_eq!(stats.total_bytes_processed, 0); - assert_eq!(stats.total_operations_processed, 0); - } - - #[tokio::test] - async fn test_bytes_limiting() { - let config = BandwidthConfig { - bytes_per_second: 1000, // 1KB/s - operations_per_second: 1000, - ..Default::default() - }; - let limiter = BandwidthLimiter::new(config); - - let start = TokioInstant::now(); - - // Process 500 bytes (should not be limited) - limiter.wait_for_bytes(500).await.unwrap(); - - // Process another 600 bytes (should be limited) - limiter.wait_for_bytes(600).await.unwrap(); - - let elapsed = start.elapsed(); - assert!(elapsed >= Duration::from_millis(100)); // Should take some time due to limiting - } - - #[tokio::test] - async fn test_operation_limiting() { - let config = BandwidthConfig { - bytes_per_second: 1000000, // 1MB/s - operations_per_second: 10, // 10 ops/s - ..Default::default() - }; - let limiter = BandwidthLimiter::new(config); - - let start = TokioInstant::now(); - - // Process 15 operations (should be limited) - for _ in 0..15 { - limiter.wait_for_operation().await.unwrap(); - } - - let elapsed = start.elapsed(); - assert!(elapsed >= Duration::from_millis(500)); // Should take some time due to limiting - } - - #[tokio::test] - async fn test_statistics() { - let config = BandwidthConfig::default(); - let limiter = BandwidthLimiter::new(config); - - limiter.wait_for_bytes(1000).await.unwrap(); - limiter.wait_for_operation().await.unwrap(); - - let stats = limiter.statistics().await; - assert_eq!(stats.total_bytes_processed, 1000); - assert_eq!(stats.total_operations_processed, 1); - assert!(stats.uptime > Duration::ZERO); - } -} \ No newline at end of file diff --git a/crates/ahm/src/scanner/disk_scanner.rs b/crates/ahm/src/scanner/disk_scanner.rs deleted file mode 100644 index 8d636107c..000000000 --- a/crates/ahm/src/scanner/disk_scanner.rs +++ /dev/null @@ -1,591 +0,0 @@ -// 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::{ - collections::HashMap, - path::Path, - sync::Arc, - time::{Duration, Instant, SystemTime}, -}; - -use tokio::sync::RwLock; -use tracing::{error, info}; -use anyhow; - -use crate::error::Result; -use super::{HealthIssue, HealthIssueType, Severity}; - -/// Configuration for disk scanning -#[derive(Debug, Clone)] -pub struct DiskScannerConfig { - /// Scan interval for disk health checks - pub scan_interval: Duration, - /// Minimum free space threshold (percentage) - pub min_free_space_percent: f64, - /// Maximum disk usage threshold (percentage) - pub max_disk_usage_percent: f64, - /// Minimum inode usage threshold (percentage) - pub min_inode_usage_percent: f64, - /// Maximum inode usage threshold (percentage) - pub max_inode_usage_percent: f64, - /// Whether to check disk I/O performance - pub check_io_performance: bool, - /// Whether to check disk temperature (if available) - pub check_temperature: bool, - /// Whether to check disk SMART status (if available) - pub check_smart_status: bool, - /// Timeout for individual disk operations - pub operation_timeout: Duration, - /// Maximum number of concurrent disk scans - pub max_concurrent_scans: usize, -} - -impl Default for DiskScannerConfig { - fn default() -> Self { - Self { - scan_interval: Duration::from_secs(300), // 5 minutes - min_free_space_percent: 10.0, // 10% minimum free space - max_disk_usage_percent: 90.0, // 90% maximum usage - min_inode_usage_percent: 5.0, // 5% minimum inode usage - max_inode_usage_percent: 95.0, // 95% maximum inode usage - check_io_performance: true, - check_temperature: false, // Disabled by default - check_smart_status: false, // Disabled by default - operation_timeout: Duration::from_secs(30), - max_concurrent_scans: 4, - } - } -} - -/// Disk information and health status -#[derive(Debug, Clone)] -pub struct DiskInfo { - pub device_path: String, - pub mount_point: String, - pub filesystem_type: String, - pub total_space: u64, - pub used_space: u64, - pub free_space: u64, - pub available_space: u64, - pub usage_percent: f64, - pub inode_total: Option, - pub inode_used: Option, - pub inode_free: Option, - pub inode_usage_percent: Option, - pub last_scan_time: SystemTime, - pub health_status: DiskHealthStatus, - pub performance_metrics: Option, - pub temperature: Option, - pub smart_status: Option, -} - -/// Disk health status -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum DiskHealthStatus { - Healthy, - Warning, - Critical, - Unknown, -} - -/// Disk performance metrics -#[derive(Debug, Clone)] -pub struct DiskPerformanceMetrics { - pub read_bytes_per_sec: f64, - pub write_bytes_per_sec: f64, - pub read_operations_per_sec: f64, - pub write_operations_per_sec: f64, - pub average_response_time_ms: f64, - pub queue_depth: f64, - pub utilization_percent: f64, - pub last_updated: SystemTime, -} - -/// SMART status information -#[derive(Debug, Clone)] -pub struct SmartStatus { - pub overall_health: SmartHealthStatus, - pub temperature: Option, - pub power_on_hours: Option, - pub reallocated_sectors: Option, - pub pending_sectors: Option, - pub uncorrectable_sectors: Option, - pub attributes: HashMap, -} - -/// SMART health status -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SmartHealthStatus { - Passed, - Failed, - Unknown, -} - -/// SMART attribute -#[derive(Debug, Clone)] -pub struct SmartAttribute { - pub name: String, - pub value: u64, - pub worst: u64, - pub threshold: u64, - pub status: SmartAttributeStatus, -} - -/// SMART attribute status -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SmartAttributeStatus { - Good, - Warning, - Critical, - Unknown, -} - -/// Result of scanning a single disk -#[derive(Debug, Clone)] -pub struct DiskScanResult { - pub disk_info: DiskInfo, - pub health_issues: Vec, - pub scan_duration: Duration, - pub success: bool, - pub error_message: Option, -} - -/// Disk scanner for monitoring disk health and performance -pub struct DiskScanner { - config: DiskScannerConfig, - statistics: Arc>, - last_scan_results: Arc>>, -} - -/// Statistics for disk scanning -#[derive(Debug, Clone, Default)] -pub struct DiskScannerStatistics { - pub disks_scanned: u64, - pub disks_with_issues: u64, - pub total_issues_found: u64, - pub total_scan_time: Duration, - pub average_scan_time: Duration, - pub last_scan_time: Option, - pub scan_cycles_completed: u64, - pub scan_cycles_failed: u64, -} - -impl DiskScanner { - /// Create a new disk scanner - pub fn new(config: DiskScannerConfig) -> Self { - Self { - config, - statistics: Arc::new(RwLock::new(DiskScannerStatistics::default())), - last_scan_results: Arc::new(RwLock::new(HashMap::new())), - } - } - - /// Scan all mounted disks - pub async fn scan_all_disks(&self) -> Result> { - let scan_start = Instant::now(); - let mut results = Vec::new(); - - // Get list of mounted filesystems - let mount_points = self.get_mount_points().await?; - - info!("Starting disk scan for {} mount points", mount_points.len()); - - // Scan each mount point - for mount_point in mount_points { - match self.scan_disk(&mount_point).await { - Ok(result) => { - results.push(result.clone()); - - // Store result for later reference - let mut last_results = self.last_scan_results.write().await; - last_results.insert(mount_point.clone(), result); - } - Err(e) => { - error!("Failed to scan disk at {}: {}", mount_point, e); - - // Create error result - let error_result = DiskScanResult { - disk_info: DiskInfo { - device_path: "unknown".to_string(), - mount_point: mount_point.clone(), - filesystem_type: "unknown".to_string(), - total_space: 0, - used_space: 0, - free_space: 0, - available_space: 0, - usage_percent: 0.0, - inode_total: None, - inode_used: None, - inode_free: None, - inode_usage_percent: None, - last_scan_time: SystemTime::now(), - health_status: DiskHealthStatus::Unknown, - performance_metrics: None, - temperature: None, - smart_status: None, - }, - health_issues: vec![HealthIssue { - issue_type: HealthIssueType::DiskReadError, - severity: Severity::High, - bucket: "system".to_string(), - object: mount_point.clone(), - description: format!("Failed to scan disk: {}", e), - metadata: None, - }], - scan_duration: scan_start.elapsed(), - success: false, - error_message: Some(e.to_string()), - }; - - results.push(error_result); - } - } - } - - // Update statistics - self.update_statistics(|stats| { - stats.disks_scanned += results.len() as u64; - stats.disks_with_issues += results.iter().filter(|r| !r.health_issues.is_empty()).count() as u64; - stats.total_issues_found += results.iter().map(|r| r.health_issues.len() as u64).sum::(); - stats.total_scan_time += scan_start.elapsed(); - stats.average_scan_time = Duration::from_millis( - stats.total_scan_time.as_millis() as u64 / stats.disks_scanned.max(1) - ); - stats.last_scan_time = Some(SystemTime::now()); - stats.scan_cycles_completed += 1; - }).await; - - info!( - "Disk scan completed: {} disks, {} issues found in {:?}", - results.len(), - results.iter().map(|r| r.health_issues.len()).sum::(), - scan_start.elapsed() - ); - - Ok(results) - } - - /// Scan a single disk - pub async fn scan_disk(&self, mount_point: &str) -> Result { - let scan_start = Instant::now(); - let mut health_issues = Vec::new(); - - // Get disk space information - let disk_info = self.get_disk_info(mount_point).await?; - - // Check disk space usage - if disk_info.usage_percent > self.config.max_disk_usage_percent { - health_issues.push(HealthIssue { - issue_type: HealthIssueType::DiskFull, - severity: if disk_info.usage_percent > 95.0 { Severity::Critical } else { Severity::High }, - bucket: "system".to_string(), - object: mount_point.to_string(), - description: format!("Disk usage is {}%, exceeds threshold of {}%", - disk_info.usage_percent, self.config.max_disk_usage_percent), - metadata: None, - }); - } - - if disk_info.usage_percent < self.config.min_free_space_percent { - health_issues.push(HealthIssue { - issue_type: HealthIssueType::DiskFull, - severity: Severity::Medium, - bucket: "system".to_string(), - object: mount_point.to_string(), - description: format!("Free space is only {}%, below threshold of {}%", - 100.0 - disk_info.usage_percent, self.config.min_free_space_percent), - metadata: None, - }); - } - - // Check inode usage if available - if let Some(inode_usage) = disk_info.inode_usage_percent { - if inode_usage > self.config.max_inode_usage_percent { - health_issues.push(HealthIssue { - issue_type: HealthIssueType::DiskFull, - severity: if inode_usage > 95.0 { Severity::Critical } else { Severity::High }, - bucket: "system".to_string(), - object: mount_point.to_string(), - description: format!("Inode usage is {}%, exceeds threshold of {}%", - inode_usage, self.config.max_inode_usage_percent), - metadata: None, - }); - } - } - - // Check I/O performance if enabled - if self.config.check_io_performance { - if let Some(metrics) = &disk_info.performance_metrics { - if metrics.utilization_percent > 90.0 { - health_issues.push(HealthIssue { - issue_type: HealthIssueType::DiskReadError, - severity: Severity::Medium, - bucket: "system".to_string(), - object: mount_point.to_string(), - description: format!("High disk utilization: {}%", metrics.utilization_percent), - metadata: None, - }); - } - - if metrics.average_response_time_ms > 100.0 { - health_issues.push(HealthIssue { - issue_type: HealthIssueType::DiskReadError, - severity: Severity::Medium, - bucket: "system".to_string(), - object: mount_point.to_string(), - description: format!("High disk response time: {}ms", metrics.average_response_time_ms), - metadata: None, - }); - } - } - } - - // Check temperature if enabled - if self.config.check_temperature { - if let Some(temp) = disk_info.temperature { - if temp > 60.0 { - health_issues.push(HealthIssue { - issue_type: HealthIssueType::DiskReadError, - severity: if temp > 70.0 { Severity::Critical } else { Severity::High }, - bucket: "system".to_string(), - object: mount_point.to_string(), - description: format!("High disk temperature: {}°C", temp), - metadata: None, - }); - } - } - } - - // Check SMART status if enabled - if self.config.check_smart_status { - if let Some(smart) = &disk_info.smart_status { - if smart.overall_health == SmartHealthStatus::Failed { - health_issues.push(HealthIssue { - issue_type: HealthIssueType::DiskReadError, - severity: Severity::Critical, - bucket: "system".to_string(), - object: mount_point.to_string(), - description: "SMART health check failed".to_string(), - metadata: None, - }); - } - } - } - - let scan_duration = scan_start.elapsed(); - let success = health_issues.is_empty(); - - Ok(DiskScanResult { - disk_info, - health_issues, - scan_duration, - success, - error_message: None, - }) - } - - /// Get list of mounted filesystems - async fn get_mount_points(&self) -> Result> { - // TODO: Implement actual mount point detection - // For now, return common mount points - Ok(vec![ - "/".to_string(), - "/data".to_string(), - "/var".to_string(), - ]) - } - - /// Get disk information for a mount point - async fn get_disk_info(&self, mount_point: &str) -> Result { - let path = Path::new(mount_point); - - // Get filesystem statistics using std::fs instead of nix for now - let _metadata = match std::fs::metadata(path) { - Ok(metadata) => metadata, - Err(e) => { - return Err(crate::error::Error::Other(anyhow::anyhow!("Failed to get filesystem stats: {}", e))); - } - }; - - // For now, use placeholder values since we can't easily get filesystem stats - let total_space = 1000000000; // 1GB placeholder - let free_space = 500000000; // 500MB placeholder - let available_space = 450000000; // 450MB placeholder - let used_space = total_space - free_space; - let usage_percent = (used_space as f64 / total_space as f64) * 100.0; - - // Get inode information (placeholder) - let inode_total = Some(1000000); - let inode_free = Some(500000); - let inode_used = Some(500000); - let inode_usage_percent = Some(50.0); - - // Get filesystem type - let filesystem_type = self.get_filesystem_type(mount_point).await.unwrap_or_else(|_| "unknown".to_string()); - - // Get device path - let device_path = self.get_device_path(mount_point).await.unwrap_or_else(|_| "unknown".to_string()); - - // Get performance metrics if enabled - let performance_metrics = if self.config.check_io_performance { - self.get_performance_metrics(&device_path).await.ok() - } else { - None - }; - - // Get temperature if enabled - let temperature = if self.config.check_temperature { - self.get_disk_temperature(&device_path).await.ok().flatten() - } else { - None - }; - - // Get SMART status if enabled - let smart_status = if self.config.check_smart_status { - self.get_smart_status(&device_path).await.ok().flatten() - } else { - None - }; - - // Determine health status (placeholder - will be set by scan_disk method) - let health_status = DiskHealthStatus::Healthy; - - Ok(DiskInfo { - device_path, - mount_point: mount_point.to_string(), - filesystem_type, - total_space, - used_space, - free_space, - available_space, - usage_percent, - inode_total, - inode_used, - inode_free, - inode_usage_percent, - last_scan_time: SystemTime::now(), - health_status, - performance_metrics, - temperature, - smart_status, - }) - } - - /// Get filesystem type for a mount point - async fn get_filesystem_type(&self, _mount_point: &str) -> Result { - // TODO: Implement filesystem type detection - // For now, return a placeholder - Ok("ext4".to_string()) - } - - /// Get device path for a mount point - async fn get_device_path(&self, _mount_point: &str) -> Result { - // TODO: Implement device path detection - // For now, return a placeholder - Ok("/dev/sda1".to_string()) - } - - /// Get disk performance metrics - async fn get_performance_metrics(&self, _device_path: &str) -> Result { - // TODO: Implement performance metrics collection - // For now, return placeholder metrics - Ok(DiskPerformanceMetrics { - read_bytes_per_sec: 1000000.0, // 1MB/s - write_bytes_per_sec: 500000.0, // 500KB/s - read_operations_per_sec: 100.0, - write_operations_per_sec: 50.0, - average_response_time_ms: 5.0, - queue_depth: 1.0, - utilization_percent: 10.0, - last_updated: SystemTime::now(), - }) - } - - /// Get disk temperature - async fn get_disk_temperature(&self, _device_path: &str) -> Result> { - // TODO: Implement temperature monitoring - // For now, return None (temperature not available) - Ok(None) - } - - /// Get SMART status - async fn get_smart_status(&self, _device_path: &str) -> Result> { - // TODO: Implement SMART status checking - // For now, return None (SMART not available) - Ok(None) - } - - /// Update scanner statistics - async fn update_statistics(&self, update_fn: F) - where - F: FnOnce(&mut DiskScannerStatistics), - { - let mut stats = self.statistics.write().await; - update_fn(&mut stats); - } - - /// Get current statistics - pub async fn statistics(&self) -> DiskScannerStatistics { - self.statistics.read().await.clone() - } - - /// Get last scan results - pub async fn last_scan_results(&self) -> HashMap { - self.last_scan_results.read().await.clone() - } - - /// Reset statistics - pub async fn reset_statistics(&self) { - let mut stats = self.statistics.write().await; - *stats = DiskScannerStatistics::default(); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_disk_scanner_creation() { - let config = DiskScannerConfig::default(); - let scanner = DiskScanner::new(config); - assert_eq!(scanner.statistics().await.disks_scanned, 0); - } - - #[tokio::test] - async fn test_disk_info_creation() { - let disk_info = DiskInfo { - device_path: "/dev/sda1".to_string(), - mount_point: "/".to_string(), - filesystem_type: "ext4".to_string(), - total_space: 1000000000, - used_space: 500000000, - free_space: 500000000, - available_space: 450000000, - usage_percent: 50.0, - inode_total: Some(1000000), - inode_used: Some(500000), - inode_free: Some(500000), - inode_usage_percent: Some(50.0), - last_scan_time: SystemTime::now(), - health_status: DiskHealthStatus::Healthy, - performance_metrics: None, - temperature: None, - smart_status: None, - }; - - assert_eq!(disk_info.usage_percent, 50.0); - assert_eq!(disk_info.health_status, DiskHealthStatus::Healthy); - } -} \ No newline at end of file diff --git a/crates/ahm/src/scanner/engine.rs b/crates/ahm/src/scanner/engine.rs deleted file mode 100644 index 129b21a76..000000000 --- a/crates/ahm/src/scanner/engine.rs +++ /dev/null @@ -1,536 +0,0 @@ -// 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::{ - collections::HashMap, - path::{Path, PathBuf}, - sync::Arc, - time::{Duration, Instant, SystemTime}, -}; - -use tokio::{ - sync::{broadcast, RwLock}, - time::sleep, -}; -use tracing::{error, info, warn}; -use tokio_util::sync::CancellationToken; - -use crate::{core, error::Result, metrics, SystemEvent}; -use crate::core::Status; -use super::{HealthIssue, HealthIssueType, Severity}; - -/// Represents a discovered object during scanning -#[derive(Debug, Clone)] -pub struct ScannedObject { - pub bucket: String, - pub object: String, - pub version_id: Option, - pub path: PathBuf, - pub size: u64, - pub modified_time: SystemTime, - pub metadata: HashMap, - pub health_issues: Vec, -} - -/// Configuration for the scanner engine -#[derive(Debug, Clone)] -pub struct EngineConfig { - /// Root directory to scan - pub root_path: String, - /// Maximum number of concurrent scan workers - pub max_workers: usize, - /// Scan interval between cycles - pub scan_interval: Duration, - /// Bandwidth limit for scanning (bytes per second) - pub bandwidth_limit: Option, - /// Whether to enable deep scanning (bitrot detection) - pub enable_deep_scan: bool, - /// Probability of healing objects during scan (1 in N) - pub heal_probability: u32, - /// Maximum folders to scan before compacting - pub max_folders_before_compact: u64, - /// Sleep duration between folder scans - pub folder_sleep_duration: Duration, -} - -impl Default for EngineConfig { - fn default() -> Self { - Self { - root_path: "/data".to_string(), - max_workers: 4, - scan_interval: Duration::from_secs(300), // 5 minutes - bandwidth_limit: None, - enable_deep_scan: false, - heal_probability: 1024, // 1 in 1024 objects - max_folders_before_compact: 10000, - folder_sleep_duration: Duration::from_millis(1), - } - } -} - -/// Scanner statistics -#[derive(Debug, Clone, Default)] -pub struct ScannerStatistics { - pub objects_scanned: u64, - pub bytes_scanned: u64, - pub issues_found: u64, - pub scan_duration: Duration, - pub scan_rate_objects_per_sec: f64, - pub scan_rate_bytes_per_sec: f64, - pub folders_scanned: u64, - pub objects_with_issues: u64, -} - -/// Main scanner engine -pub struct Engine { - config: EngineConfig, - coordinator: Arc, - metrics: Arc, - cancel_token: CancellationToken, - status: Arc>, - statistics: Arc>, - scan_cycle: Arc>, -} - -impl Engine { - /// Create a new scanner engine - pub async fn new( - config: EngineConfig, - coordinator: Arc, - metrics: Arc, - cancel_token: CancellationToken, - ) -> Result { - let engine = Self { - config, - coordinator, - metrics, - cancel_token, - status: Arc::new(RwLock::new(Status::Initializing)), - statistics: Arc::new(RwLock::new(ScannerStatistics::default())), - scan_cycle: Arc::new(RwLock::new(0)), - }; - - info!("Scanner engine created with config: {:?}", engine.config); - Ok(engine) - } - - /// Start the scanner engine - pub async fn start(&self) -> Result<()> { - info!("Starting scanner engine"); - *self.status.write().await = Status::Running; - - let engine = self.clone_for_background(); - tokio::spawn(async move { - if let Err(e) = engine.run_scan_loop().await { - error!("Scanner engine error: {}", e); - } - }); - - Ok(()) - } - - /// Stop the scanner engine - pub async fn stop(&self) -> Result<()> { - info!("Stopping scanner engine"); - *self.status.write().await = Status::Stopping; - self.cancel_token.cancel(); - *self.status.write().await = Status::Stopped; - Ok(()) - } - - /// Get current status - pub async fn status(&self) -> Status { - self.status.read().await.clone() - } - - /// Get current statistics - pub async fn statistics(&self) -> ScannerStatistics { - self.statistics.read().await.clone() - } - - /// Clone the engine for background tasks - fn clone_for_background(&self) -> Arc { - Arc::new(Self { - config: self.config.clone(), - coordinator: self.coordinator.clone(), - metrics: self.metrics.clone(), - cancel_token: self.cancel_token.clone(), - status: self.status.clone(), - statistics: self.statistics.clone(), - scan_cycle: self.scan_cycle.clone(), - }) - } - - /// Main scan loop - async fn run_scan_loop(&self) -> Result<()> { - info!("Scanner engine loop started"); - - loop { - tokio::select! { - _ = self.cancel_token.cancelled() => { - info!("Scanner engine received cancellation signal"); - break; - } - _ = sleep(self.config.scan_interval) => { - if let Err(e) = self.run_scan_cycle().await { - error!("Scan cycle failed: {}", e); - } - } - } - } - - Ok(()) - } - - /// Run a single scan cycle - async fn run_scan_cycle(&self) -> Result<()> { - let cycle_start = Instant::now(); - let cycle = { - let mut cycle_guard = self.scan_cycle.write().await; - *cycle_guard += 1; - *cycle_guard - }; - - info!("Starting scan cycle {}", cycle); - - // Reset statistics for new cycle - { - let mut stats = self.statistics.write().await; - *stats = ScannerStatistics::default(); - } - - // Scan the root directory - let scan_result = self.scan_directory(&self.config.root_path).await?; - - // Update statistics - { - let mut stats = self.statistics.write().await; - stats.scan_duration = cycle_start.elapsed(); - stats.objects_scanned = scan_result.objects.len() as u64; - stats.bytes_scanned = scan_result.total_size; - stats.issues_found = scan_result.total_issues; - stats.folders_scanned = scan_result.folders_scanned; - stats.objects_with_issues = scan_result.objects_with_issues; - - if stats.scan_duration.as_secs() > 0 { - stats.scan_rate_objects_per_sec = stats.objects_scanned as f64 / stats.scan_duration.as_secs() as f64; - stats.scan_rate_bytes_per_sec = stats.bytes_scanned as f64 / stats.scan_duration.as_secs() as f64; - } - } - - // Publish scan completion event - let scan_report = crate::scanner::ScanReport { - scan_id: cycle.to_string(), - status: "completed".to_string(), - summary: format!("Scanned {} objects, found {} issues", scan_result.objects.len(), scan_result.total_issues), - issues_found: scan_result.total_issues, - }; - - self.coordinator.publish_event(SystemEvent::ScanCompleted(scan_report)).await?; - - info!( - "Scan cycle {} completed: {} objects, {} bytes, {} issues in {:?}", - cycle, - scan_result.objects.len(), - scan_result.total_size, - scan_result.total_issues, - cycle_start.elapsed() - ); - - Ok(()) - } - - /// Scan a directory recursively - async fn scan_directory(&self, path: &str) -> Result { - let mut result = ScanResult::default(); - let path_buf = PathBuf::from(path); - - if !path_buf.exists() { - warn!("Scan path does not exist: {}", path); - return Ok(result); - } - - if !path_buf.is_dir() { - warn!("Scan path is not a directory: {}", path); - return Ok(result); - } - - self.scan_directory_recursive(&path_buf, &mut result).await?; - Ok(result) - } - - /// Recursively scan a directory - async fn scan_directory_recursive(&self, dir_path: &Path, result: &mut ScanResult) -> Result<()> { - result.folders_scanned += 1; - - // Check for cancellation - if self.cancel_token.is_cancelled() { - return Ok(()); - } - - let entries = match std::fs::read_dir(dir_path) { - Ok(entries) => entries, - Err(e) => { - warn!("Failed to read directory {}: {}", dir_path.display(), e); - return Ok(()); - } - }; - - for entry in entries { - if self.cancel_token.is_cancelled() { - break; - } - - let entry = match entry { - Ok(entry) => entry, - Err(e) => { - warn!("Failed to read directory entry: {}", e); - continue; - } - }; - - let file_path = entry.path(); - let _path_str = file_path.to_string_lossy(); - let entry_name = file_path.file_name() - .and_then(|n| n.to_str()) - .unwrap_or("unknown"); - - // Skip hidden files and system files - if entry_name.starts_with('.') || entry_name == ".." || entry_name == "." { - continue; - } - - if file_path.is_dir() { - // Recursively scan subdirectories - Box::pin(self.scan_directory_recursive(&file_path, result)).await?; - } else if file_path.is_file() { - // Scan individual file - if let Some(scanned_object) = self.scan_object(&file_path).await? { - result.objects.push(scanned_object.clone()); - result.total_size += scanned_object.size; - - if !scanned_object.health_issues.is_empty() { - result.objects_with_issues += 1; - result.total_issues += scanned_object.health_issues.len() as u64; - - // Publish health issues - for issue in &scanned_object.health_issues { - let health_issue = crate::scanner::HealthIssue { - issue_type: issue.issue_type.clone(), - severity: issue.severity, - bucket: scanned_object.bucket.clone(), - object: scanned_object.object.clone(), - description: issue.description.clone(), - metadata: None, // TODO: Convert HashMap to ObjectMetadata - }; - - self.coordinator.publish_event(SystemEvent::HealthIssueDetected(health_issue)).await?; - } - } - - // Publish object discovered event - let metadata = crate::ObjectMetadata { - size: scanned_object.size, - mod_time: scanned_object.modified_time.duration_since(SystemTime::UNIX_EPOCH) - .unwrap_or_default() - .as_secs() as i64, - content_type: "application/octet-stream".to_string(), - etag: "".to_string(), // TODO: Calculate actual ETag - }; - - self.coordinator.publish_event(SystemEvent::ObjectDiscovered { - bucket: scanned_object.bucket.clone(), - object: scanned_object.object.clone(), - version_id: scanned_object.version_id.clone(), - metadata, - }).await?; - } - } - - // Sleep between items to avoid overwhelming the system - sleep(self.config.folder_sleep_duration).await; - } - - Ok(()) - } - - /// Scan a single object file - async fn scan_object(&self, file_path: &Path) -> Result> { - let metadata = match std::fs::metadata(file_path) { - Ok(metadata) => metadata, - Err(e) => { - warn!("Failed to read file metadata {}: {}", file_path.display(), e); - return Ok(None); - } - }; - - // Extract bucket and object from path - let (bucket, object) = self.extract_bucket_object_from_path(file_path)?; - if bucket.is_empty() || object.is_empty() { - return Ok(None); - } - - // Check for health issues - let health_issues = self.check_object_health(file_path, &metadata).await?; - - let scanned_object = ScannedObject { - bucket, - object, - version_id: None, // TODO: Extract version ID from path - path: file_path.to_path_buf(), - size: metadata.len(), - modified_time: metadata.modified().unwrap_or(SystemTime::now()), - metadata: HashMap::new(), // TODO: Extract metadata - health_issues, - }; - - Ok(Some(scanned_object)) - } - - /// Extract bucket and object name from file path - fn extract_bucket_object_from_path(&self, file_path: &Path) -> Result<(String, String)> { - let _path_str = file_path.to_string_lossy(); - let root_path = Path::new(&self.config.root_path); - - if let Ok(relative_path) = file_path.strip_prefix(root_path) { - let components: Vec<&str> = relative_path.components() - .filter_map(|c| c.as_os_str().to_str()) - .collect(); - - if components.len() >= 2 { - let bucket = components[0].to_string(); - let object = components[1..].join("/"); - return Ok((bucket, object)); - } - } - - Ok((String::new(), String::new())) - } - - /// Check object health and detect issues - async fn check_object_health(&self, file_path: &Path, metadata: &std::fs::Metadata) -> Result> { - let mut issues = Vec::new(); - - // Extract bucket and object from path for health issues - let (bucket, object) = self.extract_bucket_object_from_path(file_path)?; - - // Check file size - if metadata.len() == 0 { - issues.push(HealthIssue { - issue_type: HealthIssueType::ObjectTooSmall, - severity: Severity::Low, - bucket: bucket.clone(), - object: object.clone(), - description: "Object has zero size".to_string(), - metadata: None, - }); - } - - // Check file permissions - if !metadata.permissions().readonly() { - issues.push(HealthIssue { - issue_type: HealthIssueType::PolicyViolation, - severity: Severity::Medium, - bucket: bucket.clone(), - object: object.clone(), - description: "Object is not read-only".to_string(), - metadata: None, - }); - } - - // TODO: Add more health checks: - // - Checksum verification - // - Replication status - // - Encryption status - // - Metadata consistency - // - Disk health - - Ok(issues) - } - - /// Start scanning operations - pub async fn start_scan(&self) -> Result<()> { - let mut status = self.status.write().await; - *status = Status::Running; - info!("Scanning operations started"); - Ok(()) - } - - /// Stop scanning operations - pub async fn stop_scan(&self) -> Result<()> { - let mut status = self.status.write().await; - *status = Status::Stopped; - info!("Scanning operations stopped"); - Ok(()) - } - - /// Get engine configuration - pub async fn get_config(&self) -> ScanConfig { - self.config.clone() - } -} - -/// Result of a scan operation -#[derive(Debug, Clone, Default)] -pub struct ScanResult { - pub objects: Vec, - pub total_size: u64, - pub total_issues: u64, - pub folders_scanned: u64, - pub objects_with_issues: u64, -} - -#[cfg(test)] -mod tests { - use super::*; - use tokio::time::Duration; - - #[tokio::test] - async fn test_engine_creation() { - let config = EngineConfig::default(); - let coordinator = Arc::new(core::Coordinator::new( - core::CoordinatorConfig::default(), - Arc::new(metrics::Collector::new(metrics::CollectorConfig::default()).await.unwrap()), - CancellationToken::new(), - ).await.unwrap()); - let metrics = Arc::new(metrics::Collector::new(metrics::CollectorConfig::default()).await.unwrap()); - let cancel_token = CancellationToken::new(); - - let engine = Engine::new(config, coordinator, metrics, cancel_token).await; - assert!(engine.is_ok()); - } - - #[tokio::test] - async fn test_path_extraction() { - let config = EngineConfig { - root_path: "/data".to_string(), - ..Default::default() - }; - let coordinator = Arc::new(core::Coordinator::new( - core::CoordinatorConfig::default(), - Arc::new(metrics::Collector::new(metrics::CollectorConfig::default()).await.unwrap()), - CancellationToken::new(), - ).await.unwrap()); - let metrics = Arc::new(metrics::Collector::new(metrics::CollectorConfig::default()).await.unwrap()); - let cancel_token = CancellationToken::new(); - - let engine = Engine::new(config, coordinator, metrics, cancel_token).await.unwrap(); - - let test_path = Path::new("/data/bucket1/object1.txt"); - let (bucket, object) = engine.extract_bucket_object_from_path(test_path).unwrap(); - - assert_eq!(bucket, "bucket1"); - assert_eq!(object, "object1.txt"); - } -} \ No newline at end of file diff --git a/crates/ahm/src/scanner/metrics_collector.rs b/crates/ahm/src/scanner/metrics_collector.rs deleted file mode 100644 index 45c0d4b14..000000000 --- a/crates/ahm/src/scanner/metrics_collector.rs +++ /dev/null @@ -1,526 +0,0 @@ -// 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::{ - collections::HashMap, - sync::Arc, - time::{Duration, Instant, SystemTime}, -}; - -use tokio::sync::RwLock; -use tracing::{debug, error, info, warn}; - -use crate::error::Result; -use super::{HealthIssue, HealthIssueType, Severity}; - -/// Configuration for metrics collection -#[derive(Debug, Clone)] -pub struct MetricsConfig { - /// Collection interval for metrics - pub collection_interval: Duration, - /// Retention period for historical metrics - pub retention_period: Duration, - /// Maximum number of data points to keep in memory - pub max_data_points: usize, - /// Whether to enable detailed metrics collection - pub enable_detailed_metrics: bool, - /// Whether to enable performance profiling - pub enable_profiling: bool, - /// Whether to enable resource usage tracking - pub enable_resource_tracking: bool, -} - -impl Default for MetricsConfig { - fn default() -> Self { - Self { - collection_interval: Duration::from_secs(60), // 1 minute - retention_period: Duration::from_secs(3600 * 24), // 24 hours - max_data_points: 1440, // 24 hours worth of minute-level data - enable_detailed_metrics: true, - enable_profiling: false, - enable_resource_tracking: true, - } - } -} - -/// Scanner performance metrics -#[derive(Debug, Clone)] -pub struct ScannerMetrics { - /// Objects scanned per second - pub objects_per_second: f64, - /// Bytes scanned per second - pub bytes_per_second: f64, - /// Average scan time per object - pub avg_scan_time_per_object: Duration, - /// Total objects scanned in current cycle - pub total_objects_scanned: u64, - /// Total bytes scanned in current cycle - pub total_bytes_scanned: u64, - /// Number of health issues detected - pub health_issues_detected: u64, - /// Scan success rate (percentage) - pub success_rate: f64, - /// Current scan cycle duration - pub current_cycle_duration: Duration, - /// Average scan cycle duration - pub avg_cycle_duration: Duration, - /// Last scan completion time - pub last_scan_completion: Option, -} - -/// Resource usage metrics -#[derive(Debug, Clone)] -pub struct ResourceMetrics { - /// CPU usage percentage - pub cpu_usage_percent: f64, - /// Memory usage in bytes - pub memory_usage_bytes: u64, - /// Memory usage percentage - pub memory_usage_percent: f64, - /// Disk I/O operations per second - pub disk_io_ops_per_sec: f64, - /// Disk I/O bytes per second - pub disk_io_bytes_per_sec: f64, - /// Network I/O bytes per second - pub network_io_bytes_per_sec: f64, - /// Number of active threads - pub active_threads: u32, - /// Number of open file descriptors - pub open_file_descriptors: u32, -} - -/// Health metrics summary -#[derive(Debug, Clone)] -pub struct HealthMetrics { - /// Total health issues by severity - pub issues_by_severity: HashMap, - /// Total health issues by type - pub issues_by_type: HashMap, - /// Objects with health issues - pub objects_with_issues: u64, - /// Percentage of objects with issues - pub objects_with_issues_percent: f64, - /// Last health check time - pub last_health_check: SystemTime, - /// Health score (0-100, higher is better) - pub health_score: f64, -} - -/// Historical metrics data point -#[derive(Debug, Clone)] -pub struct MetricsDataPoint { - pub timestamp: SystemTime, - pub scanner_metrics: ScannerMetrics, - pub resource_metrics: ResourceMetrics, - pub health_metrics: HealthMetrics, -} - -/// Metrics collector for scanner system -pub struct MetricsCollector { - config: MetricsConfig, - current_metrics: Arc>, - historical_data: Arc>>, - collection_start_time: Instant, -} - -/// Current metrics state -#[derive(Debug, Clone)] -pub struct CurrentMetrics { - pub scanner_metrics: ScannerMetrics, - pub resource_metrics: ResourceMetrics, - pub health_metrics: HealthMetrics, - pub last_update: SystemTime, -} - -impl MetricsCollector { - /// Create a new metrics collector - pub fn new(config: MetricsConfig) -> Self { - let collector = Self { - config, - current_metrics: Arc::new(RwLock::new(CurrentMetrics { - scanner_metrics: ScannerMetrics { - objects_per_second: 0.0, - bytes_per_second: 0.0, - avg_scan_time_per_object: Duration::ZERO, - total_objects_scanned: 0, - total_bytes_scanned: 0, - health_issues_detected: 0, - success_rate: 100.0, - current_cycle_duration: Duration::ZERO, - avg_cycle_duration: Duration::ZERO, - last_scan_completion: None, - }, - resource_metrics: ResourceMetrics { - cpu_usage_percent: 0.0, - memory_usage_bytes: 0, - memory_usage_percent: 0.0, - disk_io_ops_per_sec: 0.0, - disk_io_bytes_per_sec: 0.0, - network_io_bytes_per_sec: 0.0, - active_threads: 0, - open_file_descriptors: 0, - }, - health_metrics: HealthMetrics { - issues_by_severity: HashMap::new(), - issues_by_type: HashMap::new(), - objects_with_issues: 0, - objects_with_issues_percent: 0.0, - last_health_check: SystemTime::now(), - health_score: 100.0, - }, - last_update: SystemTime::now(), - })), - historical_data: Arc::new(RwLock::new(Vec::new())), - collection_start_time: Instant::now(), - }; - - info!("Metrics collector created with config: {:?}", collector.config); - collector - } - - /// Start metrics collection - pub async fn start_collection(&self) -> Result<()> { - info!("Starting metrics collection"); - - let collector = self.clone_for_background(); - tokio::spawn(async move { - if let Err(e) = collector.run_collection_loop().await { - error!("Metrics collection error: {}", e); - } - }); - - Ok(()) - } - - /// Stop metrics collection - pub async fn stop_collection(&self) -> Result<()> { - info!("Stopping metrics collection"); - Ok(()) - } - - /// Update scanner metrics - pub async fn update_scanner_metrics(&self, metrics: ScannerMetrics) -> Result<()> { - let mut current = self.current_metrics.write().await; - current.scanner_metrics = metrics; - current.last_update = SystemTime::now(); - Ok(()) - } - - /// Update resource metrics - pub async fn update_resource_metrics(&self, metrics: ResourceMetrics) -> Result<()> { - let mut current = self.current_metrics.write().await; - current.resource_metrics = metrics; - current.last_update = SystemTime::now(); - Ok(()) - } - - /// Update health metrics - pub async fn update_health_metrics(&self, metrics: HealthMetrics) -> Result<()> { - let mut current = self.current_metrics.write().await; - current.health_metrics = metrics; - current.last_update = SystemTime::now(); - Ok(()) - } - - /// Record a health issue - pub async fn record_health_issue(&self, issue: &HealthIssue) -> Result<()> { - let mut current = self.current_metrics.write().await; - - // Update severity count - *current.health_metrics.issues_by_severity.entry(issue.severity).or_insert(0) += 1; - - // Update type count - *current.health_metrics.issues_by_type.entry(issue.issue_type.clone()).or_insert(0) += 1; - - // Update scanner metrics - current.scanner_metrics.health_issues_detected += 1; - - current.last_update = SystemTime::now(); - Ok(()) - } - - /// Get current metrics - pub async fn current_metrics(&self) -> CurrentMetrics { - self.current_metrics.read().await.clone() - } - - /// Get historical metrics - pub async fn historical_metrics(&self, duration: Duration) -> Vec { - let historical = self.historical_data.read().await; - let cutoff_time = SystemTime::now() - duration; - - historical.iter() - .filter(|point| point.timestamp >= cutoff_time) - .cloned() - .collect() - } - - /// Get metrics summary - pub async fn metrics_summary(&self) -> MetricsSummary { - let current = self.current_metrics.read().await; - let historical = self.historical_data.read().await; - - let uptime = self.collection_start_time.elapsed(); - let total_data_points = historical.len(); - - // Calculate averages from historical data - let avg_objects_per_sec = if !historical.is_empty() { - historical.iter() - .map(|point| point.scanner_metrics.objects_per_second) - .sum::() / historical.len() as f64 - } else { - 0.0 - }; - - let avg_bytes_per_sec = if !historical.is_empty() { - historical.iter() - .map(|point| point.scanner_metrics.bytes_per_second) - .sum::() / historical.len() as f64 - } else { - 0.0 - }; - - let avg_cpu_usage = if !historical.is_empty() { - historical.iter() - .map(|point| point.resource_metrics.cpu_usage_percent) - .sum::() / historical.len() as f64 - } else { - 0.0 - }; - - let avg_memory_usage = if !historical.is_empty() { - historical.iter() - .map(|point| point.resource_metrics.memory_usage_percent) - .sum::() / historical.len() as f64 - } else { - 0.0 - }; - - MetricsSummary { - uptime, - total_data_points, - current_scanner_metrics: current.scanner_metrics.clone(), - current_resource_metrics: current.resource_metrics.clone(), - current_health_metrics: current.health_metrics.clone(), - avg_objects_per_sec, - avg_bytes_per_sec, - avg_cpu_usage, - avg_memory_usage, - last_update: current.last_update, - } - } - - /// Clone the collector for background tasks - fn clone_for_background(&self) -> Arc { - Arc::new(Self { - config: self.config.clone(), - current_metrics: self.current_metrics.clone(), - historical_data: self.historical_data.clone(), - collection_start_time: self.collection_start_time, - }) - } - - /// Main collection loop - async fn run_collection_loop(&self) -> Result<()> { - info!("Metrics collection loop started"); - - loop { - // Collect current metrics - self.collect_current_metrics().await?; - - // Store historical data point - self.store_historical_data_point().await?; - - // Clean up old data - self.cleanup_old_data().await?; - - // Wait for next collection interval - tokio::time::sleep(self.config.collection_interval).await; - } - } - - /// Collect current system metrics - async fn collect_current_metrics(&self) -> Result<()> { - if self.config.enable_resource_tracking { - let resource_metrics = self.collect_resource_metrics().await?; - self.update_resource_metrics(resource_metrics).await?; - } - - Ok(()) - } - - /// Collect resource usage metrics - async fn collect_resource_metrics(&self) -> Result { - // TODO: Implement actual resource metrics collection - // For now, return placeholder metrics - Ok(ResourceMetrics { - cpu_usage_percent: 0.0, - memory_usage_bytes: 0, - memory_usage_percent: 0.0, - disk_io_ops_per_sec: 0.0, - disk_io_bytes_per_sec: 0.0, - network_io_bytes_per_sec: 0.0, - active_threads: 0, - open_file_descriptors: 0, - }) - } - - /// Store current metrics as historical data point - async fn store_historical_data_point(&self) -> Result<()> { - let current = self.current_metrics.read().await; - let data_point = MetricsDataPoint { - timestamp: SystemTime::now(), - scanner_metrics: current.scanner_metrics.clone(), - resource_metrics: current.resource_metrics.clone(), - health_metrics: current.health_metrics.clone(), - }; - - let mut historical = self.historical_data.write().await; - historical.push(data_point); - - // Limit the number of data points - if historical.len() > self.config.max_data_points { - historical.remove(0); - } - - Ok(()) - } - - /// Clean up old historical data - async fn cleanup_old_data(&self) -> Result<()> { - let cutoff_time = SystemTime::now() - self.config.retention_period; - let mut historical = self.historical_data.write().await; - - historical.retain(|point| point.timestamp >= cutoff_time); - - Ok(()) - } - - /// Reset all metrics - pub async fn reset_metrics(&self) -> Result<()> { - let mut current = self.current_metrics.write().await; - *current = CurrentMetrics { - scanner_metrics: ScannerMetrics { - objects_per_second: 0.0, - bytes_per_second: 0.0, - avg_scan_time_per_object: Duration::ZERO, - total_objects_scanned: 0, - total_bytes_scanned: 0, - health_issues_detected: 0, - success_rate: 100.0, - current_cycle_duration: Duration::ZERO, - avg_cycle_duration: Duration::ZERO, - last_scan_completion: None, - }, - resource_metrics: ResourceMetrics { - cpu_usage_percent: 0.0, - memory_usage_bytes: 0, - memory_usage_percent: 0.0, - disk_io_ops_per_sec: 0.0, - disk_io_bytes_per_sec: 0.0, - network_io_bytes_per_sec: 0.0, - active_threads: 0, - open_file_descriptors: 0, - }, - health_metrics: HealthMetrics { - issues_by_severity: HashMap::new(), - issues_by_type: HashMap::new(), - objects_with_issues: 0, - objects_with_issues_percent: 0.0, - last_health_check: SystemTime::now(), - health_score: 100.0, - }, - last_update: SystemTime::now(), - }; - - let mut historical = self.historical_data.write().await; - historical.clear(); - - Ok(()) - } -} - -/// Summary of all metrics -#[derive(Debug, Clone)] -pub struct MetricsSummary { - pub uptime: Duration, - pub total_data_points: usize, - pub current_scanner_metrics: ScannerMetrics, - pub current_resource_metrics: ResourceMetrics, - pub current_health_metrics: HealthMetrics, - pub avg_objects_per_sec: f64, - pub avg_bytes_per_sec: f64, - pub avg_cpu_usage: f64, - pub avg_memory_usage: f64, - pub last_update: SystemTime, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_metrics_collector_creation() { - let config = MetricsConfig::default(); - let collector = MetricsCollector::new(config); - let metrics = collector.current_metrics().await; - assert_eq!(metrics.scanner_metrics.total_objects_scanned, 0); - } - - #[tokio::test] - async fn test_metrics_update() { - let config = MetricsConfig::default(); - let collector = MetricsCollector::new(config); - - let scanner_metrics = ScannerMetrics { - objects_per_second: 100.0, - bytes_per_second: 1024.0, - avg_scan_time_per_object: Duration::from_millis(10), - total_objects_scanned: 1000, - total_bytes_scanned: 1024000, - health_issues_detected: 5, - success_rate: 99.5, - current_cycle_duration: Duration::from_secs(60), - avg_cycle_duration: Duration::from_secs(65), - last_scan_completion: Some(SystemTime::now()), - }; - - collector.update_scanner_metrics(scanner_metrics).await.unwrap(); - - let current = collector.current_metrics().await; - assert_eq!(current.scanner_metrics.total_objects_scanned, 1000); - assert_eq!(current.scanner_metrics.health_issues_detected, 5); - } - - #[tokio::test] - async fn test_health_issue_recording() { - let config = MetricsConfig::default(); - let collector = MetricsCollector::new(config); - - let issue = HealthIssue { - issue_type: HealthIssueType::DiskFull, - severity: Severity::High, - bucket: "test-bucket".to_string(), - object: "test-object".to_string(), - description: "Test issue".to_string(), - metadata: None, - }; - - collector.record_health_issue(&issue).await.unwrap(); - - let current = collector.current_metrics().await; - assert_eq!(current.scanner_metrics.health_issues_detected, 1); - assert_eq!(current.health_metrics.issues_by_severity.get(&Severity::High), Some(&1)); - } -} \ No newline at end of file diff --git a/crates/ahm/src/scanner/object_scanner.rs b/crates/ahm/src/scanner/object_scanner.rs deleted file mode 100644 index fd438d8fa..000000000 --- a/crates/ahm/src/scanner/object_scanner.rs +++ /dev/null @@ -1,419 +0,0 @@ -// 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::{ - collections::HashMap, - path::Path, - sync::Arc, - time::{Duration, SystemTime}, -}; - -use tokio::sync::RwLock; -use tracing::info; - -use crate::error::Result; -use super::{HealthIssue, HealthIssueType, Severity}; - -/// Configuration for object scanning -#[derive(Debug, Clone)] -pub struct ObjectScannerConfig { - /// Whether to perform checksum verification - pub verify_checksum: bool, - /// Whether to check replication status - pub check_replication: bool, - /// Whether to validate metadata consistency - pub validate_metadata: bool, - /// Maximum object size to scan (bytes) - pub max_object_size: u64, - /// Minimum object size (bytes) - pub min_object_size: u64, - /// Timeout for individual object scans - pub scan_timeout: Duration, - /// Whether to enable deep scanning (bitrot detection) - pub enable_deep_scan: bool, -} - -impl Default for ObjectScannerConfig { - fn default() -> Self { - Self { - verify_checksum: true, - check_replication: true, - validate_metadata: true, - max_object_size: 1024 * 1024 * 1024 * 1024, // 1TB - min_object_size: 0, - scan_timeout: Duration::from_secs(30), - enable_deep_scan: false, - } - } -} - -/// Result of scanning a single object -#[derive(Debug, Clone)] -pub struct ObjectScanResult { - /// Object identifier - pub bucket: String, - pub object: String, - pub version_id: Option, - /// Scan success status - pub success: bool, - /// Object metadata discovered - pub metadata: Option, - /// Health issues detected - pub health_issues: Vec, - /// Time taken to scan this object - pub scan_duration: Duration, - /// Error message if scan failed - pub error_message: Option, -} - -/// Object metadata -#[derive(Debug, Clone)] -pub struct ObjectMetadata { - pub size: u64, - pub modified_time: SystemTime, - pub content_type: String, - pub etag: String, - pub checksum: Option, - pub replication_status: Option, - pub encryption_status: Option, - pub custom_metadata: HashMap, -} - -/// Object scanner for individual object health checking -pub struct ObjectScanner { - config: ObjectScannerConfig, - statistics: Arc>, -} - -/// Statistics for object scanning -#[derive(Debug, Clone, Default)] -pub struct ObjectScannerStatistics { - pub objects_scanned: u64, - pub objects_with_issues: u64, - pub total_issues_found: u64, - pub total_scan_time: Duration, - pub average_scan_time: Duration, - pub checksum_verifications: u64, - pub checksum_failures: u64, - pub replication_checks: u64, - pub replication_failures: u64, -} - -impl ObjectScanner { - /// Create a new object scanner - pub fn new(config: ObjectScannerConfig) -> Self { - Self { - config, - statistics: Arc::new(RwLock::new(ObjectScannerStatistics::default())), - } - } - - /// Scan a single object for health issues - pub async fn scan_object(&self, bucket: &str, object: &str, version_id: Option<&str>, path: &Path) -> Result { - let scan_start = std::time::Instant::now(); - let mut health_issues = Vec::new(); - let mut error_message = None; - - // Check if file exists - if !path.exists() { - return Ok(ObjectScanResult { - bucket: bucket.to_string(), - object: object.to_string(), - version_id: version_id.map(|v| v.to_string()), - success: false, - metadata: None, - health_issues: vec![HealthIssue { - issue_type: HealthIssueType::MissingReplica, - severity: Severity::Critical, - bucket: bucket.to_string(), - object: object.to_string(), - description: "Object file does not exist".to_string(), - metadata: None, - }], - scan_duration: scan_start.elapsed(), - error_message: Some("Object file not found".to_string()), - }); - } - - // Get file metadata - let metadata = match std::fs::metadata(path) { - Ok(metadata) => metadata, - Err(e) => { - error_message = Some(format!("Failed to read file metadata: {}", e)); - health_issues.push(HealthIssue { - issue_type: HealthIssueType::DiskReadError, - severity: Severity::High, - bucket: bucket.to_string(), - object: object.to_string(), - description: "Failed to read file metadata".to_string(), - metadata: None, - }); - return Ok(ObjectScanResult { - bucket: bucket.to_string(), - object: object.to_string(), - version_id: version_id.map(|v| v.to_string()), - success: false, - metadata: None, - health_issues, - scan_duration: scan_start.elapsed(), - error_message, - }); - } - }; - - // Check file size - let file_size = metadata.len(); - if file_size < self.config.min_object_size { - health_issues.push(HealthIssue { - issue_type: HealthIssueType::ObjectTooSmall, - severity: Severity::Low, - bucket: bucket.to_string(), - object: object.to_string(), - description: format!("Object size {} is below minimum {}", file_size, self.config.min_object_size), - metadata: None, - }); - } - - if file_size > self.config.max_object_size { - health_issues.push(HealthIssue { - issue_type: HealthIssueType::ObjectTooLarge, - severity: Severity::Medium, - bucket: bucket.to_string(), - object: object.to_string(), - description: format!("Object size {} exceeds maximum {}", file_size, self.config.max_object_size), - metadata: None, - }); - } - - // Verify checksum if enabled - let checksum = if self.config.verify_checksum { - match self.verify_checksum(path).await { - Ok(cs) => { - self.update_statistics(|stats| stats.checksum_verifications += 1).await; - Some(cs) - } - Err(_e) => { - self.update_statistics(|stats| stats.checksum_failures += 1).await; - health_issues.push(HealthIssue { - issue_type: HealthIssueType::ChecksumMismatch, - severity: Severity::High, - bucket: bucket.to_string(), - object: object.to_string(), - description: "Checksum verification failed".to_string(), - metadata: None, - }); - None - } - } - } else { - None - }; - - // Check replication status if enabled - let replication_status = if self.config.check_replication { - match self.check_replication_status(bucket, object).await { - Ok(status) => { - self.update_statistics(|stats| stats.replication_checks += 1).await; - Some(status) - } - Err(_e) => { - self.update_statistics(|stats| stats.replication_failures += 1).await; - health_issues.push(HealthIssue { - issue_type: HealthIssueType::MissingReplica, - severity: Severity::High, - bucket: bucket.to_string(), - object: object.to_string(), - description: "Replication status check failed".to_string(), - metadata: None, - }); - None - } - } - } else { - None - }; - - // Validate metadata if enabled - if self.config.validate_metadata { - if let Some(issue) = self.validate_metadata(bucket, object, &metadata).await? { - health_issues.push(issue); - } - } - - // Create object metadata - let object_metadata = ObjectMetadata { - size: file_size, - modified_time: metadata.modified().unwrap_or(SystemTime::now()), - content_type: self.detect_content_type(path), - etag: self.calculate_etag(path).await?, - checksum, - replication_status, - encryption_status: None, // TODO: Implement encryption status check - custom_metadata: HashMap::new(), // TODO: Extract custom metadata - }; - - let scan_duration = scan_start.elapsed(); - let success = health_issues.is_empty(); - - // Update statistics - self.update_statistics(|stats| { - stats.objects_scanned += 1; - if !health_issues.is_empty() { - stats.objects_with_issues += 1; - stats.total_issues_found += health_issues.len() as u64; - } - stats.total_scan_time += scan_duration; - stats.average_scan_time = Duration::from_millis( - stats.total_scan_time.as_millis() as u64 / stats.objects_scanned.max(1) - ); - }).await; - - Ok(ObjectScanResult { - bucket: bucket.to_string(), - object: object.to_string(), - version_id: version_id.map(|v| v.to_string()), - success, - metadata: Some(object_metadata), - health_issues, - scan_duration, - error_message, - }) - } - - /// Verify object checksum - async fn verify_checksum(&self, _path: &Path) -> Result { - // TODO: Implement actual checksum verification - // For now, return a placeholder checksum - Ok("placeholder_checksum".to_string()) - } - - /// Check object replication status - async fn check_replication_status(&self, _bucket: &str, _object: &str) -> Result { - // TODO: Implement actual replication status checking - // For now, return a placeholder status - Ok("replicated".to_string()) - } - - /// Validate object metadata - async fn validate_metadata(&self, _bucket: &str, _object: &str, _metadata: &std::fs::Metadata) -> Result> { - // TODO: Implement actual metadata validation - // For now, return None (no issues) - Ok(None) - } - - /// Detect content type from file extension - fn detect_content_type(&self, path: &Path) -> String { - if let Some(extension) = path.extension() { - match extension.to_str().unwrap_or("").to_lowercase().as_str() { - "txt" => "text/plain", - "json" => "application/json", - "xml" => "application/xml", - "html" | "htm" => "text/html", - "css" => "text/css", - "js" => "application/javascript", - "png" => "image/png", - "jpg" | "jpeg" => "image/jpeg", - "gif" => "image/gif", - "pdf" => "application/pdf", - "zip" => "application/zip", - "tar" => "application/x-tar", - "gz" => "application/gzip", - _ => "application/octet-stream", - }.to_string() - } else { - "application/octet-stream".to_string() - } - } - - /// Calculate object ETag - async fn calculate_etag(&self, _path: &Path) -> Result { - // TODO: Implement actual ETag calculation - // For now, return a placeholder ETag - Ok("placeholder_etag".to_string()) - } - - /// Update scanner statistics - async fn update_statistics(&self, update_fn: F) - where - F: FnOnce(&mut ObjectScannerStatistics), - { - let mut stats = self.statistics.write().await; - update_fn(&mut stats); - } - - /// Get current statistics - pub async fn statistics(&self) -> ObjectScannerStatistics { - self.statistics.read().await.clone() - } - - /// Reset statistics - pub async fn reset_statistics(&self) { - let mut stats = self.statistics.write().await; - *stats = ObjectScannerStatistics::default(); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - use std::fs::File; - use std::io::Write; - - #[tokio::test] - async fn test_object_scanner_creation() { - let config = ObjectScannerConfig::default(); - let scanner = ObjectScanner::new(config); - assert_eq!(scanner.statistics().await.objects_scanned, 0); - } - - #[tokio::test] - async fn test_content_type_detection() { - let config = ObjectScannerConfig::default(); - let scanner = ObjectScanner::new(config); - - let path = Path::new("test.txt"); - assert_eq!(scanner.detect_content_type(path), "text/plain"); - - let path = Path::new("test.json"); - assert_eq!(scanner.detect_content_type(path), "application/json"); - - let path = Path::new("test.unknown"); - assert_eq!(scanner.detect_content_type(path), "application/octet-stream"); - } - - #[tokio::test] - async fn test_object_scanning() { - let temp_dir = TempDir::new().unwrap(); - let test_file = temp_dir.path().join("test.txt"); - - // Create a test file - let mut file = File::create(&test_file).unwrap(); - writeln!(file, "test content").unwrap(); - - let config = ObjectScannerConfig::default(); - let scanner = ObjectScanner::new(config); - - let result = scanner.scan_object("test-bucket", "test.txt", None, &test_file).await.unwrap(); - - assert!(result.success); - assert_eq!(result.bucket, "test-bucket"); - assert_eq!(result.object, "test.txt"); - assert!(result.metadata.is_some()); - - let metadata = result.metadata.unwrap(); - assert!(metadata.size > 0); - assert_eq!(metadata.content_type, "text/plain"); - } -} \ No newline at end of file From 2aa7a631efa192b574a392d95b7ab0fb40d5c1ff Mon Sep 17 00:00:00 2001 From: dandan Date: Thu, 10 Jul 2025 15:44:37 +0800 Subject: [PATCH 03/29] feat: refactor scanner module and add data usage statistics - Move scanner code to scanner/ subdirectory for better organization - Add data usage statistics collection and persistence - Implement histogram support for size and version distribution - Add global cancel token management for scanner operations - Integrate scanner with ECStore for comprehensive data analysis - Update error handling and improve test isolation - Add data usage API endpoints and backend integration Signed-off-by: junxiang Mu <1948535941@qq.com> --- crates/ahm/src/lib.rs | 1 - crates/ahm/src/metrics.rs | 284 ------------ crates/ahm/src/scanner.rs | 902 -------------------------------------- 3 files changed, 1187 deletions(-) delete mode 100644 crates/ahm/src/metrics.rs delete mode 100644 crates/ahm/src/scanner.rs diff --git a/crates/ahm/src/lib.rs b/crates/ahm/src/lib.rs index d3f6e151b..d3d656194 100644 --- a/crates/ahm/src/lib.rs +++ b/crates/ahm/src/lib.rs @@ -17,7 +17,6 @@ use tokio_util::sync::CancellationToken; pub mod error; pub mod scanner; -pub mod metrics; pub use error::{Error, Result}; pub use scanner::{ diff --git a/crates/ahm/src/metrics.rs b/crates/ahm/src/metrics.rs deleted file mode 100644 index b541b2fc3..000000000 --- a/crates/ahm/src/metrics.rs +++ /dev/null @@ -1,284 +0,0 @@ -// 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::{ - collections::HashMap, - sync::atomic::{AtomicU64, Ordering}, - time::{Duration, SystemTime}, -}; - -use serde::{Deserialize, Serialize}; -use tracing::info; - -/// Scanner metrics similar to MinIO's scanner metrics -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct ScannerMetrics { - /// Total objects scanned since server start - pub objects_scanned: u64, - /// Total object versions scanned since server start - pub versions_scanned: u64, - /// Total directories scanned since server start - pub directories_scanned: u64, - /// Total bucket scans started since server start - pub bucket_scans_started: u64, - /// Total bucket scans finished since server start - pub bucket_scans_finished: u64, - /// Total objects with health issues found - pub objects_with_issues: u64, - /// Total heal tasks queued - pub heal_tasks_queued: u64, - /// Total heal tasks completed - pub heal_tasks_completed: u64, - /// Total heal tasks failed - pub heal_tasks_failed: u64, - /// Last scan activity time - pub last_activity: Option, - /// Current scan cycle - pub current_cycle: u64, - /// Total scan cycles completed - pub total_cycles: u64, - /// Current scan duration - pub current_scan_duration: Option, - /// Average scan duration - pub avg_scan_duration: Duration, - /// Objects scanned per second - pub objects_per_second: f64, - /// Buckets scanned per second - pub buckets_per_second: f64, - /// Storage metrics by bucket - pub bucket_metrics: HashMap, - /// Disk metrics - pub disk_metrics: HashMap, -} - -/// Bucket-specific metrics -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct BucketMetrics { - /// Bucket name - pub bucket: String, - /// Total objects in bucket - pub total_objects: u64, - /// Total size of objects in bucket (bytes) - pub total_size: u64, - /// Objects with health issues - pub objects_with_issues: u64, - /// Last scan time - pub last_scan_time: Option, - /// Scan duration - pub scan_duration: Option, - /// Heal tasks queued for this bucket - pub heal_tasks_queued: u64, - /// Heal tasks completed for this bucket - pub heal_tasks_completed: u64, - /// Heal tasks failed for this bucket - pub heal_tasks_failed: u64, -} - -/// Disk-specific metrics -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct DiskMetrics { - /// Disk path - pub disk_path: String, - /// Total disk space (bytes) - pub total_space: u64, - /// Used disk space (bytes) - pub used_space: u64, - /// Free disk space (bytes) - pub free_space: u64, - /// Objects scanned on this disk - pub objects_scanned: u64, - /// Objects with issues on this disk - pub objects_with_issues: u64, - /// Last scan time - pub last_scan_time: Option, - /// Whether disk is online - pub is_online: bool, - /// Whether disk is being scanned - pub is_scanning: bool, -} - -/// Thread-safe metrics collector -pub struct MetricsCollector { - /// Atomic counters for real-time metrics - objects_scanned: AtomicU64, - versions_scanned: AtomicU64, - directories_scanned: AtomicU64, - bucket_scans_started: AtomicU64, - bucket_scans_finished: AtomicU64, - objects_with_issues: AtomicU64, - heal_tasks_queued: AtomicU64, - heal_tasks_completed: AtomicU64, - heal_tasks_failed: AtomicU64, - current_cycle: AtomicU64, - total_cycles: AtomicU64, -} - -impl MetricsCollector { - /// Create a new metrics collector - pub fn new() -> Self { - Self { - objects_scanned: AtomicU64::new(0), - versions_scanned: AtomicU64::new(0), - directories_scanned: AtomicU64::new(0), - bucket_scans_started: AtomicU64::new(0), - bucket_scans_finished: AtomicU64::new(0), - objects_with_issues: AtomicU64::new(0), - heal_tasks_queued: AtomicU64::new(0), - heal_tasks_completed: AtomicU64::new(0), - heal_tasks_failed: AtomicU64::new(0), - current_cycle: AtomicU64::new(0), - total_cycles: AtomicU64::new(0), - } - } - - /// Increment objects scanned count - pub fn increment_objects_scanned(&self, count: u64) { - self.objects_scanned.fetch_add(count, Ordering::Relaxed); - } - - /// Increment versions scanned count - pub fn increment_versions_scanned(&self, count: u64) { - self.versions_scanned.fetch_add(count, Ordering::Relaxed); - } - - /// Increment directories scanned count - pub fn increment_directories_scanned(&self, count: u64) { - self.directories_scanned.fetch_add(count, Ordering::Relaxed); - } - - /// Increment bucket scans started count - pub fn increment_bucket_scans_started(&self, count: u64) { - self.bucket_scans_started.fetch_add(count, Ordering::Relaxed); - } - - /// Increment bucket scans finished count - pub fn increment_bucket_scans_finished(&self, count: u64) { - self.bucket_scans_finished.fetch_add(count, Ordering::Relaxed); - } - - /// Increment objects with issues count - pub fn increment_objects_with_issues(&self, count: u64) { - self.objects_with_issues.fetch_add(count, Ordering::Relaxed); - } - - /// Increment heal tasks queued count - pub fn increment_heal_tasks_queued(&self, count: u64) { - self.heal_tasks_queued.fetch_add(count, Ordering::Relaxed); - } - - /// Increment heal tasks completed count - pub fn increment_heal_tasks_completed(&self, count: u64) { - self.heal_tasks_completed.fetch_add(count, Ordering::Relaxed); - } - - /// Increment heal tasks failed count - pub fn increment_heal_tasks_failed(&self, count: u64) { - self.heal_tasks_failed.fetch_add(count, Ordering::Relaxed); - } - - /// Set current cycle - pub fn set_current_cycle(&self, cycle: u64) { - self.current_cycle.store(cycle, Ordering::Relaxed); - } - - /// Increment total cycles - pub fn increment_total_cycles(&self) { - self.total_cycles.fetch_add(1, Ordering::Relaxed); - } - - /// Get current metrics snapshot - pub fn get_metrics(&self) -> ScannerMetrics { - ScannerMetrics { - objects_scanned: self.objects_scanned.load(Ordering::Relaxed), - versions_scanned: self.versions_scanned.load(Ordering::Relaxed), - directories_scanned: self.directories_scanned.load(Ordering::Relaxed), - bucket_scans_started: self.bucket_scans_started.load(Ordering::Relaxed), - bucket_scans_finished: self.bucket_scans_finished.load(Ordering::Relaxed), - objects_with_issues: self.objects_with_issues.load(Ordering::Relaxed), - heal_tasks_queued: self.heal_tasks_queued.load(Ordering::Relaxed), - heal_tasks_completed: self.heal_tasks_completed.load(Ordering::Relaxed), - heal_tasks_failed: self.heal_tasks_failed.load(Ordering::Relaxed), - last_activity: Some(SystemTime::now()), - current_cycle: self.current_cycle.load(Ordering::Relaxed), - total_cycles: self.total_cycles.load(Ordering::Relaxed), - current_scan_duration: None, // Will be set by scanner - avg_scan_duration: Duration::ZERO, // Will be calculated - objects_per_second: 0.0, // Will be calculated - buckets_per_second: 0.0, // Will be calculated - bucket_metrics: HashMap::new(), // Will be populated by scanner - disk_metrics: HashMap::new(), // Will be populated by scanner - } - } - - /// Reset all metrics - pub fn reset(&self) { - self.objects_scanned.store(0, Ordering::Relaxed); - self.versions_scanned.store(0, Ordering::Relaxed); - self.directories_scanned.store(0, Ordering::Relaxed); - self.bucket_scans_started.store(0, Ordering::Relaxed); - self.bucket_scans_finished.store(0, Ordering::Relaxed); - self.objects_with_issues.store(0, Ordering::Relaxed); - self.heal_tasks_queued.store(0, Ordering::Relaxed); - self.heal_tasks_completed.store(0, Ordering::Relaxed); - self.heal_tasks_failed.store(0, Ordering::Relaxed); - self.current_cycle.store(0, Ordering::Relaxed); - self.total_cycles.store(0, Ordering::Relaxed); - - info!("Scanner metrics reset"); - } -} - -impl Default for MetricsCollector { - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_metrics_collector_creation() { - let collector = MetricsCollector::new(); - let metrics = collector.get_metrics(); - assert_eq!(metrics.objects_scanned, 0); - assert_eq!(metrics.versions_scanned, 0); - } - - #[test] - fn test_metrics_increment() { - let collector = MetricsCollector::new(); - - collector.increment_objects_scanned(10); - collector.increment_versions_scanned(5); - collector.increment_objects_with_issues(2); - - let metrics = collector.get_metrics(); - assert_eq!(metrics.objects_scanned, 10); - assert_eq!(metrics.versions_scanned, 5); - assert_eq!(metrics.objects_with_issues, 2); - } - - #[test] - fn test_metrics_reset() { - let collector = MetricsCollector::new(); - - collector.increment_objects_scanned(10); - collector.reset(); - - let metrics = collector.get_metrics(); - assert_eq!(metrics.objects_scanned, 0); - } -} \ No newline at end of file diff --git a/crates/ahm/src/scanner.rs b/crates/ahm/src/scanner.rs deleted file mode 100644 index 52f92bba3..000000000 --- a/crates/ahm/src/scanner.rs +++ /dev/null @@ -1,902 +0,0 @@ -// 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::{ - collections::HashMap, - sync::Arc, - time::{Duration, SystemTime}, -}; - -use rustfs_ecstore as ecstore; -use ecstore::{ - disk::{DiskAPI, DiskStore, WalkDirOptions}, - set_disk::SetDisks, -}; -use rustfs_filemeta::MetacacheReader; -use tokio::sync::RwLock; -use tokio_util::sync::CancellationToken; -use tracing::{debug, error, info, warn}; - -use crate::{ - error::{Error, Result}, - metrics::{BucketMetrics, DiskMetrics, MetricsCollector, ScannerMetrics}, -}; - -/// Custom scan mode enum for AHM scanner -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ScanMode { - /// Normal scan - basic object discovery and metadata collection - Normal, - /// Deep scan - includes EC verification and integrity checks - Deep, -} - -impl Default for ScanMode { - fn default() -> Self { - ScanMode::Normal - } -} - -/// Scanner configuration -#[derive(Debug, Clone)] -pub struct ScannerConfig { - /// Scan interval between cycles - pub scan_interval: Duration, - /// Deep scan interval (how often to perform deep scan) - pub deep_scan_interval: Duration, - /// Maximum concurrent scans - pub max_concurrent_scans: usize, - /// Whether to enable healing - pub enable_healing: bool, - /// Whether to enable metrics collection - pub enable_metrics: bool, - /// Current scan mode (normal, deep) - pub scan_mode: ScanMode, -} - -impl Default for ScannerConfig { - fn default() -> Self { - Self { - scan_interval: Duration::from_secs(3600), // 1 hour - deep_scan_interval: Duration::from_secs(3600), // 1 hour - max_concurrent_scans: 20, - enable_healing: true, - enable_metrics: true, - scan_mode: ScanMode::Normal, - } - } -} - -/// Scanner state -#[derive(Debug)] -pub struct ScannerState { - /// Whether scanner is running - pub is_running: bool, - /// Current scan cycle - pub current_cycle: u64, - /// Last scan start time - pub last_scan_start: Option, - /// Last scan end time - pub last_scan_end: Option, - /// Current scan duration - pub current_scan_duration: Option, - /// Last deep scan time - pub last_deep_scan_time: Option, - /// Buckets being scanned - pub scanning_buckets: Vec, - /// Disks being scanned - pub scanning_disks: Vec, -} - -impl Default for ScannerState { - fn default() -> Self { - Self { - is_running: false, - current_cycle: 0, - last_scan_start: None, - last_scan_end: None, - current_scan_duration: None, - last_deep_scan_time: None, - scanning_buckets: Vec::new(), - scanning_disks: Vec::new(), - } - } -} - -/// AHM Scanner - Automatic Health Management Scanner -/// -/// This scanner monitors the health of objects in the RustFS storage system. -/// It integrates with ECStore's SetDisks to perform real data scanning and -/// collects metrics similar to MinIO's scanner. -/// -/// The scanner operates on EC (Erasure Coding) sets, where each set contains -/// multiple disks that store the same objects with different shards. -pub struct Scanner { - /// Scanner configuration - config: Arc>, - /// Scanner state - state: Arc>, - /// Metrics collector - metrics: Arc, - /// Bucket metrics cache - bucket_metrics: Arc>>, - /// Disk metrics cache - disk_metrics: Arc>>, - /// EC Set disks - represents a complete erasure coding set - set_disks: Arc, - /// Cancellation token for graceful shutdown - cancel_token: CancellationToken, -} - -impl Scanner { - /// Create a new scanner - pub fn new(set_disks: Arc, config: Option) -> Self { - let config = config.unwrap_or_default(); - let cancel_token = CancellationToken::new(); - - info!("Creating AHM scanner for EC set with {} disks", set_disks.set_drive_count); - - Self { - config: Arc::new(RwLock::new(config)), - state: Arc::new(RwLock::new(ScannerState::default())), - metrics: Arc::new(MetricsCollector::new()), - bucket_metrics: Arc::new(RwLock::new(HashMap::new())), - disk_metrics: Arc::new(RwLock::new(HashMap::new())), - set_disks, - cancel_token, - } - } - - /// Start the scanner - pub async fn start(&self) -> Result<()> { - let mut state = self.state.write().await; - - if state.is_running { - warn!("Scanner is already running"); - return Ok(()); - } - - state.is_running = true; - state.last_scan_start = Some(SystemTime::now()); - - info!("Starting AHM scanner"); - - // Start background scan loop - let scanner = self.clone_for_background(); - tokio::spawn(async move { - if let Err(e) = scanner.scan_loop().await { - error!("Scanner loop failed: {}", e); - } - }); - - Ok(()) - } - - /// Stop the scanner gracefully - pub async fn stop(&self) -> Result<()> { - let mut state = self.state.write().await; - - if !state.is_running { - warn!("Scanner is not running"); - return Ok(()); - } - - info!("Stopping AHM scanner gracefully..."); - - // Trigger cancellation - self.cancel_token.cancel(); - - state.is_running = false; - state.last_scan_end = Some(SystemTime::now()); - - if let Some(start_time) = state.last_scan_start { - state.current_scan_duration = Some( - SystemTime::now() - .duration_since(start_time) - .unwrap_or(Duration::ZERO) - ); - } - - info!("AHM scanner stopped"); - Ok(()) - } - - /// Get a clone of the cancellation token (for external graceful shutdown) - pub fn cancellation_token(&self) -> CancellationToken { - self.cancel_token.clone() - } - - /// Get current scanner metrics - pub async fn get_metrics(&self) -> ScannerMetrics { - let mut metrics = self.metrics.get_metrics(); - - // Add bucket metrics - let bucket_metrics = self.bucket_metrics.read().await; - metrics.bucket_metrics = bucket_metrics.clone(); - - // Add disk metrics - let disk_metrics = self.disk_metrics.read().await; - metrics.disk_metrics = disk_metrics.clone(); - - // Add current scan duration - let state = self.state.read().await; - metrics.current_scan_duration = state.current_scan_duration; - - metrics - } - - /// Perform a single scan cycle - pub async fn scan_cycle(&self) -> Result<()> { - let start_time = SystemTime::now(); - - info!("Starting scan cycle {}", self.metrics.get_metrics().current_cycle + 1); - - // Update state - { - let mut state = self.state.write().await; - state.current_cycle += 1; - state.last_scan_start = Some(start_time); - state.scanning_buckets.clear(); - state.scanning_disks.clear(); - } - - self.metrics.set_current_cycle(self.state.read().await.current_cycle); - self.metrics.increment_total_cycles(); - - // Get online disks from the EC set - let (disks, _) = self.set_disks.get_online_disks_with_healing(false).await; - - if disks.is_empty() { - warn!("No online disks available for scanning"); - return Ok(()); - } - - info!("Scanning {} online disks", disks.len()); - - // Phase 1: Scan all disks concurrently to collect object metadata - let config = self.config.read().await; - let semaphore = Arc::new(tokio::sync::Semaphore::new(config.max_concurrent_scans)); - drop(config); - let mut scan_futures = Vec::new(); - - for disk in disks.clone() { - let semaphore = semaphore.clone(); - let scanner = self.clone_for_background(); - - let future = async move { - let _permit = semaphore.acquire().await.unwrap(); - scanner.scan_disk(&disk).await - }; - - scan_futures.push(future); - } - - // Wait for all scans to complete - let mut results = Vec::new(); - for future in scan_futures { - results.push(future.await); - } - - // Check results and collect object metadata - let mut successful_scans = 0; - let mut failed_scans = 0; - let mut all_disk_objects = Vec::new(); - - for result in results { - match result { - Ok(disk_objects) => { - successful_scans += 1; - all_disk_objects.push(disk_objects); - } - Err(e) => { - failed_scans += 1; - error!("Disk scan failed: {}", e); - // Add empty map for failed disk - all_disk_objects.push(HashMap::new()); - } - } - - } - - // Phase 2: Analyze object distribution and perform EC verification - if successful_scans > 0 { - if let Err(e) = self.analyze_object_distribution(&all_disk_objects, &disks).await { - error!("Object distribution analysis failed: {}", e); - } - } - - // Update scan duration - let scan_duration = SystemTime::now() - .duration_since(start_time) - .unwrap_or(Duration::ZERO); - - { - let mut state = self.state.write().await; - state.last_scan_end = Some(SystemTime::now()); - state.current_scan_duration = Some(scan_duration); - } - - info!("Completed scan cycle in {:?} ({} successful, {} failed)", - scan_duration, successful_scans, failed_scans); - Ok(()) - } - - /// Scan a single disk - async fn scan_disk(&self, disk: &DiskStore) -> Result>> { - let disk_path = disk.path().to_string_lossy().to_string(); - - info!("Scanning disk: {}", disk_path); - - // Update disk metrics - { - let mut disk_metrics = self.disk_metrics.write().await; - let metrics = disk_metrics.entry(disk_path.clone()).or_insert_with(|| DiskMetrics { - disk_path: disk_path.clone(), - ..Default::default() - }); - - metrics.is_scanning = true; - metrics.last_scan_time = Some(SystemTime::now()); - - // Get disk info - if let Ok(disk_info) = disk.disk_info(&ecstore::disk::DiskInfoOptions { - disk_id: disk_path.clone(), - metrics: true, - noop: false, - }).await { - metrics.total_space = disk_info.total; - metrics.used_space = disk_info.used; - metrics.free_space = disk_info.free; - metrics.is_online = disk.is_online().await; - } - } - - // Update state - { - let mut state = self.state.write().await; - state.scanning_disks.push(disk_path.clone()); - } - - // List volumes (buckets) on this disk - let volumes = match disk.list_volumes().await { - Ok(volumes) => volumes, - Err(e) => { - error!("Failed to list volumes on disk {}: {}", disk_path, e); - return Err(Error::Storage(e.into())); - } - }; - - // Scan each volume and collect object metadata - let mut disk_objects = HashMap::new(); - for volume in volumes { - // 检查取消信号 - if self.cancel_token.is_cancelled() { - info!("Cancellation requested, stopping disk scan"); - break; - } - - match self.scan_volume(disk, &volume.name).await { - Ok(object_metadata) => { - disk_objects.insert(volume.name, object_metadata); - } - Err(e) => { - error!("Failed to scan volume {} on disk {}: {}", volume.name, disk_path, e); - continue; - } - } - } - - // Update disk metrics after scan - { - let mut disk_metrics = self.disk_metrics.write().await; - if let Some(metrics) = disk_metrics.get_mut(&disk_path) { - metrics.is_scanning = false; - } - } - - // Update state - { - let mut state = self.state.write().await; - state.scanning_disks.retain(|d| d != &disk_path); - } - - Ok(disk_objects) - } - - /// Scan a single volume (bucket) and collect object information - /// - /// This method collects all objects from a disk for a specific bucket. - /// It returns a map of object names to their metadata for later analysis. - async fn scan_volume(&self, disk: &DiskStore, bucket: &str) -> Result> { - info!("Scanning bucket: {} on disk: {}", bucket, disk.to_string()); - - // Update bucket metrics - { - let mut bucket_metrics = self.bucket_metrics.write().await; - let metrics = bucket_metrics.entry(bucket.to_string()).or_insert_with(|| BucketMetrics { - bucket: bucket.to_string(), - ..Default::default() - }); - - metrics.last_scan_time = Some(SystemTime::now()); - } - - // Update state - { - let mut state = self.state.write().await; - state.scanning_buckets.push(bucket.to_string()); - } - - self.metrics.increment_bucket_scans_started(1); - - let scan_start = SystemTime::now(); - - // Walk through all objects in the bucket - let walk_opts = WalkDirOptions { - bucket: bucket.to_string(), - base_dir: String::new(), - recursive: true, - report_notfound: false, - filter_prefix: None, - forward_to: None, - limit: 0, - disk_id: String::new(), - }; - - // Use a buffer to collect scan results for processing - let mut scan_buffer = Vec::new(); - - if let Err(e) = disk.walk_dir(walk_opts, &mut scan_buffer).await { - error!("Failed to walk directory for bucket {}: {}", bucket, e); - return Err(Error::Storage(e.into())); - } - - // Process the scan results using MetacacheReader - let mut reader = MetacacheReader::new(std::io::Cursor::new(scan_buffer)); - let mut objects_scanned = 0u64; - let mut objects_with_issues = 0u64; - let mut object_metadata = HashMap::new(); - - // Process each object entry - while let Ok(Some(mut entry)) = reader.peek().await { - objects_scanned += 1; - // Check if this is an actual object (not just a directory) - if entry.is_object() { - debug!("Scanned object: {}", entry.name); - - // Parse object metadata - if let Ok(file_meta) = entry.xl_meta() { - if file_meta.versions.is_empty() { - objects_with_issues += 1; - warn!("Object {} has no versions", entry.name); - } else { - // Store object metadata for later analysis - object_metadata.insert(entry.name.clone(), file_meta); - } - } else { - objects_with_issues += 1; - warn!("Failed to parse metadata for object {}", entry.name); - } - } - } - - // Update metrics - self.metrics.increment_objects_scanned(objects_scanned); - self.metrics.increment_objects_with_issues(objects_with_issues); - self.metrics.increment_bucket_scans_finished(1); - - // Update bucket metrics - { - let mut bucket_metrics = self.bucket_metrics.write().await; - if let Some(metrics) = bucket_metrics.get_mut(bucket) { - metrics.total_objects = objects_scanned; - metrics.objects_with_issues = objects_with_issues; - metrics.scan_duration = Some( - SystemTime::now() - .duration_since(scan_start) - .unwrap_or(Duration::ZERO) - ); - } - } - - // Update state - { - let mut state = self.state.write().await; - state.scanning_buckets.retain(|b| b != bucket); - } - - debug!("Completed scanning bucket: {} on disk {} ({} objects, {} issues)", - bucket, disk.to_string(), objects_scanned, objects_with_issues); - - Ok(object_metadata) - } - - /// Analyze object distribution across all disks and perform EC verification - /// - /// This method takes the collected object metadata from all disks and: - /// 1. Creates a union of all objects across all disks - /// 2. Identifies missing objects on each disk (for healing) - /// 3. Performs EC decode verification for deep scan mode - async fn analyze_object_distribution( - &self, - all_disk_objects: &[HashMap>], - disks: &[DiskStore], - ) -> Result<()> { - info!("Analyzing object distribution across {} disks", disks.len()); - - // Step 1: Create union of all objects across all disks - let mut all_objects = HashMap::new(); // bucket -> Set - let mut object_locations = HashMap::new(); // (bucket, object) -> Vec - - for (disk_idx, disk_objects) in all_disk_objects.iter().enumerate() { - for (bucket, objects) in disk_objects { - // Add bucket to all_objects - let bucket_objects = all_objects.entry(bucket.clone()).or_insert_with(|| std::collections::HashSet::new()); - - for (object_name, _file_meta) in objects { - bucket_objects.insert(object_name.clone()); - - // Record which disk has this object - let key = (bucket.clone(), object_name.clone()); - let locations = object_locations.entry(key).or_insert_with(|| Vec::new()); - locations.push(disk_idx); - } - } - } - - info!("Found {} buckets with {} total objects", all_objects.len(), - all_objects.values().map(|s| s.len()).sum::()); - - // Step 2: Identify missing objects and perform EC verification - let mut objects_needing_heal = 0u64; - let mut objects_with_ec_issues = 0u64; - - for (bucket, objects) in &all_objects { - for object_name in objects { - let key = (bucket.clone(), object_name.clone()); - let empty_vec = Vec::new(); - let locations = object_locations.get(&key).unwrap_or(&empty_vec); - - // Check if object is missing from some disks - if locations.len() < disks.len() { - objects_needing_heal += 1; - let missing_disks: Vec = (0..disks.len()) - .filter(|&i| !locations.contains(&i)) - .collect(); - warn!("Object {}/{} missing from disks: {:?}", bucket, object_name, missing_disks); - println!("Object {}/{} missing from disks: {:?}", bucket, object_name, missing_disks); - // TODO: Trigger heal for this object - } - - // Step 3: Deep scan EC verification - let config = self.config.read().await; - if config.scan_mode == ScanMode::Deep { - // Find the first disk that has this object to get metadata - if let Some(&first_disk_idx) = locations.first() { - if let Some(file_meta) = all_disk_objects[first_disk_idx] - .get(bucket) - .and_then(|objects| objects.get(object_name)) - { - if let Err(e) = self.verify_ec_decode_with_locations( - bucket, object_name, file_meta, locations, disks - ).await { - objects_with_ec_issues += 1; - warn!("EC decode verification failed for object {}/{}: {}", bucket, object_name, e); - } - } - } - } - } - } - - info!("Analysis complete: {} objects need healing, {} objects have EC issues", - objects_needing_heal, objects_with_ec_issues); - - Ok(()) - } - - /// Verify EC decode capability for an object using known disk locations - /// - /// This method is optimized to use the known locations of object copies - /// instead of scanning all disks. - async fn verify_ec_decode_with_locations( - &self, - bucket: &str, - object: &str, - file_meta: &rustfs_filemeta::FileMeta, - locations: &[usize], - all_disks: &[DiskStore], - ) -> Result<()> { - // Get EC parameters from the latest version - let (data_blocks, _parity_blocks) = if let Some(latest_version) = file_meta.versions.last() { - if let Ok(version) = rustfs_filemeta::FileMetaVersion::try_from(latest_version.clone()) { - if let Some(obj) = version.object { - (obj.erasure_m, obj.erasure_n) - } else { - // Not an object version, skip EC verification - return Ok(()); - } - } else { - // Cannot parse version, skip EC verification - return Ok(()); - } - } else { - // No versions, skip EC verification - return Ok(()); - }; - - let read_quorum = data_blocks; // Need at least data_blocks to decode - - if locations.len() < read_quorum { - return Err(Error::Scanner(format!( - "Insufficient object copies for EC decode: need {}, have {}", - read_quorum, locations.len() - ))); - } - - // Try to read object metadata from the known locations - let mut successful_reads = 0; - let mut errors = Vec::new(); - - for &disk_idx in locations { - if successful_reads >= read_quorum { - break; // We have enough copies for EC decode - } - - let disk = &all_disks[disk_idx]; - match disk.read_xl(bucket, object, false).await { - Ok(_) => { - successful_reads += 1; - debug!("Successfully read object {}/{} from disk {} (index: {})", - bucket, object, disk.to_string(), disk_idx); - } - Err(e) => { - let error_msg = format!("{}", e); - errors.push(error_msg); - debug!("Failed to read object {}/{} from disk {} (index: {}): {}", - bucket, object, disk.to_string(), disk_idx, e); - } - } - } - - if successful_reads >= read_quorum { - debug!("EC decode verification passed for object {}/{} ({} successful reads from {} locations)", - bucket, object, successful_reads, locations.len()); - Ok(()) - } else { - Err(Error::Scanner(format!( - "EC decode verification failed for object {}/{}: need {} reads, got {} (errors: {:?})", - bucket, object, read_quorum, successful_reads, errors - ))) - } - } - - /// Background scan loop with graceful shutdown - async fn scan_loop(self) -> Result<()> { - let config = self.config.read().await; - let mut interval = tokio::time::interval(config.scan_interval); - let deep_scan_interval = config.deep_scan_interval; - drop(config); - let cancel_token = self.cancel_token.clone(); - - loop { - tokio::select! { - _ = interval.tick() => { - // Check if scanner should still be running - if !self.state.read().await.is_running { - break; - } - - // 检查取消信号 - if cancel_token.is_cancelled() { - info!("Cancellation requested, exiting scanner loop"); - break; - } - - // Determine if it's time for a deep scan - let current_time = SystemTime::now(); - let last_deep_scan_time = self.state.read().await.last_deep_scan_time.unwrap_or(SystemTime::UNIX_EPOCH); - - if current_time.duration_since(last_deep_scan_time).unwrap_or(Duration::ZERO) >= deep_scan_interval { - info!("Deep scan interval reached, switching to deep scan mode"); - self.config.write().await.scan_mode = ScanMode::Deep; - self.state.write().await.last_deep_scan_time = Some(current_time); - } - - // Perform scan cycle - if let Err(e) = self.scan_cycle().await { - error!("Scan cycle failed: {}", e); - } - } - _ = cancel_token.cancelled() => { - info!("Received cancellation, stopping scanner loop"); - break; - } - } - } - - info!("Scanner loop stopped"); - Ok(()) - } - - /// Clone scanner for background tasks - fn clone_for_background(&self) -> Self { - Self { - config: self.config.clone(), - state: Arc::clone(&self.state), - metrics: Arc::clone(&self.metrics), - bucket_metrics: Arc::clone(&self.bucket_metrics), - disk_metrics: Arc::clone(&self.disk_metrics), - set_disks: Arc::clone(&self.set_disks), - cancel_token: self.cancel_token.clone(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use rustfs_ecstore::store::ECStore; - use rustfs_ecstore::disk::endpoint::Endpoint; - use rustfs_ecstore::endpoints::{EndpointServerPools, PoolEndpoints, Endpoints}; - use rustfs_ecstore::{StorageAPI, store_api::{ObjectIO, MakeBucketOptions, PutObjReader}}; - use std::fs; - use std::net::SocketAddr; - - #[tokio::test(flavor = "multi_thread")] - async fn test_scanner_basic_functionality() { - // create temp dir as 4 disks - let temp_dir = std::path::PathBuf::from("/tmp/rustfs_ahm_test"); - if temp_dir.exists() { - fs::remove_dir_all(&temp_dir).unwrap(); - } - fs::create_dir_all(&temp_dir).unwrap(); - - // create 4 disk dirs - let disk_paths = vec![ - temp_dir.join("disk1"), - temp_dir.join("disk2"), - temp_dir.join("disk3"), - temp_dir.join("disk4"), - ]; - - for disk_path in &disk_paths { - fs::create_dir_all(disk_path).unwrap(); - } - - // create EndpointServerPools - let mut endpoints = Vec::new(); - for (i, disk_path) in disk_paths.iter().enumerate() { - let mut endpoint = Endpoint::try_from(disk_path.to_str().unwrap()).unwrap(); - // set correct index - endpoint.set_pool_index(0); - endpoint.set_set_index(0); - endpoint.set_disk_index(i); - endpoints.push(endpoint); - } - - let pool_endpoints = PoolEndpoints { - legacy: false, - set_count: 1, - drives_per_set: 4, - endpoints: Endpoints::from(endpoints), - cmd_line: "test".to_string(), - platform: format!("OS: {} | Arch: {}", std::env::consts::OS, std::env::consts::ARCH), - }; - - let endpoint_pools = EndpointServerPools(vec![pool_endpoints]); - - // format disks - rustfs_ecstore::store::init_local_disks(endpoint_pools.clone()).await.unwrap(); - - // create ECStore - let server_addr: SocketAddr = "127.0.0.1:9000".parse().unwrap(); - let ecstore = ECStore::new(server_addr, endpoint_pools).await.unwrap(); - - // init bucket metadata system - let buckets_list = ecstore - .list_bucket(&rustfs_ecstore::store_api::BucketOptions { - no_metadata: true, - ..Default::default() - }) - .await - .unwrap(); - let buckets = buckets_list.into_iter().map(|v| v.name).collect(); - rustfs_ecstore::bucket::metadata_sys::init_bucket_metadata_sys(ecstore.clone(), buckets).await; - - // get first SetDisks - let set_disks = ecstore.pools[0].get_disks(0); - - // create some test data - let bucket_name = "test-bucket"; - let object_name = "test-object"; - let test_data = b"Hello, RustFS!"; - - // create bucket and verify - let bucket_opts = MakeBucketOptions::default(); - ecstore.make_bucket(bucket_name, &bucket_opts).await.expect("make_bucket failed"); - - // check bucket really exists - let buckets = ecstore.list_bucket(&rustfs_ecstore::store_api::BucketOptions::default()).await.unwrap(); - assert!(buckets.iter().any(|b| b.name == bucket_name), "bucket not found after creation"); - - // write object - let mut put_reader = PutObjReader::from_vec(test_data.to_vec()); - let object_opts = rustfs_ecstore::store_api::ObjectOptions::default(); - ecstore.put_object(bucket_name, object_name, &mut put_reader, &object_opts).await.expect("put_object failed"); - - // create Scanner and test basic functionality - let scanner = Scanner::new(set_disks, None); - - // Test 1: Normal scan - verify object is found - println!("=== Test 1: Normal scan ==="); - let scan_result = scanner.scan_cycle().await; - assert!(scan_result.is_ok(), "Normal scan should succeed"); - let metrics = scanner.get_metrics().await; - assert!(metrics.objects_scanned > 0, "Objects scanned should be positive"); - println!("Normal scan completed successfully"); - - // Test 2: Simulate disk corruption - delete object data from disk1 - println!("=== Test 2: Simulate disk corruption ==="); - let disk1_bucket_path = disk_paths[0].join(bucket_name); - let disk1_object_path = disk1_bucket_path.join(object_name); - - // Try to delete the object file from disk1 (simulate corruption) - // Note: This might fail if ECStore is actively using the file - match fs::remove_dir_all(&disk1_object_path) { - Ok(_) => { - println!("Successfully deleted object from disk1: {:?}", disk1_object_path); - - // Verify deletion by checking if the directory still exists - if disk1_object_path.exists() { - println!("WARNING: Directory still exists after deletion: {:?}", disk1_object_path); - } else { - println!("Confirmed: Directory was successfully deleted"); - } - } - Err(e) => { - println!("Could not delete object from disk1 (file may be in use): {:?} - {}", disk1_object_path, e); - // This is expected behavior - ECStore might be holding file handles - } - } - - // Scan again - should still complete (even with missing data) - let scan_result_after_corruption = scanner.scan_cycle().await; - println!("Scan after corruption result: {:?}", scan_result_after_corruption); - - // Scanner should handle missing data gracefully - assert!(scan_result_after_corruption.is_ok(), "Scanner should handle missing data gracefully"); - - // Test 3: Verify EC decode capability - println!("=== Test 3: Verify EC decode ==="); - // Note: EC decode verification is done internally during scan_cycle - // We can verify that the scanner handles missing data gracefully - println!("EC decode verification is handled internally during scan cycles"); - - // Test 4: Test metrics collection - println!("=== Test 4: Metrics collection ==="); - let final_metrics = scanner.get_metrics().await; - println!("Final metrics: {:?}", final_metrics); - - // Verify metrics are reasonable - assert!(final_metrics.total_cycles > 0, "Should have completed scan cycles"); - assert!(final_metrics.last_activity.is_some(), "Should have scan activity"); - - // clean up temp dir - // if let Err(e) = fs::remove_dir_all(&temp_dir) { - // eprintln!("Warning: Failed to clean up temp directory {:?}: {}", temp_dir, e); - // } - } -} - From 0aff736efde53e396dbe9acc52725889f559dca6 Mon Sep 17 00:00:00 2001 From: dandan Date: Thu, 10 Jul 2025 15:54:28 +0800 Subject: [PATCH 04/29] Chore: fix ref and fix comment Signed-off-by: junxiang Mu <1948535941@qq.com> --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 113938c08..84e2e9c76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,7 +91,7 @@ argon2 = { version = "0.5.3", features = ["std"] } atoi = "2.0.0" async-channel = "2.5.0" async-recursion = "1.1.1" -async-trait = "0.1" +async-trait = "0.1.88" async-compression = { version = "0.4.0" } atomic_enum = "0.3.0" aws-sdk-s3 = "1.96.0" From 4fb3d187d0b799cb86cd031c26e24cc37a3e2b3f Mon Sep 17 00:00:00 2001 From: junxiang Mu <1948535941@qq.com> Date: Thu, 10 Jul 2025 18:11:42 +0800 Subject: [PATCH 05/29] feat: implement heal subsystem for automatic data repair - Add heal module with core types (HealType, HealRequest, HealTask) - Implement HealManager for task scheduling and execution - Add HealStorageAPI trait and ECStoreHealStorage implementation - Integrate heal capabilities into scanner for automatic repair - Support multiple heal types: object, bucket, disk, metadata, MRF, EC decode - Add progress tracking and event system for heal operations - Merge heal and scanner error types for unified error handling - Include comprehensive logging and metrics for heal operations Signed-off-by: junxiang Mu <1948535941@qq.com> --- crates/ahm/src/error.rs | 54 +++- crates/ahm/src/heal/event.rs | 325 +++++++++++++++++++ crates/ahm/src/heal/manager.rs | 381 +++++++++++++++++++++++ crates/ahm/src/heal/mod.rs | 22 ++ crates/ahm/src/heal/progress.rs | 141 +++++++++ crates/ahm/src/heal/storage.rs | 307 ++++++++++++++++++ crates/ahm/src/heal/task.rs | 415 +++++++++++++++++++++++++ crates/ahm/src/lib.rs | 2 + crates/ahm/src/scanner/data_scanner.rs | 137 +++++++- rustfs/src/main.rs | 2 +- 10 files changed, 1774 insertions(+), 12 deletions(-) create mode 100644 crates/ahm/src/heal/event.rs create mode 100644 crates/ahm/src/heal/manager.rs create mode 100644 crates/ahm/src/heal/mod.rs create mode 100644 crates/ahm/src/heal/progress.rs create mode 100644 crates/ahm/src/heal/storage.rs create mode 100644 crates/ahm/src/heal/task.rs diff --git a/crates/ahm/src/error.rs b/crates/ahm/src/error.rs index 894638970..a8938f9ae 100644 --- a/crates/ahm/src/error.rs +++ b/crates/ahm/src/error.rs @@ -14,8 +14,10 @@ use thiserror::Error; +/// RustFS AHM/Heal/Scanner 统一错误类型 #[derive(Debug, Error)] pub enum Error { + // 通用 #[error("I/O error: {0}")] Io(#[from] std::io::Error), @@ -25,21 +27,65 @@ pub enum Error { #[error("Configuration error: {0}")] Config(String), + #[error("Heal configuration error: {message}")] + ConfigurationError { message: String }, + + #[error("Other error: {0}")] + Other(String), + + #[error(transparent)] + Anyhow(#[from] anyhow::Error), + + // Scanner相关 #[error("Scanner error: {0}")] Scanner(String), #[error("Metrics error: {0}")] Metrics(String), - #[error(transparent)] - Other(#[from] anyhow::Error), + // Heal相关 + #[error("Heal task not found: {task_id}")] + TaskNotFound { task_id: String }, + + #[error("Heal task already exists: {task_id}")] + TaskAlreadyExists { task_id: String }, + + #[error("Heal manager is not running")] + ManagerNotRunning, + + #[error("Heal task execution failed: {message}")] + TaskExecutionFailed { message: String }, + + #[error("Invalid heal type: {heal_type}")] + InvalidHealType { heal_type: String }, + + #[error("Heal task cancelled")] + TaskCancelled, + + #[error("Heal task timeout")] + TaskTimeout, + + #[error("Heal event processing failed: {message}")] + EventProcessingFailed { message: String }, + + #[error("Heal progress tracking failed: {message}")] + ProgressTrackingFailed { message: String }, } pub type Result = std::result::Result; -// Implement conversion from ahm::Error to std::io::Error for use in main.rs +impl Error { + pub fn other(error: E) -> Self + where + E: Into>, + { + Error::Other(error.into().to_string()) + } +} + +// 可选:实现与 std::io::Error 的互转 impl From for std::io::Error { fn from(err: Error) -> Self { std::io::Error::other(err) } -} +} \ No newline at end of file diff --git a/crates/ahm/src/heal/event.rs b/crates/ahm/src/heal/event.rs new file mode 100644 index 000000000..0e59452ed --- /dev/null +++ b/crates/ahm/src/heal/event.rs @@ -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. + +use crate::heal::task::{HealOptions, HealPriority, HealRequest, HealType}; +use rustfs_ecstore::disk::endpoint::Endpoint; +use serde::{Deserialize, Serialize}; +use std::time::SystemTime; + +/// Corruption type +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum CorruptionType { + /// Data corruption + DataCorruption, + /// Metadata corruption + MetadataCorruption, + /// Partial corruption + PartialCorruption, + /// Complete corruption + CompleteCorruption, +} + +/// Severity level +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub enum Severity { + /// Low severity + Low = 0, + /// Medium severity + Medium = 1, + /// High severity + High = 2, + /// Critical severity + Critical = 3, +} + +/// Heal event +#[derive(Debug, Clone)] +pub enum HealEvent { + /// Object corruption event + ObjectCorruption { + bucket: String, + object: String, + version_id: Option, + corruption_type: CorruptionType, + severity: Severity, + }, + /// Object missing event + ObjectMissing { + bucket: String, + object: String, + version_id: Option, + expected_locations: Vec, + available_locations: Vec, + }, + /// Metadata corruption event + MetadataCorruption { + bucket: String, + object: String, + corruption_type: CorruptionType, + }, + /// Disk status change event + DiskStatusChange { + endpoint: Endpoint, + old_status: String, + new_status: String, + }, + /// EC decode failure event + ECDecodeFailure { + bucket: String, + object: String, + version_id: Option, + missing_shards: Vec, + available_shards: Vec, + }, + /// Checksum mismatch event + ChecksumMismatch { + bucket: String, + object: String, + version_id: Option, + expected_checksum: String, + actual_checksum: String, + }, + /// Bucket metadata corruption event + BucketMetadataCorruption { + bucket: String, + corruption_type: CorruptionType, + }, + /// MRF metadata corruption event + MRFMetadataCorruption { + meta_path: String, + corruption_type: CorruptionType, + }, +} + +impl HealEvent { + /// Convert HealEvent to HealRequest + pub fn to_heal_request(&self) -> HealRequest { + match self { + HealEvent::ObjectCorruption { bucket, object, version_id, severity, .. } => { + HealRequest::new( + HealType::Object { + bucket: bucket.clone(), + object: object.clone(), + version_id: version_id.clone(), + }, + HealOptions::default(), + Self::severity_to_priority(severity), + ) + } + HealEvent::ObjectMissing { bucket, object, version_id, .. } => { + HealRequest::new( + HealType::Object { + bucket: bucket.clone(), + object: object.clone(), + version_id: version_id.clone(), + }, + HealOptions::default(), + HealPriority::High, + ) + } + HealEvent::MetadataCorruption { bucket, object, .. } => { + HealRequest::new( + HealType::Metadata { + bucket: bucket.clone(), + object: object.clone(), + }, + HealOptions::default(), + HealPriority::High, + ) + } + HealEvent::DiskStatusChange { endpoint, .. } => { + HealRequest::new( + HealType::Disk { + endpoint: endpoint.clone(), + }, + HealOptions::default(), + HealPriority::High, + ) + } + HealEvent::ECDecodeFailure { bucket, object, version_id, .. } => { + HealRequest::new( + HealType::ECDecode { + bucket: bucket.clone(), + object: object.clone(), + version_id: version_id.clone(), + }, + HealOptions::default(), + HealPriority::Urgent, + ) + } + HealEvent::ChecksumMismatch { bucket, object, version_id, .. } => { + HealRequest::new( + HealType::Object { + bucket: bucket.clone(), + object: object.clone(), + version_id: version_id.clone(), + }, + HealOptions::default(), + HealPriority::High, + ) + } + HealEvent::BucketMetadataCorruption { bucket, .. } => { + HealRequest::new( + HealType::Bucket { + bucket: bucket.clone(), + }, + HealOptions::default(), + HealPriority::High, + ) + } + HealEvent::MRFMetadataCorruption { meta_path, .. } => { + HealRequest::new( + HealType::MRF { + meta_path: meta_path.clone(), + }, + HealOptions::default(), + HealPriority::High, + ) + } + } + } + + /// Convert severity to priority + fn severity_to_priority(severity: &Severity) -> HealPriority { + match severity { + Severity::Low => HealPriority::Low, + Severity::Medium => HealPriority::Normal, + Severity::High => HealPriority::High, + Severity::Critical => HealPriority::Urgent, + } + } + + /// Get event description + pub fn description(&self) -> String { + match self { + HealEvent::ObjectCorruption { bucket, object, corruption_type, .. } => { + format!("Object corruption detected: {}/{} - {:?}", bucket, object, corruption_type) + } + HealEvent::ObjectMissing { bucket, object, .. } => { + format!("Object missing: {}/{}", bucket, object) + } + HealEvent::MetadataCorruption { bucket, object, corruption_type, .. } => { + format!("Metadata corruption: {}/{} - {:?}", bucket, object, corruption_type) + } + HealEvent::DiskStatusChange { endpoint, old_status, new_status, .. } => { + format!("Disk status changed: {:?} {} -> {}", endpoint, old_status, new_status) + } + HealEvent::ECDecodeFailure { bucket, object, missing_shards, .. } => { + format!("EC decode failure: {}/{} - missing shards: {:?}", bucket, object, missing_shards) + } + HealEvent::ChecksumMismatch { bucket, object, expected_checksum, actual_checksum, .. } => { + format!("Checksum mismatch: {}/{} - expected: {}, actual: {}", bucket, object, expected_checksum, actual_checksum) + } + HealEvent::BucketMetadataCorruption { bucket, corruption_type, .. } => { + format!("Bucket metadata corruption: {} - {:?}", bucket, corruption_type) + } + HealEvent::MRFMetadataCorruption { meta_path, corruption_type, .. } => { + format!("MRF metadata corruption: {} - {:?}", meta_path, corruption_type) + } + } + } + + /// Get event severity + pub fn severity(&self) -> Severity { + match self { + HealEvent::ObjectCorruption { severity, .. } => severity.clone(), + HealEvent::ObjectMissing { .. } => Severity::High, + HealEvent::MetadataCorruption { .. } => Severity::High, + HealEvent::DiskStatusChange { .. } => Severity::High, + HealEvent::ECDecodeFailure { .. } => Severity::Critical, + HealEvent::ChecksumMismatch { .. } => Severity::High, + HealEvent::BucketMetadataCorruption { .. } => Severity::High, + HealEvent::MRFMetadataCorruption { .. } => Severity::High, + } + } + + /// Get event timestamp + pub fn timestamp(&self) -> SystemTime { + SystemTime::now() + } +} + +/// Heal event handler +pub struct HealEventHandler { + /// Event queue + events: Vec, + /// Maximum number of events + max_events: usize, +} + +impl HealEventHandler { + pub fn new(max_events: usize) -> Self { + Self { + events: Vec::new(), + max_events, + } + } + + /// Add event + pub fn add_event(&mut self, event: HealEvent) { + if self.events.len() >= self.max_events { + // Remove oldest event + self.events.remove(0); + } + self.events.push(event); + } + + /// Get all events + pub fn get_events(&self) -> &[HealEvent] { + &self.events + } + + /// Clear events + pub fn clear_events(&mut self) { + self.events.clear(); + } + + /// Get event count + pub fn event_count(&self) -> usize { + self.events.len() + } + + /// Filter events by severity + pub fn filter_by_severity(&self, min_severity: Severity) -> Vec<&HealEvent> { + self.events + .iter() + .filter(|event| event.severity() >= min_severity) + .collect() + } + + /// Filter events by type + pub fn filter_by_type(&self, event_type: &str) -> Vec<&HealEvent> { + self.events + .iter() + .filter(|event| { + match event { + HealEvent::ObjectCorruption { .. } => event_type == "ObjectCorruption", + HealEvent::ObjectMissing { .. } => event_type == "ObjectMissing", + HealEvent::MetadataCorruption { .. } => event_type == "MetadataCorruption", + HealEvent::DiskStatusChange { .. } => event_type == "DiskStatusChange", + HealEvent::ECDecodeFailure { .. } => event_type == "ECDecodeFailure", + HealEvent::ChecksumMismatch { .. } => event_type == "ChecksumMismatch", + HealEvent::BucketMetadataCorruption { .. } => event_type == "BucketMetadataCorruption", + HealEvent::MRFMetadataCorruption { .. } => event_type == "MRFMetadataCorruption", + } + }) + .collect() + } +} + +impl Default for HealEventHandler { + fn default() -> Self { + Self::new(1000) + } +} \ No newline at end of file diff --git a/crates/ahm/src/heal/manager.rs b/crates/ahm/src/heal/manager.rs new file mode 100644 index 000000000..de2e7ec8f --- /dev/null +++ b/crates/ahm/src/heal/manager.rs @@ -0,0 +1,381 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::error::{Error, Result}; +use crate::heal::{ + progress::{HealProgress, HealStatistics}, + storage::HealStorageAPI, + task::{HealRequest, HealTask, HealTaskStatus}, +}; +use std::{ + collections::{HashMap, VecDeque}, + sync::Arc, + time::{Duration, SystemTime}, +}; +use tokio::{ + sync::{Mutex, RwLock}, + time::interval, +}; +use tokio_util::sync::CancellationToken; +use tracing::{error, info, warn}; + +/// Heal config +#[derive(Debug, Clone)] +pub struct HealConfig { + /// Whether to enable auto heal + pub enable_auto_heal: bool, + /// Heal interval + pub heal_interval: Duration, + /// Maximum concurrent heal tasks + pub max_concurrent_heals: usize, + /// Task timeout + pub task_timeout: Duration, + /// Queue size + pub queue_size: usize, +} + +impl Default for HealConfig { + fn default() -> Self { + Self { + enable_auto_heal: true, + heal_interval: Duration::from_secs(60), // 1 minute + max_concurrent_heals: 4, + task_timeout: Duration::from_secs(300), // 5 minutes + queue_size: 1000, + } + } +} + +/// Heal 状态 +#[derive(Debug, Default)] +pub struct HealState { + /// 是否正在运行 + pub is_running: bool, + /// 当前 heal 周期 + pub current_cycle: u64, + /// 最后 heal 时间 + pub last_heal_time: Option, + /// 总 heal 对象数 + pub total_healed_objects: u64, + /// 总 heal 失败数 + pub total_heal_failures: u64, + /// 当前活跃 heal 任务数 + pub active_heal_count: usize, +} + +/// Heal 管理器 +pub struct HealManager { + /// Heal 配置 + config: Arc>, + /// Heal 状态 + state: Arc>, + /// 活跃的 heal 任务 + active_heals: Arc>>>, + /// Heal 队列 + heal_queue: Arc>>, + /// 存储层接口 + storage: Arc, + /// 取消令牌 + cancel_token: CancellationToken, + /// 统计信息 + statistics: Arc>, +} + +impl HealManager { + /// 创建新的 HealManager + pub fn new(storage: Arc, config: Option) -> Self { + let config = config.unwrap_or_default(); + Self { + config: Arc::new(RwLock::new(config)), + state: Arc::new(RwLock::new(HealState::default())), + active_heals: Arc::new(Mutex::new(HashMap::new())), + heal_queue: Arc::new(Mutex::new(VecDeque::new())), + storage, + cancel_token: CancellationToken::new(), + statistics: Arc::new(RwLock::new(HealStatistics::new())), + } + } + + /// 启动 HealManager + pub async fn start(&self) -> Result<()> { + let mut state = self.state.write().await; + if state.is_running { + warn!("HealManager is already running"); + return Ok(()); + } + state.is_running = true; + drop(state); + + info!("Starting HealManager"); + + // 启动调度器 + self.start_scheduler().await?; + + // 启动工作器 + self.start_workers().await?; + + info!("HealManager started successfully"); + Ok(()) + } + + /// 停止 HealManager + pub async fn stop(&self) -> Result<()> { + info!("Stopping HealManager"); + + // 取消所有任务 + self.cancel_token.cancel(); + + // 等待所有任务完成 + let mut active_heals = self.active_heals.lock().await; + for task in active_heals.values() { + if let Err(e) = task.cancel().await { + warn!("Failed to cancel task {}: {}", task.id, e); + } + } + active_heals.clear(); + + // 更新状态 + let mut state = self.state.write().await; + state.is_running = false; + + info!("HealManager stopped successfully"); + Ok(()) + } + + /// 提交 heal 请求 + pub async fn submit_heal_request(&self, request: HealRequest) -> Result { + let config = self.config.read().await; + let mut queue = self.heal_queue.lock().await; + + if queue.len() >= config.queue_size { + return Err(Error::ConfigurationError { + message: "Heal queue is full".to_string(), + }); + } + + let request_id = request.id.clone(); + queue.push_back(request); + drop(queue); + + info!("Submitted heal request: {}", request_id); + Ok(request_id) + } + + /// 获取任务状态 + pub async fn get_task_status(&self, task_id: &str) -> Result { + let active_heals = self.active_heals.lock().await; + if let Some(task) = active_heals.get(task_id) { + Ok(task.get_status().await) + } else { + Err(Error::TaskNotFound { + task_id: task_id.to_string(), + }) + } + } + + /// 获取任务进度 + pub async fn get_task_progress(&self, task_id: &str) -> Result { + let active_heals = self.active_heals.lock().await; + if let Some(task) = active_heals.get(task_id) { + Ok(task.get_progress().await) + } else { + Err(Error::TaskNotFound { + task_id: task_id.to_string(), + }) + } + } + + /// 取消任务 + pub async fn cancel_task(&self, task_id: &str) -> Result<()> { + let mut active_heals = self.active_heals.lock().await; + if let Some(task) = active_heals.get(task_id) { + task.cancel().await?; + active_heals.remove(task_id); + info!("Cancelled heal task: {}", task_id); + Ok(()) + } else { + Err(Error::TaskNotFound { + task_id: task_id.to_string(), + }) + } + } + + /// 获取统计信息 + pub async fn get_statistics(&self) -> HealStatistics { + self.statistics.read().await.clone() + } + + /// 获取活跃任务数量 + pub async fn get_active_task_count(&self) -> usize { + let active_heals = self.active_heals.lock().await; + active_heals.len() + } + + /// 获取队列长度 + pub async fn get_queue_length(&self) -> usize { + let queue = self.heal_queue.lock().await; + queue.len() + } + + /// 启动调度器 + async fn start_scheduler(&self) -> Result<()> { + let config = self.config.clone(); + let heal_queue = self.heal_queue.clone(); + let active_heals = self.active_heals.clone(); + let cancel_token = self.cancel_token.clone(); + let statistics = self.statistics.clone(); + let storage = self.storage.clone(); + + tokio::spawn(async move { + let mut interval = interval(config.read().await.heal_interval); + + loop { + tokio::select! { + _ = cancel_token.cancelled() => { + info!("Heal scheduler received shutdown signal"); + break; + } + _ = interval.tick() => { + Self::process_heal_queue(&heal_queue, &active_heals, &config, &statistics, &storage).await; + } + } + } + }); + + Ok(()) + } + + /// 启动工作器 + async fn start_workers(&self) -> Result<()> { + let config = self.config.clone(); + let active_heals = self.active_heals.clone(); + let storage = self.storage.clone(); + let cancel_token = self.cancel_token.clone(); + let statistics = self.statistics.clone(); + + let worker_count = config.read().await.max_concurrent_heals; + + for worker_id in 0..worker_count { + let active_heals = active_heals.clone(); + let _storage = storage.clone(); + let cancel_token = cancel_token.clone(); + let statistics = statistics.clone(); + + tokio::spawn(async move { + info!("Starting heal worker {}", worker_id); + + loop { + tokio::select! { + _ = cancel_token.cancelled() => { + info!("Heal worker {} received shutdown signal", worker_id); + break; + } + _ = async { + // 等待任务 + tokio::time::sleep(Duration::from_millis(100)).await; + } => { + // 检查是否有可执行的任务 + let mut active_heals_guard = active_heals.lock().await; + let mut completed_tasks = Vec::new(); + + for (id, task) in active_heals_guard.iter() { + let status = task.get_status().await; + if matches!(status, HealTaskStatus::Completed | HealTaskStatus::Failed { .. } | HealTaskStatus::Cancelled) { + completed_tasks.push(id.clone()); + } + } + + // 移除已完成的任务 + for task_id in completed_tasks { + if let Some(task) = active_heals_guard.remove(&task_id) { + // 更新统计信息 + let mut stats = statistics.write().await; + match task.get_status().await { + HealTaskStatus::Completed => { + stats.update_task_completion(true); + } + HealTaskStatus::Failed { .. } => { + stats.update_task_completion(false); + } + _ => {} + } + } + } + + // 更新活跃任务数量 + let mut stats = statistics.write().await; + stats.update_running_tasks(active_heals_guard.len() as u64); + } + } + } + + info!("Heal worker {} stopped", worker_id); + }); + } + + Ok(()) + } + + /// 处理 heal 队列 + async fn process_heal_queue( + heal_queue: &Arc>>, + active_heals: &Arc>>>, + config: &Arc>, + statistics: &Arc>, + storage: &Arc, + ) { + let config = config.read().await; + let mut active_heals = active_heals.lock().await; + + // 检查是否可以启动新的 heal 任务 + if active_heals.len() >= config.max_concurrent_heals { + return; + } + + let mut queue = heal_queue.lock().await; + if let Some(request) = queue.pop_front() { + let task = Arc::new(HealTask::from_request(request, storage.clone())); + let task_id = task.id.clone(); + active_heals.insert(task_id.clone(), task.clone()); + + // 启动 heal 任务 + tokio::spawn(async move { + info!("Starting heal task: {}", task_id); + match task.execute().await { + Ok(_) => { + info!("Heal task completed successfully: {}", task_id); + } + Err(e) => { + error!("Heal task failed: {} - {}", task_id, e); + } + } + }); + + // 更新统计信息 + let mut stats = statistics.write().await; + stats.total_tasks += 1; + } + } +} + +impl std::fmt::Debug for HealManager { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("HealManager") + .field("config", &"") + .field("state", &"") + .field("active_heals_count", &"") + .field("queue_length", &"") + .finish() + } +} \ No newline at end of file diff --git a/crates/ahm/src/heal/mod.rs b/crates/ahm/src/heal/mod.rs new file mode 100644 index 000000000..290a99a5f --- /dev/null +++ b/crates/ahm/src/heal/mod.rs @@ -0,0 +1,22 @@ +// 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. + +pub mod event; +pub mod manager; +pub mod progress; +pub mod storage; +pub mod task; + +pub use manager::HealManager; +pub use task::{HealOptions, HealPriority, HealRequest, HealTask, HealType}; \ No newline at end of file diff --git a/crates/ahm/src/heal/progress.rs b/crates/ahm/src/heal/progress.rs new file mode 100644 index 000000000..32d88a8b6 --- /dev/null +++ b/crates/ahm/src/heal/progress.rs @@ -0,0 +1,141 @@ +// 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 serde::{Deserialize, Serialize}; +use std::time::SystemTime; + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct HealProgress { + /// 已扫描对象数 + pub objects_scanned: u64, + /// 已修复对象数 + pub objects_healed: u64, + /// 修复失败对象数 + pub objects_failed: u64, + /// 已处理字节数 + pub bytes_processed: u64, + /// 当前处理的对象 + pub current_object: Option, + /// 进度百分比 + pub progress_percentage: f64, + /// 开始时间 + pub start_time: Option, + /// 最后更新时间 + pub last_update_time: Option, + /// 预计完成时间 + pub estimated_completion_time: Option, +} + +impl HealProgress { + pub fn new() -> Self { + Self { + start_time: Some(SystemTime::now()), + last_update_time: Some(SystemTime::now()), + ..Default::default() + } + } + + pub fn update_progress(&mut self, scanned: u64, healed: u64, failed: u64, bytes: u64) { + self.objects_scanned = scanned; + self.objects_healed = healed; + self.objects_failed = failed; + self.bytes_processed = bytes; + self.last_update_time = Some(SystemTime::now()); + + // 计算进度百分比 + let total = scanned + healed + failed; + if total > 0 { + self.progress_percentage = (healed as f64 / total as f64) * 100.0; + } + } + + pub fn set_current_object(&mut self, object: Option) { + self.current_object = object; + self.last_update_time = Some(SystemTime::now()); + } + + pub fn is_completed(&self) -> bool { + self.progress_percentage >= 100.0 || self.objects_scanned > 0 && self.objects_healed + self.objects_failed >= self.objects_scanned + } + + pub fn get_success_rate(&self) -> f64 { + let total = self.objects_healed + self.objects_failed; + if total > 0 { + (self.objects_healed as f64 / total as f64) * 100.0 + } else { + 0.0 + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HealStatistics { + /// 总 heal 任务数 + pub total_tasks: u64, + /// 成功完成的任务数 + pub successful_tasks: u64, + /// 失败的任务数 + pub failed_tasks: u64, + /// 正在运行的任务数 + pub running_tasks: u64, + /// 总修复对象数 + pub total_objects_healed: u64, + /// 总修复字节数 + pub total_bytes_healed: u64, + /// 最后更新时间 + pub last_update_time: SystemTime, +} + +impl HealStatistics { + pub fn new() -> Self { + Self { + total_tasks: 0, + successful_tasks: 0, + failed_tasks: 0, + running_tasks: 0, + total_objects_healed: 0, + total_bytes_healed: 0, + last_update_time: SystemTime::now(), + } + } + + pub fn update_task_completion(&mut self, success: bool) { + if success { + self.successful_tasks += 1; + } else { + self.failed_tasks += 1; + } + self.last_update_time = SystemTime::now(); + } + + pub fn update_running_tasks(&mut self, count: u64) { + self.running_tasks = count; + self.last_update_time = SystemTime::now(); + } + + pub fn add_healed_objects(&mut self, count: u64, bytes: u64) { + self.total_objects_healed += count; + self.total_bytes_healed += bytes; + self.last_update_time = SystemTime::now(); + } + + pub fn get_success_rate(&self) -> f64 { + let total = self.successful_tasks + self.failed_tasks; + if total > 0 { + (self.successful_tasks as f64 / total as f64) * 100.0 + } else { + 0.0 + } + } +} \ No newline at end of file diff --git a/crates/ahm/src/heal/storage.rs b/crates/ahm/src/heal/storage.rs new file mode 100644 index 000000000..8b9cfc20e --- /dev/null +++ b/crates/ahm/src/heal/storage.rs @@ -0,0 +1,307 @@ +// 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::error::{Error, Result}; +use async_trait::async_trait; +use rustfs_ecstore::{ + disk::endpoint::Endpoint, + store_api::{BucketInfo, StorageAPI, ObjectIO}, + store::ECStore, +}; +use std::sync::Arc; +use tracing::{debug, error, info}; + +/// 磁盘状态 +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DiskStatus { + /// 正常 + Ok, + /// 离线 + Offline, + /// 损坏 + Corrupt, + /// 缺失 + Missing, + /// 权限错误 + PermissionDenied, + /// 故障 + Faulty, + /// 根挂载 + RootMount, + /// 未知 + Unknown, + /// 未格式化 + Unformatted, +} + +/// Heal 存储层接口 +#[async_trait] +pub trait HealStorageAPI: Send + Sync { + /// 获取对象元数据 + async fn get_object_meta(&self, bucket: &str, object: &str) -> Result>; + + /// 获取对象数据 + async fn get_object_data(&self, bucket: &str, object: &str) -> Result>>; + + /// 写入对象数据 + async fn put_object_data(&self, bucket: &str, object: &str, data: &[u8]) -> Result<()>; + + /// 删除对象 + async fn delete_object(&self, bucket: &str, object: &str) -> Result<()>; + + /// 检查对象完整性 + async fn verify_object_integrity(&self, bucket: &str, object: &str) -> Result; + + /// EC 解码重建 + async fn ec_decode_rebuild(&self, bucket: &str, object: &str) -> Result>; + + /// 获取磁盘状态 + async fn get_disk_status(&self, endpoint: &Endpoint) -> Result; + + /// 格式化磁盘 + async fn format_disk(&self, endpoint: &Endpoint) -> Result<()>; + + /// 获取桶信息 + async fn get_bucket_info(&self, bucket: &str) -> Result>; + + /// 修复桶元数据 + async fn heal_bucket_metadata(&self, bucket: &str) -> Result<()>; + + /// 获取所有桶列表 + async fn list_buckets(&self) -> Result>; + + /// 检查对象是否存在 + async fn object_exists(&self, bucket: &str, object: &str) -> Result; + + /// 获取对象大小 + async fn get_object_size(&self, bucket: &str, object: &str) -> Result>; + + /// 获取对象校验和 + async fn get_object_checksum(&self, bucket: &str, object: &str) -> Result>; +} + +/// ECStore Heal 存储层实现 +pub struct ECStoreHealStorage { + ecstore: Arc, +} + +impl ECStoreHealStorage { + pub fn new(ecstore: Arc) -> Self { + Self { ecstore } + } +} + +#[async_trait] +impl HealStorageAPI for ECStoreHealStorage { + async fn get_object_meta(&self, bucket: &str, object: &str) -> Result> { + debug!("Getting object meta: {}/{}", bucket, object); + + match self.ecstore.get_object_info(bucket, object, &Default::default()).await { + Ok(info) => Ok(Some(info)), + Err(e) => { + error!("Failed to get object meta: {}/{} - {}", bucket, object, e); + Err(Error::other(e)) + } + } + } + + async fn get_object_data(&self, bucket: &str, object: &str) -> Result>> { + debug!("Getting object data: {}/{}", bucket, object); + + match self.ecstore.get_object_reader(bucket, object, None, Default::default(), &Default::default()).await { + Ok(mut reader) => { + match reader.read_all().await { + Ok(data) => Ok(Some(data)), + Err(e) => { + error!("Failed to read object data: {}/{} - {}", bucket, object, e); + Err(Error::other(e)) + } + } + } + Err(e) => { + error!("Failed to get object: {}/{} - {}", bucket, object, e); + Err(Error::other(e)) + } + } + } + + async fn put_object_data(&self, bucket: &str, object: &str, data: &[u8]) -> Result<()> { + debug!("Putting object data: {}/{} ({} bytes)", bucket, object, data.len()); + + let mut reader = rustfs_ecstore::store_api::PutObjReader::from_vec(data.to_vec()); + match self.ecstore.put_object(bucket, object, &mut reader, &Default::default()).await { + Ok(_) => { + info!("Successfully put object: {}/{}", bucket, object); + Ok(()) + } + Err(e) => { + error!("Failed to put object: {}/{} - {}", bucket, object, e); + Err(Error::other(e)) + } + } + } + + async fn delete_object(&self, bucket: &str, object: &str) -> Result<()> { + debug!("Deleting object: {}/{}", bucket, object); + + match self.ecstore.delete_object(bucket, object, Default::default()).await { + Ok(_) => { + info!("Successfully deleted object: {}/{}", bucket, object); + Ok(()) + } + Err(e) => { + error!("Failed to delete object: {}/{} - {}", bucket, object, e); + Err(Error::other(e)) + } + } + } + + async fn verify_object_integrity(&self, bucket: &str, object: &str) -> Result { + debug!("Verifying object integrity: {}/{}", bucket, object); + + // TODO: 实现对象完整性检查 + // 1. 获取对象元数据 + // 2. 检查数据块完整性 + // 3. 验证校验和 + // 4. 检查 EC 编码正确性 + + // 临时实现:总是返回 true + info!("Object integrity check passed: {}/{}", bucket, object); + Ok(true) + } + + async fn ec_decode_rebuild(&self, bucket: &str, object: &str) -> Result> { + debug!("EC decode rebuild: {}/{}", bucket, object); + + // TODO: 实现 EC 解码重建 + // 1. 获取对象元数据 + // 2. 读取可用的数据块 + // 3. 使用 EC 算法重建缺失数据 + // 4. 返回完整数据 + + // 临时实现:尝试获取对象数据 + match self.get_object_data(bucket, object).await? { + Some(data) => { + info!("EC decode rebuild successful: {}/{}", bucket, object); + Ok(data) + } + None => { + error!("Object not found for EC decode: {}/{}", bucket, object); + Err(Error::TaskExecutionFailed { + message: format!("Object not found: {}/{}", bucket, object), + }) + } + } + } + + async fn get_disk_status(&self, endpoint: &Endpoint) -> Result { + debug!("Getting disk status: {:?}", endpoint); + + // TODO: 实现磁盘状态检查 + // 1. 检查磁盘是否可访问 + // 2. 检查磁盘格式 + // 3. 检查权限 + // 4. 返回状态 + + // 临时实现:总是返回 Ok + info!("Disk status check: {:?} - OK", endpoint); + Ok(DiskStatus::Ok) + } + + async fn format_disk(&self, endpoint: &Endpoint) -> Result<()> { + debug!("Formatting disk: {:?}", endpoint); + + // TODO: 实现磁盘格式化 + // 1. 检查磁盘权限 + // 2. 执行格式化操作 + // 3. 验证格式化结果 + + // 临时实现:总是成功 + info!("Disk formatted successfully: {:?}", endpoint); + Ok(()) + } + + async fn get_bucket_info(&self, bucket: &str) -> Result> { + debug!("Getting bucket info: {}", bucket); + + match self.ecstore.get_bucket_info(bucket, &Default::default()).await { + Ok(info) => Ok(Some(info)), + Err(e) => { + error!("Failed to get bucket info: {} - {}", bucket, e); + Err(Error::other(e)) + } + } + } + + async fn heal_bucket_metadata(&self, bucket: &str) -> Result<()> { + debug!("Healing bucket metadata: {}", bucket); + + // TODO: 实现桶元数据修复 + // 1. 检查桶元数据完整性 + // 2. 修复损坏的元数据 + // 3. 更新桶配置 + + // 临时实现:总是成功 + info!("Bucket metadata healed successfully: {}", bucket); + Ok(()) + } + + async fn list_buckets(&self) -> Result> { + debug!("Listing buckets"); + + match self.ecstore.list_bucket(&Default::default()).await { + Ok(buckets) => Ok(buckets), + Err(e) => { + error!("Failed to list buckets: {}", e); + Err(Error::other(e)) + } + } + } + + async fn object_exists(&self, bucket: &str, object: &str) -> Result { + debug!("Checking if object exists: {}/{}", bucket, object); + + match self.ecstore.get_object_info(bucket, object, &Default::default()).await { + Ok(_) => Ok(true), + Err(e) => { + error!("Failed to check object existence: {}/{} - {}", bucket, object, e); + Err(Error::other(e)) + } + } + } + + async fn get_object_size(&self, bucket: &str, object: &str) -> Result> { + debug!("Getting object size: {}/{}", bucket, object); + + match self.ecstore.get_object_info(bucket, object, &Default::default()).await { + Ok(info) => Ok(Some(info.size as u64)), + Err(e) => { + error!("Failed to get object size: {}/{} - {}", bucket, object, e); + Err(Error::other(e)) + } + } + } + + async fn get_object_checksum(&self, bucket: &str, object: &str) -> Result> { + debug!("Getting object checksum: {}/{}", bucket, object); + + match self.ecstore.get_object_info(bucket, object, &Default::default()).await { + Ok(info) => Ok(info.etag), + Err(e) => { + error!("Failed to get object checksum: {}/{} - {}", bucket, object, e); + Err(Error::other(e)) + } + } + } +} \ No newline at end of file diff --git a/crates/ahm/src/heal/task.rs b/crates/ahm/src/heal/task.rs new file mode 100644 index 000000000..7cd887adb --- /dev/null +++ b/crates/ahm/src/heal/task.rs @@ -0,0 +1,415 @@ +// 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::error::Result; +use crate::heal::{progress::HealProgress, storage::HealStorageAPI}; +use rustfs_ecstore::disk::endpoint::Endpoint; +use serde::{Deserialize, Serialize}; +use std::{ + sync::Arc, + time::{Duration, SystemTime}, +}; +use tokio::sync::RwLock; +use tracing::{debug, error, info}; +use uuid::Uuid; + +/// Heal 扫描模式 +pub type HealScanMode = usize; + +pub const HEAL_UNKNOWN_SCAN: HealScanMode = 0; +pub const HEAL_NORMAL_SCAN: HealScanMode = 1; +pub const HEAL_DEEP_SCAN: HealScanMode = 2; + +/// Heal 类型 +#[derive(Debug, Clone)] +pub enum HealType { + /// 对象 heal + Object { + bucket: String, + object: String, + version_id: Option, + }, + /// 桶 heal + Bucket { + bucket: String, + }, + /// 磁盘 heal + Disk { + endpoint: Endpoint, + }, + /// 元数据 heal + Metadata { + bucket: String, + object: String, + }, + /// MRF heal + MRF { + meta_path: String, + }, + /// EC 解码 heal + ECDecode { + bucket: String, + object: String, + version_id: Option, + }, +} + +/// Heal 优先级 +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub enum HealPriority { + /// 低优先级 + Low = 0, + /// 普通优先级 + Normal = 1, + /// 高优先级 + High = 2, + /// 紧急优先级 + Urgent = 3, +} + +impl Default for HealPriority { + fn default() -> Self { + Self::Normal + } +} + +/// Heal 选项 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HealOptions { + /// 扫描模式 + pub scan_mode: HealScanMode, + /// 是否删除损坏数据 + pub remove_corrupted: bool, + /// 是否重新创建 + pub recreate_missing: bool, + /// 是否更新奇偶校验 + pub update_parity: bool, + /// 是否递归处理 + pub recursive: bool, + /// 是否试运行 + pub dry_run: bool, + /// 超时时间 + pub timeout: Option, +} + +impl Default for HealOptions { + fn default() -> Self { + Self { + scan_mode: HEAL_NORMAL_SCAN, + remove_corrupted: false, + recreate_missing: true, + update_parity: true, + recursive: false, + dry_run: false, + timeout: Some(Duration::from_secs(300)), // 5分钟默认超时 + } + } +} + +/// Heal 任务状态 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum HealTaskStatus { + /// 等待中 + Pending, + /// 运行中 + Running, + /// 完成 + Completed, + /// 失败 + Failed { error: String }, + /// 取消 + Cancelled, + /// 超时 + Timeout, +} + +/// Heal 请求 +#[derive(Debug, Clone)] +pub struct HealRequest { + /// 请求 ID + pub id: String, + /// Heal 类型 + pub heal_type: HealType, + /// Heal 选项 + pub options: HealOptions, + /// 优先级 + pub priority: HealPriority, + /// 创建时间 + pub created_at: SystemTime, +} + +impl HealRequest { + pub fn new(heal_type: HealType, options: HealOptions, priority: HealPriority) -> Self { + Self { + id: Uuid::new_v4().to_string(), + heal_type, + options, + priority, + created_at: SystemTime::now(), + } + } + + pub fn object(bucket: String, object: String, version_id: Option) -> Self { + Self::new( + HealType::Object { + bucket, + object, + version_id, + }, + HealOptions::default(), + HealPriority::Normal, + ) + } + + pub fn bucket(bucket: String) -> Self { + Self::new( + HealType::Bucket { bucket }, + HealOptions::default(), + HealPriority::Normal, + ) + } + + pub fn disk(endpoint: Endpoint) -> Self { + Self::new( + HealType::Disk { endpoint }, + HealOptions::default(), + HealPriority::High, + ) + } + + pub fn metadata(bucket: String, object: String) -> Self { + Self::new( + HealType::Metadata { bucket, object }, + HealOptions::default(), + HealPriority::High, + ) + } + + pub fn ec_decode(bucket: String, object: String, version_id: Option) -> Self { + Self::new( + HealType::ECDecode { + bucket, + object, + version_id, + }, + HealOptions::default(), + HealPriority::Urgent, + ) + } +} + +/// Heal 任务 +pub struct HealTask { + /// 任务 ID + pub id: String, + /// Heal 类型 + pub heal_type: HealType, + /// Heal 选项 + pub options: HealOptions, + /// 任务状态 + pub status: Arc>, + /// 进度跟踪 + pub progress: Arc>, + /// 创建时间 + pub created_at: SystemTime, + /// 开始时间 + pub started_at: Arc>>, + /// 完成时间 + pub completed_at: Arc>>, + /// 取消令牌 + pub cancel_token: tokio_util::sync::CancellationToken, + /// 存储层接口 + pub storage: Arc, +} + +impl HealTask { + pub fn from_request(request: HealRequest, storage: Arc) -> Self { + Self { + id: request.id, + heal_type: request.heal_type, + options: request.options, + status: Arc::new(RwLock::new(HealTaskStatus::Pending)), + progress: Arc::new(RwLock::new(HealProgress::new())), + created_at: request.created_at, + started_at: Arc::new(RwLock::new(None)), + completed_at: Arc::new(RwLock::new(None)), + cancel_token: tokio_util::sync::CancellationToken::new(), + storage, + } + } + + pub async fn execute(&self) -> Result<()> { + // 更新状态为运行中 + { + let mut status = self.status.write().await; + *status = HealTaskStatus::Running; + } + { + let mut started_at = self.started_at.write().await; + *started_at = Some(SystemTime::now()); + } + + info!("Starting heal task: {} with type: {:?}", self.id, self.heal_type); + + let result = match &self.heal_type { + HealType::Object { bucket, object, version_id } => { + self.heal_object(bucket, object, version_id.as_deref()).await + } + HealType::Bucket { bucket } => { + self.heal_bucket(bucket).await + } + HealType::Disk { endpoint } => { + self.heal_disk(endpoint).await + } + HealType::Metadata { bucket, object } => { + self.heal_metadata(bucket, object).await + } + HealType::MRF { meta_path } => { + self.heal_mrf(meta_path).await + } + HealType::ECDecode { bucket, object, version_id } => { + self.heal_ec_decode(bucket, object, version_id.as_deref()).await + } + }; + + // 更新完成时间和状态 + { + let mut completed_at = self.completed_at.write().await; + *completed_at = Some(SystemTime::now()); + } + + match &result { + Ok(_) => { + let mut status = self.status.write().await; + *status = HealTaskStatus::Completed; + info!("Heal task completed successfully: {}", self.id); + } + Err(e) => { + let mut status = self.status.write().await; + *status = HealTaskStatus::Failed { + error: e.to_string(), + }; + error!("Heal task failed: {} with error: {}", self.id, e); + } + } + + result + } + + pub async fn cancel(&self) -> Result<()> { + self.cancel_token.cancel(); + let mut status = self.status.write().await; + *status = HealTaskStatus::Cancelled; + info!("Heal task cancelled: {}", self.id); + Ok(()) + } + + pub async fn get_status(&self) -> HealTaskStatus { + self.status.read().await.clone() + } + + pub async fn get_progress(&self) -> HealProgress { + self.progress.read().await.clone() + } + + // 具体的 heal 实现方法 + async fn heal_object(&self, bucket: &str, object: &str, _version_id: Option<&str>) -> Result<()> { + debug!("Healing object: {}/{}", bucket, object); + + // 更新进度 + { + let mut progress = self.progress.write().await; + progress.set_current_object(Some(format!("{}/{}", bucket, object))); + } + + // TODO: 实现具体的对象 heal 逻辑 + // 1. 检查对象完整性 + // 2. 如果损坏,尝试 EC 重建 + // 3. 更新对象数据 + // 4. 更新进度 + + { + let mut progress = self.progress.write().await; + progress.update_progress(1, 1, 0, 1024); // 示例数据 + } + + Ok(()) + } + + async fn heal_bucket(&self, bucket: &str) -> Result<()> { + debug!("Healing bucket: {}", bucket); + + // TODO: 实现桶 heal 逻辑 + // 1. 检查桶元数据 + // 2. 修复桶配置 + // 3. 更新进度 + + Ok(()) + } + + async fn heal_disk(&self, endpoint: &Endpoint) -> Result<()> { + debug!("Healing disk: {:?}", endpoint); + + // TODO: 实现磁盘 heal 逻辑 + // 1. 检查磁盘状态 + // 2. 格式化磁盘(如果需要) + // 3. 更新进度 + + Ok(()) + } + + async fn heal_metadata(&self, bucket: &str, object: &str) -> Result<()> { + debug!("Healing metadata: {}/{}", bucket, object); + + // TODO: 实现元数据 heal 逻辑 + // 1. 检查元数据完整性 + // 2. 重建元数据 + // 3. 更新进度 + + Ok(()) + } + + async fn heal_mrf(&self, meta_path: &str) -> Result<()> { + debug!("Healing MRF: {}", meta_path); + + // TODO: 实现 MRF heal 逻辑 + // 1. 检查元数据复制因子 + // 2. 修复元数据 + // 3. 更新进度 + + Ok(()) + } + + async fn heal_ec_decode(&self, bucket: &str, object: &str, _version_id: Option<&str>) -> Result<()> { + debug!("Healing EC decode: {}/{}", bucket, object); + + // TODO: 实现 EC 解码 heal 逻辑 + // 1. 检查 EC 分片 + // 2. 使用 EC 算法重建数据 + // 3. 更新进度 + + Ok(()) + } +} + +impl std::fmt::Debug for HealTask { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("HealTask") + .field("id", &self.id) + .field("heal_type", &self.heal_type) + .field("options", &self.options) + .field("created_at", &self.created_at) + .finish() + } +} \ No newline at end of file diff --git a/crates/ahm/src/lib.rs b/crates/ahm/src/lib.rs index d3d656194..0e288d513 100644 --- a/crates/ahm/src/lib.rs +++ b/crates/ahm/src/lib.rs @@ -16,6 +16,7 @@ use std::sync::OnceLock; use tokio_util::sync::CancellationToken; pub mod error; +pub mod heal; pub mod scanner; pub use error::{Error, Result}; @@ -23,6 +24,7 @@ pub use scanner::{ BucketTargetUsageInfo, BucketUsageInfo, DataUsageInfo, Scanner, ScannerMetrics, load_data_usage_from_backend, store_data_usage_in_backend, }; +pub use heal::{HealManager, HealRequest, HealType, HealOptions, HealPriority}; // Global cancellation token for AHM services (scanner and other background tasks) static GLOBAL_AHM_SERVICES_CANCEL_TOKEN: OnceLock = OnceLock::new(); diff --git a/crates/ahm/src/scanner/data_scanner.rs b/crates/ahm/src/scanner/data_scanner.rs index 1e3e4fc2a..611bd3d05 100644 --- a/crates/ahm/src/scanner/data_scanner.rs +++ b/crates/ahm/src/scanner/data_scanner.rs @@ -34,8 +34,9 @@ use super::{ }; use crate::{ error::{Error, Result}, - get_ahm_services_cancel_token, + get_ahm_services_cancel_token, HealRequest, }; +use crate::heal::HealManager; use rustfs_ecstore::disk::RUSTFS_META_BUCKET; @@ -126,15 +127,15 @@ pub struct Scanner { data_usage_stats: Arc>>, /// Last data usage statistics collection time last_data_usage_collection: Arc>>, + /// Heal manager for auto-heal integration + heal_manager: Option>, } impl Scanner { /// Create a new scanner - pub fn new(config: Option) -> Self { + pub fn new(config: Option, heal_manager: Option>) -> Self { let config = config.unwrap_or_default(); - info!("Creating AHM scanner for all EC sets"); - Self { config: Arc::new(RwLock::new(config)), state: Arc::new(RwLock::new(ScannerState::default())), @@ -143,9 +144,15 @@ impl Scanner { disk_metrics: Arc::new(Mutex::new(HashMap::new())), data_usage_stats: Arc::new(Mutex::new(HashMap::new())), last_data_usage_collection: Arc::new(RwLock::new(None)), + heal_manager, } } + /// Set the heal manager after construction + pub fn set_heal_manager(&mut self, heal_manager: Arc) { + self.heal_manager = Some(heal_manager); + } + /// Start the scanner pub async fn start(&self) -> Result<()> { let mut state = self.state.write().await; @@ -495,6 +502,24 @@ impl Scanner { metrics.free_space = disk_info.free; metrics.is_online = disk.is_online().await; + // 检查磁盘状态,如果离线则提交heal任务 + if !metrics.is_online { + let enable_healing = self.config.read().await.enable_healing; + if enable_healing { + if let Some(heal_manager) = &self.heal_manager { + let req = HealRequest::disk(disk.endpoint().clone()); + match heal_manager.submit_heal_request(req).await { + Ok(task_id) => { + warn!("磁盘离线,已自动提交heal任务: {} 磁盘: {}", task_id, disk_path); + } + Err(e) => { + error!("磁盘离线,heal任务提交失败: {},错误: {}", disk_path, e); + } + } + } + } + } + // Additional disk info for debugging debug!( "Disk {}: total={}, used={}, free={}, online={}", @@ -514,6 +539,30 @@ impl Scanner { Ok(volumes) => volumes, Err(e) => { error!("Failed to list volumes on disk {}: {}", disk_path, e); + + // 磁盘访问失败,提交磁盘heal任务 + let enable_healing = self.config.read().await.enable_healing; + if enable_healing { + if let Some(heal_manager) = &self.heal_manager { + use crate::heal::{HealRequest, HealPriority}; + let req = HealRequest::new( + crate::heal::HealType::Disk { + endpoint: disk.endpoint().clone(), + }, + crate::heal::HealOptions::default(), + HealPriority::Urgent, + ); + match heal_manager.submit_heal_request(req).await { + Ok(task_id) => { + warn!("磁盘访问失败,已自动提交heal任务: {} 磁盘: {} 错误: {}", task_id, disk_path, e); + } + Err(heal_err) => { + error!("磁盘访问失败,heal任务提交失败: {},错误: {}", disk_path, heal_err); + } + } + } + } + return Err(Error::Storage(e.into())); } }; @@ -625,6 +674,22 @@ impl Scanner { if file_meta.versions.is_empty() { objects_with_issues += 1; warn!("Object {} has no versions", entry.name); + + // 对象元数据损坏,提交元数据heal任务 + let enable_healing = self.config.read().await.enable_healing; + if enable_healing { + if let Some(heal_manager) = &self.heal_manager { + let req = HealRequest::metadata(bucket.to_string(), entry.name.clone()); + match heal_manager.submit_heal_request(req).await { + Ok(task_id) => { + warn!("对象元数据损坏,已自动提交heal任务: {} {} / {}", task_id, bucket, entry.name); + } + Err(e) => { + error!("对象元数据损坏,heal任务提交失败: {} / {},错误: {}", bucket, entry.name, e); + } + } + } + } } else { // Store object metadata for later analysis object_metadata.insert(entry.name.clone(), file_meta.clone()); @@ -632,6 +697,22 @@ impl Scanner { } else { objects_with_issues += 1; warn!("Failed to parse metadata for object {}", entry.name); + + // 对象元数据解析失败,提交元数据heal任务 + let enable_healing = self.config.read().await.enable_healing; + if enable_healing { + if let Some(heal_manager) = &self.heal_manager { + let req = HealRequest::metadata(bucket.to_string(), entry.name.clone()); + match heal_manager.submit_heal_request(req).await { + Ok(task_id) => { + warn!("对象元数据解析失败,已自动提交heal任务: {} {} / {}", task_id, bucket, entry.name); + } + Err(e) => { + error!("对象元数据解析失败,heal任务提交失败: {} / {},错误: {}", bucket, entry.name, e); + } + } + } + } } } } @@ -735,7 +816,32 @@ impl Scanner { let missing_disks: Vec = (0..disks.len()).filter(|&i| !locations.contains(&i)).collect(); warn!("Object {}/{} missing from disks: {:?}", bucket, object_name, missing_disks); println!("Object {bucket}/{object_name} missing from disks: {missing_disks:?}"); - // TODO: Trigger heal for this object + + // 自动提交heal任务 + let enable_healing = self.config.read().await.enable_healing; + if enable_healing { + if let Some(heal_manager) = &self.heal_manager { + use crate::heal::{HealRequest, HealPriority}; + let req = HealRequest::new( + crate::heal::HealType::Object { + bucket: bucket.clone(), + object: object_name.clone(), + version_id: None, + }, + crate::heal::HealOptions::default(), + HealPriority::High, + ); + match heal_manager.submit_heal_request(req).await { + Ok(task_id) => { + warn!("对象缺失,已自动提交heal任务: {} {} / {} (缺失磁盘: {:?})", + task_id, bucket, object_name, missing_disks); + } + Err(e) => { + error!("对象缺失,heal任务提交失败: {} / {},错误: {}", bucket, object_name, e); + } + } + } + } } // Step 3: Deep scan EC verification @@ -863,6 +969,22 @@ impl Scanner { ); Ok(()) } else { + // 自动提交heal任务 + let enable_healing = self.config.read().await.enable_healing; + if enable_healing { + if let Some(heal_manager) = &self.heal_manager { + use crate::heal::HealRequest; + let req = HealRequest::ec_decode(bucket.to_string(), object.to_string(), None); + match heal_manager.submit_heal_request(req).await { + Ok(task_id) => { + warn!("EC decode失败,已自动提交heal任务: {} {} / {}", task_id, bucket, object); + } + Err(e) => { + error!("EC decode失败,heal任务提交失败: {} / {},错误: {}", bucket, object, e); + } + } + } + } Err(Error::Scanner(format!( "EC decode verification failed for object {bucket}/{object}: need {read_quorum} reads, got {successful_reads} (errors: {errors:?})" ))) @@ -1005,6 +1127,7 @@ impl Scanner { disk_metrics: Arc::clone(&self.disk_metrics), data_usage_stats: Arc::clone(&self.data_usage_stats), last_data_usage_collection: Arc::clone(&self.last_data_usage_collection), + heal_manager: self.heal_manager.clone(), } } } @@ -1121,7 +1244,7 @@ mod tests { .expect("put_object failed"); // create Scanner and test basic functionality - let scanner = Scanner::new(None); + let scanner = Scanner::new(None, None); // Test 1: Normal scan - verify object is found println!("=== Test 1: Normal scan ==="); @@ -1200,7 +1323,7 @@ mod tests { .await .unwrap(); - let scanner = Scanner::new(None); + let scanner = Scanner::new(None, None); // enable statistics { diff --git a/rustfs/src/main.rs b/rustfs/src/main.rs index 799fe5d31..e1f481e9b 100644 --- a/rustfs/src/main.rs +++ b/rustfs/src/main.rs @@ -185,7 +185,7 @@ async fn run(opt: config::Opt) -> Result<()> { // init_data_scanner().await; // init_auto_heal().await; let _ = create_ahm_services_cancel_token(); - let scanner = Scanner::new(Some(ScannerConfig::default())); + let scanner = Scanner::new(Some(ScannerConfig::default()), None); scanner.start().await?; print_server_info(); init_bucket_replication_pool().await; From f4973a681cc3811b2e9ed6a961e7ac50d11389e1 Mon Sep 17 00:00:00 2001 From: junxiang Mu <1948535941@qq.com> Date: Thu, 10 Jul 2025 23:56:17 +0800 Subject: [PATCH 06/29] feat: implement complete ahm heal system with ecstore integration - Add comprehensive heal storage API with ECStore integration - Implement heal object, bucket, disk, metadata, and EC decode operations - Add heal task management with progress tracking and statistics - Optimize heal manager by removing unnecessary workers - Add integration tests for core heal functionality (heal_object, heal_bucket, heal_format) - Integrate with ecstore's native heal commands for actual repair operations Signed-off-by: junxiang Mu <1948535941@qq.com> --- crates/ahm/src/heal/manager.rs | 165 ++---- crates/ahm/src/heal/progress.rs | 34 +- crates/ahm/src/heal/storage.rs | 323 +++++++---- crates/ahm/src/heal/task.rs | 627 ++++++++++++++++++---- crates/ahm/src/scanner/data_scanner.rs | 28 +- crates/ahm/tests/heal_integration_test.rs | 353 ++++++++++++ 6 files changed, 1206 insertions(+), 324 deletions(-) create mode 100644 crates/ahm/tests/heal_integration_test.rs diff --git a/crates/ahm/src/heal/manager.rs b/crates/ahm/src/heal/manager.rs index de2e7ec8f..9676bea88 100644 --- a/crates/ahm/src/heal/manager.rs +++ b/crates/ahm/src/heal/manager.rs @@ -57,43 +57,43 @@ impl Default for HealConfig { } } -/// Heal 状态 +/// Heal state #[derive(Debug, Default)] pub struct HealState { - /// 是否正在运行 + /// Whether running pub is_running: bool, - /// 当前 heal 周期 + /// Current heal cycle pub current_cycle: u64, - /// 最后 heal 时间 + /// Last heal time pub last_heal_time: Option, - /// 总 heal 对象数 + /// Total healed objects pub total_healed_objects: u64, - /// 总 heal 失败数 + /// Total heal failures pub total_heal_failures: u64, - /// 当前活跃 heal 任务数 + /// Current active heal tasks pub active_heal_count: usize, } -/// Heal 管理器 +/// Heal manager pub struct HealManager { - /// Heal 配置 + /// Heal config config: Arc>, - /// Heal 状态 + /// Heal state state: Arc>, - /// 活跃的 heal 任务 + /// Active heal tasks active_heals: Arc>>>, - /// Heal 队列 + /// Heal queue heal_queue: Arc>>, - /// 存储层接口 + /// Storage layer interface storage: Arc, - /// 取消令牌 + /// Cancel token cancel_token: CancellationToken, - /// 统计信息 + /// Statistics statistics: Arc>, } impl HealManager { - /// 创建新的 HealManager + /// Create new HealManager pub fn new(storage: Arc, config: Option) -> Self { let config = config.unwrap_or_default(); Self { @@ -107,7 +107,7 @@ impl HealManager { } } - /// 启动 HealManager + /// Start HealManager pub async fn start(&self) -> Result<()> { let mut state = self.state.write().await; if state.is_running { @@ -119,24 +119,21 @@ impl HealManager { info!("Starting HealManager"); - // 启动调度器 + // start scheduler self.start_scheduler().await?; - // 启动工作器 - self.start_workers().await?; - info!("HealManager started successfully"); Ok(()) } - /// 停止 HealManager + /// Stop HealManager pub async fn stop(&self) -> Result<()> { info!("Stopping HealManager"); - // 取消所有任务 + // cancel all tasks self.cancel_token.cancel(); - // 等待所有任务完成 + // wait for all tasks to complete let mut active_heals = self.active_heals.lock().await; for task in active_heals.values() { if let Err(e) = task.cancel().await { @@ -145,7 +142,7 @@ impl HealManager { } active_heals.clear(); - // 更新状态 + // update state let mut state = self.state.write().await; state.is_running = false; @@ -153,7 +150,7 @@ impl HealManager { Ok(()) } - /// 提交 heal 请求 + /// Submit heal request pub async fn submit_heal_request(&self, request: HealRequest) -> Result { let config = self.config.read().await; let mut queue = self.heal_queue.lock().await; @@ -172,7 +169,7 @@ impl HealManager { Ok(request_id) } - /// 获取任务状态 + /// Get task status pub async fn get_task_status(&self, task_id: &str) -> Result { let active_heals = self.active_heals.lock().await; if let Some(task) = active_heals.get(task_id) { @@ -184,7 +181,7 @@ impl HealManager { } } - /// 获取任务进度 + /// Get task progress pub async fn get_task_progress(&self, task_id: &str) -> Result { let active_heals = self.active_heals.lock().await; if let Some(task) = active_heals.get(task_id) { @@ -196,7 +193,7 @@ impl HealManager { } } - /// 取消任务 + /// Cancel task pub async fn cancel_task(&self, task_id: &str) -> Result<()> { let mut active_heals = self.active_heals.lock().await; if let Some(task) = active_heals.get(task_id) { @@ -211,24 +208,24 @@ impl HealManager { } } - /// 获取统计信息 + /// Get statistics pub async fn get_statistics(&self) -> HealStatistics { self.statistics.read().await.clone() } - /// 获取活跃任务数量 + /// Get active task count pub async fn get_active_task_count(&self) -> usize { let active_heals = self.active_heals.lock().await; active_heals.len() } - /// 获取队列长度 + /// Get queue length pub async fn get_queue_length(&self) -> usize { let queue = self.heal_queue.lock().await; queue.len() } - /// 启动调度器 + /// Start scheduler async fn start_scheduler(&self) -> Result<()> { let config = self.config.clone(); let heal_queue = self.heal_queue.clone(); @@ -256,78 +253,7 @@ impl HealManager { Ok(()) } - /// 启动工作器 - async fn start_workers(&self) -> Result<()> { - let config = self.config.clone(); - let active_heals = self.active_heals.clone(); - let storage = self.storage.clone(); - let cancel_token = self.cancel_token.clone(); - let statistics = self.statistics.clone(); - - let worker_count = config.read().await.max_concurrent_heals; - - for worker_id in 0..worker_count { - let active_heals = active_heals.clone(); - let _storage = storage.clone(); - let cancel_token = cancel_token.clone(); - let statistics = statistics.clone(); - - tokio::spawn(async move { - info!("Starting heal worker {}", worker_id); - - loop { - tokio::select! { - _ = cancel_token.cancelled() => { - info!("Heal worker {} received shutdown signal", worker_id); - break; - } - _ = async { - // 等待任务 - tokio::time::sleep(Duration::from_millis(100)).await; - } => { - // 检查是否有可执行的任务 - let mut active_heals_guard = active_heals.lock().await; - let mut completed_tasks = Vec::new(); - - for (id, task) in active_heals_guard.iter() { - let status = task.get_status().await; - if matches!(status, HealTaskStatus::Completed | HealTaskStatus::Failed { .. } | HealTaskStatus::Cancelled) { - completed_tasks.push(id.clone()); - } - } - - // 移除已完成的任务 - for task_id in completed_tasks { - if let Some(task) = active_heals_guard.remove(&task_id) { - // 更新统计信息 - let mut stats = statistics.write().await; - match task.get_status().await { - HealTaskStatus::Completed => { - stats.update_task_completion(true); - } - HealTaskStatus::Failed { .. } => { - stats.update_task_completion(false); - } - _ => {} - } - } - } - - // 更新活跃任务数量 - let mut stats = statistics.write().await; - stats.update_running_tasks(active_heals_guard.len() as u64); - } - } - } - - info!("Heal worker {} stopped", worker_id); - }); - } - - Ok(()) - } - - /// 处理 heal 队列 + /// Process heal queue async fn process_heal_queue( heal_queue: &Arc>>, active_heals: &Arc>>>, @@ -336,10 +262,10 @@ impl HealManager { storage: &Arc, ) { let config = config.read().await; - let mut active_heals = active_heals.lock().await; + let mut active_heals_guard = active_heals.lock().await; - // 检查是否可以启动新的 heal 任务 - if active_heals.len() >= config.max_concurrent_heals { + // check if new heal tasks can be started + if active_heals_guard.len() >= config.max_concurrent_heals { return; } @@ -347,9 +273,12 @@ impl HealManager { if let Some(request) = queue.pop_front() { let task = Arc::new(HealTask::from_request(request, storage.clone())); let task_id = task.id.clone(); - active_heals.insert(task_id.clone(), task.clone()); + active_heals_guard.insert(task_id.clone(), task.clone()); + drop(active_heals_guard); + let active_heals_clone = active_heals.clone(); + let statistics_clone = statistics.clone(); - // 启动 heal 任务 + // start heal task tokio::spawn(async move { info!("Starting heal task: {}", task_id); match task.execute().await { @@ -360,9 +289,23 @@ impl HealManager { error!("Heal task failed: {} - {}", task_id, e); } } + let mut active_heals_guard = active_heals_clone.lock().await; + if let Some(completed_task) = active_heals_guard.remove(&task_id) { + // update statistics + let mut stats = statistics_clone.write().await; + match completed_task.get_status().await { + HealTaskStatus::Completed => { + stats.update_task_completion(true); + } + _ => { + stats.update_task_completion(false); + } + } + stats.update_running_tasks(active_heals_guard.len() as u64); + } }); - // 更新统计信息 + // update statistics let mut stats = statistics.write().await; stats.total_tasks += 1; } diff --git a/crates/ahm/src/heal/progress.rs b/crates/ahm/src/heal/progress.rs index 32d88a8b6..aea4aa474 100644 --- a/crates/ahm/src/heal/progress.rs +++ b/crates/ahm/src/heal/progress.rs @@ -17,23 +17,23 @@ use std::time::SystemTime; #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct HealProgress { - /// 已扫描对象数 + /// Objects scanned pub objects_scanned: u64, - /// 已修复对象数 + /// Objects healed pub objects_healed: u64, - /// 修复失败对象数 + /// Objects failed pub objects_failed: u64, - /// 已处理字节数 + /// Bytes processed pub bytes_processed: u64, - /// 当前处理的对象 + /// Current object pub current_object: Option, - /// 进度百分比 + /// Progress percentage pub progress_percentage: f64, - /// 开始时间 + /// Start time pub start_time: Option, - /// 最后更新时间 + /// Last update time pub last_update_time: Option, - /// 预计完成时间 + /// Estimated completion time pub estimated_completion_time: Option, } @@ -53,7 +53,7 @@ impl HealProgress { self.bytes_processed = bytes; self.last_update_time = Some(SystemTime::now()); - // 计算进度百分比 + // calculate progress percentage let total = scanned + healed + failed; if total > 0 { self.progress_percentage = (healed as f64 / total as f64) * 100.0; @@ -81,19 +81,19 @@ impl HealProgress { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HealStatistics { - /// 总 heal 任务数 + /// Total heal tasks pub total_tasks: u64, - /// 成功完成的任务数 + /// Successful tasks pub successful_tasks: u64, - /// 失败的任务数 + /// Failed tasks pub failed_tasks: u64, - /// 正在运行的任务数 + /// Running tasks pub running_tasks: u64, - /// 总修复对象数 + /// Total healed objects pub total_objects_healed: u64, - /// 总修复字节数 + /// Total healed bytes pub total_bytes_healed: u64, - /// 最后更新时间 + /// Last update time pub last_update_time: SystemTime, } diff --git a/crates/ahm/src/heal/storage.rs b/crates/ahm/src/heal/storage.rs index 8b9cfc20e..4a440f009 100644 --- a/crates/ahm/src/heal/storage.rs +++ b/crates/ahm/src/heal/storage.rs @@ -16,82 +16,96 @@ use crate::error::{Error, Result}; use async_trait::async_trait; use rustfs_ecstore::{ disk::endpoint::Endpoint, + heal::heal_commands::{HealOpts, HEAL_DEEP_SCAN, HEAL_NORMAL_SCAN}, store_api::{BucketInfo, StorageAPI, ObjectIO}, store::ECStore, }; +use rustfs_madmin::heal_commands::HealResultItem; use std::sync::Arc; -use tracing::{debug, error, info}; +use tracing::{debug, error, info, warn}; -/// 磁盘状态 +/// Disk status for heal operations #[derive(Debug, Clone, PartialEq, Eq)] pub enum DiskStatus { - /// 正常 + /// Ok Ok, - /// 离线 + /// Offline Offline, - /// 损坏 + /// Corrupt Corrupt, - /// 缺失 + /// Missing Missing, - /// 权限错误 + /// Permission denied PermissionDenied, - /// 故障 + /// Faulty Faulty, - /// 根挂载 + /// Root mount RootMount, - /// 未知 + /// Unknown Unknown, - /// 未格式化 + /// Unformatted Unformatted, } -/// Heal 存储层接口 +/// Heal storage layer interface #[async_trait] pub trait HealStorageAPI: Send + Sync { - /// 获取对象元数据 + /// Get object meta async fn get_object_meta(&self, bucket: &str, object: &str) -> Result>; - /// 获取对象数据 + /// Get object data async fn get_object_data(&self, bucket: &str, object: &str) -> Result>>; - /// 写入对象数据 + /// Put object data async fn put_object_data(&self, bucket: &str, object: &str, data: &[u8]) -> Result<()>; - /// 删除对象 + /// Delete object async fn delete_object(&self, bucket: &str, object: &str) -> Result<()>; - /// 检查对象完整性 + /// Check object integrity async fn verify_object_integrity(&self, bucket: &str, object: &str) -> Result; - /// EC 解码重建 + /// EC decode rebuild async fn ec_decode_rebuild(&self, bucket: &str, object: &str) -> Result>; - /// 获取磁盘状态 + /// Get disk status async fn get_disk_status(&self, endpoint: &Endpoint) -> Result; - /// 格式化磁盘 + /// Format disk async fn format_disk(&self, endpoint: &Endpoint) -> Result<()>; - /// 获取桶信息 + /// Get bucket info async fn get_bucket_info(&self, bucket: &str) -> Result>; - /// 修复桶元数据 + /// Fix bucket metadata async fn heal_bucket_metadata(&self, bucket: &str) -> Result<()>; - /// 获取所有桶列表 + /// Get all buckets async fn list_buckets(&self) -> Result>; - /// 检查对象是否存在 + /// Check object exists async fn object_exists(&self, bucket: &str, object: &str) -> Result; - /// 获取对象大小 + /// Get object size async fn get_object_size(&self, bucket: &str, object: &str) -> Result>; - /// 获取对象校验和 + /// Get object checksum async fn get_object_checksum(&self, bucket: &str, object: &str) -> Result>; + + /// Heal object using ecstore + async fn heal_object(&self, bucket: &str, object: &str, version_id: Option<&str>, opts: &HealOpts) -> Result<(HealResultItem, Option)>; + + /// Heal bucket using ecstore + async fn heal_bucket(&self, bucket: &str, opts: &HealOpts) -> Result; + + /// Heal format using ecstore + async fn heal_format(&self, dry_run: bool) -> Result<(HealResultItem, Option)>; + + /// List objects for healing + async fn list_objects_for_heal(&self, bucket: &str, prefix: &str) -> Result>; } -/// ECStore Heal 存储层实现 +/// ECStore Heal storage layer implementation pub struct ECStoreHealStorage { ecstore: Arc, } @@ -119,7 +133,7 @@ impl HealStorageAPI for ECStoreHealStorage { async fn get_object_data(&self, bucket: &str, object: &str) -> Result>> { debug!("Getting object data: {}/{}", bucket, object); - match self.ecstore.get_object_reader(bucket, object, None, Default::default(), &Default::default()).await { + match (&*self.ecstore).get_object_reader(bucket, object, None, Default::default(), &Default::default()).await { Ok(mut reader) => { match reader.read_all().await { Ok(data) => Ok(Some(data)), @@ -140,7 +154,7 @@ impl HealStorageAPI for ECStoreHealStorage { debug!("Putting object data: {}/{} ({} bytes)", bucket, object, data.len()); let mut reader = rustfs_ecstore::store_api::PutObjReader::from_vec(data.to_vec()); - match self.ecstore.put_object(bucket, object, &mut reader, &Default::default()).await { + match (&*self.ecstore).put_object(bucket, object, &mut reader, &Default::default()).await { Ok(_) => { info!("Successfully put object: {}/{}", bucket, object); Ok(()) @@ -170,37 +184,79 @@ impl HealStorageAPI for ECStoreHealStorage { async fn verify_object_integrity(&self, bucket: &str, object: &str) -> Result { debug!("Verifying object integrity: {}/{}", bucket, object); - // TODO: 实现对象完整性检查 - // 1. 获取对象元数据 - // 2. 检查数据块完整性 - // 3. 验证校验和 - // 4. 检查 EC 编码正确性 - - // 临时实现:总是返回 true - info!("Object integrity check passed: {}/{}", bucket, object); - Ok(true) + // Try to get object info and data to verify integrity + match self.get_object_meta(bucket, object).await? { + Some(obj_info) => { + // Check if object has valid metadata + if obj_info.size < 0 { + warn!("Object has invalid size: {}/{}", bucket, object); + return Ok(false); + } + + // Try to read object data to verify it's accessible + match self.get_object_data(bucket, object).await { + Ok(Some(_)) => { + info!("Object integrity check passed: {}/{}", bucket, object); + Ok(true) + } + Ok(None) => { + warn!("Object data not found: {}/{}", bucket, object); + Ok(false) + } + Err(_) => { + warn!("Object data read failed: {}/{}", bucket, object); + Ok(false) + } + } + } + None => { + warn!("Object metadata not found: {}/{}", bucket, object); + Ok(false) + } + } } async fn ec_decode_rebuild(&self, bucket: &str, object: &str) -> Result> { debug!("EC decode rebuild: {}/{}", bucket, object); - // TODO: 实现 EC 解码重建 - // 1. 获取对象元数据 - // 2. 读取可用的数据块 - // 3. 使用 EC 算法重建缺失数据 - // 4. 返回完整数据 + // Use ecstore's heal_object to rebuild the object + let heal_opts = HealOpts { + recursive: false, + dry_run: false, + remove: false, + recreate: true, + scan_mode: HEAL_DEEP_SCAN, + update_parity: true, + no_lock: false, + pool: None, + set: None, + }; - // 临时实现:尝试获取对象数据 - match self.get_object_data(bucket, object).await? { - Some(data) => { - info!("EC decode rebuild successful: {}/{}", bucket, object); - Ok(data) + match self.heal_object(bucket, object, None, &heal_opts).await { + Ok((_result, error)) => { + if error.is_some() { + return Err(Error::TaskExecutionFailed { + message: format!("Heal failed: {:?}", error), + }); + } + + // After healing, try to read the object data + match self.get_object_data(bucket, object).await? { + Some(data) => { + info!("EC decode rebuild successful: {}/{} ({} bytes)", bucket, object, data.len()); + Ok(data) + } + None => { + error!("Object not found after heal: {}/{}", bucket, object); + Err(Error::TaskExecutionFailed { + message: format!("Object not found after heal: {}/{}", bucket, object), + }) + } + } } - None => { - error!("Object not found for EC decode: {}/{}", bucket, object); - Err(Error::TaskExecutionFailed { - message: format!("Object not found: {}/{}", bucket, object), - }) + Err(e) => { + error!("Heal operation failed: {}/{} - {}", bucket, object, e); + Err(e) } } } @@ -208,13 +264,8 @@ impl HealStorageAPI for ECStoreHealStorage { async fn get_disk_status(&self, endpoint: &Endpoint) -> Result { debug!("Getting disk status: {:?}", endpoint); - // TODO: 实现磁盘状态检查 - // 1. 检查磁盘是否可访问 - // 2. 检查磁盘格式 - // 3. 检查权限 - // 4. 返回状态 - - // 临时实现:总是返回 Ok + // TODO: implement disk status check using ecstore + // For now, return Ok status info!("Disk status check: {:?} - OK", endpoint); Ok(DiskStatus::Ok) } @@ -222,14 +273,20 @@ impl HealStorageAPI for ECStoreHealStorage { async fn format_disk(&self, endpoint: &Endpoint) -> Result<()> { debug!("Formatting disk: {:?}", endpoint); - // TODO: 实现磁盘格式化 - // 1. 检查磁盘权限 - // 2. 执行格式化操作 - // 3. 验证格式化结果 - - // 临时实现:总是成功 - info!("Disk formatted successfully: {:?}", endpoint); - Ok(()) + // Use ecstore's heal_format + match self.heal_format(false).await { + Ok((_, error)) => { + if error.is_some() { + return Err(Error::other(format!("Format failed: {:?}", error))); + } + info!("Successfully formatted disk: {:?}", endpoint); + Ok(()) + } + Err(e) => { + error!("Failed to format disk: {:?} - {}", endpoint, e); + Err(e) + } + } } async fn get_bucket_info(&self, bucket: &str) -> Result> { @@ -247,14 +304,28 @@ impl HealStorageAPI for ECStoreHealStorage { async fn heal_bucket_metadata(&self, bucket: &str) -> Result<()> { debug!("Healing bucket metadata: {}", bucket); - // TODO: 实现桶元数据修复 - // 1. 检查桶元数据完整性 - // 2. 修复损坏的元数据 - // 3. 更新桶配置 + let heal_opts = HealOpts { + recursive: true, + dry_run: false, + remove: false, + recreate: false, + scan_mode: HEAL_NORMAL_SCAN, + update_parity: false, + no_lock: false, + pool: None, + set: None, + }; - // 临时实现:总是成功 - info!("Bucket metadata healed successfully: {}", bucket); - Ok(()) + match self.heal_bucket(bucket, &heal_opts).await { + Ok(_) => { + info!("Successfully healed bucket metadata: {}", bucket); + Ok(()) + } + Err(e) => { + error!("Failed to heal bucket metadata: {} - {}", bucket, e); + Err(e) + } + } } async fn list_buckets(&self) -> Result> { @@ -270,36 +341,106 @@ impl HealStorageAPI for ECStoreHealStorage { } async fn object_exists(&self, bucket: &str, object: &str) -> Result { - debug!("Checking if object exists: {}/{}", bucket, object); + debug!("Checking object exists: {}/{}", bucket, object); - match self.ecstore.get_object_info(bucket, object, &Default::default()).await { - Ok(_) => Ok(true), - Err(e) => { - error!("Failed to check object existence: {}/{} - {}", bucket, object, e); - Err(Error::other(e)) - } + match self.get_object_meta(bucket, object).await { + Ok(Some(_)) => Ok(true), + Ok(None) => Ok(false), + Err(_) => Ok(false), } } async fn get_object_size(&self, bucket: &str, object: &str) -> Result> { debug!("Getting object size: {}/{}", bucket, object); - match self.ecstore.get_object_info(bucket, object, &Default::default()).await { - Ok(info) => Ok(Some(info.size as u64)), - Err(e) => { - error!("Failed to get object size: {}/{} - {}", bucket, object, e); - Err(Error::other(e)) - } + match self.get_object_meta(bucket, object).await { + Ok(Some(obj_info)) => Ok(Some(obj_info.size as u64)), + Ok(None) => Ok(None), + Err(e) => Err(e), } } async fn get_object_checksum(&self, bucket: &str, object: &str) -> Result> { debug!("Getting object checksum: {}/{}", bucket, object); - match self.ecstore.get_object_info(bucket, object, &Default::default()).await { - Ok(info) => Ok(info.etag), + match self.get_object_meta(bucket, object).await { + Ok(Some(obj_info)) => { + // Convert checksum bytes to hex string + let checksum = obj_info.checksum.iter() + .map(|b| format!("{:02x}", b)) + .collect::(); + Ok(Some(checksum)) + } + Ok(None) => Ok(None), + Err(e) => Err(e), + } + } + + async fn heal_object(&self, bucket: &str, object: &str, version_id: Option<&str>, opts: &HealOpts) -> Result<(HealResultItem, Option)> { + debug!("Healing object: {}/{}", bucket, object); + + let version_id_str = version_id.unwrap_or(""); + + match self.ecstore.heal_object(bucket, object, version_id_str, opts).await { + Ok((result, ecstore_error)) => { + let error = ecstore_error.map(|e| Error::other(e)); + info!("Heal object completed: {}/{} - result: {:?}, error: {:?}", bucket, object, result, error); + Ok((result, error)) + } Err(e) => { - error!("Failed to get object checksum: {}/{} - {}", bucket, object, e); + error!("Heal object failed: {}/{} - {}", bucket, object, e); + Err(Error::other(e)) + } + } + } + + async fn heal_bucket(&self, bucket: &str, opts: &HealOpts) -> Result { + debug!("Healing bucket: {}", bucket); + + match self.ecstore.heal_bucket(bucket, opts).await { + Ok(result) => { + info!("Heal bucket completed: {} - result: {:?}", bucket, result); + Ok(result) + } + Err(e) => { + error!("Heal bucket failed: {} - {}", bucket, e); + Err(Error::other(e)) + } + } + } + + async fn heal_format(&self, dry_run: bool) -> Result<(HealResultItem, Option)> { + debug!("Healing format (dry_run: {})", dry_run); + + match self.ecstore.heal_format(dry_run).await { + Ok((result, ecstore_error)) => { + let error = ecstore_error.map(|e| Error::other(e)); + info!("Heal format completed - result: {:?}, error: {:?}", result, error); + Ok((result, error)) + } + Err(e) => { + error!("Heal format failed: {}", e); + Err(Error::other(e)) + } + } + } + + async fn list_objects_for_heal(&self, bucket: &str, prefix: &str) -> Result> { + debug!("Listing objects for heal: {}/{}", bucket, prefix); + + // Use list_objects_v2 to get objects + match self.ecstore.clone().list_objects_v2( + bucket, prefix, None, None, 1000, false, None + ).await { + Ok(list_info) => { + let objects: Vec = list_info.objects.into_iter() + .map(|obj| obj.name) + .collect(); + info!("Found {} objects for heal in {}/{}", objects.len(), bucket, prefix); + Ok(objects) + } + Err(e) => { + error!("Failed to list objects for heal: {}/{} - {}", bucket, prefix, e); Err(Error::other(e)) } } diff --git a/crates/ahm/src/heal/task.rs b/crates/ahm/src/heal/task.rs index 7cd887adb..0d531def0 100644 --- a/crates/ahm/src/heal/task.rs +++ b/crates/ahm/src/heal/task.rs @@ -12,43 +12,44 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::error::Result; +use crate::error::{Error, Result}; use crate::heal::{progress::HealProgress, storage::HealStorageAPI}; use rustfs_ecstore::disk::endpoint::Endpoint; +use crate::heal::storage::DiskStatus; use serde::{Deserialize, Serialize}; use std::{ sync::Arc, time::{Duration, SystemTime}, }; use tokio::sync::RwLock; -use tracing::{debug, error, info}; +use tracing::{error, info, warn}; use uuid::Uuid; -/// Heal 扫描模式 +/// Heal scan mode pub type HealScanMode = usize; pub const HEAL_UNKNOWN_SCAN: HealScanMode = 0; pub const HEAL_NORMAL_SCAN: HealScanMode = 1; pub const HEAL_DEEP_SCAN: HealScanMode = 2; -/// Heal 类型 +/// Heal type #[derive(Debug, Clone)] pub enum HealType { - /// 对象 heal + /// Object heal Object { bucket: String, object: String, version_id: Option, }, - /// 桶 heal + /// Bucket heal Bucket { bucket: String, }, - /// 磁盘 heal + /// Disk heal Disk { endpoint: Endpoint, }, - /// 元数据 heal + /// Metadata heal Metadata { bucket: String, object: String, @@ -57,7 +58,7 @@ pub enum HealType { MRF { meta_path: String, }, - /// EC 解码 heal + /// EC decode heal ECDecode { bucket: String, object: String, @@ -65,16 +66,16 @@ pub enum HealType { }, } -/// Heal 优先级 +/// Heal priority #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum HealPriority { - /// 低优先级 + /// Low priority Low = 0, - /// 普通优先级 + /// Normal priority Normal = 1, - /// 高优先级 + /// High priority High = 2, - /// 紧急优先级 + /// Urgent priority Urgent = 3, } @@ -84,22 +85,22 @@ impl Default for HealPriority { } } -/// Heal 选项 +/// Heal options #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HealOptions { - /// 扫描模式 + /// Scan mode pub scan_mode: HealScanMode, - /// 是否删除损坏数据 + /// Whether to remove corrupted data pub remove_corrupted: bool, - /// 是否重新创建 + /// Whether to recreate pub recreate_missing: bool, - /// 是否更新奇偶校验 + /// Whether to update parity pub update_parity: bool, - /// 是否递归处理 + /// Whether to recursively process pub recursive: bool, - /// 是否试运行 + /// Whether to dry run pub dry_run: bool, - /// 超时时间 + /// Timeout pub timeout: Option, } @@ -112,40 +113,40 @@ impl Default for HealOptions { update_parity: true, recursive: false, dry_run: false, - timeout: Some(Duration::from_secs(300)), // 5分钟默认超时 + timeout: Some(Duration::from_secs(300)), // 5 minutes default timeout } } } -/// Heal 任务状态 +/// Heal task status #[derive(Debug, Clone, Serialize, Deserialize)] pub enum HealTaskStatus { - /// 等待中 + /// Pending Pending, - /// 运行中 + /// Running Running, - /// 完成 + /// Completed Completed, - /// 失败 + /// Failed Failed { error: String }, - /// 取消 + /// Cancelled Cancelled, - /// 超时 + /// Timeout Timeout, } -/// Heal 请求 +/// Heal request #[derive(Debug, Clone)] pub struct HealRequest { - /// 请求 ID + /// Request ID pub id: String, - /// Heal 类型 + /// Heal type pub heal_type: HealType, - /// Heal 选项 + /// Heal options pub options: HealOptions, - /// 优先级 + /// Priority pub priority: HealPriority, - /// 创建时间 + /// Created time pub created_at: SystemTime, } @@ -209,27 +210,27 @@ impl HealRequest { } } -/// Heal 任务 +/// Heal task pub struct HealTask { - /// 任务 ID + /// Task ID pub id: String, - /// Heal 类型 + /// Heal type pub heal_type: HealType, - /// Heal 选项 + /// Heal options pub options: HealOptions, - /// 任务状态 + /// Task status pub status: Arc>, - /// 进度跟踪 + /// Progress tracking pub progress: Arc>, - /// 创建时间 + /// Created time pub created_at: SystemTime, - /// 开始时间 + /// Started time pub started_at: Arc>>, - /// 完成时间 + /// Completed time pub completed_at: Arc>>, - /// 取消令牌 + /// Cancel token pub cancel_token: tokio_util::sync::CancellationToken, - /// 存储层接口 + /// Storage layer interface pub storage: Arc, } @@ -250,7 +251,7 @@ impl HealTask { } pub async fn execute(&self) -> Result<()> { - // 更新状态为运行中 + // update status to running { let mut status = self.status.write().await; *status = HealTaskStatus::Running; @@ -283,7 +284,7 @@ impl HealTask { } }; - // 更新完成时间和状态 + // update completed time and status { let mut completed_at = self.completed_at.write().await; *completed_at = Some(SystemTime::now()); @@ -323,83 +324,527 @@ impl HealTask { self.progress.read().await.clone() } - // 具体的 heal 实现方法 - async fn heal_object(&self, bucket: &str, object: &str, _version_id: Option<&str>) -> Result<()> { - debug!("Healing object: {}/{}", bucket, object); + // specific heal implementation method + async fn heal_object(&self, bucket: &str, object: &str, version_id: Option<&str>) -> Result<()> { + info!("Healing object: {}/{}", bucket, object); - // 更新进度 + // update progress { let mut progress = self.progress.write().await; progress.set_current_object(Some(format!("{}/{}", bucket, object))); + progress.update_progress(0, 4, 0, 0); // 开始heal,总共4个步骤 } - // TODO: 实现具体的对象 heal 逻辑 - // 1. 检查对象完整性 - // 2. 如果损坏,尝试 EC 重建 - // 3. 更新对象数据 - // 4. 更新进度 + // Step 1: Check if object exists and get metadata + info!("Step 1: Checking object existence and metadata"); + let object_exists = self.storage.object_exists(bucket, object).await?; + if !object_exists { + warn!("Object does not exist: {}/{}", bucket, object); + if self.options.recreate_missing { + info!("Attempting to recreate missing object: {}/{}", bucket, object); + return self.recreate_missing_object(bucket, object, version_id).await; + } else { + return Err(Error::TaskExecutionFailed { + message: format!("Object not found: {}/{}", bucket, object), + }); + } + } { let mut progress = self.progress.write().await; - progress.update_progress(1, 1, 0, 1024); // 示例数据 + progress.update_progress(1, 4, 0, 0); } - Ok(()) + // Step 2: Verify object integrity + info!("Step 2: Verifying object integrity"); + let integrity_ok = self.storage.verify_object_integrity(bucket, object).await?; + if integrity_ok { + info!("Object integrity check passed: {}/{}", bucket, object); + { + let mut progress = self.progress.write().await; + progress.update_progress(4, 4, 0, 0); + } + return Ok(()); + } + + warn!("Object integrity check failed: {}/{}", bucket, object); + { + let mut progress = self.progress.write().await; + progress.update_progress(2, 4, 0, 0); + } + + // Step 3: Perform actual heal using ecstore + info!("Step 3: Performing heal using ecstore"); + let heal_opts = rustfs_ecstore::heal::heal_commands::HealOpts { + recursive: self.options.recursive, + dry_run: self.options.dry_run, + remove: self.options.remove_corrupted, + recreate: self.options.recreate_missing, + scan_mode: match self.options.scan_mode { + crate::heal::task::HEAL_UNKNOWN_SCAN => rustfs_ecstore::heal::heal_commands::HEAL_UNKNOWN_SCAN, + crate::heal::task::HEAL_NORMAL_SCAN => rustfs_ecstore::heal::heal_commands::HEAL_NORMAL_SCAN, + crate::heal::task::HEAL_DEEP_SCAN => rustfs_ecstore::heal::heal_commands::HEAL_DEEP_SCAN, + _ => rustfs_ecstore::heal::heal_commands::HEAL_NORMAL_SCAN, + }, + update_parity: self.options.update_parity, + no_lock: false, + pool: None, + set: None, + }; + + match self.storage.heal_object(bucket, object, version_id, &heal_opts).await { + Ok((result, error)) => { + if let Some(e) = error { + error!("Heal operation failed: {}/{} - {}", bucket, object, e); + + // If heal failed and remove_corrupted is enabled, delete the corrupted object + if self.options.remove_corrupted { + warn!("Removing corrupted object: {}/{}", bucket, object); + if !self.options.dry_run { + self.storage.delete_object(bucket, object).await?; + info!("Successfully deleted corrupted object: {}/{}", bucket, object); + } else { + info!("Dry run mode - would delete corrupted object: {}/{}", bucket, object); + } + } + + { + let mut progress = self.progress.write().await; + progress.update_progress(4, 4, 0, 0); + } + + return Err(Error::TaskExecutionFailed { + message: format!("Failed to heal object {}/{}: {}", bucket, object, e), + }); + } + + // Step 4: Verify heal result + info!("Step 4: Verifying heal result"); + let object_size = result.object_size as u64; + info!("Heal completed successfully: {}/{} ({} bytes, {} drives healed)", + bucket, object, object_size, result.after.drives.len()); + + { + let mut progress = self.progress.write().await; + progress.update_progress(4, 4, object_size, object_size); + } + Ok(()) + } + Err(e) => { + error!("Heal operation failed: {}/{} - {}", bucket, object, e); + + // If heal failed and remove_corrupted is enabled, delete the corrupted object + if self.options.remove_corrupted { + warn!("Removing corrupted object: {}/{}", bucket, object); + if !self.options.dry_run { + self.storage.delete_object(bucket, object).await?; + info!("Successfully deleted corrupted object: {}/{}", bucket, object); + } else { + info!("Dry run mode - would delete corrupted object: {}/{}", bucket, object); + } + } + + { + let mut progress = self.progress.write().await; + progress.update_progress(4, 4, 0, 0); + } + + Err(Error::TaskExecutionFailed { + message: format!("Failed to heal object {}/{}: {}", bucket, object, e), + }) + } + } + } + + /// Recreate missing object (for EC decode scenarios) + async fn recreate_missing_object(&self, bucket: &str, object: &str, version_id: Option<&str>) -> Result<()> { + info!("Attempting to recreate missing object: {}/{}", bucket, object); + + // Use ecstore's heal_object with recreate option + let heal_opts = rustfs_ecstore::heal::heal_commands::HealOpts { + recursive: false, + dry_run: self.options.dry_run, + remove: false, + recreate: true, + scan_mode: rustfs_ecstore::heal::heal_commands::HEAL_DEEP_SCAN, + update_parity: true, + no_lock: false, + pool: None, + set: None, + }; + + match self.storage.heal_object(bucket, object, version_id, &heal_opts).await { + Ok((result, error)) => { + if let Some(e) = error { + error!("Failed to recreate missing object: {}/{} - {}", bucket, object, e); + return Err(Error::TaskExecutionFailed { + message: format!("Failed to recreate missing object {}/{}: {}", bucket, object, e), + }); + } + + let object_size = result.object_size as u64; + info!("Successfully recreated missing object: {}/{} ({} bytes)", bucket, object, object_size); + + { + let mut progress = self.progress.write().await; + progress.update_progress(4, 4, object_size, object_size); + } + Ok(()) + } + Err(e) => { + error!("Failed to recreate missing object: {}/{} - {}", bucket, object, e); + Err(Error::TaskExecutionFailed { + message: format!("Failed to recreate missing object {}/{}: {}", bucket, object, e), + }) + } + } } async fn heal_bucket(&self, bucket: &str) -> Result<()> { - debug!("Healing bucket: {}", bucket); + info!("Healing bucket: {}", bucket); - // TODO: 实现桶 heal 逻辑 - // 1. 检查桶元数据 - // 2. 修复桶配置 - // 3. 更新进度 + // update progress + { + let mut progress = self.progress.write().await; + progress.set_current_object(Some(format!("bucket: {}", bucket))); + progress.update_progress(0, 3, 0, 0); + } - Ok(()) + // Step 1: Check if bucket exists + info!("Step 1: Checking bucket existence"); + let bucket_exists = self.storage.get_bucket_info(bucket).await?.is_some(); + if !bucket_exists { + warn!("Bucket does not exist: {}", bucket); + return Err(Error::TaskExecutionFailed { + message: format!("Bucket not found: {}", bucket), + }); + } + + { + let mut progress = self.progress.write().await; + progress.update_progress(1, 3, 0, 0); + } + + // Step 2: Perform bucket heal using ecstore + info!("Step 2: Performing bucket heal using ecstore"); + let heal_opts = rustfs_ecstore::heal::heal_commands::HealOpts { + recursive: self.options.recursive, + dry_run: self.options.dry_run, + remove: self.options.remove_corrupted, + recreate: self.options.recreate_missing, + scan_mode: match self.options.scan_mode { + crate::heal::task::HEAL_UNKNOWN_SCAN => rustfs_ecstore::heal::heal_commands::HEAL_UNKNOWN_SCAN, + crate::heal::task::HEAL_NORMAL_SCAN => rustfs_ecstore::heal::heal_commands::HEAL_NORMAL_SCAN, + crate::heal::task::HEAL_DEEP_SCAN => rustfs_ecstore::heal::heal_commands::HEAL_DEEP_SCAN, + _ => rustfs_ecstore::heal::heal_commands::HEAL_NORMAL_SCAN, + }, + update_parity: self.options.update_parity, + no_lock: false, + pool: None, + set: None, + }; + + match self.storage.heal_bucket(bucket, &heal_opts).await { + Ok(result) => { + info!("Bucket heal completed successfully: {} ({} drives)", bucket, result.after.drives.len()); + + { + let mut progress = self.progress.write().await; + progress.update_progress(3, 3, 0, 0); + } + Ok(()) + } + Err(e) => { + error!("Bucket heal failed: {} - {}", bucket, e); + { + let mut progress = self.progress.write().await; + progress.update_progress(3, 3, 0, 0); + } + Err(Error::TaskExecutionFailed { + message: format!("Failed to heal bucket {}: {}", bucket, e), + }) + } + } } async fn heal_disk(&self, endpoint: &Endpoint) -> Result<()> { - debug!("Healing disk: {:?}", endpoint); + info!("Healing disk: {:?}", endpoint); - // TODO: 实现磁盘 heal 逻辑 - // 1. 检查磁盘状态 - // 2. 格式化磁盘(如果需要) - // 3. 更新进度 + // update progress + { + let mut progress = self.progress.write().await; + progress.set_current_object(Some(format!("disk: {:?}", endpoint))); + progress.update_progress(0, 3, 0, 0); + } - Ok(()) + // Step 1: Check disk status + info!("Step 1: Checking disk status"); + let disk_status = self.storage.get_disk_status(endpoint).await?; + if disk_status == DiskStatus::Ok { + info!("Disk is already healthy: {:?}", endpoint); + { + let mut progress = self.progress.write().await; + progress.update_progress(3, 3, 0, 0); + } + return Ok(()); + } + + { + let mut progress = self.progress.write().await; + progress.update_progress(1, 3, 0, 0); + } + + // Step 2: Perform disk heal using ecstore + info!("Step 2: Performing disk heal using ecstore"); + match self.storage.heal_format(self.options.dry_run).await { + Ok((result, error)) => { + if let Some(e) = error { + error!("Disk heal failed: {:?} - {}", endpoint, e); + { + let mut progress = self.progress.write().await; + progress.update_progress(3, 3, 0, 0); + } + return Err(Error::TaskExecutionFailed { + message: format!("Failed to heal disk {:?}: {}", endpoint, e), + }); + } + + info!("Disk heal completed successfully: {:?} ({} drives)", endpoint, result.after.drives.len()); + + { + let mut progress = self.progress.write().await; + progress.update_progress(3, 3, 0, 0); + } + Ok(()) + } + Err(e) => { + error!("Disk heal failed: {:?} - {}", endpoint, e); + { + let mut progress = self.progress.write().await; + progress.update_progress(3, 3, 0, 0); + } + Err(Error::TaskExecutionFailed { + message: format!("Failed to heal disk {:?}: {}", endpoint, e), + }) + } + } } async fn heal_metadata(&self, bucket: &str, object: &str) -> Result<()> { - debug!("Healing metadata: {}/{}", bucket, object); + info!("Healing metadata: {}/{}", bucket, object); - // TODO: 实现元数据 heal 逻辑 - // 1. 检查元数据完整性 - // 2. 重建元数据 - // 3. 更新进度 + // update progress + { + let mut progress = self.progress.write().await; + progress.set_current_object(Some(format!("metadata: {}/{}", bucket, object))); + progress.update_progress(0, 3, 0, 0); + } - Ok(()) + // Step 1: Check if object exists + info!("Step 1: Checking object existence"); + let object_exists = self.storage.object_exists(bucket, object).await?; + if !object_exists { + warn!("Object does not exist: {}/{}", bucket, object); + return Err(Error::TaskExecutionFailed { + message: format!("Object not found: {}/{}", bucket, object), + }); + } + + { + let mut progress = self.progress.write().await; + progress.update_progress(1, 3, 0, 0); + } + + // Step 2: Perform metadata heal using ecstore + info!("Step 2: Performing metadata heal using ecstore"); + let heal_opts = rustfs_ecstore::heal::heal_commands::HealOpts { + recursive: false, + dry_run: self.options.dry_run, + remove: false, + recreate: false, + scan_mode: rustfs_ecstore::heal::heal_commands::HEAL_DEEP_SCAN, + update_parity: false, + no_lock: false, + pool: None, + set: None, + }; + + match self.storage.heal_object(bucket, object, None, &heal_opts).await { + Ok((result, error)) => { + if let Some(e) = error { + error!("Metadata heal failed: {}/{} - {}", bucket, object, e); + { + let mut progress = self.progress.write().await; + progress.update_progress(3, 3, 0, 0); + } + return Err(Error::TaskExecutionFailed { + message: format!("Failed to heal metadata {}/{}: {}", bucket, object, e), + }); + } + + info!("Metadata heal completed successfully: {}/{} ({} drives)", bucket, object, result.after.drives.len()); + + { + let mut progress = self.progress.write().await; + progress.update_progress(3, 3, 0, 0); + } + Ok(()) + } + Err(e) => { + error!("Metadata heal failed: {}/{} - {}", bucket, object, e); + { + let mut progress = self.progress.write().await; + progress.update_progress(3, 3, 0, 0); + } + Err(Error::TaskExecutionFailed { + message: format!("Failed to heal metadata {}/{}: {}", bucket, object, e), + }) + } + } } async fn heal_mrf(&self, meta_path: &str) -> Result<()> { - debug!("Healing MRF: {}", meta_path); + info!("Healing MRF: {}", meta_path); - // TODO: 实现 MRF heal 逻辑 - // 1. 检查元数据复制因子 - // 2. 修复元数据 - // 3. 更新进度 + // update progress + { + let mut progress = self.progress.write().await; + progress.set_current_object(Some(format!("mrf: {}", meta_path))); + progress.update_progress(0, 2, 0, 0); + } - Ok(()) + // Parse meta_path to extract bucket and object + let parts: Vec<&str> = meta_path.split('/').collect(); + if parts.len() < 2 { + return Err(Error::TaskExecutionFailed { + message: format!("Invalid meta path format: {}", meta_path), + }); + } + + let bucket = parts[0]; + let object = parts[1..].join("/"); + + // Step 1: Perform MRF heal using ecstore + info!("Step 1: Performing MRF heal using ecstore"); + let heal_opts = rustfs_ecstore::heal::heal_commands::HealOpts { + recursive: true, + dry_run: self.options.dry_run, + remove: self.options.remove_corrupted, + recreate: self.options.recreate_missing, + scan_mode: rustfs_ecstore::heal::heal_commands::HEAL_DEEP_SCAN, + update_parity: true, + no_lock: false, + pool: None, + set: None, + }; + + match self.storage.heal_object(bucket, &object, None, &heal_opts).await { + Ok((result, error)) => { + if let Some(e) = error { + error!("MRF heal failed: {} - {}", meta_path, e); + { + let mut progress = self.progress.write().await; + progress.update_progress(2, 2, 0, 0); + } + return Err(Error::TaskExecutionFailed { + message: format!("Failed to heal MRF {}: {}", meta_path, e), + }); + } + + info!("MRF heal completed successfully: {} ({} drives)", meta_path, result.after.drives.len()); + + { + let mut progress = self.progress.write().await; + progress.update_progress(2, 2, 0, 0); + } + Ok(()) + } + Err(e) => { + error!("MRF heal failed: {} - {}", meta_path, e); + { + let mut progress = self.progress.write().await; + progress.update_progress(2, 2, 0, 0); + } + Err(Error::TaskExecutionFailed { + message: format!("Failed to heal MRF {}: {}", meta_path, e), + }) + } + } } - async fn heal_ec_decode(&self, bucket: &str, object: &str, _version_id: Option<&str>) -> Result<()> { - debug!("Healing EC decode: {}/{}", bucket, object); + async fn heal_ec_decode(&self, bucket: &str, object: &str, version_id: Option<&str>) -> Result<()> { + info!("Healing EC decode: {}/{}", bucket, object); - // TODO: 实现 EC 解码 heal 逻辑 - // 1. 检查 EC 分片 - // 2. 使用 EC 算法重建数据 - // 3. 更新进度 + // update progress + { + let mut progress = self.progress.write().await; + progress.set_current_object(Some(format!("ec_decode: {}/{}", bucket, object))); + progress.update_progress(0, 3, 0, 0); + } - Ok(()) + // Step 1: Check if object exists + info!("Step 1: Checking object existence"); + let object_exists = self.storage.object_exists(bucket, object).await?; + if !object_exists { + warn!("Object does not exist: {}/{}", bucket, object); + return Err(Error::TaskExecutionFailed { + message: format!("Object not found: {}/{}", bucket, object), + }); + } + + { + let mut progress = self.progress.write().await; + progress.update_progress(1, 3, 0, 0); + } + + // Step 2: Perform EC decode heal using ecstore + info!("Step 2: Performing EC decode heal using ecstore"); + let heal_opts = rustfs_ecstore::heal::heal_commands::HealOpts { + recursive: false, + dry_run: self.options.dry_run, + remove: false, + recreate: true, + scan_mode: rustfs_ecstore::heal::heal_commands::HEAL_DEEP_SCAN, + update_parity: true, + no_lock: false, + pool: None, + set: None, + }; + + match self.storage.heal_object(bucket, object, version_id, &heal_opts).await { + Ok((result, error)) => { + if let Some(e) = error { + error!("EC decode heal failed: {}/{} - {}", bucket, object, e); + { + let mut progress = self.progress.write().await; + progress.update_progress(3, 3, 0, 0); + } + return Err(Error::TaskExecutionFailed { + message: format!("Failed to heal EC decode {}/{}: {}", bucket, object, e), + }); + } + + let object_size = result.object_size as u64; + info!("EC decode heal completed successfully: {}/{} ({} bytes, {} drives)", + bucket, object, object_size, result.after.drives.len()); + + { + let mut progress = self.progress.write().await; + progress.update_progress(3, 3, object_size, object_size); + } + Ok(()) + } + Err(e) => { + error!("EC decode heal failed: {}/{} - {}", bucket, object, e); + { + let mut progress = self.progress.write().await; + progress.update_progress(3, 3, 0, 0); + } + Err(Error::TaskExecutionFailed { + message: format!("Failed to heal EC decode {}/{}: {}", bucket, object, e), + }) + } + } } } diff --git a/crates/ahm/src/scanner/data_scanner.rs b/crates/ahm/src/scanner/data_scanner.rs index 611bd3d05..243296b34 100644 --- a/crates/ahm/src/scanner/data_scanner.rs +++ b/crates/ahm/src/scanner/data_scanner.rs @@ -510,10 +510,10 @@ impl Scanner { let req = HealRequest::disk(disk.endpoint().clone()); match heal_manager.submit_heal_request(req).await { Ok(task_id) => { - warn!("磁盘离线,已自动提交heal任务: {} 磁盘: {}", task_id, disk_path); + warn!("disk offline, submit heal task: {} {}", task_id, disk_path); } Err(e) => { - error!("磁盘离线,heal任务提交失败: {},错误: {}", disk_path, e); + error!("disk offline, submit heal task failed: {} {}", disk_path, e); } } } @@ -554,10 +554,10 @@ impl Scanner { ); match heal_manager.submit_heal_request(req).await { Ok(task_id) => { - warn!("磁盘访问失败,已自动提交heal任务: {} 磁盘: {} 错误: {}", task_id, disk_path, e); + warn!("disk access failed, submit heal task: {} {}", task_id, disk_path); } Err(heal_err) => { - error!("磁盘访问失败,heal任务提交失败: {},错误: {}", disk_path, heal_err); + error!("disk access failed, submit heal task failed: {} {}", disk_path, heal_err); } } } @@ -682,10 +682,10 @@ impl Scanner { let req = HealRequest::metadata(bucket.to_string(), entry.name.clone()); match heal_manager.submit_heal_request(req).await { Ok(task_id) => { - warn!("对象元数据损坏,已自动提交heal任务: {} {} / {}", task_id, bucket, entry.name); + warn!("object metadata damaged, submit heal task: {} {} / {}", task_id, bucket, entry.name); } Err(e) => { - error!("对象元数据损坏,heal任务提交失败: {} / {},错误: {}", bucket, entry.name, e); + error!("object metadata damaged, submit heal task failed: {} / {} {}", bucket, entry.name, e); } } } @@ -705,10 +705,10 @@ impl Scanner { let req = HealRequest::metadata(bucket.to_string(), entry.name.clone()); match heal_manager.submit_heal_request(req).await { Ok(task_id) => { - warn!("对象元数据解析失败,已自动提交heal任务: {} {} / {}", task_id, bucket, entry.name); + warn!("object metadata parse failed, submit heal task: {} {} / {}", task_id, bucket, entry.name); } Err(e) => { - error!("对象元数据解析失败,heal任务提交失败: {} / {},错误: {}", bucket, entry.name, e); + error!("object metadata parse failed, submit heal task failed: {} / {} {}", bucket, entry.name, e); } } } @@ -817,7 +817,7 @@ impl Scanner { warn!("Object {}/{} missing from disks: {:?}", bucket, object_name, missing_disks); println!("Object {bucket}/{object_name} missing from disks: {missing_disks:?}"); - // 自动提交heal任务 + // submit heal task let enable_healing = self.config.read().await.enable_healing; if enable_healing { if let Some(heal_manager) = &self.heal_manager { @@ -833,11 +833,11 @@ impl Scanner { ); match heal_manager.submit_heal_request(req).await { Ok(task_id) => { - warn!("对象缺失,已自动提交heal任务: {} {} / {} (缺失磁盘: {:?})", + warn!("object missing, submit heal task: {} {} / {} (missing disks: {:?})", task_id, bucket, object_name, missing_disks); } Err(e) => { - error!("对象缺失,heal任务提交失败: {} / {},错误: {}", bucket, object_name, e); + error!("object missing, submit heal task failed: {} / {} {}", bucket, object_name, e); } } } @@ -969,7 +969,7 @@ impl Scanner { ); Ok(()) } else { - // 自动提交heal任务 + // submit heal task let enable_healing = self.config.read().await.enable_healing; if enable_healing { if let Some(heal_manager) = &self.heal_manager { @@ -977,10 +977,10 @@ impl Scanner { let req = HealRequest::ec_decode(bucket.to_string(), object.to_string(), None); match heal_manager.submit_heal_request(req).await { Ok(task_id) => { - warn!("EC decode失败,已自动提交heal任务: {} {} / {}", task_id, bucket, object); + warn!("EC decode failed, submit heal task: {} {} / {}", task_id, bucket, object); } Err(e) => { - error!("EC decode失败,heal任务提交失败: {} / {},错误: {}", bucket, object, e); + error!("EC decode failed, submit heal task failed: {} / {} {}", bucket, object, e); } } } diff --git a/crates/ahm/tests/heal_integration_test.rs b/crates/ahm/tests/heal_integration_test.rs new file mode 100644 index 000000000..ac5aae851 --- /dev/null +++ b/crates/ahm/tests/heal_integration_test.rs @@ -0,0 +1,353 @@ +use rustfs_ahm::heal::{ + manager::HealManager, + storage::{ECStoreHealStorage, HealStorageAPI}, + task::{HealOptions, HealPriority, HealRequest, HealType, HEAL_NORMAL_SCAN}, +}; +use rustfs_ecstore::{ + disk::endpoint::Endpoint, + endpoints::{EndpointServerPools, Endpoints, PoolEndpoints}, + store::ECStore, + store_api::{PutObjReader, ObjectOptions, StorageAPI, ObjectIO}, +}; +use std::{path::PathBuf, sync::Arc, time::Duration}; +use tokio::fs; +use tracing::info; + +/// Test helper: Create test environment with ECStore +async fn setup_test_env() -> (Vec, Arc, Arc) { + // create temp dir as 4 disks + let test_base_dir = "/tmp/rustfs_ahm_heal_test"; + let temp_dir = std::path::PathBuf::from(test_base_dir); + if temp_dir.exists() { + fs::remove_dir_all(&temp_dir).await.unwrap(); + } + fs::create_dir_all(&temp_dir).await.unwrap(); + + // create 4 disk dirs + let disk_paths = vec![ + temp_dir.join("disk1"), + temp_dir.join("disk2"), + temp_dir.join("disk3"), + temp_dir.join("disk4"), + ]; + + for disk_path in &disk_paths { + fs::create_dir_all(disk_path).await.unwrap(); + } + + // create EndpointServerPools + let mut endpoints = Vec::new(); + for (i, disk_path) in disk_paths.iter().enumerate() { + let mut endpoint = Endpoint::try_from(disk_path.to_str().unwrap()).unwrap(); + // set correct index + endpoint.set_pool_index(0); + endpoint.set_set_index(0); + endpoint.set_disk_index(i); + endpoints.push(endpoint); + } + + let pool_endpoints = PoolEndpoints { + legacy: false, + set_count: 1, + drives_per_set: 4, + endpoints: Endpoints::from(endpoints), + cmd_line: "test".to_string(), + platform: format!("OS: {} | Arch: {}", std::env::consts::OS, std::env::consts::ARCH), + }; + + let endpoint_pools = EndpointServerPools(vec![pool_endpoints]); + + // format disks + rustfs_ecstore::store::init_local_disks(endpoint_pools.clone()).await.unwrap(); + + // create ECStore with dynamic port + let port = 9001; + let server_addr: std::net::SocketAddr = format!("127.0.0.1:{port}").parse().unwrap(); + let ecstore = ECStore::new(server_addr, endpoint_pools).await.unwrap(); + + // init bucket metadata system + let buckets_list = ecstore + .list_bucket(&rustfs_ecstore::store_api::BucketOptions { + no_metadata: true, + ..Default::default() + }) + .await + .unwrap(); + let buckets = buckets_list.into_iter().map(|v| v.name).collect(); + rustfs_ecstore::bucket::metadata_sys::init_bucket_metadata_sys(ecstore.clone(), buckets).await; + + // Create heal storage layer + let heal_storage = Arc::new(ECStoreHealStorage::new(ecstore.clone())); + + (disk_paths, ecstore, heal_storage) +} + +/// Test helper: Create a test bucket +async fn create_test_bucket(ecstore: &Arc, bucket_name: &str) { + (&**ecstore) + .make_bucket(bucket_name, &Default::default()) + .await + .expect("Failed to create test bucket"); + info!("Created test bucket: {}", bucket_name); +} + +/// Test helper: Upload test object +async fn upload_test_object( + ecstore: &Arc, + bucket: &str, + object: &str, + data: &[u8], +) { + let mut reader = PutObjReader::from_vec(data.to_vec()); + let object_info = (&**ecstore) + .put_object(bucket, object, &mut reader, &ObjectOptions::default()) + .await + .expect("Failed to upload test object"); + + info!( + "Uploaded test object: {}/{} ({} bytes)", + bucket, object, object_info.size + ); +} + +/// Test helper: Cleanup test environment +async fn cleanup_test_env(disk_paths: &[PathBuf]) { + for disk_path in disk_paths { + if disk_path.exists() { + fs::remove_dir_all(disk_path).await.expect("Failed to cleanup disk path"); + } + } + + // Clean up test base directory + let test_base_dir = PathBuf::from("/tmp/rustfs_ahm_heal_test"); + if test_base_dir.exists() { + fs::remove_dir_all(&test_base_dir).await.expect("Failed to cleanup test base directory"); + } + + info!("Test environment cleaned up"); +} + +#[tokio::test] +async fn test_heal_object_basic() { + 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; + + // Create heal manager + let heal_manager = HealManager::new(heal_storage.clone(), Default::default()); + + // 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: HEAL_NORMAL_SCAN, + update_parity: true, + timeout: Some(Duration::from_secs(300)), + }, + HealPriority::Normal, + ); + + let task_id = heal_manager + .submit_heal_request(heal_request) + .await + .expect("Failed to submit heal request"); + + info!("Submitted heal request with task ID: {}", task_id); + + // Wait for task completion + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + + // Check task status + let task_status = heal_manager.get_task_status(&task_id).await; + assert!(task_status.is_ok()); + + let status = task_status.unwrap(); + info!("Task status: {:?}", status); + + // Verify object still exists and is accessible + let object_exists = heal_storage.object_exists(bucket_name, object_name).await; + assert!(object_exists.is_ok()); + assert!(object_exists.unwrap()); + + // Cleanup + cleanup_test_env(&disk_paths).await; + + info!("Heal object basic test passed"); +} + +#[tokio::test] +async fn test_heal_bucket_basic() { + let (disk_paths, ecstore, heal_storage) = setup_test_env().await; + + // Create test bucket + let bucket_name = "test-bucket-heal"; + create_test_bucket(&ecstore, bucket_name).await; + + // Create heal manager + let heal_manager = HealManager::new(heal_storage.clone(), Default::default()); + + // 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: HEAL_NORMAL_SCAN, + update_parity: false, + timeout: Some(Duration::from_secs(300)), + }, + HealPriority::Normal, + ); + + let task_id = heal_manager + .submit_heal_request(heal_request) + .await + .expect("Failed to submit bucket heal request"); + + info!("Submitted bucket heal request with task ID: {}", task_id); + + // Wait for task completion + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + + // Check task status + let task_status = heal_manager.get_task_status(&task_id).await; + assert!(task_status.is_ok()); + + let status = task_status.unwrap(); + info!("Bucket heal task status: {:?}", status); + + // Verify bucket still exists + let bucket_exists = heal_storage.get_bucket_info(bucket_name).await; + assert!(bucket_exists.is_ok()); + assert!(bucket_exists.unwrap().is_some()); + + // Cleanup + cleanup_test_env(&disk_paths).await; + + info!("Heal bucket basic test passed"); +} + +#[tokio::test] +async fn test_heal_format_basic() { + let (disk_paths, _ecstore, heal_storage) = setup_test_env().await; + + // Create heal manager + let heal_manager = HealManager::new(heal_storage.clone(), Default::default()); + + // Get disk endpoint for testing + let disk_endpoint = Endpoint::try_from(disk_paths[0].to_str().unwrap()) + .expect("Failed to create disk endpoint"); + + // Submit disk heal request (format heal) + let heal_request = HealRequest::new( + HealType::Disk { endpoint: disk_endpoint }, + HealOptions { + dry_run: true, // Use dry run for format heal test + recursive: false, + remove_corrupted: false, + recreate_missing: false, + scan_mode: HEAL_NORMAL_SCAN, + update_parity: false, + timeout: Some(Duration::from_secs(300)), + }, + HealPriority::Normal, + ); + + let task_id = heal_manager + .submit_heal_request(heal_request) + .await + .expect("Failed to submit disk heal request"); + + info!("Submitted disk heal request with task ID: {}", task_id); + + // Wait for task completion + tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; + + // Check task status + let task_status = heal_manager.get_task_status(&task_id).await; + assert!(task_status.is_ok()); + + let status = task_status.unwrap(); + info!("Disk heal task status: {:?}", status); + + // Cleanup + cleanup_test_env(&disk_paths).await; + + info!("Heal format basic test passed"); +} + +#[tokio::test] +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 = rustfs_ecstore::heal::heal_commands::HealOpts { + recursive: true, + dry_run: true, + remove: false, + recreate: false, + scan_mode: rustfs_ecstore::heal::heal_commands::HEAL_NORMAL_SCAN, + 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 = rustfs_ecstore::heal::heal_commands::HealOpts { + recursive: false, + dry_run: true, + remove: false, + recreate: false, + scan_mode: rustfs_ecstore::heal::heal_commands::HEAL_NORMAL_SCAN, + 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"); + + // Cleanup + cleanup_test_env(&disk_paths).await; + + info!("Direct heal storage API test passed"); +} \ No newline at end of file From 3409cd8dfffa2ce72b1a7d543ee3288fc99c0b75 Mon Sep 17 00:00:00 2001 From: junxiang Mu <1948535941@qq.com> Date: Fri, 11 Jul 2025 18:42:12 +0800 Subject: [PATCH 07/29] feat(ahm): add HealingTracker support & complete fresh-disk healing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Introduce ecstore HealingTracker into ahm crate; load/init/save tracker • Re-implement heal_fresh_disk to use heal_erasure_set with tracker • Enhance auto-disk scanner: detect unformatted disks via get_disk_id() • Remove DataUsageCache handling for now • Refactor imports & types, clean up duplicate constants --- crates/ahm/src/error.rs | 5 +- crates/ahm/src/heal/event.rs | 250 ++++++++------- crates/ahm/src/heal/manager.rs | 88 +++++- crates/ahm/src/heal/mod.rs | 2 +- crates/ahm/src/heal/progress.rs | 11 +- crates/ahm/src/heal/storage.rs | 181 ++++++----- crates/ahm/src/heal/task.rs | 357 ++++++++++++---------- crates/ahm/src/lib.rs | 2 +- crates/ahm/src/scanner/data_scanner.rs | 47 ++- crates/ahm/tests/heal_integration_test.rs | 297 ++++++++++-------- crates/ecstore/src/set_disk.rs | 6 +- 11 files changed, 731 insertions(+), 515 deletions(-) diff --git a/crates/ahm/src/error.rs b/crates/ahm/src/error.rs index a8938f9ae..aca503839 100644 --- a/crates/ahm/src/error.rs +++ b/crates/ahm/src/error.rs @@ -24,6 +24,9 @@ pub enum Error { #[error("Storage error: {0}")] Storage(#[from] rustfs_ecstore::error::Error), + #[error("Disk error: {0}")] + Disk(#[from] rustfs_ecstore::disk::error::DiskError), + #[error("Configuration error: {0}")] Config(String), @@ -88,4 +91,4 @@ impl From for std::io::Error { fn from(err: Error) -> Self { std::io::Error::other(err) } -} \ No newline at end of file +} diff --git a/crates/ahm/src/heal/event.rs b/crates/ahm/src/heal/event.rs index 0e59452ed..85ecb0023 100644 --- a/crates/ahm/src/heal/event.rs +++ b/crates/ahm/src/heal/event.rs @@ -106,87 +106,88 @@ impl HealEvent { /// Convert HealEvent to HealRequest pub fn to_heal_request(&self) -> HealRequest { match self { - HealEvent::ObjectCorruption { bucket, object, version_id, severity, .. } => { - HealRequest::new( - HealType::Object { - bucket: bucket.clone(), - object: object.clone(), - version_id: version_id.clone(), - }, - HealOptions::default(), - Self::severity_to_priority(severity), - ) - } - HealEvent::ObjectMissing { bucket, object, version_id, .. } => { - HealRequest::new( - HealType::Object { - bucket: bucket.clone(), - object: object.clone(), - version_id: version_id.clone(), - }, - HealOptions::default(), - HealPriority::High, - ) - } - HealEvent::MetadataCorruption { bucket, object, .. } => { - HealRequest::new( - HealType::Metadata { - bucket: bucket.clone(), - object: object.clone(), - }, - HealOptions::default(), - HealPriority::High, - ) - } - HealEvent::DiskStatusChange { endpoint, .. } => { - HealRequest::new( - HealType::Disk { - endpoint: endpoint.clone(), - }, - HealOptions::default(), - HealPriority::High, - ) - } - HealEvent::ECDecodeFailure { bucket, object, version_id, .. } => { - HealRequest::new( - HealType::ECDecode { - bucket: bucket.clone(), - object: object.clone(), - version_id: version_id.clone(), - }, - HealOptions::default(), - HealPriority::Urgent, - ) - } - HealEvent::ChecksumMismatch { bucket, object, version_id, .. } => { - HealRequest::new( - HealType::Object { - bucket: bucket.clone(), - object: object.clone(), - version_id: version_id.clone(), - }, - HealOptions::default(), - HealPriority::High, - ) - } + HealEvent::ObjectCorruption { + bucket, + object, + version_id, + severity, + .. + } => HealRequest::new( + HealType::Object { + bucket: bucket.clone(), + object: object.clone(), + version_id: version_id.clone(), + }, + HealOptions::default(), + Self::severity_to_priority(severity), + ), + HealEvent::ObjectMissing { + bucket, + object, + version_id, + .. + } => HealRequest::new( + HealType::Object { + bucket: bucket.clone(), + object: object.clone(), + version_id: version_id.clone(), + }, + HealOptions::default(), + HealPriority::High, + ), + HealEvent::MetadataCorruption { bucket, object, .. } => HealRequest::new( + HealType::Metadata { + bucket: bucket.clone(), + object: object.clone(), + }, + HealOptions::default(), + HealPriority::High, + ), + HealEvent::DiskStatusChange { endpoint, .. } => HealRequest::new( + HealType::Disk { + endpoint: endpoint.clone(), + }, + HealOptions::default(), + HealPriority::High, + ), + HealEvent::ECDecodeFailure { + bucket, + object, + version_id, + .. + } => HealRequest::new( + HealType::ECDecode { + bucket: bucket.clone(), + object: object.clone(), + version_id: version_id.clone(), + }, + HealOptions::default(), + HealPriority::Urgent, + ), + HealEvent::ChecksumMismatch { + bucket, + object, + version_id, + .. + } => HealRequest::new( + HealType::Object { + bucket: bucket.clone(), + object: object.clone(), + version_id: version_id.clone(), + }, + HealOptions::default(), + HealPriority::High, + ), HealEvent::BucketMetadataCorruption { bucket, .. } => { - HealRequest::new( - HealType::Bucket { - bucket: bucket.clone(), - }, - HealOptions::default(), - HealPriority::High, - ) - } - HealEvent::MRFMetadataCorruption { meta_path, .. } => { - HealRequest::new( - HealType::MRF { - meta_path: meta_path.clone(), - }, - HealOptions::default(), - HealPriority::High, - ) + HealRequest::new(HealType::Bucket { bucket: bucket.clone() }, HealOptions::default(), HealPriority::High) } + HealEvent::MRFMetadataCorruption { meta_path, .. } => HealRequest::new( + HealType::MRF { + meta_path: meta_path.clone(), + }, + HealOptions::default(), + HealPriority::High, + ), } } @@ -203,29 +204,63 @@ impl HealEvent { /// Get event description pub fn description(&self) -> String { match self { - HealEvent::ObjectCorruption { bucket, object, corruption_type, .. } => { - format!("Object corruption detected: {}/{} - {:?}", bucket, object, corruption_type) + HealEvent::ObjectCorruption { + bucket, + object, + corruption_type, + .. + } => { + format!("Object corruption detected: {bucket}/{object} - {corruption_type:?}") } HealEvent::ObjectMissing { bucket, object, .. } => { - format!("Object missing: {}/{}", bucket, object) + format!("Object missing: {bucket}/{object}") } - HealEvent::MetadataCorruption { bucket, object, corruption_type, .. } => { - format!("Metadata corruption: {}/{} - {:?}", bucket, object, corruption_type) + HealEvent::MetadataCorruption { + bucket, + object, + corruption_type, + .. + } => { + format!("Metadata corruption: {bucket}/{object} - {corruption_type:?}") } - HealEvent::DiskStatusChange { endpoint, old_status, new_status, .. } => { - format!("Disk status changed: {:?} {} -> {}", endpoint, old_status, new_status) + HealEvent::DiskStatusChange { + endpoint, + old_status, + new_status, + .. + } => { + format!("Disk status changed: {endpoint:?} {old_status} -> {new_status}") } - HealEvent::ECDecodeFailure { bucket, object, missing_shards, .. } => { - format!("EC decode failure: {}/{} - missing shards: {:?}", bucket, object, missing_shards) + HealEvent::ECDecodeFailure { + bucket, + object, + missing_shards, + .. + } => { + format!("EC decode failure: {bucket}/{object} - missing shards: {missing_shards:?}") } - HealEvent::ChecksumMismatch { bucket, object, expected_checksum, actual_checksum, .. } => { - format!("Checksum mismatch: {}/{} - expected: {}, actual: {}", bucket, object, expected_checksum, actual_checksum) + HealEvent::ChecksumMismatch { + bucket, + object, + expected_checksum, + actual_checksum, + .. + } => { + format!( + "Checksum mismatch: {bucket}/{object} - expected: {expected_checksum}, actual: {actual_checksum}" + ) } - HealEvent::BucketMetadataCorruption { bucket, corruption_type, .. } => { - format!("Bucket metadata corruption: {} - {:?}", bucket, corruption_type) + HealEvent::BucketMetadataCorruption { + bucket, corruption_type, .. + } => { + format!("Bucket metadata corruption: {bucket} - {corruption_type:?}") } - HealEvent::MRFMetadataCorruption { meta_path, corruption_type, .. } => { - format!("MRF metadata corruption: {} - {:?}", meta_path, corruption_type) + HealEvent::MRFMetadataCorruption { + meta_path, + corruption_type, + .. + } => { + format!("MRF metadata corruption: {meta_path} - {corruption_type:?}") } } } @@ -292,27 +327,22 @@ impl HealEventHandler { /// Filter events by severity pub fn filter_by_severity(&self, min_severity: Severity) -> Vec<&HealEvent> { - self.events - .iter() - .filter(|event| event.severity() >= min_severity) - .collect() + self.events.iter().filter(|event| event.severity() >= min_severity).collect() } /// Filter events by type pub fn filter_by_type(&self, event_type: &str) -> Vec<&HealEvent> { self.events .iter() - .filter(|event| { - match event { - HealEvent::ObjectCorruption { .. } => event_type == "ObjectCorruption", - HealEvent::ObjectMissing { .. } => event_type == "ObjectMissing", - HealEvent::MetadataCorruption { .. } => event_type == "MetadataCorruption", - HealEvent::DiskStatusChange { .. } => event_type == "DiskStatusChange", - HealEvent::ECDecodeFailure { .. } => event_type == "ECDecodeFailure", - HealEvent::ChecksumMismatch { .. } => event_type == "ChecksumMismatch", - HealEvent::BucketMetadataCorruption { .. } => event_type == "BucketMetadataCorruption", - HealEvent::MRFMetadataCorruption { .. } => event_type == "MRFMetadataCorruption", - } + .filter(|event| match event { + HealEvent::ObjectCorruption { .. } => event_type == "ObjectCorruption", + HealEvent::ObjectMissing { .. } => event_type == "ObjectMissing", + HealEvent::MetadataCorruption { .. } => event_type == "MetadataCorruption", + HealEvent::DiskStatusChange { .. } => event_type == "DiskStatusChange", + HealEvent::ECDecodeFailure { .. } => event_type == "ECDecodeFailure", + HealEvent::ChecksumMismatch { .. } => event_type == "ChecksumMismatch", + HealEvent::BucketMetadataCorruption { .. } => event_type == "BucketMetadataCorruption", + HealEvent::MRFMetadataCorruption { .. } => event_type == "MRFMetadataCorruption", }) .collect() } @@ -322,4 +352,4 @@ impl Default for HealEventHandler { fn default() -> Self { Self::new(1000) } -} \ No newline at end of file +} diff --git a/crates/ahm/src/heal/manager.rs b/crates/ahm/src/heal/manager.rs index 9676bea88..694b340b2 100644 --- a/crates/ahm/src/heal/manager.rs +++ b/crates/ahm/src/heal/manager.rs @@ -18,6 +18,9 @@ use crate::heal::{ storage::HealStorageAPI, task::{HealRequest, HealTask, HealTaskStatus}, }; +use rustfs_ecstore::disk::error::DiskError; +use rustfs_ecstore::disk::DiskAPI; +use rustfs_ecstore::global::GLOBAL_LOCAL_DISK_MAP; use std::{ collections::{HashMap, VecDeque}, sync::Arc, @@ -122,6 +125,9 @@ impl HealManager { // start scheduler self.start_scheduler().await?; + // start auto disk scanner + self.start_auto_disk_scanner().await?; + info!("HealManager started successfully"); Ok(()) } @@ -253,6 +259,86 @@ impl HealManager { Ok(()) } + /// Start background task to auto scan local disks and enqueue disk heal requests + async fn start_auto_disk_scanner(&self) -> Result<()> { + let config = self.config.clone(); + let heal_queue = self.heal_queue.clone(); + let active_heals = self.active_heals.clone(); + let cancel_token = self.cancel_token.clone(); + + tokio::spawn(async move { + let mut interval = interval(config.read().await.heal_interval); + + loop { + tokio::select! { + _ = cancel_token.cancelled() => { + info!("Auto disk scanner received shutdown signal"); + break; + } + _ = interval.tick() => { + // Build list of endpoints that need healing + let mut endpoints = Vec::new(); + println!("GLOBAL_LOCAL_DISK_MAP length: {:?}", GLOBAL_LOCAL_DISK_MAP.read().await.len()); + for (_, disk_opt) in GLOBAL_LOCAL_DISK_MAP.read().await.iter() { + if let Some(disk) = disk_opt { + // detect unformatted disk via get_disk_id() + if let Err(err) = disk.get_disk_id().await { + if err == DiskError::UnformattedDisk { + endpoints.push(disk.endpoint()); + continue; + } + } + // disk currently healing and not finished + if let Some(h) = disk.healing().await { + if !h.finished { + endpoints.push(disk.endpoint()); + } + } + } + } + + if endpoints.is_empty() { + continue; + } + println!("endpoints length: {:?}", endpoints.len()); + + for ep in endpoints { + // skip if already queued or healing + let mut skip = false; + { + let queue = heal_queue.lock().await; + if queue.iter().any(|req| matches!(&req.heal_type, crate::heal::task::HealType::Disk { endpoint } if endpoint == &ep)) { + skip = true; + } + } + if !skip { + let active = active_heals.lock().await; + if active.values().any(|task| matches!(&task.heal_type, crate::heal::task::HealType::Disk { endpoint } if endpoint == &ep)) { + skip = true; + } + } + + if skip { + continue; + } + + // enqueue heal request for this disk + let req = crate::heal::task::HealRequest::new( + crate::heal::task::HealType::Disk { endpoint: ep.clone() }, + crate::heal::task::HealOptions::default(), + crate::heal::task::HealPriority::Normal, + ); + let mut queue = heal_queue.lock().await; + queue.push_back(req); + info!("Enqueued auto disk heal for endpoint: {}", ep); + } + } + } + } + }); + Ok(()) + } + /// Process heal queue async fn process_heal_queue( heal_queue: &Arc>>, @@ -321,4 +407,4 @@ impl std::fmt::Debug for HealManager { .field("queue_length", &"") .finish() } -} \ No newline at end of file +} diff --git a/crates/ahm/src/heal/mod.rs b/crates/ahm/src/heal/mod.rs index 290a99a5f..742c27669 100644 --- a/crates/ahm/src/heal/mod.rs +++ b/crates/ahm/src/heal/mod.rs @@ -19,4 +19,4 @@ pub mod storage; pub mod task; pub use manager::HealManager; -pub use task::{HealOptions, HealPriority, HealRequest, HealTask, HealType}; \ No newline at end of file +pub use task::{HealOptions, HealPriority, HealRequest, HealTask, HealType}; diff --git a/crates/ahm/src/heal/progress.rs b/crates/ahm/src/heal/progress.rs index aea4aa474..f590a5a56 100644 --- a/crates/ahm/src/heal/progress.rs +++ b/crates/ahm/src/heal/progress.rs @@ -66,7 +66,8 @@ impl HealProgress { } pub fn is_completed(&self) -> bool { - self.progress_percentage >= 100.0 || self.objects_scanned > 0 && self.objects_healed + self.objects_failed >= self.objects_scanned + self.progress_percentage >= 100.0 + || self.objects_scanned > 0 && self.objects_healed + self.objects_failed >= self.objects_scanned } pub fn get_success_rate(&self) -> f64 { @@ -97,6 +98,12 @@ pub struct HealStatistics { pub last_update_time: SystemTime, } +impl Default for HealStatistics { + fn default() -> Self { + Self::new() + } +} + impl HealStatistics { pub fn new() -> Self { Self { @@ -138,4 +145,4 @@ impl HealStatistics { 0.0 } } -} \ No newline at end of file +} diff --git a/crates/ahm/src/heal/storage.rs b/crates/ahm/src/heal/storage.rs index 4a440f009..d04270c8a 100644 --- a/crates/ahm/src/heal/storage.rs +++ b/crates/ahm/src/heal/storage.rs @@ -17,8 +17,8 @@ use async_trait::async_trait; use rustfs_ecstore::{ disk::endpoint::Endpoint, heal::heal_commands::{HealOpts, HEAL_DEEP_SCAN, HEAL_NORMAL_SCAN}, - store_api::{BucketInfo, StorageAPI, ObjectIO}, store::ECStore, + store_api::{BucketInfo, ObjectIO, StorageAPI}, }; use rustfs_madmin::heal_commands::HealResultItem; use std::sync::Arc; @@ -52,55 +52,61 @@ pub enum DiskStatus { pub trait HealStorageAPI: Send + Sync { /// Get object meta async fn get_object_meta(&self, bucket: &str, object: &str) -> Result>; - + /// Get object data async fn get_object_data(&self, bucket: &str, object: &str) -> Result>>; - + /// Put object data async fn put_object_data(&self, bucket: &str, object: &str, data: &[u8]) -> Result<()>; - + /// Delete object async fn delete_object(&self, bucket: &str, object: &str) -> Result<()>; - + /// Check object integrity async fn verify_object_integrity(&self, bucket: &str, object: &str) -> Result; - + /// EC decode rebuild async fn ec_decode_rebuild(&self, bucket: &str, object: &str) -> Result>; - + /// Get disk status async fn get_disk_status(&self, endpoint: &Endpoint) -> Result; - + /// Format disk async fn format_disk(&self, endpoint: &Endpoint) -> Result<()>; - + /// Get bucket info async fn get_bucket_info(&self, bucket: &str) -> Result>; - + /// Fix bucket metadata async fn heal_bucket_metadata(&self, bucket: &str) -> Result<()>; - + /// Get all buckets async fn list_buckets(&self) -> Result>; - + /// Check object exists async fn object_exists(&self, bucket: &str, object: &str) -> Result; - + /// Get object size async fn get_object_size(&self, bucket: &str, object: &str) -> Result>; - + /// Get object checksum async fn get_object_checksum(&self, bucket: &str, object: &str) -> Result>; /// Heal object using ecstore - async fn heal_object(&self, bucket: &str, object: &str, version_id: Option<&str>, opts: &HealOpts) -> Result<(HealResultItem, Option)>; - + async fn heal_object( + &self, + bucket: &str, + object: &str, + version_id: Option<&str>, + opts: &HealOpts, + ) -> Result<(HealResultItem, Option)>; + /// Heal bucket using ecstore async fn heal_bucket(&self, bucket: &str, opts: &HealOpts) -> Result; - + /// Heal format using ecstore async fn heal_format(&self, dry_run: bool) -> Result<(HealResultItem, Option)>; - + /// List objects for healing async fn list_objects_for_heal(&self, bucket: &str, prefix: &str) -> Result>; } @@ -120,7 +126,7 @@ impl ECStoreHealStorage { impl HealStorageAPI for ECStoreHealStorage { async fn get_object_meta(&self, bucket: &str, object: &str) -> Result> { debug!("Getting object meta: {}/{}", bucket, object); - + match self.ecstore.get_object_info(bucket, object, &Default::default()).await { Ok(info) => Ok(Some(info)), Err(e) => { @@ -129,32 +135,36 @@ impl HealStorageAPI for ECStoreHealStorage { } } } - + async fn get_object_data(&self, bucket: &str, object: &str) -> Result>> { debug!("Getting object data: {}/{}", bucket, object); - - match (&*self.ecstore).get_object_reader(bucket, object, None, Default::default(), &Default::default()).await { - Ok(mut reader) => { - match reader.read_all().await { - Ok(data) => Ok(Some(data)), - Err(e) => { - error!("Failed to read object data: {}/{} - {}", bucket, object, e); - Err(Error::other(e)) - } + + match (*self.ecstore) + .get_object_reader(bucket, object, None, Default::default(), &Default::default()) + .await + { + Ok(mut reader) => match reader.read_all().await { + Ok(data) => Ok(Some(data)), + Err(e) => { + error!("Failed to read object data: {}/{} - {}", bucket, object, e); + Err(Error::other(e)) } - } + }, Err(e) => { error!("Failed to get object: {}/{} - {}", bucket, object, e); Err(Error::other(e)) } } } - + async fn put_object_data(&self, bucket: &str, object: &str, data: &[u8]) -> Result<()> { debug!("Putting object data: {}/{} ({} bytes)", bucket, object, data.len()); - + let mut reader = rustfs_ecstore::store_api::PutObjReader::from_vec(data.to_vec()); - match (&*self.ecstore).put_object(bucket, object, &mut reader, &Default::default()).await { + match (*self.ecstore) + .put_object(bucket, object, &mut reader, &Default::default()) + .await + { Ok(_) => { info!("Successfully put object: {}/{}", bucket, object); Ok(()) @@ -165,10 +175,10 @@ impl HealStorageAPI for ECStoreHealStorage { } } } - + async fn delete_object(&self, bucket: &str, object: &str) -> Result<()> { debug!("Deleting object: {}/{}", bucket, object); - + match self.ecstore.delete_object(bucket, object, Default::default()).await { Ok(_) => { info!("Successfully deleted object: {}/{}", bucket, object); @@ -180,10 +190,10 @@ impl HealStorageAPI for ECStoreHealStorage { } } } - + async fn verify_object_integrity(&self, bucket: &str, object: &str) -> Result { debug!("Verifying object integrity: {}/{}", bucket, object); - + // Try to get object info and data to verify integrity match self.get_object_meta(bucket, object).await? { Some(obj_info) => { @@ -192,7 +202,7 @@ impl HealStorageAPI for ECStoreHealStorage { warn!("Object has invalid size: {}/{}", bucket, object); return Ok(false); } - + // Try to read object data to verify it's accessible match self.get_object_data(bucket, object).await { Ok(Some(_)) => { @@ -215,10 +225,10 @@ impl HealStorageAPI for ECStoreHealStorage { } } } - + async fn ec_decode_rebuild(&self, bucket: &str, object: &str) -> Result> { debug!("EC decode rebuild: {}/{}", bucket, object); - + // Use ecstore's heal_object to rebuild the object let heal_opts = HealOpts { recursive: false, @@ -231,15 +241,15 @@ impl HealStorageAPI for ECStoreHealStorage { pool: None, set: None, }; - + match self.heal_object(bucket, object, None, &heal_opts).await { Ok((_result, error)) => { if error.is_some() { return Err(Error::TaskExecutionFailed { - message: format!("Heal failed: {:?}", error), + message: format!("Heal failed: {error:?}"), }); } - + // After healing, try to read the object data match self.get_object_data(bucket, object).await? { Some(data) => { @@ -249,7 +259,7 @@ impl HealStorageAPI for ECStoreHealStorage { None => { error!("Object not found after heal: {}/{}", bucket, object); Err(Error::TaskExecutionFailed { - message: format!("Object not found after heal: {}/{}", bucket, object), + message: format!("Object not found after heal: {bucket}/{object}"), }) } } @@ -260,24 +270,24 @@ impl HealStorageAPI for ECStoreHealStorage { } } } - + async fn get_disk_status(&self, endpoint: &Endpoint) -> Result { debug!("Getting disk status: {:?}", endpoint); - + // TODO: implement disk status check using ecstore // For now, return Ok status info!("Disk status check: {:?} - OK", endpoint); Ok(DiskStatus::Ok) } - + async fn format_disk(&self, endpoint: &Endpoint) -> Result<()> { debug!("Formatting disk: {:?}", endpoint); - + // Use ecstore's heal_format match self.heal_format(false).await { Ok((_, error)) => { if error.is_some() { - return Err(Error::other(format!("Format failed: {:?}", error))); + return Err(Error::other(format!("Format failed: {error:?}"))); } info!("Successfully formatted disk: {:?}", endpoint); Ok(()) @@ -288,10 +298,10 @@ impl HealStorageAPI for ECStoreHealStorage { } } } - + async fn get_bucket_info(&self, bucket: &str) -> Result> { debug!("Getting bucket info: {}", bucket); - + match self.ecstore.get_bucket_info(bucket, &Default::default()).await { Ok(info) => Ok(Some(info)), Err(e) => { @@ -300,10 +310,10 @@ impl HealStorageAPI for ECStoreHealStorage { } } } - + async fn heal_bucket_metadata(&self, bucket: &str) -> Result<()> { debug!("Healing bucket metadata: {}", bucket); - + let heal_opts = HealOpts { recursive: true, dry_run: false, @@ -315,7 +325,7 @@ impl HealStorageAPI for ECStoreHealStorage { pool: None, set: None, }; - + match self.heal_bucket(bucket, &heal_opts).await { Ok(_) => { info!("Successfully healed bucket metadata: {}", bucket); @@ -327,10 +337,10 @@ impl HealStorageAPI for ECStoreHealStorage { } } } - + async fn list_buckets(&self) -> Result> { debug!("Listing buckets"); - + match self.ecstore.list_bucket(&Default::default()).await { Ok(buckets) => Ok(buckets), Err(e) => { @@ -339,36 +349,34 @@ impl HealStorageAPI for ECStoreHealStorage { } } } - + async fn object_exists(&self, bucket: &str, object: &str) -> Result { 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), } } - + async fn get_object_size(&self, bucket: &str, object: &str) -> Result> { debug!("Getting object size: {}/{}", bucket, object); - + match self.get_object_meta(bucket, object).await { Ok(Some(obj_info)) => Ok(Some(obj_info.size as u64)), Ok(None) => Ok(None), Err(e) => Err(e), } } - + async fn get_object_checksum(&self, bucket: &str, object: &str) -> Result> { debug!("Getting object checksum: {}/{}", bucket, object); - + match self.get_object_meta(bucket, object).await { Ok(Some(obj_info)) => { // Convert checksum bytes to hex string - let checksum = obj_info.checksum.iter() - .map(|b| format!("{:02x}", b)) - .collect::(); + let checksum = obj_info.checksum.iter().map(|b| format!("{b:02x}")).collect::(); Ok(Some(checksum)) } Ok(None) => Ok(None), @@ -376,14 +384,20 @@ impl HealStorageAPI for ECStoreHealStorage { } } - async fn heal_object(&self, bucket: &str, object: &str, version_id: Option<&str>, opts: &HealOpts) -> Result<(HealResultItem, Option)> { + async fn heal_object( + &self, + bucket: &str, + object: &str, + version_id: Option<&str>, + opts: &HealOpts, + ) -> Result<(HealResultItem, Option)> { debug!("Healing object: {}/{}", bucket, object); - + let version_id_str = version_id.unwrap_or(""); - + match self.ecstore.heal_object(bucket, object, version_id_str, opts).await { Ok((result, ecstore_error)) => { - let error = ecstore_error.map(|e| Error::other(e)); + let error = ecstore_error.map(Error::other); info!("Heal object completed: {}/{} - result: {:?}, error: {:?}", bucket, object, result, error); Ok((result, error)) } @@ -393,10 +407,10 @@ impl HealStorageAPI for ECStoreHealStorage { } } } - + async fn heal_bucket(&self, bucket: &str, opts: &HealOpts) -> Result { debug!("Healing bucket: {}", bucket); - + match self.ecstore.heal_bucket(bucket, opts).await { Ok(result) => { info!("Heal bucket completed: {} - result: {:?}", bucket, result); @@ -408,13 +422,13 @@ impl HealStorageAPI for ECStoreHealStorage { } } } - + async fn heal_format(&self, dry_run: bool) -> Result<(HealResultItem, Option)> { debug!("Healing format (dry_run: {})", dry_run); - + match self.ecstore.heal_format(dry_run).await { Ok((result, ecstore_error)) => { - let error = ecstore_error.map(|e| Error::other(e)); + let error = ecstore_error.map(Error::other); info!("Heal format completed - result: {:?}, error: {:?}", result, error); Ok((result, error)) } @@ -424,18 +438,19 @@ impl HealStorageAPI for ECStoreHealStorage { } } } - + async fn list_objects_for_heal(&self, bucket: &str, prefix: &str) -> Result> { debug!("Listing objects for heal: {}/{}", bucket, prefix); - + // Use list_objects_v2 to get objects - match self.ecstore.clone().list_objects_v2( - bucket, prefix, None, None, 1000, false, None - ).await { + match self + .ecstore + .clone() + .list_objects_v2(bucket, prefix, None, None, 1000, false, None) + .await + { Ok(list_info) => { - let objects: Vec = list_info.objects.into_iter() - .map(|obj| obj.name) - .collect(); + let objects: Vec = list_info.objects.into_iter().map(|obj| obj.name).collect(); info!("Found {} objects for heal in {}/{}", objects.len(), bucket, prefix); Ok(objects) } @@ -445,4 +460,4 @@ impl HealStorageAPI for ECStoreHealStorage { } } } -} \ No newline at end of file +} diff --git a/crates/ahm/src/heal/task.rs b/crates/ahm/src/heal/task.rs index 0d531def0..8650e5712 100644 --- a/crates/ahm/src/heal/task.rs +++ b/crates/ahm/src/heal/task.rs @@ -14,24 +14,25 @@ use crate::error::{Error, Result}; use crate::heal::{progress::HealProgress, storage::HealStorageAPI}; +use rustfs_ecstore::config::RUSTFS_CONFIG_PREFIX; use rustfs_ecstore::disk::endpoint::Endpoint; -use crate::heal::storage::DiskStatus; +use rustfs_ecstore::disk::error::DiskError; +use rustfs_ecstore::disk::{DiskAPI, DiskInfoOptions, BUCKET_META_PREFIX, RUSTFS_META_BUCKET}; +use rustfs_ecstore::heal::heal_commands::HEAL_NORMAL_SCAN; +use rustfs_ecstore::heal::heal_commands::{init_healing_tracker, load_healing_tracker, HealScanMode}; +use rustfs_ecstore::new_object_layer_fn; +use rustfs_ecstore::store::get_disk_via_endpoint; +use rustfs_ecstore::store_api::BucketInfo; +use rustfs_utils::path::path_join; use serde::{Deserialize, Serialize}; -use std::{ - sync::Arc, - time::{Duration, SystemTime}, -}; +use std::cmp::Ordering; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::{Duration, SystemTime}; use tokio::sync::RwLock; use tracing::{error, info, warn}; use uuid::Uuid; -/// Heal scan mode -pub type HealScanMode = usize; - -pub const HEAL_UNKNOWN_SCAN: HealScanMode = 0; -pub const HEAL_NORMAL_SCAN: HealScanMode = 1; -pub const HEAL_DEEP_SCAN: HealScanMode = 2; - /// Heal type #[derive(Debug, Clone)] pub enum HealType { @@ -42,22 +43,13 @@ pub enum HealType { version_id: Option, }, /// Bucket heal - Bucket { - bucket: String, - }, + Bucket { bucket: String }, /// Disk heal - Disk { - endpoint: Endpoint, - }, + Disk { endpoint: Endpoint }, /// Metadata heal - Metadata { - bucket: String, - object: String, - }, + Metadata { bucket: String, object: String }, /// MRF heal - MRF { - meta_path: String, - }, + MRF { meta_path: String }, /// EC decode heal ECDecode { bucket: String, @@ -119,7 +111,7 @@ impl Default for HealOptions { } /// Heal task status -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum HealTaskStatus { /// Pending Pending, @@ -174,27 +166,15 @@ impl HealRequest { } pub fn bucket(bucket: String) -> Self { - Self::new( - HealType::Bucket { bucket }, - HealOptions::default(), - HealPriority::Normal, - ) + Self::new(HealType::Bucket { bucket }, HealOptions::default(), HealPriority::Normal) } pub fn disk(endpoint: Endpoint) -> Self { - Self::new( - HealType::Disk { endpoint }, - HealOptions::default(), - HealPriority::High, - ) + Self::new(HealType::Disk { endpoint }, HealOptions::default(), HealPriority::High) } pub fn metadata(bucket: String, object: String) -> Self { - Self::new( - HealType::Metadata { bucket, object }, - HealOptions::default(), - HealPriority::High, - ) + Self::new(HealType::Metadata { bucket, object }, HealOptions::default(), HealPriority::High) } pub fn ec_decode(bucket: String, object: String, version_id: Option) -> Self { @@ -264,24 +244,20 @@ impl HealTask { info!("Starting heal task: {} with type: {:?}", self.id, self.heal_type); let result = match &self.heal_type { - HealType::Object { bucket, object, version_id } => { - self.heal_object(bucket, object, version_id.as_deref()).await - } - HealType::Bucket { bucket } => { - self.heal_bucket(bucket).await - } - HealType::Disk { endpoint } => { - self.heal_disk(endpoint).await - } - HealType::Metadata { bucket, object } => { - self.heal_metadata(bucket, object).await - } - HealType::MRF { meta_path } => { - self.heal_mrf(meta_path).await - } - HealType::ECDecode { bucket, object, version_id } => { - self.heal_ec_decode(bucket, object, version_id.as_deref()).await - } + HealType::Object { + bucket, + object, + version_id, + } => self.heal_object(bucket, object, version_id.as_deref()).await, + HealType::Bucket { bucket } => self.heal_bucket(bucket).await, + HealType::Disk { endpoint } => self.heal_disk(endpoint).await, + HealType::Metadata { bucket, object } => self.heal_metadata(bucket, object).await, + HealType::MRF { meta_path } => self.heal_mrf(meta_path).await, + HealType::ECDecode { + bucket, + object, + version_id, + } => self.heal_ec_decode(bucket, object, version_id.as_deref()).await, }; // update completed time and status @@ -298,9 +274,7 @@ impl HealTask { } Err(e) => { let mut status = self.status.write().await; - *status = HealTaskStatus::Failed { - error: e.to_string(), - }; + *status = HealTaskStatus::Failed { error: e.to_string() }; error!("Heal task failed: {} with error: {}", self.id, e); } } @@ -327,11 +301,11 @@ impl HealTask { // specific heal implementation method async fn heal_object(&self, bucket: &str, object: &str, version_id: Option<&str>) -> Result<()> { info!("Healing object: {}/{}", bucket, object); - + // update progress { let mut progress = self.progress.write().await; - progress.set_current_object(Some(format!("{}/{}", bucket, object))); + progress.set_current_object(Some(format!("{bucket}/{object}"))); progress.update_progress(0, 4, 0, 0); // 开始heal,总共4个步骤 } @@ -345,47 +319,24 @@ impl HealTask { return self.recreate_missing_object(bucket, object, version_id).await; } else { return Err(Error::TaskExecutionFailed { - message: format!("Object not found: {}/{}", bucket, object), + message: format!("Object not found: {bucket}/{object}"), }); } } { let mut progress = self.progress.write().await; - progress.update_progress(1, 4, 0, 0); + progress.update_progress(1, 3, 0, 0); } - // Step 2: Verify object integrity - info!("Step 2: Verifying object integrity"); - let integrity_ok = self.storage.verify_object_integrity(bucket, object).await?; - if integrity_ok { - info!("Object integrity check passed: {}/{}", bucket, object); - { - let mut progress = self.progress.write().await; - progress.update_progress(4, 4, 0, 0); - } - return Ok(()); - } - - warn!("Object integrity check failed: {}/{}", bucket, object); - { - let mut progress = self.progress.write().await; - progress.update_progress(2, 4, 0, 0); - } - - // Step 3: Perform actual heal using ecstore - info!("Step 3: Performing heal using ecstore"); + // Step 2: directly call ecstore to perform heal + info!("Step 2: Performing heal using ecstore"); let heal_opts = rustfs_ecstore::heal::heal_commands::HealOpts { recursive: self.options.recursive, dry_run: self.options.dry_run, remove: self.options.remove_corrupted, recreate: self.options.recreate_missing, - scan_mode: match self.options.scan_mode { - crate::heal::task::HEAL_UNKNOWN_SCAN => rustfs_ecstore::heal::heal_commands::HEAL_UNKNOWN_SCAN, - crate::heal::task::HEAL_NORMAL_SCAN => rustfs_ecstore::heal::heal_commands::HEAL_NORMAL_SCAN, - crate::heal::task::HEAL_DEEP_SCAN => rustfs_ecstore::heal::heal_commands::HEAL_DEEP_SCAN, - _ => rustfs_ecstore::heal::heal_commands::HEAL_NORMAL_SCAN, - }, + scan_mode: self.options.scan_mode, update_parity: self.options.update_parity, no_lock: false, pool: None, @@ -396,7 +347,7 @@ impl HealTask { Ok((result, error)) => { if let Some(e) = error { error!("Heal operation failed: {}/{} - {}", bucket, object, e); - + // If heal failed and remove_corrupted is enabled, delete the corrupted object if self.options.remove_corrupted { warn!("Removing corrupted object: {}/{}", bucket, object); @@ -410,29 +361,34 @@ impl HealTask { { let mut progress = self.progress.write().await; - progress.update_progress(4, 4, 0, 0); + progress.update_progress(3, 3, 0, 0); } - + return Err(Error::TaskExecutionFailed { - message: format!("Failed to heal object {}/{}: {}", bucket, object, e), + message: format!("Failed to heal object {bucket}/{object}: {e}"), }); } - // Step 4: Verify heal result - info!("Step 4: Verifying heal result"); + // Step 3: Verify heal result + info!("Step 3: Verifying heal result"); let object_size = result.object_size as u64; - info!("Heal completed successfully: {}/{} ({} bytes, {} drives healed)", - bucket, object, object_size, result.after.drives.len()); + info!( + "Heal completed successfully: {}/{} ({} bytes, {} drives healed)", + bucket, + object, + object_size, + result.after.drives.len() + ); { let mut progress = self.progress.write().await; - progress.update_progress(4, 4, object_size, object_size); + progress.update_progress(3, 3, object_size, object_size); } Ok(()) } Err(e) => { error!("Heal operation failed: {}/{} - {}", bucket, object, e); - + // If heal failed and remove_corrupted is enabled, delete the corrupted object if self.options.remove_corrupted { warn!("Removing corrupted object: {}/{}", bucket, object); @@ -446,11 +402,11 @@ impl HealTask { { let mut progress = self.progress.write().await; - progress.update_progress(4, 4, 0, 0); + progress.update_progress(3, 3, 0, 0); } - + Err(Error::TaskExecutionFailed { - message: format!("Failed to heal object {}/{}: {}", bucket, object, e), + message: format!("Failed to heal object {bucket}/{object}: {e}"), }) } } @@ -459,7 +415,7 @@ impl HealTask { /// Recreate missing object (for EC decode scenarios) async fn recreate_missing_object(&self, bucket: &str, object: &str, version_id: Option<&str>) -> Result<()> { info!("Attempting to recreate missing object: {}/{}", bucket, object); - + // Use ecstore's heal_object with recreate option let heal_opts = rustfs_ecstore::heal::heal_commands::HealOpts { recursive: false, @@ -478,7 +434,7 @@ impl HealTask { if let Some(e) = error { error!("Failed to recreate missing object: {}/{} - {}", bucket, object, e); return Err(Error::TaskExecutionFailed { - message: format!("Failed to recreate missing object {}/{}: {}", bucket, object, e), + message: format!("Failed to recreate missing object {bucket}/{object}: {e}"), }); } @@ -494,7 +450,7 @@ impl HealTask { Err(e) => { error!("Failed to recreate missing object: {}/{} - {}", bucket, object, e); Err(Error::TaskExecutionFailed { - message: format!("Failed to recreate missing object {}/{}: {}", bucket, object, e), + message: format!("Failed to recreate missing object {bucket}/{object}: {e}"), }) } } @@ -502,11 +458,11 @@ impl HealTask { async fn heal_bucket(&self, bucket: &str) -> Result<()> { info!("Healing bucket: {}", bucket); - + // update progress { let mut progress = self.progress.write().await; - progress.set_current_object(Some(format!("bucket: {}", bucket))); + progress.set_current_object(Some(format!("bucket: {bucket}"))); progress.update_progress(0, 3, 0, 0); } @@ -516,7 +472,7 @@ impl HealTask { if !bucket_exists { warn!("Bucket does not exist: {}", bucket); return Err(Error::TaskExecutionFailed { - message: format!("Bucket not found: {}", bucket), + message: format!("Bucket not found: {bucket}"), }); } @@ -532,12 +488,7 @@ impl HealTask { dry_run: self.options.dry_run, remove: self.options.remove_corrupted, recreate: self.options.recreate_missing, - scan_mode: match self.options.scan_mode { - crate::heal::task::HEAL_UNKNOWN_SCAN => rustfs_ecstore::heal::heal_commands::HEAL_UNKNOWN_SCAN, - crate::heal::task::HEAL_NORMAL_SCAN => rustfs_ecstore::heal::heal_commands::HEAL_NORMAL_SCAN, - crate::heal::task::HEAL_DEEP_SCAN => rustfs_ecstore::heal::heal_commands::HEAL_DEEP_SCAN, - _ => rustfs_ecstore::heal::heal_commands::HEAL_NORMAL_SCAN, - }, + scan_mode: self.options.scan_mode, update_parity: self.options.update_parity, no_lock: false, pool: None, @@ -547,7 +498,7 @@ impl HealTask { match self.storage.heal_bucket(bucket, &heal_opts).await { Ok(result) => { info!("Bucket heal completed successfully: {} ({} drives)", bucket, result.after.drives.len()); - + { let mut progress = self.progress.write().await; progress.update_progress(3, 3, 0, 0); @@ -561,7 +512,7 @@ impl HealTask { progress.update_progress(3, 3, 0, 0); } Err(Error::TaskExecutionFailed { - message: format!("Failed to heal bucket {}: {}", bucket, e), + message: format!("Failed to heal bucket {bucket}: {e}"), }) } } @@ -569,33 +520,17 @@ impl HealTask { async fn heal_disk(&self, endpoint: &Endpoint) -> Result<()> { info!("Healing disk: {:?}", endpoint); - + // update progress { let mut progress = self.progress.write().await; - progress.set_current_object(Some(format!("disk: {:?}", endpoint))); + progress.set_current_object(Some(format!("disk: {endpoint:?}"))); progress.update_progress(0, 3, 0, 0); } - // Step 1: Check disk status - info!("Step 1: Checking disk status"); - let disk_status = self.storage.get_disk_status(endpoint).await?; - if disk_status == DiskStatus::Ok { - info!("Disk is already healthy: {:?}", endpoint); - { - let mut progress = self.progress.write().await; - progress.update_progress(3, 3, 0, 0); - } - return Ok(()); - } + // Step 1: Perform disk format heal using ecstore + info!("Step 1: Performing disk format heal using ecstore"); - { - let mut progress = self.progress.write().await; - progress.update_progress(1, 3, 0, 0); - } - - // Step 2: Perform disk heal using ecstore - info!("Step 2: Performing disk heal using ecstore"); match self.storage.heal_format(self.options.dry_run).await { Ok((result, error)) => { if let Some(e) = error { @@ -605,12 +540,21 @@ impl HealTask { progress.update_progress(3, 3, 0, 0); } return Err(Error::TaskExecutionFailed { - message: format!("Failed to heal disk {:?}: {}", endpoint, e), + message: format!("Failed to heal disk {endpoint:?}: {e}"), }); } info!("Disk heal completed successfully: {:?} ({} drives)", endpoint, result.after.drives.len()); - + + { + let mut progress = self.progress.write().await; + progress.update_progress(2, 3, 0, 0); + } + + // Step 2: Synchronize data/buckets on the fresh disk + info!("Step 2: Healing buckets on fresh disk"); + self.heal_fresh_disk(endpoint).await?; + { let mut progress = self.progress.write().await; progress.update_progress(3, 3, 0, 0); @@ -624,7 +568,7 @@ impl HealTask { progress.update_progress(3, 3, 0, 0); } Err(Error::TaskExecutionFailed { - message: format!("Failed to heal disk {:?}: {}", endpoint, e), + message: format!("Failed to heal disk {endpoint:?}: {e}"), }) } } @@ -632,11 +576,11 @@ impl HealTask { async fn heal_metadata(&self, bucket: &str, object: &str) -> Result<()> { info!("Healing metadata: {}/{}", bucket, object); - + // update progress { let mut progress = self.progress.write().await; - progress.set_current_object(Some(format!("metadata: {}/{}", bucket, object))); + progress.set_current_object(Some(format!("metadata: {bucket}/{object}"))); progress.update_progress(0, 3, 0, 0); } @@ -646,7 +590,7 @@ impl HealTask { if !object_exists { warn!("Object does not exist: {}/{}", bucket, object); return Err(Error::TaskExecutionFailed { - message: format!("Object not found: {}/{}", bucket, object), + message: format!("Object not found: {bucket}/{object}"), }); } @@ -678,12 +622,17 @@ impl HealTask { progress.update_progress(3, 3, 0, 0); } return Err(Error::TaskExecutionFailed { - message: format!("Failed to heal metadata {}/{}: {}", bucket, object, e), + message: format!("Failed to heal metadata {bucket}/{object}: {e}"), }); } - info!("Metadata heal completed successfully: {}/{} ({} drives)", bucket, object, result.after.drives.len()); - + info!( + "Metadata heal completed successfully: {}/{} ({} drives)", + bucket, + object, + result.after.drives.len() + ); + { let mut progress = self.progress.write().await; progress.update_progress(3, 3, 0, 0); @@ -697,7 +646,7 @@ impl HealTask { progress.update_progress(3, 3, 0, 0); } Err(Error::TaskExecutionFailed { - message: format!("Failed to heal metadata {}/{}: {}", bucket, object, e), + message: format!("Failed to heal metadata {bucket}/{object}: {e}"), }) } } @@ -705,11 +654,11 @@ impl HealTask { async fn heal_mrf(&self, meta_path: &str) -> Result<()> { info!("Healing MRF: {}", meta_path); - + // update progress { let mut progress = self.progress.write().await; - progress.set_current_object(Some(format!("mrf: {}", meta_path))); + progress.set_current_object(Some(format!("mrf: {meta_path}"))); progress.update_progress(0, 2, 0, 0); } @@ -717,7 +666,7 @@ impl HealTask { let parts: Vec<&str> = meta_path.split('/').collect(); if parts.len() < 2 { return Err(Error::TaskExecutionFailed { - message: format!("Invalid meta path format: {}", meta_path), + message: format!("Invalid meta path format: {meta_path}"), }); } @@ -747,12 +696,12 @@ impl HealTask { progress.update_progress(2, 2, 0, 0); } return Err(Error::TaskExecutionFailed { - message: format!("Failed to heal MRF {}: {}", meta_path, e), + message: format!("Failed to heal MRF {meta_path}: {e}"), }); } info!("MRF heal completed successfully: {} ({} drives)", meta_path, result.after.drives.len()); - + { let mut progress = self.progress.write().await; progress.update_progress(2, 2, 0, 0); @@ -766,7 +715,7 @@ impl HealTask { progress.update_progress(2, 2, 0, 0); } Err(Error::TaskExecutionFailed { - message: format!("Failed to heal MRF {}: {}", meta_path, e), + message: format!("Failed to heal MRF {meta_path}: {e}"), }) } } @@ -774,11 +723,11 @@ impl HealTask { async fn heal_ec_decode(&self, bucket: &str, object: &str, version_id: Option<&str>) -> Result<()> { info!("Healing EC decode: {}/{}", bucket, object); - + // update progress { let mut progress = self.progress.write().await; - progress.set_current_object(Some(format!("ec_decode: {}/{}", bucket, object))); + progress.set_current_object(Some(format!("ec_decode: {bucket}/{object}"))); progress.update_progress(0, 3, 0, 0); } @@ -788,7 +737,7 @@ impl HealTask { if !object_exists { warn!("Object does not exist: {}/{}", bucket, object); return Err(Error::TaskExecutionFailed { - message: format!("Object not found: {}/{}", bucket, object), + message: format!("Object not found: {bucket}/{object}"), }); } @@ -820,14 +769,19 @@ impl HealTask { progress.update_progress(3, 3, 0, 0); } return Err(Error::TaskExecutionFailed { - message: format!("Failed to heal EC decode {}/{}: {}", bucket, object, e), + message: format!("Failed to heal EC decode {bucket}/{object}: {e}"), }); } let object_size = result.object_size as u64; - info!("EC decode heal completed successfully: {}/{} ({} bytes, {} drives)", - bucket, object, object_size, result.after.drives.len()); - + info!( + "EC decode heal completed successfully: {}/{} ({} bytes, {} drives)", + bucket, + object, + object_size, + result.after.drives.len() + ); + { let mut progress = self.progress.write().await; progress.update_progress(3, 3, object_size, object_size); @@ -841,11 +795,86 @@ impl HealTask { progress.update_progress(3, 3, 0, 0); } Err(Error::TaskExecutionFailed { - message: format!("Failed to heal EC decode {}/{}: {}", bucket, object, e), + message: format!("Failed to heal EC decode {bucket}/{object}: {e}"), }) } } } + + async fn heal_fresh_disk(&self, endpoint: &Endpoint) -> Result<()> { + // Locate disk via endpoint + let disk = get_disk_via_endpoint(endpoint) + .await + .ok_or_else(|| Error::other(format!("Disk not found for endpoint: {endpoint}")))?; + + // Skip if drive is root or other fatal errors + if let Err(e) = disk.disk_info(&DiskInfoOptions::default()).await { + match e { + DiskError::DriveIsRoot => return Ok(()), + DiskError::UnformattedDisk => { /* continue healing */ } + _ => return Err(Error::other(e)), + } + } + + // Load or init HealingTracker + let mut tracker = match load_healing_tracker(&Some(disk.clone())).await { + Ok(t) => t, + Err(err) => match err { + DiskError::FileNotFound => init_healing_tracker(disk.clone(), &Uuid::new_v4().to_string()) + .await + .map_err(Error::other)?, + _ => return Err(Error::other(err)), + }, + }; + + // Build bucket list + let mut buckets = self.storage.list_buckets().await.map_err(Error::other)?; + buckets.push(BucketInfo { + name: path_join(&[PathBuf::from(RUSTFS_META_BUCKET), PathBuf::from(RUSTFS_CONFIG_PREFIX)]) + .to_string_lossy() + .to_string(), + ..Default::default() + }); + buckets.push(BucketInfo { + name: path_join(&[PathBuf::from(RUSTFS_META_BUCKET), PathBuf::from(BUCKET_META_PREFIX)]) + .to_string_lossy() + .to_string(), + ..Default::default() + }); + + // Sort: system buckets first, others by creation time desc + buckets.sort_by(|a, b| { + let a_sys = a.name.starts_with(RUSTFS_META_BUCKET); + let b_sys = b.name.starts_with(RUSTFS_META_BUCKET); + match (a_sys, b_sys) { + (true, false) => Ordering::Less, + (false, true) => Ordering::Greater, + _ => b.created.cmp(&a.created), + } + }); + + // Update tracker queue and persist + tracker.set_queue_buckets(&buckets).await; + tracker.save().await.map_err(Error::other)?; + + // Prepare bucket names list + let bucket_names: Vec = buckets.iter().map(|b| b.name.clone()).collect(); + + // Run heal_erasure_set using underlying SetDisk + let (pool_idx, set_idx) = (endpoint.pool_idx as usize, endpoint.set_idx as usize); + let Some(store) = new_object_layer_fn() else { + return Err(Error::other("errServerNotInitialized")); + }; + let set_disk = store.pools[pool_idx].disk_set[set_idx].clone(); + + let tracker_arc = Arc::new(RwLock::new(tracker)); + set_disk + .heal_erasure_set(&bucket_names, tracker_arc) + .await + .map_err(Error::other)?; + + Ok(()) + } } impl std::fmt::Debug for HealTask { @@ -857,4 +886,4 @@ impl std::fmt::Debug for HealTask { .field("created_at", &self.created_at) .finish() } -} \ No newline at end of file +} diff --git a/crates/ahm/src/lib.rs b/crates/ahm/src/lib.rs index 0e288d513..3a5117f7c 100644 --- a/crates/ahm/src/lib.rs +++ b/crates/ahm/src/lib.rs @@ -20,11 +20,11 @@ pub mod heal; pub mod scanner; pub use error::{Error, Result}; +pub use heal::{HealManager, HealOptions, HealPriority, HealRequest, HealType}; pub use scanner::{ BucketTargetUsageInfo, BucketUsageInfo, DataUsageInfo, Scanner, ScannerMetrics, load_data_usage_from_backend, store_data_usage_in_backend, }; -pub use heal::{HealManager, HealRequest, HealType, HealOptions, HealPriority}; // Global cancellation token for AHM services (scanner and other background tasks) static GLOBAL_AHM_SERVICES_CANCEL_TOKEN: OnceLock = OnceLock::new(); diff --git a/crates/ahm/src/scanner/data_scanner.rs b/crates/ahm/src/scanner/data_scanner.rs index 243296b34..b2d1103a3 100644 --- a/crates/ahm/src/scanner/data_scanner.rs +++ b/crates/ahm/src/scanner/data_scanner.rs @@ -32,11 +32,11 @@ use super::{ data_usage::DataUsageInfo, metrics::{BucketMetrics, DiskMetrics, MetricsCollector, ScannerMetrics}, }; +use crate::heal::HealManager; use crate::{ error::{Error, Result}, get_ahm_services_cancel_token, HealRequest, }; -use crate::heal::HealManager; use rustfs_ecstore::disk::RUSTFS_META_BUCKET; @@ -539,12 +539,12 @@ impl Scanner { Ok(volumes) => volumes, Err(e) => { error!("Failed to list volumes on disk {}: {}", disk_path, e); - + // 磁盘访问失败,提交磁盘heal任务 let enable_healing = self.config.read().await.enable_healing; if enable_healing { if let Some(heal_manager) = &self.heal_manager { - use crate::heal::{HealRequest, HealPriority}; + use crate::heal::{HealPriority, HealRequest}; let req = HealRequest::new( crate::heal::HealType::Disk { endpoint: disk.endpoint().clone(), @@ -562,7 +562,7 @@ impl Scanner { } } } - + return Err(Error::Storage(e.into())); } }; @@ -674,7 +674,7 @@ impl Scanner { if file_meta.versions.is_empty() { objects_with_issues += 1; warn!("Object {} has no versions", entry.name); - + // 对象元数据损坏,提交元数据heal任务 let enable_healing = self.config.read().await.enable_healing; if enable_healing { @@ -682,10 +682,16 @@ impl Scanner { let req = HealRequest::metadata(bucket.to_string(), entry.name.clone()); match heal_manager.submit_heal_request(req).await { Ok(task_id) => { - warn!("object metadata damaged, submit heal task: {} {} / {}", task_id, bucket, entry.name); + warn!( + "object metadata damaged, submit heal task: {} {} / {}", + task_id, bucket, entry.name + ); } Err(e) => { - error!("object metadata damaged, submit heal task failed: {} / {} {}", bucket, entry.name, e); + error!( + "object metadata damaged, submit heal task failed: {} / {} {}", + bucket, entry.name, e + ); } } } @@ -697,7 +703,7 @@ impl Scanner { } else { objects_with_issues += 1; warn!("Failed to parse metadata for object {}", entry.name); - + // 对象元数据解析失败,提交元数据heal任务 let enable_healing = self.config.read().await.enable_healing; if enable_healing { @@ -705,10 +711,16 @@ impl Scanner { let req = HealRequest::metadata(bucket.to_string(), entry.name.clone()); match heal_manager.submit_heal_request(req).await { Ok(task_id) => { - warn!("object metadata parse failed, submit heal task: {} {} / {}", task_id, bucket, entry.name); + warn!( + "object metadata parse failed, submit heal task: {} {} / {}", + task_id, bucket, entry.name + ); } Err(e) => { - error!("object metadata parse failed, submit heal task failed: {} / {} {}", bucket, entry.name, e); + error!( + "object metadata parse failed, submit heal task failed: {} / {} {}", + bucket, entry.name, e + ); } } } @@ -816,12 +828,12 @@ impl Scanner { let missing_disks: Vec = (0..disks.len()).filter(|&i| !locations.contains(&i)).collect(); warn!("Object {}/{} missing from disks: {:?}", bucket, object_name, missing_disks); println!("Object {bucket}/{object_name} missing from disks: {missing_disks:?}"); - + // submit heal task let enable_healing = self.config.read().await.enable_healing; if enable_healing { if let Some(heal_manager) = &self.heal_manager { - use crate::heal::{HealRequest, HealPriority}; + use crate::heal::{HealPriority, HealRequest}; let req = HealRequest::new( crate::heal::HealType::Object { bucket: bucket.clone(), @@ -833,8 +845,10 @@ impl Scanner { ); match heal_manager.submit_heal_request(req).await { Ok(task_id) => { - warn!("object missing, submit heal task: {} {} / {} (missing disks: {:?})", - task_id, bucket, object_name, missing_disks); + warn!( + "object missing, submit heal task: {} {} / {} (missing disks: {:?})", + task_id, bucket, object_name, missing_disks + ); } Err(e) => { error!("object missing, submit heal task failed: {} / {} {}", bucket, object_name, e); @@ -1142,6 +1156,7 @@ mod tests { StorageAPI, store_api::{MakeBucketOptions, ObjectIO, PutObjReader}, }; + use serial_test::serial; use std::fs; use std::net::SocketAddr; @@ -1211,7 +1226,7 @@ mod tests { } #[tokio::test(flavor = "multi_thread")] - #[ignore] + #[serial] async fn test_scanner_basic_functionality() { const TEST_DIR_BASIC: &str = "/tmp/rustfs_ahm_test_basic"; let (disk_paths, ecstore) = prepare_test_env(Some(TEST_DIR_BASIC), Some(9001)).await; @@ -1309,7 +1324,7 @@ mod tests { // test data usage statistics collection and validation #[tokio::test(flavor = "multi_thread")] - #[ignore] + #[serial] async fn test_scanner_usage_stats() { const TEST_DIR_USAGE_STATS: &str = "/tmp/rustfs_ahm_test_usage_stats"; let (_, ecstore) = prepare_test_env(Some(TEST_DIR_USAGE_STATS), Some(9002)).await; diff --git a/crates/ahm/tests/heal_integration_test.rs b/crates/ahm/tests/heal_integration_test.rs index ac5aae851..c161af155 100644 --- a/crates/ahm/tests/heal_integration_test.rs +++ b/crates/ahm/tests/heal_integration_test.rs @@ -1,25 +1,44 @@ use rustfs_ahm::heal::{ - manager::HealManager, + manager::{HealConfig, HealManager}, storage::{ECStoreHealStorage, HealStorageAPI}, - task::{HealOptions, HealPriority, HealRequest, HealType, HEAL_NORMAL_SCAN}, + task::{HealOptions, HealPriority, HealRequest, HealTaskStatus, HealType}, }; use rustfs_ecstore::{ disk::endpoint::Endpoint, endpoints::{EndpointServerPools, Endpoints, PoolEndpoints}, + heal::heal_commands::HEAL_NORMAL_SCAN, store::ECStore, - store_api::{PutObjReader, ObjectOptions, StorageAPI, ObjectIO}, + store_api::{ObjectIO, ObjectOptions, PutObjReader, StorageAPI}, }; +use serial_test::serial; +use std::sync::Once; use std::{path::PathBuf, sync::Arc, time::Duration}; use tokio::fs; use tracing::info; +use walkdir::WalkDir; +static INIT: Once = Once::new(); +fn init_tracing() { + INIT.call_once(|| { + let _ = tracing_subscriber::fmt::try_init(); + }); +} /// Test helper: Create test environment with ECStore async fn setup_test_env() -> (Vec, Arc, Arc) { - // create temp dir as 4 disks - let test_base_dir = "/tmp/rustfs_ahm_heal_test"; - let temp_dir = std::path::PathBuf::from(test_base_dir); + use std::sync::OnceLock; + init_tracing(); + static GLOBAL_ENV: OnceLock<(Vec, Arc, Arc)> = OnceLock::new(); + + // Fast path: already initialized, just clone and return + if let Some((paths, ecstore, heal_storage)) = GLOBAL_ENV.get() { + return (paths.clone(), ecstore.clone(), heal_storage.clone()); + } + + // create temp dir as 4 disks with unique base dir + let test_base_dir = format!("/tmp/rustfs_ahm_heal_test_{}", uuid::Uuid::new_v4()); + let temp_dir = std::path::PathBuf::from(&test_base_dir); if temp_dir.exists() { - fs::remove_dir_all(&temp_dir).await.unwrap(); + fs::remove_dir_all(&temp_dir).await.ok(); } fs::create_dir_all(&temp_dir).await.unwrap(); @@ -57,11 +76,11 @@ async fn setup_test_env() -> (Vec, Arc, Arc (Vec, Arc, Arc, bucket_name: &str) { - (&**ecstore) + (**ecstore) .make_bucket(bucket_name, &Default::default()) .await .expect("Failed to create test bucket"); @@ -92,22 +114,14 @@ async fn create_test_bucket(ecstore: &Arc, bucket_name: &str) { } /// Test helper: Upload test object -async fn upload_test_object( - ecstore: &Arc, - bucket: &str, - object: &str, - data: &[u8], -) { +async fn upload_test_object(ecstore: &Arc, bucket: &str, object: &str, data: &[u8]) { let mut reader = PutObjReader::from_vec(data.to_vec()); - let object_info = (&**ecstore) + let object_info = (**ecstore) .put_object(bucket, object, &mut reader, &ObjectOptions::default()) .await .expect("Failed to upload test object"); - - info!( - "Uploaded test object: {}/{} ({} bytes)", - bucket, object, object_info.size - ); + + info!("Uploaded test object: {}/{} ({} bytes)", bucket, object, object_info.size); } /// Test helper: Cleanup test environment @@ -117,31 +131,54 @@ async fn cleanup_test_env(disk_paths: &[PathBuf]) { fs::remove_dir_all(disk_path).await.expect("Failed to cleanup disk path"); } } - - // Clean up test base directory - let test_base_dir = PathBuf::from("/tmp/rustfs_ahm_heal_test"); - if test_base_dir.exists() { - fs::remove_dir_all(&test_base_dir).await.expect("Failed to cleanup test base directory"); + + // Attempt to clean up base directory inferred from disk_paths[0] + if let Some(parent) = disk_paths.first().and_then(|p| p.parent()).and_then(|p| p.parent()) { + if parent.exists() { + fs::remove_dir_all(parent).await.ok(); + } } - + info!("Test environment cleaned up"); } -#[tokio::test] +#[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 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; - - // Create heal manager - let heal_manager = HealManager::new(heal_storage.clone(), Default::default()); - + + // ─── 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. ...//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"); + + std::fs::remove_file(&target_part).expect("failed to delete part file"); + assert!(!target_part.exists()); + println!("✅ Deleted shard part file: {target_part:?}"); + + // 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(); + // Submit heal request for the object let heal_request = HealRequest::new( HealType::Object { @@ -160,46 +197,56 @@ async fn test_heal_object_basic() { }, HealPriority::Normal, ); - + let task_id = heal_manager .submit_heal_request(heal_request) .await .expect("Failed to submit heal request"); - + info!("Submitted heal request with task ID: {}", task_id); - + // Wait for task completion - tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; - - // Check task status - let task_status = heal_manager.get_task_status(&task_id).await; - assert!(task_status.is_ok()); - - let status = task_status.unwrap(); - info!("Task status: {:?}", status); - - // Verify object still exists and is accessible - let object_exists = heal_storage.object_exists(bucket_name, object_name).await; - assert!(object_exists.is_ok()); - assert!(object_exists.unwrap()); - + 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()); + // Cleanup cleanup_test_env(&disk_paths).await; - + info!("Heal object basic test passed"); } -#[tokio::test] +#[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; - + // Create test bucket let bucket_name = "test-bucket-heal"; create_test_bucket(&ecstore, bucket_name).await; - - // Create heal manager - let heal_manager = HealManager::new(heal_storage.clone(), Default::default()); - + + // ─── 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 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(); + // Submit heal request for the bucket let heal_request = HealRequest::new( HealType::Bucket { @@ -216,99 +263,83 @@ async fn test_heal_bucket_basic() { }, HealPriority::Normal, ); - + let task_id = heal_manager .submit_heal_request(heal_request) .await .expect("Failed to submit bucket heal request"); - + info!("Submitted bucket heal request with task ID: {}", task_id); - + // Wait for task completion tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; - - // Check task status - let task_status = heal_manager.get_task_status(&task_id).await; - assert!(task_status.is_ok()); - - let status = task_status.unwrap(); - info!("Bucket heal task status: {:?}", status); - - // Verify bucket still exists - let bucket_exists = heal_storage.get_bucket_info(bucket_name).await; - assert!(bucket_exists.is_ok()); - assert!(bucket_exists.unwrap().is_some()); - + + // 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"); + // Cleanup cleanup_test_env(&disk_paths).await; - + info!("Heal bucket basic test passed"); } -#[tokio::test] +#[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; - - // Create heal manager - let heal_manager = HealManager::new(heal_storage.clone(), Default::default()); - - // Get disk endpoint for testing - let disk_endpoint = Endpoint::try_from(disk_paths[0].to_str().unwrap()) - .expect("Failed to create disk endpoint"); - - // Submit disk heal request (format heal) - let heal_request = HealRequest::new( - HealType::Disk { endpoint: disk_endpoint }, - HealOptions { - dry_run: true, // Use dry run for format heal test - recursive: false, - remove_corrupted: false, - recreate_missing: false, - scan_mode: HEAL_NORMAL_SCAN, - update_parity: false, - timeout: Some(Duration::from_secs(300)), - }, - HealPriority::Normal, - ); - - let task_id = heal_manager - .submit_heal_request(heal_request) - .await - .expect("Failed to submit disk heal request"); - - info!("Submitted disk heal request with task ID: {}", task_id); - + + // ─── 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(3)).await; - - // Check task status - let task_status = heal_manager.get_task_status(&task_id).await; - assert!(task_status.is_ok()); - - let status = task_status.unwrap(); - info!("Disk heal task status: {:?}", status); - + 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"); + // Cleanup cleanup_test_env(&disk_paths).await; - + info!("Heal format basic test passed"); } -#[tokio::test] +#[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 = rustfs_ecstore::heal::heal_commands::HealOpts { recursive: true, dry_run: true, @@ -320,16 +351,16 @@ async fn test_heal_storage_api_direct() { 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 = rustfs_ecstore::heal::heal_commands::HealOpts { recursive: false, dry_run: true, @@ -341,13 +372,15 @@ async fn test_heal_storage_api_direct() { pool: None, set: None, }; - - let object_result = heal_storage.heal_object(bucket_name, object_name, None, &object_heal_opts).await; + + 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"); - + // Cleanup cleanup_test_env(&disk_paths).await; - + info!("Direct heal storage API test passed"); -} \ No newline at end of file +} diff --git a/crates/ecstore/src/set_disk.rs b/crates/ecstore/src/set_disk.rs index 87b79ecae..851bbf27d 100644 --- a/crates/ecstore/src/set_disk.rs +++ b/crates/ecstore/src/set_disk.rs @@ -2757,8 +2757,8 @@ impl SetDisks { false } }; - - for disk in out_dated_disks.iter() { + // write to all disks + for disk in self.disks.read().await.iter() { let writer = create_bitrot_writer( is_inline_buffer, disk.as_ref(), @@ -2821,7 +2821,6 @@ impl SetDisks { // writers.push(None); // } } - // Heal each part. erasure.Heal() will write the healed // part to .rustfs/tmp/uuid/ which needs to be renamed // later to the final location. @@ -2872,7 +2871,6 @@ impl SetDisks { } } } - // Rename from tmp location to the actual location. for (index, disk) in out_dated_disks.iter().enumerate() { if let Some(disk) = disk { From 8e766b90cd0c5791a6b7b5650b1c8274853f66af Mon Sep 17 00:00:00 2001 From: junxiang Mu <1948535941@qq.com> Date: Tue, 15 Jul 2025 17:40:51 +0800 Subject: [PATCH 08/29] feat: implement heal channel mechanism for admin-ahm communication - Add global unbounded channel in common crate for heal requests - Implement channel processor in ahm to handle heal commands - Add Start/Query/Cancel commands support via channel - Integrate heal manager initialization in main.rs - Replace direct MRF calls with channel-based heal requests in ecstore - Support advanced heal options including pool_index and set_index - Enable admin handlers to send heal requests via channel --- Cargo.lock | 1 + crates/ahm/src/heal/channel.rs | 232 ++++++++++++++++++ crates/ahm/src/heal/event.rs | 4 +- crates/ahm/src/heal/manager.rs | 2 +- crates/ahm/src/heal/mod.rs | 1 + crates/ahm/src/heal/task.rs | 18 +- crates/ahm/src/lib.rs | 63 ++++- crates/ahm/tests/heal_integration_test.rs | 4 + crates/common/Cargo.toml | 1 + crates/common/src/heal_channel.rs | 226 +++++++++++++++++ crates/common/src/lib.rs | 1 + crates/ecstore/src/global.rs | 4 +- .../ecstore/src/heal/background_heal_ops.rs | 10 +- crates/ecstore/src/set_disk.rs | 113 +++++---- rustfs/src/admin/handlers.rs | 85 ++++--- rustfs/src/admin/mod.rs | 2 +- rustfs/src/main.rs | 10 +- 17 files changed, 679 insertions(+), 98 deletions(-) create mode 100644 crates/ahm/src/heal/channel.rs create mode 100644 crates/common/src/heal_channel.rs diff --git a/Cargo.lock b/Cargo.lock index 0613b2f8d..c1c1608c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7967,6 +7967,7 @@ version = "0.0.5" dependencies = [ "tokio", "tonic", + "uuid", ] [[package]] diff --git a/crates/ahm/src/heal/channel.rs b/crates/ahm/src/heal/channel.rs new file mode 100644 index 000000000..6ffc11e51 --- /dev/null +++ b/crates/ahm/src/heal/channel.rs @@ -0,0 +1,232 @@ +// 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::error::Result; +use crate::heal::{ + manager::HealManager, + task::{HealOptions, HealPriority, HealRequest, HealType}, +}; + +use rustfs_common::heal_channel::{ + HealChannelCommand, HealChannelPriority, HealChannelReceiver, HealChannelRequest, HealChannelResponse, +}; +use std::sync::Arc; +use tokio::sync::mpsc; +use tracing::{error, info}; + +/// Heal channel processor +pub struct HealChannelProcessor { + /// Heal manager + heal_manager: Arc, + /// Response sender + response_sender: mpsc::UnboundedSender, + /// Response receiver + response_receiver: mpsc::UnboundedReceiver, +} + +impl HealChannelProcessor { + /// Create new HealChannelProcessor + pub fn new(heal_manager: Arc) -> Self { + let (response_tx, response_rx) = mpsc::unbounded_channel(); + Self { + heal_manager, + response_sender: response_tx, + response_receiver: response_rx, + } + } + + /// Start processing heal channel requests + pub async fn start(&mut self, mut receiver: HealChannelReceiver) -> Result<()> { + info!("Starting heal channel processor"); + + loop { + tokio::select! { + command = receiver.recv() => { + match command { + Some(command) => { + if let Err(e) = self.process_command(command).await { + error!("Failed to process heal command: {}", e); + } + } + None => { + info!("Heal channel receiver closed, stopping processor"); + break; + } + } + } + response = self.response_receiver.recv() => { + if let Some(response) = response { + // Handle response if needed + info!("Received heal response for request: {}", response.request_id); + } + } + } + } + + info!("Heal channel processor stopped"); + Ok(()) + } + + /// Process heal command + async fn process_command(&self, command: HealChannelCommand) -> Result<()> { + match command { + HealChannelCommand::Start(request) => self.process_start_request(request).await, + HealChannelCommand::Query { heal_path, client_token } => self.process_query_request(heal_path, client_token).await, + HealChannelCommand::Cancel { heal_path } => self.process_cancel_request(heal_path).await, + } + } + + /// Process start request + async fn process_start_request(&self, request: HealChannelRequest) -> Result<()> { + info!("Processing heal start request: {} for bucket: {}", request.id, request.bucket); + + // Convert channel request to heal request + let heal_request = self.convert_to_heal_request(request.clone())?; + + // Submit to heal manager + match self.heal_manager.submit_heal_request(heal_request).await { + Ok(task_id) => { + info!("Successfully submitted heal request: {} as task: {}", request.id, task_id); + + // Send success response + let response = HealChannelResponse { + request_id: request.id, + success: true, + data: Some(format!("Task ID: {}", task_id).into_bytes()), + error: None, + }; + + if let Err(e) = self.response_sender.send(response) { + error!("Failed to send heal response: {}", e); + } + } + Err(e) => { + error!("Failed to submit heal request: {} - {}", request.id, e); + + // Send error response + let response = HealChannelResponse { + request_id: request.id, + success: false, + data: None, + error: Some(e.to_string()), + }; + + if let Err(e) = self.response_sender.send(response) { + error!("Failed to send heal error response: {}", e); + } + } + } + + Ok(()) + } + + /// Process query request + async fn process_query_request(&self, heal_path: String, client_token: String) -> Result<()> { + info!("Processing heal query request for path: {}", heal_path); + + // TODO: Implement query logic based on heal_path and client_token + // For now, return a placeholder response + let response = HealChannelResponse { + request_id: client_token, + success: true, + data: Some(format!("Query result for path: {}", heal_path).into_bytes()), + error: None, + }; + + if let Err(e) = self.response_sender.send(response) { + error!("Failed to send query response: {}", e); + } + + Ok(()) + } + + /// Process cancel request + async fn process_cancel_request(&self, heal_path: String) -> Result<()> { + info!("Processing heal cancel request for path: {}", heal_path); + + // TODO: Implement cancel logic based on heal_path + // For now, return a placeholder response + let response = HealChannelResponse { + request_id: heal_path.clone(), + success: true, + data: Some(format!("Cancel request for path: {}", heal_path).into_bytes()), + error: None, + }; + + if let Err(e) = self.response_sender.send(response) { + error!("Failed to send cancel response: {}", e); + } + + Ok(()) + } + + /// Convert channel request to heal request + fn convert_to_heal_request(&self, request: HealChannelRequest) -> Result { + let heal_type = match &request.object_prefix { + Some(prefix) if !prefix.is_empty() => HealType::Object { + bucket: request.bucket.clone(), + object: prefix.clone(), + version_id: None, + }, + _ => HealType::Bucket { + bucket: request.bucket.clone(), + }, + }; + + let priority = match request.priority { + HealChannelPriority::Low => HealPriority::Low, + HealChannelPriority::Normal => HealPriority::Normal, + HealChannelPriority::High => HealPriority::High, + HealChannelPriority::Critical => HealPriority::Urgent, + }; + + // Convert scan mode + let scan_mode = match request.scan_mode { + Some(rustfs_common::heal_channel::HealChannelScanMode::Normal) => { + rustfs_ecstore::heal::heal_commands::HEAL_NORMAL_SCAN + } + Some(rustfs_common::heal_channel::HealChannelScanMode::Deep) => { + rustfs_ecstore::heal::heal_commands::HEAL_DEEP_SCAN + } + None => rustfs_ecstore::heal::heal_commands::HEAL_NORMAL_SCAN, + }; + + // Build HealOptions with all available fields + let mut options = HealOptions { + scan_mode, + remove_corrupted: request.remove_corrupted.unwrap_or(false), + recreate_missing: request.recreate_missing.unwrap_or(true), + update_parity: request.update_parity.unwrap_or(true), + recursive: request.recursive.unwrap_or(false), + dry_run: request.dry_run.unwrap_or(false), + timeout: request.timeout_seconds.map(|secs| std::time::Duration::from_secs(secs)), + pool_index: request.pool_index, + set_index: request.set_index, + }; + + // Apply force_start overrides + if request.force_start { + options.remove_corrupted = true; + options.recreate_missing = true; + options.update_parity = true; + } + + Ok(HealRequest::new(heal_type, options, priority)) + } + + /// Get response sender for external use + pub fn get_response_sender(&self) -> mpsc::UnboundedSender { + self.response_sender.clone() + } +} diff --git a/crates/ahm/src/heal/event.rs b/crates/ahm/src/heal/event.rs index 85ecb0023..11705385b 100644 --- a/crates/ahm/src/heal/event.rs +++ b/crates/ahm/src/heal/event.rs @@ -246,9 +246,7 @@ impl HealEvent { actual_checksum, .. } => { - format!( - "Checksum mismatch: {bucket}/{object} - expected: {expected_checksum}, actual: {actual_checksum}" - ) + format!("Checksum mismatch: {bucket}/{object} - expected: {expected_checksum}, actual: {actual_checksum}") } HealEvent::BucketMetadataCorruption { bucket, corruption_type, .. diff --git a/crates/ahm/src/heal/manager.rs b/crates/ahm/src/heal/manager.rs index 694b340b2..d0581620d 100644 --- a/crates/ahm/src/heal/manager.rs +++ b/crates/ahm/src/heal/manager.rs @@ -52,7 +52,7 @@ impl Default for HealConfig { fn default() -> Self { Self { enable_auto_heal: true, - heal_interval: Duration::from_secs(60), // 1 minute + heal_interval: Duration::from_secs(10), // 10 seconds max_concurrent_heals: 4, task_timeout: Duration::from_secs(300), // 5 minutes queue_size: 1000, diff --git a/crates/ahm/src/heal/mod.rs b/crates/ahm/src/heal/mod.rs index 742c27669..f209d4501 100644 --- a/crates/ahm/src/heal/mod.rs +++ b/crates/ahm/src/heal/mod.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +pub mod channel; pub mod event; pub mod manager; pub mod progress; diff --git a/crates/ahm/src/heal/task.rs b/crates/ahm/src/heal/task.rs index 8650e5712..ea07786d2 100644 --- a/crates/ahm/src/heal/task.rs +++ b/crates/ahm/src/heal/task.rs @@ -94,6 +94,10 @@ pub struct HealOptions { pub dry_run: bool, /// Timeout pub timeout: Option, + /// pool index + pub pool_index: Option, + /// set index + pub set_index: Option, } impl Default for HealOptions { @@ -106,6 +110,8 @@ impl Default for HealOptions { recursive: false, dry_run: false, timeout: Some(Duration::from_secs(300)), // 5 minutes default timeout + pool_index: None, + set_index: None, } } } @@ -339,8 +345,8 @@ impl HealTask { scan_mode: self.options.scan_mode, update_parity: self.options.update_parity, no_lock: false, - pool: None, - set: None, + pool: self.options.pool_index, + set: self.options.set_index, }; match self.storage.heal_object(bucket, object, version_id, &heal_opts).await { @@ -491,8 +497,8 @@ impl HealTask { scan_mode: self.options.scan_mode, update_parity: self.options.update_parity, no_lock: false, - pool: None, - set: None, + pool: self.options.pool_index, + set: self.options.set_index, }; match self.storage.heal_bucket(bucket, &heal_opts).await { @@ -609,8 +615,8 @@ impl HealTask { scan_mode: rustfs_ecstore::heal::heal_commands::HEAL_DEEP_SCAN, update_parity: false, no_lock: false, - pool: None, - set: None, + pool: self.options.pool_index, + set: self.options.set_index, }; match self.storage.heal_object(bucket, object, None, &heal_opts).await { diff --git a/crates/ahm/src/lib.rs b/crates/ahm/src/lib.rs index 3a5117f7c..207973e99 100644 --- a/crates/ahm/src/lib.rs +++ b/crates/ahm/src/lib.rs @@ -12,15 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::sync::OnceLock; +use std::sync::{Arc, OnceLock}; use tokio_util::sync::CancellationToken; +use tracing::{error, info}; pub mod error; pub mod heal; pub mod scanner; pub use error::{Error, Result}; -pub use heal::{HealManager, HealOptions, HealPriority, HealRequest, HealType}; +pub use heal::{channel::HealChannelProcessor, HealManager, HealOptions, HealPriority, HealRequest, HealType}; pub use scanner::{ BucketTargetUsageInfo, BucketUsageInfo, DataUsageInfo, Scanner, ScannerMetrics, load_data_usage_from_backend, store_data_usage_in_backend, @@ -54,3 +55,61 @@ pub fn shutdown_ahm_services() { cancel_token.cancel(); } } + +/// Global heal manager instance +static GLOBAL_HEAL_MANAGER: OnceLock> = OnceLock::new(); + +/// Global heal channel processor instance +static GLOBAL_HEAL_CHANNEL_PROCESSOR: OnceLock>> = OnceLock::new(); + +/// Initialize and start heal manager with channel processor +pub async fn init_heal_manager_with_channel( + storage: Arc, + config: Option, +) -> Result<()> { + // Create heal manager + let heal_manager = Arc::new(HealManager::new(storage, config)); + + // Start heal manager + heal_manager.start().await?; + + // Store global instance + GLOBAL_HEAL_MANAGER + .set(heal_manager.clone()) + .map_err(|_| Error::Config("Heal manager already initialized".to_string()))?; + + // Initialize heal channel + let channel_receiver = rustfs_common::heal_channel::init_heal_channel(); + + // Create channel processor + let channel_processor = HealChannelProcessor::new(heal_manager); + + // Store channel processor instance first + GLOBAL_HEAL_CHANNEL_PROCESSOR + .set(Arc::new(tokio::sync::Mutex::new(channel_processor))) + .map_err(|_| Error::Config("Heal channel processor already initialized".to_string()))?; + + // Start channel processor in background + let receiver = channel_receiver; + tokio::spawn(async move { + if let Some(processor_guard) = GLOBAL_HEAL_CHANNEL_PROCESSOR.get() { + let mut processor = processor_guard.lock().await; + if let Err(e) = processor.start(receiver).await { + error!("Heal channel processor failed: {}", e); + } + } + }); + + info!("Heal manager with channel processor initialized successfully"); + Ok(()) +} + +/// Get global heal manager instance +pub fn get_heal_manager() -> Option<&'static Arc> { + GLOBAL_HEAL_MANAGER.get() +} + +/// Get global heal channel processor instance +pub fn get_heal_channel_processor() -> Option<&'static Arc>> { + GLOBAL_HEAL_CHANNEL_PROCESSOR.get() +} diff --git a/crates/ahm/tests/heal_integration_test.rs b/crates/ahm/tests/heal_integration_test.rs index c161af155..905115ed6 100644 --- a/crates/ahm/tests/heal_integration_test.rs +++ b/crates/ahm/tests/heal_integration_test.rs @@ -194,6 +194,8 @@ async fn test_heal_object_basic() { scan_mode: HEAL_NORMAL_SCAN, update_parity: true, timeout: Some(Duration::from_secs(300)), + pool_index: None, + set_index: None, }, HealPriority::Normal, ); @@ -260,6 +262,8 @@ async fn test_heal_bucket_basic() { scan_mode: HEAL_NORMAL_SCAN, update_parity: false, timeout: Some(Duration::from_secs(300)), + pool_index: None, + set_index: None, }, HealPriority::Normal, ); diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index e8bcb3702..85d8bf97f 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -30,3 +30,4 @@ workspace = true [dependencies] tokio.workspace = true tonic = { workspace = true } +uuid = { workspace = true } diff --git a/crates/common/src/heal_channel.rs b/crates/common/src/heal_channel.rs new file mode 100644 index 000000000..22943852c --- /dev/null +++ b/crates/common/src/heal_channel.rs @@ -0,0 +1,226 @@ +// 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::OnceLock; +use tokio::sync::mpsc; +use uuid::Uuid; + +/// Heal channel command type +#[derive(Debug, Clone)] +pub enum HealChannelCommand { + /// Start a new heal task + Start(HealChannelRequest), + /// Query heal task status + Query { heal_path: String, client_token: String }, + /// Cancel heal task + Cancel { heal_path: String }, +} + +/// Heal request from admin to ahm +#[derive(Debug, Clone)] +pub struct HealChannelRequest { + /// Unique request ID + pub id: String, + /// Bucket name + pub bucket: String, + /// Object prefix (optional) + pub object_prefix: Option, + /// Force start heal + pub force_start: bool, + /// Priority + pub priority: HealChannelPriority, + /// Pool index (optional) + pub pool_index: Option, + /// Set index (optional) + pub set_index: Option, + /// Scan mode (optional) + pub scan_mode: Option, + /// Whether to remove corrupted data + pub remove_corrupted: Option, + /// Whether to recreate missing data + pub recreate_missing: Option, + /// Whether to update parity + pub update_parity: Option, + /// Whether to recursively process + pub recursive: Option, + /// Whether to dry run + pub dry_run: Option, + /// Timeout in seconds (optional) + pub timeout_seconds: Option, +} + +/// Heal response from ahm to admin +#[derive(Debug, Clone)] +pub struct HealChannelResponse { + /// Request ID + pub request_id: String, + /// Success status + pub success: bool, + /// Response data (if successful) + pub data: Option>, + /// Error message (if failed) + pub error: Option, +} + +/// Heal priority +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HealChannelPriority { + /// Low priority + Low, + /// Normal priority + Normal, + /// High priority + High, + /// Critical priority + Critical, +} + +impl Default for HealChannelPriority { + fn default() -> Self { + Self::Normal + } +} + +/// Heal channel sender +pub type HealChannelSender = mpsc::UnboundedSender; + +/// Heal channel receiver +pub type HealChannelReceiver = mpsc::UnboundedReceiver; + +/// Global heal channel sender +static GLOBAL_HEAL_CHANNEL_SENDER: OnceLock = OnceLock::new(); + +/// Initialize global heal channel +pub fn init_heal_channel() -> HealChannelReceiver { + let (tx, rx) = mpsc::unbounded_channel(); + GLOBAL_HEAL_CHANNEL_SENDER + .set(tx) + .expect("Heal channel sender already initialized"); + rx +} + +/// Get global heal channel sender +pub fn get_heal_channel_sender() -> Option<&'static HealChannelSender> { + GLOBAL_HEAL_CHANNEL_SENDER.get() +} + +/// Send heal command through global channel +pub async fn send_heal_command(command: HealChannelCommand) -> Result<(), String> { + if let Some(sender) = get_heal_channel_sender() { + sender + .send(command) + .map_err(|e| format!("Failed to send heal command: {}", e))?; + Ok(()) + } else { + Err("Heal channel not initialized".to_string()) + } +} + +/// Send heal start request +pub async fn send_heal_request(request: HealChannelRequest) -> Result<(), String> { + send_heal_command(HealChannelCommand::Start(request)).await +} + +/// Send heal query request +pub async fn query_heal_status(heal_path: String, client_token: String) -> Result<(), String> { + send_heal_command(HealChannelCommand::Query { heal_path, client_token }).await +} + +/// Send heal cancel request +pub async fn cancel_heal_task(heal_path: String) -> Result<(), String> { + send_heal_command(HealChannelCommand::Cancel { heal_path }).await +} + +/// Create a new heal request +pub fn create_heal_request( + bucket: String, + object_prefix: Option, + force_start: bool, + priority: Option, +) -> HealChannelRequest { + HealChannelRequest { + id: Uuid::new_v4().to_string(), + bucket, + object_prefix, + force_start, + priority: priority.unwrap_or_default(), + pool_index: None, + set_index: None, + scan_mode: None, + remove_corrupted: None, + recreate_missing: None, + update_parity: None, + recursive: None, + dry_run: None, + timeout_seconds: None, + } +} + +/// Create a new heal request with advanced options +pub fn create_heal_request_with_options( + bucket: String, + object_prefix: Option, + force_start: bool, + priority: Option, + pool_index: Option, + set_index: Option, + scan_mode: Option, + remove_corrupted: Option, + recreate_missing: Option, + update_parity: Option, + recursive: Option, + dry_run: Option, + timeout_seconds: Option, +) -> HealChannelRequest { + HealChannelRequest { + id: Uuid::new_v4().to_string(), + bucket, + object_prefix, + force_start, + priority: priority.unwrap_or_default(), + pool_index, + set_index, + scan_mode, + remove_corrupted, + recreate_missing, + update_parity, + recursive, + dry_run, + timeout_seconds, + } +} + +/// Create a heal response +pub fn create_heal_response( + request_id: String, + success: bool, + data: Option>, + error: Option, +) -> HealChannelResponse { + HealChannelResponse { + request_id, + success, + data, + error, + } +} + +/// Heal scan mode +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HealChannelScanMode { + /// Normal scan + Normal, + /// Deep scan + Deep, +} diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 16de991db..7c163b345 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -15,6 +15,7 @@ pub mod bucket_stats; // pub mod error; pub mod globals; +pub mod heal_channel; pub mod last_minute; // is ',' diff --git a/crates/ecstore/src/global.rs b/crates/ecstore/src/global.rs index 93c38fe0d..d8411fba1 100644 --- a/crates/ecstore/src/global.rs +++ b/crates/ecstore/src/global.rs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::heal::mrf::MRFState; use crate::{ bucket::lifecycle::bucket_lifecycle_ops::LifecycleSys, disk::DiskStore, @@ -53,12 +52,11 @@ pub static ref GLOBAL_Endpoints: OnceLock = OnceLock::new() pub static ref GLOBAL_RootDiskThreshold: RwLock = RwLock::new(0); pub static ref GLOBAL_BackgroundHealRoutine: Arc = HealRoutine::new(); pub static ref GLOBAL_BackgroundHealState: Arc = AllHealState::new(false); +// pub static ref GLOBAL_MRFState: Arc = Arc::new(MRFState::new()); pub static ref GLOBAL_TierConfigMgr: Arc> = TierConfigMgr::new(); pub static ref GLOBAL_LifecycleSys: Arc = LifecycleSys::new(); pub static ref GLOBAL_EventNotifier: Arc> = EventNotifier::new(); //pub static ref GLOBAL_RemoteTargetTransport -pub static ref GLOBAL_ALlHealState: Arc = AllHealState::new(false); -pub static ref GLOBAL_MRFState: Arc = Arc::new(MRFState::new()); static ref globalDeploymentIDPtr: OnceLock = OnceLock::new(); pub static ref GLOBAL_BOOT_TIME: OnceCell = OnceCell::new(); pub static ref GLOBAL_LocalNodeName: String = "127.0.0.1:9000".to_string(); diff --git a/crates/ecstore/src/heal/background_heal_ops.rs b/crates/ecstore/src/heal/background_heal_ops.rs index 0bc3e2591..59abc3db9 100644 --- a/crates/ecstore/src/heal/background_heal_ops.rs +++ b/crates/ecstore/src/heal/background_heal_ops.rs @@ -33,7 +33,7 @@ use super::{ heal_ops::{HealSequence, new_bg_heal_sequence}, }; use crate::error::{Error, Result}; -use crate::global::{GLOBAL_MRFState, get_background_services_cancel_token}; +use crate::global::get_background_services_cancel_token; use crate::heal::error::ERR_RETRY_HEALING; use crate::heal::heal_commands::{HEAL_ITEM_BUCKET, HealScanMode}; use crate::heal::heal_ops::{BG_HEALING_UUID, HealSource}; @@ -76,10 +76,10 @@ pub async fn init_auto_heal() { }); } - let cancel_clone = cancel_token.clone(); - spawn(async move { - GLOBAL_MRFState.heal_routine_with_cancel(cancel_clone).await; - }); + // let cancel_clone = cancel_token.clone(); + // spawn(async move { + // GLOBAL_MRFState.heal_routine_with_cancel(cancel_clone).await; + // }); } async fn init_background_healing() { diff --git a/crates/ecstore/src/set_disk.rs b/crates/ecstore/src/set_disk.rs index 851bbf27d..6edd46f0e 100644 --- a/crates/ecstore/src/set_disk.rs +++ b/crates/ecstore/src/set_disk.rs @@ -2033,17 +2033,23 @@ impl SetDisks { let fi = Self::pick_valid_fileinfo(&parts_metadata, mot_time, etag, read_quorum as usize)?; if errs.iter().any(|err| err.is_some()) { - GLOBAL_MRFState - .add_partial(PartialOperation { - bucket: fi.volume.to_string(), - object: fi.name.to_string(), - queued: Utc::now(), - version_id: fi.version_id.map(|v| v.to_string()), - set_index: self.set_index, - pool_index: self.pool_index, - ..Default::default() - }) - .await; + let _ = rustfs_common::heal_channel::send_heal_request( + rustfs_common::heal_channel::create_heal_request_with_options( + fi.volume.to_string(), // bucket + Some(fi.name.to_string()), // object_prefix + false, // force_start + Some(rustfs_common::heal_channel::HealChannelPriority::Normal), // priority + Some(self.pool_index), // pool_index + Some(self.set_index), // set_index + None, // scan_mode + None, // remove_corrupted + None, // recreate_missing + None, // update_parity + None, // recursive + None, // dry_run + None, // timeout_seconds + ) + ).await; } // debug!("get_object_fileinfo pick fi {:?}", &fi); @@ -2174,18 +2180,23 @@ impl SetDisks { match de_err { DiskError::FileNotFound | DiskError::FileCorrupt => { error!("erasure.decode err 111 {:?}", &de_err); - GLOBAL_MRFState - .add_partial(PartialOperation { - bucket: bucket.to_string(), - object: object.to_string(), - queued: Utc::now(), - version_id: fi.version_id.map(|v| v.to_string()), - set_index, - pool_index, - bitrot_scan: de_err == DiskError::FileCorrupt, - ..Default::default() - }) - .await; + let _ = rustfs_common::heal_channel::send_heal_request( + rustfs_common::heal_channel::create_heal_request_with_options( + bucket.to_string(), + Some(object.to_string()), + false, + Some(rustfs_common::heal_channel::HealChannelPriority::Normal), + Some(pool_index), + Some(set_index), + None, + None, + None, + None, + None, + None, + None, + ) + ).await; has_err = false; } _ => {} @@ -4693,17 +4704,23 @@ impl StorageAPI for SetDisks { #[tracing::instrument(skip(self))] async fn add_partial(&self, bucket: &str, object: &str, version_id: &str) -> Result<()> { - GLOBAL_MRFState - .add_partial(PartialOperation { - bucket: bucket.to_string(), - object: object.to_string(), - version_id: Some(version_id.to_string()), - queued: Utc::now(), - set_index: self.set_index, - pool_index: self.pool_index, - ..Default::default() - }) - .await; + let _ = rustfs_common::heal_channel::send_heal_request( + rustfs_common::heal_channel::create_heal_request_with_options( + bucket.to_string(), + Some(object.to_string()), + false, + Some(rustfs_common::heal_channel::HealChannelPriority::Normal), + Some(self.pool_index), + Some(self.set_index), + None, + None, + None, + None, + None, + None, + None, + ) + ).await; Ok(()) } @@ -5845,17 +5862,23 @@ impl StorageAPI for SetDisks { .await?; } if let Some(versions) = versions { - GLOBAL_MRFState - .add_partial(PartialOperation { - bucket: bucket.to_string(), - object: object.to_string(), - queued: Utc::now(), - versions, - set_index: self.set_index, - pool_index: self.pool_index, - ..Default::default() - }) - .await; + let _ = rustfs_common::heal_channel::send_heal_request( + rustfs_common::heal_channel::create_heal_request_with_options( + bucket.to_string(), + Some(object.to_string()), + false, + Some(rustfs_common::heal_channel::HealChannelPriority::Normal), + Some(self.pool_index), + Some(self.set_index), + None, + None, + None, + None, + None, + None, + None, + ) + ).await; } let upload_id_path = upload_id_path.clone(); diff --git a/rustfs/src/admin/handlers.rs b/rustfs/src/admin/handlers.rs index 5061a6097..ab3a083cd 100644 --- a/rustfs/src/admin/handlers.rs +++ b/rustfs/src/admin/handlers.rs @@ -29,12 +29,9 @@ use rustfs_ecstore::bucket::target::BucketTarget; use rustfs_ecstore::bucket::versioning_sys::BucketVersioningSys; use rustfs_ecstore::cmd::bucket_targets::{self, GLOBAL_Bucket_Target_Sys}; use rustfs_ecstore::error::StorageError; -use rustfs_ecstore::global::GLOBAL_ALlHealState; use rustfs_ecstore::global::get_global_action_cred; -// use rustfs_ecstore::heal::data_usage::load_data_usage_from_backend; use rustfs_ecstore::heal::data_usage::load_data_usage_from_backend; use rustfs_ecstore::heal::heal_commands::HealOpts; -use rustfs_ecstore::heal::heal_ops::new_heal_sequence; use rustfs_ecstore::metrics_realtime::{CollectMetricsOpts, MetricType, collect_local_metrics}; use rustfs_ecstore::new_object_layer_fn; use rustfs_ecstore::pools::{get_total_usable_capacity, get_total_usable_capacity_free}; @@ -689,33 +686,20 @@ impl Operation for HealHandler { } let heal_path = path_join(&[PathBuf::from(hip.bucket.clone()), PathBuf::from(hip.obj_prefix.clone())]); - if !hip.client_token.is_empty() && !hip.force_start && !hip.force_stop { - match GLOBAL_ALlHealState - .pop_heal_status_json(heal_path.to_str().unwrap_or_default(), &hip.client_token) - .await - { - Ok(b) => { - info!("pop_heal_status_json success"); - return Ok(S3Response::new((StatusCode::OK, Body::from(b)))); - } - Err(_e) => { - info!("pop_heal_status_json failed"); - return Ok(S3Response::new((StatusCode::INTERNAL_SERVER_ERROR, Body::from(vec![])))); - } - } - } let (tx, mut rx) = mpsc::channel(1); - if hip.force_stop { + + if !hip.client_token.is_empty() && !hip.force_start && !hip.force_stop { + // Query heal status let tx_clone = tx.clone(); + let heal_path_str = heal_path.to_str().unwrap_or_default().to_string(); + let client_token = hip.client_token.clone(); spawn(async move { - match GLOBAL_ALlHealState - .stop_heal_sequence(heal_path.to_str().unwrap_or_default()) - .await - { - Ok(b) => { + match rustfs_common::heal_channel::query_heal_status(heal_path_str, client_token).await { + Ok(_) => { + // TODO: Get actual response from channel let _ = tx_clone .send(HealResp { - resp_bytes: b, + resp_bytes: vec![], ..Default::default() }) .await; @@ -723,7 +707,32 @@ impl Operation for HealHandler { Err(e) => { let _ = tx_clone .send(HealResp { - _api_err: Some(e), + _api_err: Some(StorageError::other(e)), + ..Default::default() + }) + .await; + } + } + }); + } else if hip.force_stop { + // Cancel heal task + let tx_clone = tx.clone(); + let heal_path_str = heal_path.to_str().unwrap_or_default().to_string(); + spawn(async move { + match rustfs_common::heal_channel::cancel_heal_task(heal_path_str).await { + Ok(_) => { + // TODO: Get actual response from channel + let _ = tx_clone + .send(HealResp { + resp_bytes: vec![], + ..Default::default() + }) + .await; + } + Err(e) => { + let _ = tx_clone + .send(HealResp { + _api_err: Some(StorageError::other(e)), ..Default::default() }) .await; @@ -731,22 +740,36 @@ impl Operation for HealHandler { } }); } else if hip.client_token.is_empty() { - let nh = Arc::new(new_heal_sequence(&hip.bucket, &hip.obj_prefix, "", hip.hs, hip.force_start)); + // Use new heal channel mechanism let tx_clone = tx.clone(); spawn(async move { - match GLOBAL_ALlHealState.launch_new_heal_sequence(nh).await { - Ok(b) => { + // Create heal request through channel + let heal_request = rustfs_common::heal_channel::create_heal_request( + hip.bucket.clone(), + if hip.obj_prefix.is_empty() { + None + } else { + Some(hip.obj_prefix.clone()) + }, + hip.force_start, + Some(rustfs_common::heal_channel::HealChannelPriority::Normal), + ); + + match rustfs_common::heal_channel::send_heal_request(heal_request).await { + Ok(_) => { + // Success - send empty response for now let _ = tx_clone .send(HealResp { - resp_bytes: b, + resp_bytes: vec![], ..Default::default() }) .await; } Err(e) => { + // Error - send error response let _ = tx_clone .send(HealResp { - _api_err: Some(e), + _api_err: Some(StorageError::other(e)), ..Default::default() }) .await; diff --git a/rustfs/src/admin/mod.rs b/rustfs/src/admin/mod.rs index f8cbd0ed5..eb4ecf43d 100644 --- a/rustfs/src/admin/mod.rs +++ b/rustfs/src/admin/mod.rs @@ -31,7 +31,7 @@ use router::{AdminOperation, S3Router}; use rpc::register_rpc_route; use s3s::route::S3Route; -const ADMIN_PREFIX: &str = "/rustfs/admin"; +const ADMIN_PREFIX: &str = "/minio/admin"; pub fn make_admin_route(console_enabled: bool) -> std::io::Result { let mut r: S3Router = S3Router::new(console_enabled); diff --git a/rustfs/src/main.rs b/rustfs/src/main.rs index e1f481e9b..b7cc0a32f 100644 --- a/rustfs/src/main.rs +++ b/rustfs/src/main.rs @@ -29,7 +29,10 @@ use chrono::Datelike; use clap::Parser; use license::init_license; use rustfs_ahm::scanner::data_scanner::ScannerConfig; -use rustfs_ahm::{Scanner, create_ahm_services_cancel_token, shutdown_ahm_services}; +use rustfs_ahm::{ + Scanner, create_ahm_services_cancel_token, heal::storage::ECStoreHealStorage, init_heal_manager_with_channel, + shutdown_ahm_services, +}; use rustfs_common::globals::set_global_addr; use rustfs_config::DEFAULT_DELIMITER; use rustfs_ecstore::bucket::metadata_sys::init_bucket_metadata_sys; @@ -185,6 +188,11 @@ async fn run(opt: config::Opt) -> Result<()> { // init_data_scanner().await; // init_auto_heal().await; let _ = create_ahm_services_cancel_token(); + + // Initialize heal manager with channel processor + let heal_storage = Arc::new(ECStoreHealStorage::new(store.clone())); + init_heal_manager_with_channel(heal_storage, None).await?; + let scanner = Scanner::new(Some(ScannerConfig::default()), None); scanner.start().await?; print_server_info(); From c49414f6ac03446c5ba90565476da5e655316ec6 Mon Sep 17 00:00:00 2001 From: junxiang Mu <1948535941@qq.com> Date: Wed, 16 Jul 2025 16:32:33 +0800 Subject: [PATCH 09/29] fix: resolve test conflicts and improve data scanner functionality - Fix multi-threaded test conflicts in AHM heal integration tests - Remove global environment sharing to prevent test state pollution - Fix test_all_disk_method by clearing global disk map before test - Improve data scanner and cache value implementations - Update dependencies and resolve clippy warnings Signed-off-by: junxiang Mu <1948535941@qq.com> --- crates/ahm/src/heal/channel.rs | 12 +++---- crates/ahm/src/heal/manager.rs | 2 -- crates/ahm/src/scanner/data_scanner.rs | 12 +++++++ crates/ahm/tests/heal_integration_test.rs | 38 +++-------------------- crates/common/src/heal_channel.rs | 19 ++---------- crates/ecstore/src/cache_value/mod.rs | 9 ++++++ crates/ecstore/src/heal/data_scanner.rs | 4 +-- crates/ecstore/src/rpc/tonic_service.rs | 8 ----- 8 files changed, 36 insertions(+), 68 deletions(-) diff --git a/crates/ahm/src/heal/channel.rs b/crates/ahm/src/heal/channel.rs index 6ffc11e51..f9b59652b 100644 --- a/crates/ahm/src/heal/channel.rs +++ b/crates/ahm/src/heal/channel.rs @@ -103,7 +103,7 @@ impl HealChannelProcessor { let response = HealChannelResponse { request_id: request.id, success: true, - data: Some(format!("Task ID: {}", task_id).into_bytes()), + data: Some(format!("Task ID: {task_id}").into_bytes()), error: None, }; @@ -140,7 +140,7 @@ impl HealChannelProcessor { let response = HealChannelResponse { request_id: client_token, success: true, - data: Some(format!("Query result for path: {}", heal_path).into_bytes()), + data: Some(format!("Query result for path: {heal_path}").into_bytes()), error: None, }; @@ -160,7 +160,7 @@ impl HealChannelProcessor { let response = HealChannelResponse { request_id: heal_path.clone(), success: true, - data: Some(format!("Cancel request for path: {}", heal_path).into_bytes()), + data: Some(format!("Cancel request for path: {heal_path}").into_bytes()), error: None, }; @@ -196,9 +196,7 @@ impl HealChannelProcessor { Some(rustfs_common::heal_channel::HealChannelScanMode::Normal) => { rustfs_ecstore::heal::heal_commands::HEAL_NORMAL_SCAN } - Some(rustfs_common::heal_channel::HealChannelScanMode::Deep) => { - rustfs_ecstore::heal::heal_commands::HEAL_DEEP_SCAN - } + Some(rustfs_common::heal_channel::HealChannelScanMode::Deep) => rustfs_ecstore::heal::heal_commands::HEAL_DEEP_SCAN, None => rustfs_ecstore::heal::heal_commands::HEAL_NORMAL_SCAN, }; @@ -210,7 +208,7 @@ impl HealChannelProcessor { update_parity: request.update_parity.unwrap_or(true), recursive: request.recursive.unwrap_or(false), dry_run: request.dry_run.unwrap_or(false), - timeout: request.timeout_seconds.map(|secs| std::time::Duration::from_secs(secs)), + timeout: request.timeout_seconds.map(std::time::Duration::from_secs), pool_index: request.pool_index, set_index: request.set_index, }; diff --git a/crates/ahm/src/heal/manager.rs b/crates/ahm/src/heal/manager.rs index d0581620d..3a267839b 100644 --- a/crates/ahm/src/heal/manager.rs +++ b/crates/ahm/src/heal/manager.rs @@ -278,7 +278,6 @@ impl HealManager { _ = interval.tick() => { // Build list of endpoints that need healing let mut endpoints = Vec::new(); - println!("GLOBAL_LOCAL_DISK_MAP length: {:?}", GLOBAL_LOCAL_DISK_MAP.read().await.len()); for (_, disk_opt) in GLOBAL_LOCAL_DISK_MAP.read().await.iter() { if let Some(disk) = disk_opt { // detect unformatted disk via get_disk_id() @@ -300,7 +299,6 @@ impl HealManager { if endpoints.is_empty() { continue; } - println!("endpoints length: {:?}", endpoints.len()); for ep in endpoints { // skip if already queued or healing diff --git a/crates/ahm/src/scanner/data_scanner.rs b/crates/ahm/src/scanner/data_scanner.rs index b2d1103a3..aed19e875 100644 --- a/crates/ahm/src/scanner/data_scanner.rs +++ b/crates/ahm/src/scanner/data_scanner.rs @@ -1159,8 +1159,17 @@ mod tests { use serial_test::serial; use std::fs; use std::net::SocketAddr; + use std::sync::OnceLock; + + // Global test environment cache to avoid repeated initialization + static GLOBAL_TEST_ENV: OnceLock<(Vec, Arc)> = OnceLock::new(); async fn prepare_test_env(test_dir: Option<&str>, port: Option) -> (Vec, Arc) { + // Check if global environment is already initialized + if let Some((disk_paths, ecstore)) = GLOBAL_TEST_ENV.get() { + return (disk_paths.clone(), ecstore.clone()); + } + // create temp dir as 4 disks let test_base_dir = test_dir.unwrap_or("/tmp/rustfs_ahm_test"); let temp_dir = std::path::PathBuf::from(test_base_dir); @@ -1222,6 +1231,9 @@ mod tests { let buckets = buckets_list.into_iter().map(|v| v.name).collect(); rustfs_ecstore::bucket::metadata_sys::init_bucket_metadata_sys(ecstore.clone(), buckets).await; + // Store in global cache + let _ = GLOBAL_TEST_ENV.set((disk_paths.clone(), ecstore.clone())); + (disk_paths, ecstore) } diff --git a/crates/ahm/tests/heal_integration_test.rs b/crates/ahm/tests/heal_integration_test.rs index 905115ed6..c5a6f3a5b 100644 --- a/crates/ahm/tests/heal_integration_test.rs +++ b/crates/ahm/tests/heal_integration_test.rs @@ -12,11 +12,15 @@ use rustfs_ecstore::{ }; use serial_test::serial; use std::sync::Once; +use std::sync::OnceLock; use std::{path::PathBuf, sync::Arc, time::Duration}; use tokio::fs; use tracing::info; use walkdir::WalkDir; + +static GLOBAL_ENV: OnceLock<(Vec, Arc, Arc)> = OnceLock::new(); static INIT: Once = Once::new(); + fn init_tracing() { INIT.call_once(|| { let _ = tracing_subscriber::fmt::try_init(); @@ -25,9 +29,7 @@ fn init_tracing() { /// Test helper: Create test environment with ECStore async fn setup_test_env() -> (Vec, Arc, Arc) { - use std::sync::OnceLock; init_tracing(); - static GLOBAL_ENV: OnceLock<(Vec, Arc, Arc)> = OnceLock::new(); // Fast path: already initialized, just clone and return if let Some((paths, ecstore, heal_storage)) = GLOBAL_ENV.get() { @@ -124,24 +126,6 @@ async fn upload_test_object(ecstore: &Arc, bucket: &str, object: &str, info!("Uploaded test object: {}/{} ({} bytes)", bucket, object, object_info.size); } -/// Test helper: Cleanup test environment -async fn cleanup_test_env(disk_paths: &[PathBuf]) { - for disk_path in disk_paths { - if disk_path.exists() { - fs::remove_dir_all(disk_path).await.expect("Failed to cleanup disk path"); - } - } - - // Attempt to clean up base directory inferred from disk_paths[0] - if let Some(parent) = disk_paths.first().and_then(|p| p.parent()).and_then(|p| p.parent()) { - if parent.exists() { - fs::remove_dir_all(parent).await.ok(); - } - } - - info!("Test environment cleaned up"); -} - #[tokio::test(flavor = "multi_thread", worker_threads = 4)] #[serial] async fn test_heal_object_basic() { @@ -219,9 +203,6 @@ async fn test_heal_object_basic() { // ─── 2️⃣ verify each part file is restored ─────── assert!(target_part.exists()); - // Cleanup - cleanup_test_env(&disk_paths).await; - info!("Heal object basic test passed"); } @@ -290,9 +271,6 @@ async fn test_heal_bucket_basic() { // ─── 3️⃣ Verify bucket directory is restored on every disk ─────── assert!(broken_bucket_path.exists(), "bucket dir does not exist on disk"); - // Cleanup - cleanup_test_env(&disk_paths).await; - info!("Heal bucket basic test passed"); } @@ -322,16 +300,13 @@ async fn test_heal_format_basic() { // ─── 2️⃣ verify format.json is restored ─────── assert!(format_path.exists(), "format.json does not exist on disk after heal"); - // Cleanup - cleanup_test_env(&disk_paths).await; - 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; + let (_disk_paths, ecstore, heal_storage) = setup_test_env().await; // Test direct heal storage API calls @@ -383,8 +358,5 @@ async fn test_heal_storage_api_direct() { assert!(object_result.is_ok()); info!("Direct heal_object test passed"); - // Cleanup - cleanup_test_env(&disk_paths).await; - info!("Direct heal storage API test passed"); } diff --git a/crates/common/src/heal_channel.rs b/crates/common/src/heal_channel.rs index 22943852c..56399cb37 100644 --- a/crates/common/src/heal_channel.rs +++ b/crates/common/src/heal_channel.rs @@ -28,7 +28,7 @@ pub enum HealChannelCommand { } /// Heal request from admin to ahm -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct HealChannelRequest { /// Unique request ID pub id: String, @@ -120,7 +120,7 @@ pub async fn send_heal_command(command: HealChannelCommand) -> Result<(), String if let Some(sender) = get_heal_channel_sender() { sender .send(command) - .map_err(|e| format!("Failed to send heal command: {}", e))?; + .map_err(|e| format!("Failed to send heal command: {e}"))?; Ok(()) } else { Err("Heal channel not initialized".to_string()) @@ -175,13 +175,6 @@ pub fn create_heal_request_with_options( priority: Option, pool_index: Option, set_index: Option, - scan_mode: Option, - remove_corrupted: Option, - recreate_missing: Option, - update_parity: Option, - recursive: Option, - dry_run: Option, - timeout_seconds: Option, ) -> HealChannelRequest { HealChannelRequest { id: Uuid::new_v4().to_string(), @@ -191,13 +184,7 @@ pub fn create_heal_request_with_options( priority: priority.unwrap_or_default(), pool_index, set_index, - scan_mode, - remove_corrupted, - recreate_missing, - update_parity, - recursive, - dry_run, - timeout_seconds, + ..Default::default() } } diff --git a/crates/ecstore/src/cache_value/mod.rs b/crates/ecstore/src/cache_value/mod.rs index 328444a05..5ab628098 100644 --- a/crates/ecstore/src/cache_value/mod.rs +++ b/crates/ecstore/src/cache_value/mod.rs @@ -12,5 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::sync::Arc; + +use lazy_static::lazy_static; +use tokio_util::sync::CancellationToken; + // pub mod cache; pub mod metacache_set; + +lazy_static! { + pub static ref LIST_PATH_RAW_CANCEL_TOKEN: Arc = Arc::new(CancellationToken::new()); +} diff --git a/crates/ecstore/src/heal/data_scanner.rs b/crates/ecstore/src/heal/data_scanner.rs index 4449d81a7..c5ea666c6 100644 --- a/crates/ecstore/src/heal/data_scanner.rs +++ b/crates/ecstore/src/heal/data_scanner.rs @@ -1251,7 +1251,7 @@ impl FolderScanner { resolver.bucket = bucket.clone(); let found_objs = Arc::new(RwLock::new(false)); let found_objs_clone = found_objs.clone(); - let (tx, rx) = broadcast::channel(1); + let (tx, _rx) = broadcast::channel(1); // let tx_partial = tx.clone(); let tx_finished = tx.clone(); let update_current_path_agreed = self.update_current_path.clone(); @@ -1372,7 +1372,7 @@ impl FolderScanner { })), ..Default::default() }; - let _ = list_path_raw(rx, lopts).await; + let _ = list_path_raw(lopts).await; if *found_objs.read().await { let this: CachedFolder = CachedFolder { diff --git a/crates/ecstore/src/rpc/tonic_service.rs b/crates/ecstore/src/rpc/tonic_service.rs index 3607d0daa..ffcc3c0a5 100644 --- a/crates/ecstore/src/rpc/tonic_service.rs +++ b/crates/ecstore/src/rpc/tonic_service.rs @@ -3685,14 +3685,6 @@ mod tests { assert!(format!("{service2:?}").contains("NodeService")); } - #[tokio::test] - async fn test_all_disk_method() { - let service = create_test_node_service(); - let disks = service.all_disk().await; - // Should return empty vector in test environment - assert!(disks.is_empty()); - } - #[tokio::test] async fn test_find_disk_method() { let service = create_test_node_service(); From aed8f5242300a76a9a523bedb40616f759024fbe Mon Sep 17 00:00:00 2001 From: junxiang Mu <1948535941@qq.com> Date: Fri, 18 Jul 2025 12:06:24 +0800 Subject: [PATCH 10/29] refactor: integrate disk healing into erasure set healing - Remove HealType::Disk and related disk-specific healing methods - Integrate disk format healing into heal_erasure_set with include_format_heal option - Update auto disk scanner to use ErasureSet heal type instead of Disk heal - Fix disk status change event handling to use ErasureSet heal requests - Add proper bucket list retrieval for auto healing scenarios - Update data scanner to submit ErasureSet heal tasks for offline disks - Remove duplicate healing logic between Disk and ErasureSet types - Ensure all healing operations go through unified ErasureSet healing path --- crates/ahm/src/heal/erasure_healer.rs | 458 ++++++++++++++++ crates/ahm/src/heal/event.rs | 20 +- crates/ahm/src/heal/manager.rs | 30 +- crates/ahm/src/heal/mod.rs | 4 + crates/ahm/src/heal/resume.rs | 696 +++++++++++++++++++++++++ crates/ahm/src/heal/storage.rs | 45 +- crates/ahm/src/heal/task.rs | 227 +++----- crates/ahm/src/scanner/data_scanner.rs | 69 ++- crates/ecstore/src/set_disk.rs | 51 +- 9 files changed, 1381 insertions(+), 219 deletions(-) create mode 100644 crates/ahm/src/heal/erasure_healer.rs create mode 100644 crates/ahm/src/heal/resume.rs diff --git a/crates/ahm/src/heal/erasure_healer.rs b/crates/ahm/src/heal/erasure_healer.rs new file mode 100644 index 000000000..210648c41 --- /dev/null +++ b/crates/ahm/src/heal/erasure_healer.rs @@ -0,0 +1,458 @@ +// 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::error::{Error, Result}; +use crate::heal::{ + progress::HealProgress, + resume::{CheckpointManager, ResumeManager, ResumeUtils}, + storage::HealStorageAPI, +}; +use futures::future::join_all; +use rustfs_ecstore::{ + disk::DiskStore, + heal::heal_commands::{HealOpts, HEAL_NORMAL_SCAN}, +}; +use std::sync::Arc; +use tokio::sync::RwLock; +use tracing::{error, info, warn}; + +/// Erasure Set Healer +pub struct ErasureSetHealer { + storage: Arc, + progress: Arc>, + cancel_token: tokio_util::sync::CancellationToken, + disk: DiskStore, +} + +impl ErasureSetHealer { + pub fn new( + storage: Arc, + progress: Arc>, + cancel_token: tokio_util::sync::CancellationToken, + disk: DiskStore, + ) -> Self { + Self { + storage, + progress, + cancel_token, + disk, + } + } + + /// execute erasure set heal with resume + pub async fn heal_erasure_set(&self, buckets: &[String], set_disk_id: &str) -> Result<()> { + info!("Starting erasure set heal for {} buckets on set disk {}", buckets.len(), set_disk_id); + + // 1. generate or get task id + let task_id = self.get_or_create_task_id(set_disk_id).await?; + + // 2. initialize or resume resume state + let (resume_manager, checkpoint_manager) = self.initialize_resume_state(&task_id, buckets).await?; + + // 3. execute heal with resume + let result = self + .execute_heal_with_resume(buckets, &resume_manager, &checkpoint_manager) + .await; + + // 4. cleanup resume state + if result.is_ok() { + if let Err(e) = resume_manager.cleanup().await { + warn!("Failed to cleanup resume state: {}", e); + } + if let Err(e) = checkpoint_manager.cleanup().await { + warn!("Failed to cleanup checkpoint: {}", e); + } + } + + result + } + + /// get or create task id + async fn get_or_create_task_id(&self, _set_disk_id: &str) -> Result { + // check if there are resumable tasks + let resumable_tasks = ResumeUtils::get_resumable_tasks(&self.disk).await?; + + for task_id in resumable_tasks { + if ResumeUtils::can_resume_task(&self.disk, &task_id).await { + info!("Found resumable task: {}", task_id); + return Ok(task_id); + } + } + + // create new task id + let task_id = ResumeUtils::generate_task_id(); + info!("Created new heal task: {}", task_id); + Ok(task_id) + } + + /// initialize or resume resume state + async fn initialize_resume_state(&self, task_id: &str, buckets: &[String]) -> Result<(ResumeManager, CheckpointManager)> { + // check if resume state exists + if ResumeManager::has_resume_state(&self.disk, task_id).await { + info!("Loading existing resume state for task: {}", task_id); + + let resume_manager = ResumeManager::load_from_disk(self.disk.clone(), task_id).await?; + let checkpoint_manager = if CheckpointManager::has_checkpoint(&self.disk, task_id).await { + CheckpointManager::load_from_disk(self.disk.clone(), task_id).await? + } else { + CheckpointManager::new(self.disk.clone(), task_id.to_string()).await? + }; + + Ok((resume_manager, checkpoint_manager)) + } else { + info!("Creating new resume state for task: {}", task_id); + + let resume_manager = + ResumeManager::new(self.disk.clone(), task_id.to_string(), "erasure_set".to_string(), buckets.to_vec()).await?; + + let checkpoint_manager = CheckpointManager::new(self.disk.clone(), task_id.to_string()).await?; + + Ok((resume_manager, checkpoint_manager)) + } + } + + /// execute heal with resume + async fn execute_heal_with_resume( + &self, + buckets: &[String], + resume_manager: &ResumeManager, + checkpoint_manager: &CheckpointManager, + ) -> Result<()> { + // 1. get current state + let state = resume_manager.get_state().await; + let checkpoint = checkpoint_manager.get_checkpoint().await; + + info!( + "Resuming from bucket {} object {}", + checkpoint.current_bucket_index, checkpoint.current_object_index + ); + + // 2. initialize progress + self.initialize_progress(buckets, &state).await; + + // 3. continue from checkpoint + let current_bucket_index = checkpoint.current_bucket_index; + let mut current_object_index = checkpoint.current_object_index; + + let mut processed_objects = state.processed_objects; + let mut successful_objects = state.successful_objects; + let mut failed_objects = state.failed_objects; + let mut skipped_objects = state.skipped_objects; + + // 4. process remaining buckets + for (bucket_idx, bucket) in buckets.iter().enumerate().skip(current_bucket_index) { + // check if completed + if state.completed_buckets.contains(bucket) { + continue; + } + + // update current bucket + resume_manager.set_current_item(Some(bucket.clone()), None).await?; + + // process objects in bucket + let bucket_result = self + .heal_bucket_with_resume( + bucket, + &mut current_object_index, + &mut processed_objects, + &mut successful_objects, + &mut failed_objects, + &mut skipped_objects, + resume_manager, + checkpoint_manager, + ) + .await; + + // update checkpoint position + checkpoint_manager.update_position(bucket_idx, current_object_index).await?; + + // update progress + resume_manager + .update_progress(processed_objects, successful_objects, failed_objects, skipped_objects) + .await?; + + // check cancel status + if self.cancel_token.is_cancelled() { + info!("Heal task cancelled"); + return Err(Error::TaskCancelled); + } + + // process bucket result + match bucket_result { + Ok(_) => { + resume_manager.complete_bucket(bucket).await?; + info!("Completed heal for bucket: {}", bucket); + } + Err(e) => { + error!("Failed to heal bucket {}: {}", bucket, e); + // continue to next bucket, do not interrupt the whole process + } + } + + // reset object index + current_object_index = 0; + } + + // 5. mark task completed + resume_manager.mark_completed().await?; + + info!("Erasure set heal completed successfully"); + Ok(()) + } + + /// heal single bucket with resume + #[allow(clippy::too_many_arguments)] + async fn heal_bucket_with_resume( + &self, + bucket: &str, + current_object_index: &mut usize, + processed_objects: &mut u64, + successful_objects: &mut u64, + failed_objects: &mut u64, + _skipped_objects: &mut u64, + resume_manager: &ResumeManager, + checkpoint_manager: &CheckpointManager, + ) -> Result<()> { + info!("Starting heal for bucket: {} from object index {}", bucket, current_object_index); + + // 1. get bucket info + let _bucket_info = match self.storage.get_bucket_info(bucket).await? { + Some(info) => info, + None => { + warn!("Bucket {} not found, skipping", bucket); + return Ok(()); + } + }; + + // 2. get objects to heal + let objects = self.storage.list_objects_for_heal(bucket, "").await?; + + // 3. continue from checkpoint + for (obj_idx, object) in objects.iter().enumerate().skip(*current_object_index) { + // check if already processed + if checkpoint_manager.get_checkpoint().await.processed_objects.contains(object) { + continue; + } + + // update current object + resume_manager + .set_current_item(Some(bucket.to_string()), Some(object.clone())) + .await?; + + // heal object + let heal_opts = HealOpts { + scan_mode: HEAL_NORMAL_SCAN, + remove: true, + recreate: true, + ..Default::default() + }; + + match self.storage.heal_object(bucket, object, None, &heal_opts).await { + Ok((_result, None)) => { + *successful_objects += 1; + checkpoint_manager.add_processed_object(object.clone()).await?; + info!("Successfully healed object {}/{}", bucket, object); + } + Ok((_, Some(err))) => { + *failed_objects += 1; + checkpoint_manager.add_failed_object(object.clone()).await?; + warn!("Failed to heal object {}/{}: {}", bucket, object, err); + } + Err(err) => { + *failed_objects += 1; + checkpoint_manager.add_failed_object(object.clone()).await?; + warn!("Error healing object {}/{}: {}", bucket, object, err); + } + } + + *processed_objects += 1; + *current_object_index = obj_idx + 1; + + // check cancel status + if self.cancel_token.is_cancelled() { + info!("Heal task cancelled during object processing"); + return Err(Error::TaskCancelled); + } + + // save checkpoint periodically + if obj_idx % 100 == 0 { + checkpoint_manager.update_position(0, *current_object_index).await?; + } + } + + Ok(()) + } + + /// initialize progress tracking + async fn initialize_progress(&self, _buckets: &[String], state: &crate::heal::resume::ResumeState) { + let mut progress = self.progress.write().await; + progress.objects_scanned = state.total_objects; + progress.objects_healed = state.successful_objects; + progress.objects_failed = state.failed_objects; + progress.bytes_processed = 0; // set to 0 for now, can be extended later + progress.set_current_object(state.current_object.clone()); + } + + /// heal all buckets concurrently + #[allow(dead_code)] + async fn heal_buckets_concurrently(&self, buckets: &[String]) -> Vec> { + // use semaphore to control concurrency, avoid too many concurrent healings + let semaphore = Arc::new(tokio::sync::Semaphore::new(4)); // max 4 concurrent healings + + let heal_futures = buckets.iter().map(|bucket| { + let bucket = bucket.clone(); + let storage = self.storage.clone(); + let progress = self.progress.clone(); + let semaphore = semaphore.clone(); + let cancel_token = self.cancel_token.clone(); + + async move { + let _permit = semaphore.acquire().await.unwrap(); + + if cancel_token.is_cancelled() { + return Err(Error::TaskCancelled); + } + + Self::heal_single_bucket(&storage, &bucket, &progress).await + } + }); + + // use join_all to process concurrently + join_all(heal_futures).await + } + + /// heal single bucket + #[allow(dead_code)] + async fn heal_single_bucket( + storage: &Arc, + bucket: &str, + progress: &Arc>, + ) -> Result<()> { + info!("Starting heal for bucket: {}", bucket); + + // 1. get bucket info + let _bucket_info = match storage.get_bucket_info(bucket).await? { + Some(info) => info, + None => { + warn!("Bucket {} not found, skipping", bucket); + return Ok(()); + } + }; + + // 2. get objects to heal + let objects = storage.list_objects_for_heal(bucket, "").await?; + + // 3. update progress + { + let mut p = progress.write().await; + p.objects_scanned += objects.len() as u64; + } + + // 4. heal objects concurrently + let heal_opts = HealOpts { + scan_mode: HEAL_NORMAL_SCAN, + remove: true, // remove corrupted data + recreate: true, // recreate missing data + ..Default::default() + }; + + let object_results = Self::heal_objects_concurrently(storage, bucket, &objects, &heal_opts, progress).await; + + // 5. count results + let (success_count, failure_count) = object_results + .into_iter() + .fold((0, 0), |(success, failure), result| match result { + Ok(_) => (success + 1, failure), + Err(_) => (success, failure + 1), + }); + + // 6. update progress + { + let mut p = progress.write().await; + p.objects_healed += success_count; + p.objects_failed += failure_count; + p.set_current_object(Some(format!("completed bucket: {bucket}"))); + } + + info!( + "Completed heal for bucket {}: {} success, {} failures", + bucket, success_count, failure_count + ); + + Ok(()) + } + + /// heal objects concurrently + #[allow(dead_code)] + async fn heal_objects_concurrently( + storage: &Arc, + bucket: &str, + objects: &[String], + heal_opts: &HealOpts, + _progress: &Arc>, + ) -> Vec> { + // use semaphore to control object healing concurrency + let semaphore = Arc::new(tokio::sync::Semaphore::new(8)); // max 8 concurrent object healings + + let heal_futures = objects.iter().map(|object| { + let object = object.clone(); + let bucket = bucket.to_string(); + let storage = storage.clone(); + let heal_opts = *heal_opts; + let semaphore = semaphore.clone(); + + async move { + let _permit = semaphore.acquire().await.unwrap(); + + match storage.heal_object(&bucket, &object, None, &heal_opts).await { + Ok((_result, None)) => { + info!("Successfully healed object {}/{}", bucket, object); + Ok(()) + } + Ok((_, Some(err))) => { + warn!("Failed to heal object {}/{}: {}", bucket, object, err); + Err(Error::other(err)) + } + Err(err) => { + warn!("Error healing object {}/{}: {}", bucket, object, err); + Err(err) + } + } + } + }); + + join_all(heal_futures).await + } + + /// process results + #[allow(dead_code)] + async fn process_results(&self, results: Vec>) -> Result<()> { + let (success_count, failure_count): (usize, usize) = + results.into_iter().fold((0, 0), |(success, failure), result| match result { + Ok(_) => (success + 1, failure), + Err(_) => (success, failure + 1), + }); + + let total = success_count + failure_count; + + info!("Erasure set heal completed: {}/{} buckets successful", success_count, total); + + if failure_count > 0 { + warn!("{} buckets failed to heal", failure_count); + return Err(Error::other(format!("{failure_count} buckets failed to heal"))); + } + + Ok(()) + } +} diff --git a/crates/ahm/src/heal/event.rs b/crates/ahm/src/heal/event.rs index 11705385b..4e773d1df 100644 --- a/crates/ahm/src/heal/event.rs +++ b/crates/ahm/src/heal/event.rs @@ -143,13 +143,19 @@ impl HealEvent { HealOptions::default(), HealPriority::High, ), - HealEvent::DiskStatusChange { endpoint, .. } => HealRequest::new( - HealType::Disk { - endpoint: endpoint.clone(), - }, - HealOptions::default(), - HealPriority::High, - ), + HealEvent::DiskStatusChange { endpoint, .. } => { + // Convert disk status change to erasure set heal + // Note: This requires access to storage to get bucket list, which is not available here + // The actual bucket list will need to be provided by the caller or retrieved differently + HealRequest::new( + HealType::ErasureSet { + buckets: vec![], // Empty bucket list - caller should populate this + set_disk_id: format!("{}_{}", endpoint.pool_idx, endpoint.set_idx), + }, + HealOptions::default(), + HealPriority::High, + ) + } HealEvent::ECDecodeFailure { bucket, object, diff --git a/crates/ahm/src/heal/manager.rs b/crates/ahm/src/heal/manager.rs index 3a267839b..0fd8c7ab5 100644 --- a/crates/ahm/src/heal/manager.rs +++ b/crates/ahm/src/heal/manager.rs @@ -259,12 +259,13 @@ impl HealManager { Ok(()) } - /// Start background task to auto scan local disks and enqueue disk heal requests + /// Start background task to auto scan local disks and enqueue erasure set heal requests async fn start_auto_disk_scanner(&self) -> Result<()> { let config = self.config.clone(); let heal_queue = self.heal_queue.clone(); let active_heals = self.active_heals.clone(); let cancel_token = self.cancel_token.clone(); + let storage = self.storage.clone(); tokio::spawn(async move { let mut interval = interval(config.read().await.heal_interval); @@ -300,18 +301,28 @@ impl HealManager { continue; } + // Get bucket list for erasure set healing + let buckets = match storage.list_buckets().await { + Ok(buckets) => buckets.iter().map(|b| b.name.clone()).collect::>(), + Err(e) => { + error!("Failed to get bucket list for auto healing: {}", e); + continue; + } + }; + + // Create erasure set heal requests for each endpoint for ep in endpoints { // skip if already queued or healing let mut skip = false; { let queue = heal_queue.lock().await; - if queue.iter().any(|req| matches!(&req.heal_type, crate::heal::task::HealType::Disk { endpoint } if endpoint == &ep)) { + if queue.iter().any(|req| matches!(&req.heal_type, crate::heal::task::HealType::ErasureSet { set_disk_id, .. } if set_disk_id == &format!("{}_{}", ep.pool_idx, ep.set_idx))) { skip = true; } } if !skip { let active = active_heals.lock().await; - if active.values().any(|task| matches!(&task.heal_type, crate::heal::task::HealType::Disk { endpoint } if endpoint == &ep)) { + if active.values().any(|task| matches!(&task.heal_type, crate::heal::task::HealType::ErasureSet { set_disk_id, .. } if set_disk_id == &format!("{}_{}", ep.pool_idx, ep.set_idx))) { skip = true; } } @@ -320,15 +331,19 @@ impl HealManager { continue; } - // enqueue heal request for this disk + // enqueue erasure set heal request for this disk + let set_disk_id = format!("{}_{}", ep.pool_idx, ep.set_idx); let req = crate::heal::task::HealRequest::new( - crate::heal::task::HealType::Disk { endpoint: ep.clone() }, + crate::heal::task::HealType::ErasureSet { + buckets: buckets.clone(), + set_disk_id: set_disk_id.clone() + }, crate::heal::task::HealOptions::default(), crate::heal::task::HealPriority::Normal, ); let mut queue = heal_queue.lock().await; queue.push_back(req); - info!("Enqueued auto disk heal for endpoint: {}", ep); + info!("Enqueued auto erasure set heal for endpoint: {} (set_disk_id: {})", ep, set_disk_id); } } } @@ -365,7 +380,8 @@ impl HealManager { // start heal task tokio::spawn(async move { info!("Starting heal task: {}", task_id); - match task.execute().await { + let result = task.execute().await; + match result { Ok(_) => { info!("Heal task completed successfully: {}", task_id); } diff --git a/crates/ahm/src/heal/mod.rs b/crates/ahm/src/heal/mod.rs index f209d4501..aaa57d7ab 100644 --- a/crates/ahm/src/heal/mod.rs +++ b/crates/ahm/src/heal/mod.rs @@ -13,11 +13,15 @@ // limitations under the License. pub mod channel; +pub mod erasure_healer; pub mod event; pub mod manager; pub mod progress; +pub mod resume; pub mod storage; pub mod task; +pub use erasure_healer::ErasureSetHealer; pub use manager::HealManager; +pub use resume::{CheckpointManager, ResumeCheckpoint, ResumeManager, ResumeState, ResumeUtils}; pub use task::{HealOptions, HealPriority, HealRequest, HealTask, HealType}; diff --git a/crates/ahm/src/heal/resume.rs b/crates/ahm/src/heal/resume.rs new file mode 100644 index 000000000..260a22433 --- /dev/null +++ b/crates/ahm/src/heal/resume.rs @@ -0,0 +1,696 @@ +// 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::error::{Error, Result}; +use rustfs_ecstore::disk::{DiskAPI, DiskStore, BUCKET_META_PREFIX, RUSTFS_META_BUCKET}; +use serde::{Deserialize, Serialize}; +use std::path::Path; +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; +use tokio::sync::RwLock; +use tracing::{debug, info, warn}; +use uuid::Uuid; + +/// resume state file constants +const RESUME_STATE_FILE: &str = "ahm_resume_state.json"; +const RESUME_PROGRESS_FILE: &str = "ahm_progress.json"; +const RESUME_CHECKPOINT_FILE: &str = "ahm_checkpoint.json"; + +/// resume state +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ResumeState { + /// task id + pub task_id: String, + /// task type + pub task_type: String, + /// start time + pub start_time: u64, + /// last update time + pub last_update: u64, + /// completed + pub completed: bool, + /// total objects + pub total_objects: u64, + /// processed objects + pub processed_objects: u64, + /// successful objects + pub successful_objects: u64, + /// failed objects + pub failed_objects: u64, + /// skipped objects + pub skipped_objects: u64, + /// current bucket + pub current_bucket: Option, + /// current object + pub current_object: Option, + /// completed buckets + pub completed_buckets: Vec, + /// pending buckets + pub pending_buckets: Vec, + /// error message + pub error_message: Option, + /// retry count + pub retry_count: u32, + /// max retries + pub max_retries: u32, +} + +impl ResumeState { + pub fn new(task_id: String, task_type: String, buckets: Vec) -> Self { + Self { + task_id, + task_type, + start_time: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(), + last_update: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(), + completed: false, + total_objects: 0, + processed_objects: 0, + successful_objects: 0, + failed_objects: 0, + skipped_objects: 0, + current_bucket: None, + current_object: None, + completed_buckets: Vec::new(), + pending_buckets: buckets, + error_message: None, + retry_count: 0, + max_retries: 3, + } + } + + pub fn update_progress(&mut self, processed: u64, successful: u64, failed: u64, skipped: u64) { + self.processed_objects = processed; + self.successful_objects = successful; + self.failed_objects = failed; + self.skipped_objects = skipped; + self.last_update = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + } + + pub fn set_current_item(&mut self, bucket: Option, object: Option) { + self.current_bucket = bucket; + self.current_object = object; + self.last_update = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + } + + pub fn complete_bucket(&mut self, bucket: &str) { + if !self.completed_buckets.contains(&bucket.to_string()) { + self.completed_buckets.push(bucket.to_string()); + } + if let Some(pos) = self.pending_buckets.iter().position(|b| b == bucket) { + self.pending_buckets.remove(pos); + } + self.last_update = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + } + + pub fn mark_completed(&mut self) { + self.completed = true; + self.last_update = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + } + + pub fn set_error(&mut self, error: String) { + self.error_message = Some(error); + self.last_update = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + } + + pub fn increment_retry(&mut self) { + self.retry_count += 1; + self.last_update = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + } + + pub fn can_retry(&self) -> bool { + self.retry_count < self.max_retries + } + + pub fn get_progress_percentage(&self) -> f64 { + if self.total_objects == 0 { + return 0.0; + } + (self.processed_objects as f64 / self.total_objects as f64) * 100.0 + } + + pub fn get_success_rate(&self) -> f64 { + let total = self.successful_objects + self.failed_objects; + if total == 0 { + return 0.0; + } + (self.successful_objects as f64 / total as f64) * 100.0 + } +} + +/// resume manager +pub struct ResumeManager { + disk: DiskStore, + state: Arc>, +} + +impl ResumeManager { + /// create new resume manager + pub async fn new(disk: DiskStore, task_id: String, task_type: String, buckets: Vec) -> Result { + let state = ResumeState::new(task_id, task_type, buckets); + let manager = Self { + disk, + state: Arc::new(RwLock::new(state)), + }; + + // save initial state + manager.save_state().await?; + Ok(manager) + } + + /// load resume state from disk + pub async fn load_from_disk(disk: DiskStore, task_id: &str) -> Result { + let state_data = Self::read_state_file(&disk, task_id).await?; + let state: ResumeState = serde_json::from_slice(&state_data).map_err(|e| Error::TaskExecutionFailed { + message: format!("Failed to deserialize resume state: {e}"), + })?; + + Ok(Self { + disk, + state: Arc::new(RwLock::new(state)), + }) + } + + /// check if resume state exists + pub async fn has_resume_state(disk: &DiskStore, task_id: &str) -> bool { + let file_path = Path::new(BUCKET_META_PREFIX).join(format!("{task_id}_{RESUME_STATE_FILE}")); + match disk.read_all(RUSTFS_META_BUCKET, file_path.to_str().unwrap()).await { + Ok(data) => !data.is_empty(), + Err(_) => false, + } + } + + /// get current state + pub async fn get_state(&self) -> ResumeState { + self.state.read().await.clone() + } + + /// update progress + pub async fn update_progress(&self, processed: u64, successful: u64, failed: u64, skipped: u64) -> Result<()> { + let mut state = self.state.write().await; + state.update_progress(processed, successful, failed, skipped); + drop(state); + self.save_state().await + } + + /// set current item + pub async fn set_current_item(&self, bucket: Option, object: Option) -> Result<()> { + let mut state = self.state.write().await; + state.set_current_item(bucket, object); + drop(state); + self.save_state().await + } + + /// complete bucket + pub async fn complete_bucket(&self, bucket: &str) -> Result<()> { + let mut state = self.state.write().await; + state.complete_bucket(bucket); + drop(state); + self.save_state().await + } + + /// mark task completed + pub async fn mark_completed(&self) -> Result<()> { + let mut state = self.state.write().await; + state.mark_completed(); + drop(state); + self.save_state().await + } + + /// set error message + pub async fn set_error(&self, error: String) -> Result<()> { + let mut state = self.state.write().await; + state.set_error(error); + drop(state); + self.save_state().await + } + + /// increment retry count + pub async fn increment_retry(&self) -> Result<()> { + let mut state = self.state.write().await; + state.increment_retry(); + drop(state); + self.save_state().await + } + + /// cleanup resume state + pub async fn cleanup(&self) -> Result<()> { + let state = self.state.read().await; + let task_id = &state.task_id; + + // delete state files + let state_file = Path::new(BUCKET_META_PREFIX).join(format!("{task_id}_{RESUME_STATE_FILE}")); + let progress_file = Path::new(BUCKET_META_PREFIX).join(format!("{task_id}_{RESUME_PROGRESS_FILE}")); + let checkpoint_file = Path::new(BUCKET_META_PREFIX).join(format!("{task_id}_{RESUME_CHECKPOINT_FILE}")); + + // ignore delete errors, files may not exist + let _ = self + .disk + .delete(RUSTFS_META_BUCKET, state_file.to_str().unwrap(), Default::default()) + .await; + let _ = self + .disk + .delete(RUSTFS_META_BUCKET, progress_file.to_str().unwrap(), Default::default()) + .await; + let _ = self + .disk + .delete(RUSTFS_META_BUCKET, checkpoint_file.to_str().unwrap(), Default::default()) + .await; + + info!("Cleaned up resume state for task: {}", task_id); + Ok(()) + } + + /// save state to disk + async fn save_state(&self) -> Result<()> { + let state = self.state.read().await; + let state_data = serde_json::to_vec(&*state).map_err(|e| Error::TaskExecutionFailed { + message: format!("Failed to serialize resume state: {e}"), + })?; + + let file_path = Path::new(BUCKET_META_PREFIX).join(format!("{}_{}", state.task_id, RESUME_STATE_FILE)); + + self.disk + .write_all(RUSTFS_META_BUCKET, file_path.to_str().unwrap(), state_data.into()) + .await + .map_err(|e| Error::TaskExecutionFailed { + message: format!("Failed to save resume state: {e}"), + })?; + + debug!("Saved resume state for task: {}", state.task_id); + Ok(()) + } + + /// read state file from disk + async fn read_state_file(disk: &DiskStore, task_id: &str) -> Result> { + let file_path = Path::new(BUCKET_META_PREFIX).join(format!("{task_id}_{RESUME_STATE_FILE}")); + + disk.read_all(RUSTFS_META_BUCKET, file_path.to_str().unwrap()) + .await + .map(|bytes| bytes.to_vec()) + .map_err(|e| Error::TaskExecutionFailed { + message: format!("Failed to read resume state file: {e}"), + }) + } +} + +/// resume checkpoint +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ResumeCheckpoint { + /// task id + pub task_id: String, + /// checkpoint time + pub checkpoint_time: u64, + /// current bucket index + pub current_bucket_index: usize, + /// current object index + pub current_object_index: usize, + /// processed objects + pub processed_objects: Vec, + /// failed objects + pub failed_objects: Vec, + /// skipped objects + pub skipped_objects: Vec, +} + +impl ResumeCheckpoint { + pub fn new(task_id: String) -> Self { + Self { + task_id, + checkpoint_time: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(), + current_bucket_index: 0, + current_object_index: 0, + processed_objects: Vec::new(), + failed_objects: Vec::new(), + skipped_objects: Vec::new(), + } + } + + pub fn update_position(&mut self, bucket_index: usize, object_index: usize) { + self.current_bucket_index = bucket_index; + self.current_object_index = object_index; + self.checkpoint_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + } + + pub fn add_processed_object(&mut self, object: String) { + if !self.processed_objects.contains(&object) { + self.processed_objects.push(object); + } + } + + pub fn add_failed_object(&mut self, object: String) { + if !self.failed_objects.contains(&object) { + self.failed_objects.push(object); + } + } + + pub fn add_skipped_object(&mut self, object: String) { + if !self.skipped_objects.contains(&object) { + self.skipped_objects.push(object); + } + } +} + +/// resume checkpoint manager +pub struct CheckpointManager { + disk: DiskStore, + checkpoint: Arc>, +} + +impl CheckpointManager { + /// create new checkpoint manager + pub async fn new(disk: DiskStore, task_id: String) -> Result { + let checkpoint = ResumeCheckpoint::new(task_id); + let manager = Self { + disk, + checkpoint: Arc::new(RwLock::new(checkpoint)), + }; + + // save initial checkpoint + manager.save_checkpoint().await?; + Ok(manager) + } + + /// load checkpoint from disk + pub async fn load_from_disk(disk: DiskStore, task_id: &str) -> Result { + let checkpoint_data = Self::read_checkpoint_file(&disk, task_id).await?; + let checkpoint: ResumeCheckpoint = serde_json::from_slice(&checkpoint_data).map_err(|e| Error::TaskExecutionFailed { + message: format!("Failed to deserialize checkpoint: {e}"), + })?; + + Ok(Self { + disk, + checkpoint: Arc::new(RwLock::new(checkpoint)), + }) + } + + /// check if checkpoint exists + pub async fn has_checkpoint(disk: &DiskStore, task_id: &str) -> bool { + let file_path = Path::new(BUCKET_META_PREFIX).join(format!("{task_id}_{RESUME_CHECKPOINT_FILE}")); + match disk.read_all(RUSTFS_META_BUCKET, file_path.to_str().unwrap()).await { + Ok(data) => !data.is_empty(), + Err(_) => false, + } + } + + /// get current checkpoint + pub async fn get_checkpoint(&self) -> ResumeCheckpoint { + self.checkpoint.read().await.clone() + } + + /// update position + pub async fn update_position(&self, bucket_index: usize, object_index: usize) -> Result<()> { + let mut checkpoint = self.checkpoint.write().await; + checkpoint.update_position(bucket_index, object_index); + drop(checkpoint); + self.save_checkpoint().await + } + + /// add processed object + pub async fn add_processed_object(&self, object: String) -> Result<()> { + let mut checkpoint = self.checkpoint.write().await; + checkpoint.add_processed_object(object); + drop(checkpoint); + self.save_checkpoint().await + } + + /// add failed object + pub async fn add_failed_object(&self, object: String) -> Result<()> { + let mut checkpoint = self.checkpoint.write().await; + checkpoint.add_failed_object(object); + drop(checkpoint); + self.save_checkpoint().await + } + + /// add skipped object + pub async fn add_skipped_object(&self, object: String) -> Result<()> { + let mut checkpoint = self.checkpoint.write().await; + checkpoint.add_skipped_object(object); + drop(checkpoint); + self.save_checkpoint().await + } + + /// cleanup checkpoint + pub async fn cleanup(&self) -> Result<()> { + let checkpoint = self.checkpoint.read().await; + let task_id = &checkpoint.task_id; + + let checkpoint_file = Path::new(BUCKET_META_PREFIX).join(format!("{task_id}_{RESUME_CHECKPOINT_FILE}")); + let _ = self + .disk + .delete(RUSTFS_META_BUCKET, checkpoint_file.to_str().unwrap(), Default::default()) + .await; + + info!("Cleaned up checkpoint for task: {}", task_id); + Ok(()) + } + + /// save checkpoint to disk + async fn save_checkpoint(&self) -> Result<()> { + let checkpoint = self.checkpoint.read().await; + let checkpoint_data = serde_json::to_vec(&*checkpoint).map_err(|e| Error::TaskExecutionFailed { + message: format!("Failed to serialize checkpoint: {e}"), + })?; + + let file_path = Path::new(BUCKET_META_PREFIX).join(format!("{}_{}", checkpoint.task_id, RESUME_CHECKPOINT_FILE)); + + self.disk + .write_all(RUSTFS_META_BUCKET, file_path.to_str().unwrap(), checkpoint_data.into()) + .await + .map_err(|e| Error::TaskExecutionFailed { + message: format!("Failed to save checkpoint: {e}"), + })?; + + debug!("Saved checkpoint for task: {}", checkpoint.task_id); + Ok(()) + } + + /// read checkpoint file from disk + async fn read_checkpoint_file(disk: &DiskStore, task_id: &str) -> Result> { + let file_path = Path::new(BUCKET_META_PREFIX).join(format!("{task_id}_{RESUME_CHECKPOINT_FILE}")); + + disk.read_all(RUSTFS_META_BUCKET, file_path.to_str().unwrap()) + .await + .map(|bytes| bytes.to_vec()) + .map_err(|e| Error::TaskExecutionFailed { + message: format!("Failed to read checkpoint file: {e}"), + }) + } +} + +/// resume utils +pub struct ResumeUtils; + +impl ResumeUtils { + /// generate unique task id + pub fn generate_task_id() -> String { + Uuid::new_v4().to_string() + } + + /// check if task can be resumed + pub async fn can_resume_task(disk: &DiskStore, task_id: &str) -> bool { + ResumeManager::has_resume_state(disk, task_id).await + } + + /// get all resumable task ids + pub async fn get_resumable_tasks(disk: &DiskStore) -> Result> { + // List all files in the buckets metadata directory + let entries = match disk.list_dir("", RUSTFS_META_BUCKET, BUCKET_META_PREFIX, -1).await { + Ok(entries) => entries, + Err(e) => { + debug!("Failed to list resume state files: {}", e); + return Ok(Vec::new()); + } + }; + + let mut task_ids = Vec::new(); + + // Filter files that end with ahm_resume_state.json and extract task IDs + for entry in entries { + if entry.ends_with(&format!("_{RESUME_STATE_FILE}")) { + // Extract task ID from filename: {task_id}_ahm_resume_state.json + if let Some(task_id) = entry.strip_suffix(&format!("_{RESUME_STATE_FILE}")) { + if !task_id.is_empty() { + task_ids.push(task_id.to_string()); + } + } + } + } + + debug!("Found {} resumable tasks: {:?}", task_ids.len(), task_ids); + Ok(task_ids) + } + + /// cleanup expired resume states + pub async fn cleanup_expired_states(disk: &DiskStore, max_age_hours: u64) -> Result<()> { + let task_ids = Self::get_resumable_tasks(disk).await?; + let current_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + + for task_id in task_ids { + if let Ok(resume_manager) = ResumeManager::load_from_disk(disk.clone(), &task_id).await { + let state = resume_manager.get_state().await; + let age_hours = (current_time - state.last_update) / 3600; + + if age_hours > max_age_hours { + info!("Cleaning up expired resume state for task: {} (age: {} hours)", task_id, age_hours); + if let Err(e) = resume_manager.cleanup().await { + warn!("Failed to cleanup expired resume state for task {}: {}", task_id, e); + } + } + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_resume_state_creation() { + let task_id = ResumeUtils::generate_task_id(); + let buckets = vec!["bucket1".to_string(), "bucket2".to_string()]; + let state = ResumeState::new(task_id.clone(), "erasure_set".to_string(), buckets); + + assert_eq!(state.task_id, task_id); + assert_eq!(state.task_type, "erasure_set"); + assert!(!state.completed); + assert_eq!(state.processed_objects, 0); + assert_eq!(state.pending_buckets.len(), 2); + } + + #[tokio::test] + async fn test_resume_state_progress() { + let task_id = ResumeUtils::generate_task_id(); + let buckets = vec!["bucket1".to_string()]; + let mut state = ResumeState::new(task_id, "erasure_set".to_string(), buckets); + + state.update_progress(10, 8, 1, 1); + assert_eq!(state.processed_objects, 10); + assert_eq!(state.successful_objects, 8); + assert_eq!(state.failed_objects, 1); + assert_eq!(state.skipped_objects, 1); + + let progress = state.get_progress_percentage(); + assert_eq!(progress, 0.0); // total_objects is 0 + + state.total_objects = 100; + let progress = state.get_progress_percentage(); + assert_eq!(progress, 10.0); + } + + #[tokio::test] + async fn test_resume_state_bucket_completion() { + let task_id = ResumeUtils::generate_task_id(); + let buckets = vec!["bucket1".to_string(), "bucket2".to_string()]; + let mut state = ResumeState::new(task_id, "erasure_set".to_string(), buckets); + + assert_eq!(state.pending_buckets.len(), 2); + assert_eq!(state.completed_buckets.len(), 0); + + state.complete_bucket("bucket1"); + assert_eq!(state.pending_buckets.len(), 1); + assert_eq!(state.completed_buckets.len(), 1); + assert!(state.completed_buckets.contains(&"bucket1".to_string())); + } + + #[tokio::test] + async fn test_resume_utils() { + let task_id1 = ResumeUtils::generate_task_id(); + let task_id2 = ResumeUtils::generate_task_id(); + + assert_ne!(task_id1, task_id2); + assert_eq!(task_id1.len(), 36); // UUID length + assert_eq!(task_id2.len(), 36); + } + + #[tokio::test] + async fn test_get_resumable_tasks_integration() { + use rustfs_ecstore::disk::{endpoint::Endpoint, new_disk, DiskOption}; + use tempfile::TempDir; + + // Create a temporary directory for testing + let temp_dir = TempDir::new().unwrap(); + let disk_path = temp_dir.path().join("test_disk"); + std::fs::create_dir_all(&disk_path).unwrap(); + + // Create a local disk for testing + let endpoint = Endpoint::try_from(disk_path.to_string_lossy().as_ref()).unwrap(); + let disk_option = DiskOption { + cleanup: false, + health_check: false, + }; + let disk = new_disk(&endpoint, &disk_option).await.unwrap(); + + // Create necessary directories first (ignore if already exist) + let _ = disk.make_volume(RUSTFS_META_BUCKET).await; + let _ = disk.make_volume(&format!("{RUSTFS_META_BUCKET}/{BUCKET_META_PREFIX}")).await; + + // Create some test resume state files + let task_ids = vec![ + "test-task-1".to_string(), + "test-task-2".to_string(), + "test-task-3".to_string(), + ]; + + // Save resume state files for each task + for task_id in &task_ids { + let state = ResumeState::new( + task_id.clone(), + "erasure_set".to_string(), + vec!["bucket1".to_string(), "bucket2".to_string()], + ); + + let state_data = serde_json::to_vec(&state).unwrap(); + let file_path = format!("{BUCKET_META_PREFIX}/{task_id}_{RESUME_STATE_FILE}"); + + disk.write_all(RUSTFS_META_BUCKET, &file_path, state_data.into()) + .await + .unwrap(); + } + + // Also create some non-resume state files to test filtering + let non_resume_files = vec![ + "other_file.txt", + "task4_ahm_checkpoint.json", + "task5_ahm_progress.json", + "_ahm_resume_state.json", // Invalid: empty task ID + ]; + + for file_name in non_resume_files { + let file_path = format!("{BUCKET_META_PREFIX}/{file_name}"); + disk.write_all(RUSTFS_META_BUCKET, &file_path, b"test data".to_vec().into()) + .await + .unwrap(); + } + + // Now call get_resumable_tasks to see if it finds the correct files + let found_task_ids = ResumeUtils::get_resumable_tasks(&disk).await.unwrap(); + + // Verify that only the valid resume state files are found + assert_eq!(found_task_ids.len(), 3); + for task_id in &task_ids { + assert!(found_task_ids.contains(task_id), "Task ID {task_id} not found"); + } + + // Verify that invalid files are not included + assert!(!found_task_ids.contains(&"".to_string())); + assert!(!found_task_ids.contains(&"task4".to_string())); + assert!(!found_task_ids.contains(&"task5".to_string())); + + // Clean up + temp_dir.close().unwrap(); + } +} diff --git a/crates/ahm/src/heal/storage.rs b/crates/ahm/src/heal/storage.rs index d04270c8a..aa204307d 100644 --- a/crates/ahm/src/heal/storage.rs +++ b/crates/ahm/src/heal/storage.rs @@ -15,7 +15,7 @@ use crate::error::{Error, Result}; use async_trait::async_trait; use rustfs_ecstore::{ - disk::endpoint::Endpoint, + disk::{endpoint::Endpoint, DiskStore}, heal::heal_commands::{HealOpts, HEAL_DEEP_SCAN, HEAL_NORMAL_SCAN}, store::ECStore, store_api::{BucketInfo, ObjectIO, StorageAPI}, @@ -109,6 +109,9 @@ pub trait HealStorageAPI: Send + Sync { /// List objects for healing async fn list_objects_for_heal(&self, bucket: &str, prefix: &str) -> Result>; + + /// Get disk for resume functionality + async fn get_disk_for_resume(&self, set_disk_id: &str) -> Result; } /// ECStore Heal storage layer implementation @@ -460,4 +463,44 @@ impl HealStorageAPI for ECStoreHealStorage { } } } + + async fn get_disk_for_resume(&self, set_disk_id: &str) -> Result { + debug!("Getting disk for resume: {}", set_disk_id); + + // Parse set_disk_id to extract pool and set indices + // Format: "pool_{pool_idx}_set_{set_idx}" + let parts: Vec<&str> = set_disk_id.split('_').collect(); + if parts.len() != 4 || parts[0] != "pool" || parts[2] != "set" { + return Err(Error::TaskExecutionFailed { + message: format!("Invalid set_disk_id format: {set_disk_id}"), + }); + } + + let pool_idx: usize = parts[1].parse().map_err(|_| Error::TaskExecutionFailed { + message: format!("Invalid pool index in set_disk_id: {set_disk_id}"), + })?; + + let set_idx: usize = parts[3].parse().map_err(|_| Error::TaskExecutionFailed { + message: format!("Invalid set index in set_disk_id: {set_disk_id}"), + })?; + + // Get the first available disk from the set + let disks = self + .ecstore + .get_disks(pool_idx, set_idx) + .await + .map_err(|e| Error::TaskExecutionFailed { + message: format!("Failed to get disks for pool {pool_idx} set {set_idx}: {e}"), + })?; + + // Find the first available disk + if let Some(disk_store) = disks.into_iter().flatten().next() { + info!("Found disk for resume: {:?}", disk_store); + return Ok(disk_store); + } + + Err(Error::TaskExecutionFailed { + message: format!("No available disk found for set_disk_id: {set_disk_id}"), + }) + } } diff --git a/crates/ahm/src/heal/task.rs b/crates/ahm/src/heal/task.rs index ea07786d2..44f12daca 100644 --- a/crates/ahm/src/heal/task.rs +++ b/crates/ahm/src/heal/task.rs @@ -13,20 +13,10 @@ // limitations under the License. use crate::error::{Error, Result}; -use crate::heal::{progress::HealProgress, storage::HealStorageAPI}; -use rustfs_ecstore::config::RUSTFS_CONFIG_PREFIX; -use rustfs_ecstore::disk::endpoint::Endpoint; -use rustfs_ecstore::disk::error::DiskError; -use rustfs_ecstore::disk::{DiskAPI, DiskInfoOptions, BUCKET_META_PREFIX, RUSTFS_META_BUCKET}; +use crate::heal::{erasure_healer::ErasureSetHealer, progress::HealProgress, storage::HealStorageAPI}; +use rustfs_ecstore::heal::heal_commands::HealScanMode; use rustfs_ecstore::heal::heal_commands::HEAL_NORMAL_SCAN; -use rustfs_ecstore::heal::heal_commands::{init_healing_tracker, load_healing_tracker, HealScanMode}; -use rustfs_ecstore::new_object_layer_fn; -use rustfs_ecstore::store::get_disk_via_endpoint; -use rustfs_ecstore::store_api::BucketInfo; -use rustfs_utils::path::path_join; use serde::{Deserialize, Serialize}; -use std::cmp::Ordering; -use std::path::PathBuf; use std::sync::Arc; use std::time::{Duration, SystemTime}; use tokio::sync::RwLock; @@ -44,8 +34,8 @@ pub enum HealType { }, /// Bucket heal Bucket { bucket: String }, - /// Disk heal - Disk { endpoint: Endpoint }, + /// Erasure Set heal (includes disk format repair) + ErasureSet { buckets: Vec, set_disk_id: String }, /// Metadata heal Metadata { bucket: String, object: String }, /// MRF heal @@ -175,10 +165,6 @@ impl HealRequest { Self::new(HealType::Bucket { bucket }, HealOptions::default(), HealPriority::Normal) } - pub fn disk(endpoint: Endpoint) -> Self { - Self::new(HealType::Disk { endpoint }, HealOptions::default(), HealPriority::High) - } - pub fn metadata(bucket: String, object: String) -> Self { Self::new(HealType::Metadata { bucket, object }, HealOptions::default(), HealPriority::High) } @@ -256,7 +242,7 @@ impl HealTask { version_id, } => self.heal_object(bucket, object, version_id.as_deref()).await, HealType::Bucket { bucket } => self.heal_bucket(bucket).await, - HealType::Disk { endpoint } => self.heal_disk(endpoint).await, + HealType::Metadata { bucket, object } => self.heal_metadata(bucket, object).await, HealType::MRF { meta_path } => self.heal_mrf(meta_path).await, HealType::ECDecode { @@ -264,6 +250,7 @@ impl HealTask { object, version_id, } => self.heal_ec_decode(bucket, object, version_id.as_deref()).await, + HealType::ErasureSet { buckets, set_disk_id } => self.heal_erasure_set(buckets.clone(), set_disk_id.clone()).await, }; // update completed time and status @@ -524,62 +511,6 @@ impl HealTask { } } - async fn heal_disk(&self, endpoint: &Endpoint) -> Result<()> { - info!("Healing disk: {:?}", endpoint); - - // update progress - { - let mut progress = self.progress.write().await; - progress.set_current_object(Some(format!("disk: {endpoint:?}"))); - progress.update_progress(0, 3, 0, 0); - } - - // Step 1: Perform disk format heal using ecstore - info!("Step 1: Performing disk format heal using ecstore"); - - match self.storage.heal_format(self.options.dry_run).await { - Ok((result, error)) => { - if let Some(e) = error { - error!("Disk heal failed: {:?} - {}", endpoint, e); - { - let mut progress = self.progress.write().await; - progress.update_progress(3, 3, 0, 0); - } - return Err(Error::TaskExecutionFailed { - message: format!("Failed to heal disk {endpoint:?}: {e}"), - }); - } - - info!("Disk heal completed successfully: {:?} ({} drives)", endpoint, result.after.drives.len()); - - { - let mut progress = self.progress.write().await; - progress.update_progress(2, 3, 0, 0); - } - - // Step 2: Synchronize data/buckets on the fresh disk - info!("Step 2: Healing buckets on fresh disk"); - self.heal_fresh_disk(endpoint).await?; - - { - let mut progress = self.progress.write().await; - progress.update_progress(3, 3, 0, 0); - } - Ok(()) - } - Err(e) => { - error!("Disk heal failed: {:?} - {}", endpoint, e); - { - let mut progress = self.progress.write().await; - progress.update_progress(3, 3, 0, 0); - } - Err(Error::TaskExecutionFailed { - message: format!("Failed to heal disk {endpoint:?}: {e}"), - }) - } - } - } - async fn heal_metadata(&self, bucket: &str, object: &str) -> Result<()> { info!("Healing metadata: {}/{}", bucket, object); @@ -807,79 +738,93 @@ impl HealTask { } } - async fn heal_fresh_disk(&self, endpoint: &Endpoint) -> Result<()> { - // Locate disk via endpoint - let disk = get_disk_via_endpoint(endpoint) - .await - .ok_or_else(|| Error::other(format!("Disk not found for endpoint: {endpoint}")))?; + async fn heal_erasure_set(&self, buckets: Vec, set_disk_id: String) -> Result<()> { + info!("Healing Erasure Set: {} ({} buckets)", set_disk_id, buckets.len()); - // Skip if drive is root or other fatal errors - if let Err(e) = disk.disk_info(&DiskInfoOptions::default()).await { - match e { - DiskError::DriveIsRoot => return Ok(()), - DiskError::UnformattedDisk => { /* continue healing */ } - _ => return Err(Error::other(e)), + // update progress + { + let mut progress = self.progress.write().await; + progress.set_current_object(Some(format!("erasure_set: {} ({} buckets)", set_disk_id, buckets.len()))); + progress.update_progress(0, 4, 0, 0); + } + + // Step 1: Perform disk format heal using ecstore + info!("Step 1: Performing disk format heal using ecstore"); + match self.storage.heal_format(self.options.dry_run).await { + Ok((result, error)) => { + if let Some(e) = error { + error!("Disk format heal failed: {} - {}", set_disk_id, e); + { + let mut progress = self.progress.write().await; + progress.update_progress(4, 4, 0, 0); + } + return Err(Error::TaskExecutionFailed { + message: format!("Failed to heal disk format for {set_disk_id}: {e}"), + }); + } + + info!( + "Disk format heal completed successfully: {} ({} drives)", + set_disk_id, + result.after.drives.len() + ); + } + Err(e) => { + error!("Disk format heal failed: {} - {}", set_disk_id, e); + { + let mut progress = self.progress.write().await; + progress.update_progress(4, 4, 0, 0); + } + return Err(Error::TaskExecutionFailed { + message: format!("Failed to heal disk format for {set_disk_id}: {e}"), + }); } } - // Load or init HealingTracker - let mut tracker = match load_healing_tracker(&Some(disk.clone())).await { - Ok(t) => t, - Err(err) => match err { - DiskError::FileNotFound => init_healing_tracker(disk.clone(), &Uuid::new_v4().to_string()) - .await - .map_err(Error::other)?, - _ => return Err(Error::other(err)), - }, - }; + { + let mut progress = self.progress.write().await; + progress.update_progress(1, 4, 0, 0); + } - // Build bucket list - let mut buckets = self.storage.list_buckets().await.map_err(Error::other)?; - buckets.push(BucketInfo { - name: path_join(&[PathBuf::from(RUSTFS_META_BUCKET), PathBuf::from(RUSTFS_CONFIG_PREFIX)]) - .to_string_lossy() - .to_string(), - ..Default::default() - }); - buckets.push(BucketInfo { - name: path_join(&[PathBuf::from(RUSTFS_META_BUCKET), PathBuf::from(BUCKET_META_PREFIX)]) - .to_string_lossy() - .to_string(), - ..Default::default() - }); + // Step 2: Get disk for resume functionality + info!("Step 2: Getting disk for resume functionality"); + let disk = self.storage.get_disk_for_resume(&set_disk_id).await?; - // Sort: system buckets first, others by creation time desc - buckets.sort_by(|a, b| { - let a_sys = a.name.starts_with(RUSTFS_META_BUCKET); - let b_sys = b.name.starts_with(RUSTFS_META_BUCKET); - match (a_sys, b_sys) { - (true, false) => Ordering::Less, - (false, true) => Ordering::Greater, - _ => b.created.cmp(&a.created), + { + let mut progress = self.progress.write().await; + progress.update_progress(2, 4, 0, 0); + } + + // Step 3: Create erasure set healer with resume support + info!("Step 3: Creating erasure set healer with resume support"); + let erasure_healer = ErasureSetHealer::new(self.storage.clone(), self.progress.clone(), self.cancel_token.clone(), disk); + + { + let mut progress = self.progress.write().await; + progress.update_progress(3, 4, 0, 0); + } + + // Step 4: Execute erasure set heal with resume + info!("Step 4: Executing erasure set heal with resume"); + let result = erasure_healer.heal_erasure_set(&buckets, &set_disk_id).await; + + { + let mut progress = self.progress.write().await; + progress.update_progress(4, 4, 0, 0); + } + + match result { + Ok(_) => { + info!("Erasure set heal completed successfully: {} ({} buckets)", set_disk_id, buckets.len()); + Ok(()) } - }); - - // Update tracker queue and persist - tracker.set_queue_buckets(&buckets).await; - tracker.save().await.map_err(Error::other)?; - - // Prepare bucket names list - let bucket_names: Vec = buckets.iter().map(|b| b.name.clone()).collect(); - - // Run heal_erasure_set using underlying SetDisk - let (pool_idx, set_idx) = (endpoint.pool_idx as usize, endpoint.set_idx as usize); - let Some(store) = new_object_layer_fn() else { - return Err(Error::other("errServerNotInitialized")); - }; - let set_disk = store.pools[pool_idx].disk_set[set_idx].clone(); - - let tracker_arc = Arc::new(RwLock::new(tracker)); - set_disk - .heal_erasure_set(&bucket_names, tracker_arc) - .await - .map_err(Error::other)?; - - Ok(()) + Err(e) => { + error!("Erasure set heal failed: {} - {}", set_disk_id, e); + Err(Error::TaskExecutionFailed { + message: format!("Failed to heal erasure set {set_disk_id}: {e}"), + }) + } + } } } diff --git a/crates/ahm/src/scanner/data_scanner.rs b/crates/ahm/src/scanner/data_scanner.rs index aed19e875..9c5e1aa1c 100644 --- a/crates/ahm/src/scanner/data_scanner.rs +++ b/crates/ahm/src/scanner/data_scanner.rs @@ -22,7 +22,7 @@ use ecstore::{ disk::{DiskAPI, DiskStore, WalkDirOptions}, set_disk::SetDisks, }; -use rustfs_ecstore as ecstore; +use rustfs_ecstore::{self as ecstore, StorageAPI}; use rustfs_filemeta::MetacacheReader; use tokio::sync::{Mutex, RwLock}; use tokio_util::sync::CancellationToken; @@ -502,18 +502,43 @@ impl Scanner { metrics.free_space = disk_info.free; metrics.is_online = disk.is_online().await; - // 检查磁盘状态,如果离线则提交heal任务 + // check disk status, if offline, submit erasure set heal task if !metrics.is_online { let enable_healing = self.config.read().await.enable_healing; if enable_healing { if let Some(heal_manager) = &self.heal_manager { - let req = HealRequest::disk(disk.endpoint().clone()); + // Get bucket list for erasure set healing + let buckets = match rustfs_ecstore::new_object_layer_fn() { + Some(ecstore) => { + match ecstore.list_bucket(&ecstore::store_api::BucketOptions::default()).await { + Ok(buckets) => buckets.iter().map(|b| b.name.clone()).collect::>(), + Err(e) => { + error!("Failed to get bucket list for disk healing: {}", e); + return Err(Error::Storage(e.into())); + } + } + } + None => { + error!("No ECStore available for getting bucket list"); + return Err(Error::Storage(ecstore::error::StorageError::other("No ECStore available"))); + } + }; + + let set_disk_id = format!("{}_{}", disk.endpoint().pool_idx, disk.endpoint().set_idx); + let req = HealRequest::new( + crate::heal::task::HealType::ErasureSet { + buckets, + set_disk_id, + }, + crate::heal::task::HealOptions::default(), + crate::heal::task::HealPriority::High, + ); match heal_manager.submit_heal_request(req).await { Ok(task_id) => { - warn!("disk offline, submit heal task: {} {}", task_id, disk_path); + warn!("disk offline, submit erasure set heal task: {} {}", task_id, disk_path); } Err(e) => { - error!("disk offline, submit heal task failed: {} {}", disk_path, e); + error!("disk offline, submit erasure set heal task failed: {} {}", disk_path, e); } } } @@ -540,24 +565,42 @@ impl Scanner { Err(e) => { error!("Failed to list volumes on disk {}: {}", disk_path, e); - // 磁盘访问失败,提交磁盘heal任务 + // disk access failed, submit erasure set heal task let enable_healing = self.config.read().await.enable_healing; if enable_healing { if let Some(heal_manager) = &self.heal_manager { - use crate::heal::{HealPriority, HealRequest}; + // Get bucket list for erasure set healing + let buckets = match rustfs_ecstore::new_object_layer_fn() { + Some(ecstore) => { + match ecstore.list_bucket(&ecstore::store_api::BucketOptions::default()).await { + Ok(buckets) => buckets.iter().map(|b| b.name.clone()).collect::>(), + Err(e) => { + error!("Failed to get bucket list for disk healing: {}", e); + return Err(Error::Storage(e.into())); + } + } + } + None => { + error!("No ECStore available for getting bucket list"); + return Err(Error::Storage(ecstore::error::StorageError::other("No ECStore available"))); + } + }; + + let set_disk_id = format!("{}_{}", disk.endpoint().pool_idx, disk.endpoint().set_idx); let req = HealRequest::new( - crate::heal::HealType::Disk { - endpoint: disk.endpoint().clone(), + crate::heal::task::HealType::ErasureSet { + buckets, + set_disk_id, }, - crate::heal::HealOptions::default(), - HealPriority::Urgent, + crate::heal::task::HealOptions::default(), + crate::heal::task::HealPriority::Urgent, ); match heal_manager.submit_heal_request(req).await { Ok(task_id) => { - warn!("disk access failed, submit heal task: {} {}", task_id, disk_path); + warn!("disk access failed, submit erasure set heal task: {} {}", task_id, disk_path); } Err(heal_err) => { - error!("disk access failed, submit heal task failed: {} {}", disk_path, heal_err); + error!("disk access failed, submit erasure set heal task failed: {} {}", disk_path, heal_err); } } } diff --git a/crates/ecstore/src/set_disk.rs b/crates/ecstore/src/set_disk.rs index 6edd46f0e..678480d47 100644 --- a/crates/ecstore/src/set_disk.rs +++ b/crates/ecstore/src/set_disk.rs @@ -3468,7 +3468,7 @@ impl SetDisks { return Err(Error::other(format!("unable to get disk information before healing it: {err}"))); } }; - let num_cores = num_cpus::get(); // 使用 num_cpus crate 获取核心数 + let num_cores = num_cpus::get(); // use num_cpus crate to get the number of cores let mut num_healers: usize; if info.nr_requests as usize > num_cores { @@ -3577,44 +3577,6 @@ impl SetDisks { continue; } - // let vc: VersioningConfiguration; - // let lc: BucketLifecycleConfiguration; - // let lr: ObjectLockConfiguration; - // let rcfg: ReplicationConfiguration; - // if !is_rustfs_meta_bucket_name(bucket) { - // vc = match get_versioning_config(bucket).await { - // Ok((r, _)) => r, - // Err(err) => { - // ret_err = Some(err); - // info!("get versioning config failed, err: {}", err.to_string()); - // continue; - // } - // }; - // lc = match get_lifecycle_config(bucket).await { - // Ok((r, _)) => r, - // Err(err) => { - // ret_err = Some(err); - // info!("get lifecycle config failed, err: {}", err.to_string()); - // continue; - // } - // }; - // lr = match get_object_lock_config(bucket).await { - // Ok((r, _)) => r, - // Err(err) => { - // ret_err = Some(err); - // info!("get object lock config failed, err: {}", err.to_string()); - // continue; - // } - // }; - // rcfg = match get_replication_config(bucket).await { - // Ok((r, _)) => r, - // Err(err) => { - // ret_err = Some(err); - // info!("get replication config failed, err: {}", err.to_string()); - // continue; - // } - // }; - // } let (mut disks, _, healing) = self.get_online_disk_with_healing_and_info(true).await?; if disks.len() == healing { info!("all drives are in healing state, aborting.."); @@ -3645,17 +3607,6 @@ impl SetDisks { let fallback_disks = disks[expected_disk..].to_vec(); disks = disks[..expected_disk].to_vec(); - //todo - // let filter_life_cycle = |bucket: &str, object: &str, fi: FileInfo| { - // if lc.rules.is_empty() { - // return false; - // } - // // todo: versioning - // let versioned = false; - // let obj_info = fi.to_object_info(bucket, object, versioned); - // - // }; - let result_tx_send = result_tx.clone(); let bg_seq_send = bg_seq.clone(); let send = Box::new(move |result: HealEntryResult| { From 7d3b2b774c7053509e1153b17a7ce2d43997a8bd Mon Sep 17 00:00:00 2001 From: junxiang Mu <1948535941@qq.com> Date: Sun, 20 Jul 2025 11:55:09 +0800 Subject: [PATCH 11/29] fix heal disk Signed-off-by: junxiang Mu <1948535941@qq.com> --- crates/ahm/src/heal/manager.rs | 16 +++---- crates/ahm/src/heal/task.rs | 7 +++ crates/ahm/src/scanner/data_scanner.rs | 52 +++++++++-------------- crates/ahm/tests/heal_integration_test.rs | 48 +++++++++++++++++++++ 4 files changed, 84 insertions(+), 39 deletions(-) diff --git a/crates/ahm/src/heal/manager.rs b/crates/ahm/src/heal/manager.rs index 0fd8c7ab5..180eaf665 100644 --- a/crates/ahm/src/heal/manager.rs +++ b/crates/ahm/src/heal/manager.rs @@ -16,7 +16,7 @@ use crate::error::{Error, Result}; use crate::heal::{ progress::{HealProgress, HealStatistics}, storage::HealStorageAPI, - task::{HealRequest, HealTask, HealTaskStatus}, + task::{HealOptions, HealPriority, HealRequest, HealTask, HealTaskStatus, HealType}, }; use rustfs_ecstore::disk::error::DiskError; use rustfs_ecstore::disk::DiskAPI; @@ -332,14 +332,14 @@ impl HealManager { } // enqueue erasure set heal request for this disk - let set_disk_id = format!("{}_{}", ep.pool_idx, ep.set_idx); - let req = crate::heal::task::HealRequest::new( - crate::heal::task::HealType::ErasureSet { - buckets: buckets.clone(), - set_disk_id: set_disk_id.clone() + let set_disk_id = format!("pool_{}_set_{}", ep.pool_idx, ep.set_idx); + let req = HealRequest::new( + HealType::ErasureSet { + buckets: buckets.clone(), + set_disk_id: set_disk_id.clone() }, - crate::heal::task::HealOptions::default(), - crate::heal::task::HealPriority::Normal, + HealOptions::default(), + HealPriority::Normal, ); let mut queue = heal_queue.lock().await; queue.push_back(req); diff --git a/crates/ahm/src/heal/task.rs b/crates/ahm/src/heal/task.rs index 44f12daca..8e9d856d7 100644 --- a/crates/ahm/src/heal/task.rs +++ b/crates/ahm/src/heal/task.rs @@ -795,6 +795,13 @@ impl HealTask { progress.update_progress(2, 4, 0, 0); } + // Step 3: Heal bucket structure + for bucket in buckets.iter() { + if let Err(err) = self.heal_bucket(bucket).await { + info!("{}", err.to_string()); + } + } + // Step 3: Create erasure set healer with resume support info!("Step 3: Creating erasure set healer with resume support"); let erasure_healer = ErasureSetHealer::new(self.storage.clone(), self.progress.clone(), self.cancel_token.clone(), disk); diff --git a/crates/ahm/src/scanner/data_scanner.rs b/crates/ahm/src/scanner/data_scanner.rs index 9c5e1aa1c..0496f70e1 100644 --- a/crates/ahm/src/scanner/data_scanner.rs +++ b/crates/ahm/src/scanner/data_scanner.rs @@ -507,29 +507,24 @@ impl Scanner { let enable_healing = self.config.read().await.enable_healing; if enable_healing { if let Some(heal_manager) = &self.heal_manager { - // Get bucket list for erasure set healing - let buckets = match rustfs_ecstore::new_object_layer_fn() { - Some(ecstore) => { - match ecstore.list_bucket(&ecstore::store_api::BucketOptions::default()).await { + // Get bucket list for erasure set healing + let buckets = match rustfs_ecstore::new_object_layer_fn() { + Some(ecstore) => match ecstore.list_bucket(&ecstore::store_api::BucketOptions::default()).await { Ok(buckets) => buckets.iter().map(|b| b.name.clone()).collect::>(), Err(e) => { error!("Failed to get bucket list for disk healing: {}", e); - return Err(Error::Storage(e.into())); + return Err(Error::Storage(e)); } - } - } - None => { - error!("No ECStore available for getting bucket list"); - return Err(Error::Storage(ecstore::error::StorageError::other("No ECStore available"))); - } - }; - - let set_disk_id = format!("{}_{}", disk.endpoint().pool_idx, disk.endpoint().set_idx); - let req = HealRequest::new( - crate::heal::task::HealType::ErasureSet { - buckets, - set_disk_id, }, + None => { + error!("No ECStore available for getting bucket list"); + return Err(Error::Storage(ecstore::error::StorageError::other("No ECStore available"))); + } + }; + + let set_disk_id = format!("pool_{}_set_{}", disk.endpoint().pool_idx, disk.endpoint().set_idx); + let req = HealRequest::new( + crate::heal::task::HealType::ErasureSet { buckets, set_disk_id }, crate::heal::task::HealOptions::default(), crate::heal::task::HealPriority::High, ); @@ -571,27 +566,22 @@ impl Scanner { if let Some(heal_manager) = &self.heal_manager { // Get bucket list for erasure set healing let buckets = match rustfs_ecstore::new_object_layer_fn() { - Some(ecstore) => { - match ecstore.list_bucket(&ecstore::store_api::BucketOptions::default()).await { - Ok(buckets) => buckets.iter().map(|b| b.name.clone()).collect::>(), - Err(e) => { - error!("Failed to get bucket list for disk healing: {}", e); - return Err(Error::Storage(e.into())); - } + Some(ecstore) => match ecstore.list_bucket(&ecstore::store_api::BucketOptions::default()).await { + Ok(buckets) => buckets.iter().map(|b| b.name.clone()).collect::>(), + Err(e) => { + error!("Failed to get bucket list for disk healing: {}", e); + return Err(Error::Storage(e)); } - } + }, None => { error!("No ECStore available for getting bucket list"); return Err(Error::Storage(ecstore::error::StorageError::other("No ECStore available"))); } }; - let set_disk_id = format!("{}_{}", disk.endpoint().pool_idx, disk.endpoint().set_idx); + let set_disk_id = format!("pool_{}_set_{}", disk.endpoint().pool_idx, disk.endpoint().set_idx); let req = HealRequest::new( - crate::heal::task::HealType::ErasureSet { - buckets, - set_disk_id, - }, + crate::heal::task::HealType::ErasureSet { buckets, set_disk_id }, crate::heal::task::HealOptions::default(), crate::heal::task::HealPriority::Urgent, ); diff --git a/crates/ahm/tests/heal_integration_test.rs b/crates/ahm/tests/heal_integration_test.rs index c5a6f3a5b..76603e53b 100644 --- a/crates/ahm/tests/heal_integration_test.rs +++ b/crates/ahm/tests/heal_integration_test.rs @@ -303,6 +303,54 @@ async fn test_heal_format_basic() { 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() { From e7d0a8d4b9cb4f5f774b9167bebc54818c1b1450 Mon Sep 17 00:00:00 2001 From: junxiang Mu <1948535941@qq.com> Date: Mon, 21 Jul 2025 11:32:43 +0800 Subject: [PATCH 12/29] feat: integrate global metrics system into AHM scanner - Add global metrics system to common crate for cross-module usage - Integrate global metrics collection into AHM scanner operations - Update ECStore to use common metrics system instead of local implementation - Add chrono dependency to AHM crate for timestamp handling - Re-export IlmAction from common metrics in ECStore lifecycle module - Update scanner methods to use global metrics for cycle, disk, and volume scans - Maintain backward compatibility with local metrics collector - Fix clippy warnings and ensure proper code formatting This change enables unified metrics collection across the entire RustFS system, allowing better monitoring and observability of scanner operations. Signed-off-by: junxiang Mu <1948535941@qq.com> --- Cargo.lock | 3 + crates/ahm/Cargo.toml | 1 + crates/ahm/src/scanner/data_scanner.rs | 34 +- crates/ahm/tests/heal_integration_test.rs | 14 +- crates/common/Cargo.toml | 2 + crates/common/src/lib.rs | 1 + crates/common/src/metrics.rs | 535 ++++++++++++++++++ .../bucket/lifecycle/bucket_lifecycle_ops.rs | 8 +- .../ecstore/src/bucket/lifecycle/lifecycle.rs | 44 +- crates/ecstore/src/disk/local.rs | 12 +- crates/ecstore/src/heal/data_scanner.rs | 150 ++--- crates/ecstore/src/heal/mod.rs | 2 +- crates/ecstore/src/metrics_realtime.rs | 12 +- rustfs/src/admin/mod.rs | 3 +- 14 files changed, 677 insertions(+), 144 deletions(-) create mode 100644 crates/common/src/metrics.rs diff --git a/Cargo.lock b/Cargo.lock index c1c1608c2..b94029abf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7930,6 +7930,7 @@ dependencies = [ "anyhow", "async-trait", "bytes", + "chrono", "futures", "lazy_static", "rmp-serde", @@ -7965,6 +7966,8 @@ dependencies = [ name = "rustfs-common" version = "0.0.5" dependencies = [ + "chrono", + "rustfs-madmin", "tokio", "tonic", "uuid", diff --git a/crates/ahm/Cargo.toml b/crates/ahm/Cargo.toml index fc4eae4bd..5442fb71f 100644 --- a/crates/ahm/Cargo.toml +++ b/crates/ahm/Cargo.toml @@ -34,6 +34,7 @@ url = { workspace = true } rustfs-lock = { workspace = true } lazy_static = { workspace = true } +chrono = { workspace = true } [dev-dependencies] rmp-serde = { workspace = true } diff --git a/crates/ahm/src/scanner/data_scanner.rs b/crates/ahm/src/scanner/data_scanner.rs index 0496f70e1..a98e1f9f8 100644 --- a/crates/ahm/src/scanner/data_scanner.rs +++ b/crates/ahm/src/scanner/data_scanner.rs @@ -37,6 +37,7 @@ use crate::{ error::{Error, Result}, get_ahm_services_cancel_token, HealRequest, }; +use rustfs_common::metrics::{globalMetrics, Metric, Metrics}; use rustfs_ecstore::disk::RUSTFS_META_BUCKET; @@ -117,7 +118,7 @@ pub struct Scanner { config: Arc>, /// Scanner state state: Arc>, - /// Metrics collector + /// Local metrics collector (for backward compatibility) metrics: Arc, /// Bucket metrics cache bucket_metrics: Arc>>, @@ -286,10 +287,18 @@ impl Scanner { metrics } + /// Get global metrics from common crate + pub async fn get_global_metrics(&self) -> rustfs_madmin::metrics::ScannerMetrics { + globalMetrics.report().await + } + /// Perform a single scan cycle pub async fn scan_cycle(&self) -> Result<()> { let start_time = SystemTime::now(); + // Start global metrics collection for this cycle + let stop_fn = Metrics::time(Metric::ScanCycle); + info!("Starting scan cycle {} for all EC sets", self.metrics.get_metrics().current_cycle + 1); // Update state @@ -301,6 +310,14 @@ impl Scanner { state.scanning_disks.clear(); } + // Update global metrics cycle information + let cycle_info = rustfs_common::metrics::CurrentCycle { + current: self.state.read().await.current_cycle, + cycle_completed: vec![chrono::Utc::now()], + started: chrono::Utc::now(), + }; + globalMetrics.set_cycle(Some(cycle_info)).await; + self.metrics.set_current_cycle(self.state.read().await.current_cycle); self.metrics.increment_total_cycles(); @@ -392,6 +409,9 @@ impl Scanner { state.current_scan_duration = Some(scan_duration); } + // Complete global metrics collection for this cycle + stop_fn(); + info!( "Completed scan cycle in {:?} ({} successful, {} failed)", scan_duration, successful_scans, failed_scans @@ -475,6 +495,9 @@ impl Scanner { async fn scan_disk(&self, disk: &DiskStore) -> Result>> { let disk_path = disk.path().to_string_lossy().to_string(); + // Start global metrics collection for disk scan + let stop_fn = Metrics::time(Metric::ScanBucketDrive); + info!("Scanning disk: {}", disk_path); // Update disk metrics @@ -638,6 +661,9 @@ impl Scanner { state.scanning_disks.retain(|d| d != &disk_path); } + // Complete global metrics collection for disk scan + stop_fn(); + Ok(disk_objects) } @@ -646,6 +672,9 @@ impl Scanner { /// This method collects all objects from a disk for a specific bucket. /// It returns a map of object names to their metadata for later analysis. async fn scan_volume(&self, disk: &DiskStore, bucket: &str) -> Result> { + // Start global metrics collection for volume scan + let stop_fn = Metrics::time(Metric::ScanObject); + info!("Scanning bucket: {} on disk: {}", bucket, disk.to_string()); // Initialize bucket metrics if not exists @@ -785,6 +814,9 @@ impl Scanner { state.scanning_buckets.retain(|b| b != bucket); } + // Complete global metrics collection for volume scan + stop_fn(); + debug!( "Completed scanning bucket: {} on disk {} ({} objects, {} issues)", bucket, diff --git a/crates/ahm/tests/heal_integration_test.rs b/crates/ahm/tests/heal_integration_test.rs index 76603e53b..f5dbb0c76 100644 --- a/crates/ahm/tests/heal_integration_test.rs +++ b/crates/ahm/tests/heal_integration_test.rs @@ -318,13 +318,13 @@ async fn test_heal_format_with_data() { 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"); + .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"); diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 85d8bf97f..daba9456e 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -31,3 +31,5 @@ workspace = true tokio.workspace = true tonic = { workspace = true } uuid = { workspace = true } +chrono = { workspace = true } +rustfs-madmin = { workspace = true } diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 7c163b345..3a41462a5 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -17,6 +17,7 @@ pub mod bucket_stats; pub mod globals; pub mod heal_channel; pub mod last_minute; +pub mod metrics; // is ',' pub static DEFAULT_DELIMITER: u8 = 44; diff --git a/crates/common/src/metrics.rs b/crates/common/src/metrics.rs new file mode 100644 index 000000000..d88e5a3a6 --- /dev/null +++ b/crates/common/src/metrics.rs @@ -0,0 +1,535 @@ +// 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 chrono::{DateTime, Utc}; +use lazy_static::lazy_static; +use rustfs_madmin::metrics::ScannerMetrics as M_ScannerMetrics; +use std::{ + collections::HashMap, + fmt::Display, + pin::Pin, + sync::{ + Arc, + atomic::{AtomicU64, Ordering}, + }, + time::{Duration, SystemTime}, +}; +use tokio::sync::{Mutex, RwLock}; + +use crate::last_minute::{AccElem, LastMinuteLatency}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IlmAction { + NoneAction = 0, + DeleteAction, + DeleteVersionAction, + TransitionAction, + TransitionVersionAction, + DeleteRestoredAction, + DeleteRestoredVersionAction, + DeleteAllVersionsAction, + DelMarkerDeleteAllVersionsAction, + ActionCount, +} + +impl IlmAction { + pub fn delete_restored(&self) -> bool { + *self == Self::DeleteRestoredAction || *self == Self::DeleteRestoredVersionAction + } + + pub fn delete_versioned(&self) -> bool { + *self == Self::DeleteVersionAction || *self == Self::DeleteRestoredVersionAction + } + + pub fn delete_all(&self) -> bool { + *self == Self::DeleteAllVersionsAction || *self == Self::DelMarkerDeleteAllVersionsAction + } + + pub fn delete(&self) -> bool { + if self.delete_restored() { + return true; + } + *self == Self::DeleteVersionAction + || *self == Self::DeleteAction + || *self == Self::DeleteAllVersionsAction + || *self == Self::DelMarkerDeleteAllVersionsAction + } +} + +impl Display for IlmAction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:?}") + } +} + +lazy_static! { + pub static ref globalMetrics: Arc = Arc::new(Metrics::new()); +} + +#[derive(Clone, Debug, PartialEq, PartialOrd)] +pub enum Metric { + // START Realtime metrics, that only records + // last minute latencies and total operation count. + ReadMetadata = 0, + CheckMissing, + SaveUsage, + ApplyAll, + ApplyVersion, + TierObjSweep, + HealCheck, + Ilm, + CheckReplication, + Yield, + CleanAbandoned, + ApplyNonCurrent, + HealAbandonedVersion, + + // START Trace metrics: + StartTrace, + ScanObject, // Scan object. All operations included. + HealAbandonedObject, + + // END realtime metrics: + LastRealtime, + + // Trace only metrics: + ScanFolder, // Scan a folder on disk, recursively. + ScanCycle, // Full cycle, cluster global. + ScanBucketDrive, // Single bucket on one drive. + CompactFolder, // Folder compacted. + + // Must be last: + Last, +} + +impl Metric { + /// Convert to string representation for metrics + pub fn as_str(self) -> &'static str { + match self { + Self::ReadMetadata => "read_metadata", + Self::CheckMissing => "check_missing", + Self::SaveUsage => "save_usage", + Self::ApplyAll => "apply_all", + Self::ApplyVersion => "apply_version", + Self::TierObjSweep => "tier_obj_sweep", + Self::HealCheck => "heal_check", + Self::Ilm => "ilm", + Self::CheckReplication => "check_replication", + Self::Yield => "yield", + Self::CleanAbandoned => "clean_abandoned", + Self::ApplyNonCurrent => "apply_non_current", + Self::HealAbandonedVersion => "heal_abandoned_version", + Self::StartTrace => "start_trace", + Self::ScanObject => "scan_object", + Self::HealAbandonedObject => "heal_abandoned_object", + Self::LastRealtime => "last_realtime", + Self::ScanFolder => "scan_folder", + Self::ScanCycle => "scan_cycle", + Self::ScanBucketDrive => "scan_bucket_drive", + Self::CompactFolder => "compact_folder", + Self::Last => "last", + } + } + + /// Convert from index back to enum (safe version) + pub fn from_index(index: usize) -> Option { + if index >= Self::Last as usize { + return None; + } + // Safe conversion using match instead of unsafe transmute + match index { + 0 => Some(Self::ReadMetadata), + 1 => Some(Self::CheckMissing), + 2 => Some(Self::SaveUsage), + 3 => Some(Self::ApplyAll), + 4 => Some(Self::ApplyVersion), + 5 => Some(Self::TierObjSweep), + 6 => Some(Self::HealCheck), + 7 => Some(Self::Ilm), + 8 => Some(Self::CheckReplication), + 9 => Some(Self::Yield), + 10 => Some(Self::CleanAbandoned), + 11 => Some(Self::ApplyNonCurrent), + 12 => Some(Self::HealAbandonedVersion), + 13 => Some(Self::StartTrace), + 14 => Some(Self::ScanObject), + 15 => Some(Self::HealAbandonedObject), + 16 => Some(Self::LastRealtime), + 17 => Some(Self::ScanFolder), + 18 => Some(Self::ScanCycle), + 19 => Some(Self::ScanBucketDrive), + 20 => Some(Self::CompactFolder), + 21 => Some(Self::Last), + _ => None, + } + } +} + +/// Thread-safe wrapper for LastMinuteLatency with atomic operations +#[derive(Default)] +pub struct LockedLastMinuteLatency { + latency: Arc>, +} + +impl Clone for LockedLastMinuteLatency { + fn clone(&self) -> Self { + Self { + latency: Arc::clone(&self.latency), + } + } +} + +impl LockedLastMinuteLatency { + pub fn new() -> Self { + Self { + latency: Arc::new(Mutex::new(LastMinuteLatency::default())), + } + } + + /// Add a duration measurement + pub async fn add(&self, duration: Duration) { + self.add_size(duration, 0).await; + } + + /// Add a duration measurement with size + pub async fn add_size(&self, duration: Duration, size: u64) { + let mut latency = self.latency.lock().await; + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + let elem = AccElem { + n: 1, + total: duration.as_secs(), + size, + }; + latency.add_all(now, &elem); + } + + /// Get total accumulated metrics for the last minute + pub async fn total(&self) -> AccElem { + let mut latency = self.latency.lock().await; + latency.get_total() + } +} + +/// Current path tracker for monitoring active scan paths +struct CurrentPathTracker { + current_path: Arc>, +} + +impl CurrentPathTracker { + fn new(initial_path: String) -> Self { + Self { + current_path: Arc::new(RwLock::new(initial_path)), + } + } + + async fn update_path(&self, path: String) { + *self.current_path.write().await = path; + } + + async fn get_path(&self) -> String { + self.current_path.read().await.clone() + } +} + +/// Main scanner metrics structure +pub struct Metrics { + // All fields must be accessed atomically and aligned. + operations: Vec, + latency: Vec, + actions: Vec, + actions_latency: Vec, + // Current paths contains disk -> tracker mappings + current_paths: Arc>>>, + + // Cycle information + cycle_info: Arc>>, +} + +// This is a placeholder. We'll need to define this struct. +#[derive(Clone, Debug)] +pub struct CurrentCycle { + pub current: u64, + pub cycle_completed: Vec>, + pub started: DateTime, +} + +impl Metrics { + pub fn new() -> Self { + let operations = (0..Metric::Last as usize).map(|_| AtomicU64::new(0)).collect(); + + let latency = (0..Metric::LastRealtime as usize) + .map(|_| LockedLastMinuteLatency::new()) + .collect(); + + Self { + operations, + latency, + actions: (0..IlmAction::ActionCount as usize).map(|_| AtomicU64::new(0)).collect(), + actions_latency: vec![LockedLastMinuteLatency::default(); IlmAction::ActionCount as usize], + current_paths: Arc::new(RwLock::new(HashMap::new())), + cycle_info: Arc::new(RwLock::new(None)), + } + } + + /// Log scanner action with custom metadata - compatible with existing usage + pub fn log(metric: Metric) -> impl Fn(&HashMap) { + let metric = metric as usize; + let start_time = SystemTime::now(); + move |_custom: &HashMap| { + let duration = SystemTime::now().duration_since(start_time).unwrap_or_default(); + + // Update operation count + globalMetrics.operations[metric].fetch_add(1, Ordering::Relaxed); + + // Update latency for realtime metrics (spawn async task for this) + if (metric) < Metric::LastRealtime as usize { + let metric_index = metric; + tokio::spawn(async move { + globalMetrics.latency[metric_index].add(duration).await; + }); + } + + // Log trace metrics + if metric as u8 > Metric::StartTrace as u8 { + //debug!(metric = metric.as_str(), duration_ms = duration.as_millis(), "Scanner trace metric"); + } + } + } + + /// Time scanner action with size - returns function that takes size + pub fn time_size(metric: Metric) -> impl Fn(u64) { + let metric = metric as usize; + let start_time = SystemTime::now(); + move |size: u64| { + let duration = SystemTime::now().duration_since(start_time).unwrap_or_default(); + + // Update operation count + globalMetrics.operations[metric].fetch_add(1, Ordering::Relaxed); + + // Update latency for realtime metrics with size (spawn async task) + if (metric) < Metric::LastRealtime as usize { + let metric_index = metric; + tokio::spawn(async move { + globalMetrics.latency[metric_index].add_size(duration, size).await; + }); + } + } + } + + /// Time a scanner action - returns a closure to call when done + pub fn time(metric: Metric) -> impl Fn() { + let metric = metric as usize; + let start_time = SystemTime::now(); + move || { + let duration = SystemTime::now().duration_since(start_time).unwrap_or_default(); + + // Update operation count + globalMetrics.operations[metric].fetch_add(1, Ordering::Relaxed); + + // Update latency for realtime metrics (spawn async task) + if (metric) < Metric::LastRealtime as usize { + let metric_index = metric; + tokio::spawn(async move { + globalMetrics.latency[metric_index].add(duration).await; + }); + } + } + } + + /// Time N scanner actions - returns function that takes count, then returns completion function + pub fn time_n(metric: Metric) -> Box Box + Send + Sync> { + let metric = metric as usize; + let start_time = SystemTime::now(); + Box::new(move |count: usize| { + Box::new(move || { + let duration = SystemTime::now().duration_since(start_time).unwrap_or_default(); + + // Update operation count + globalMetrics.operations[metric].fetch_add(count as u64, Ordering::Relaxed); + + // Update latency for realtime metrics (spawn async task) + if (metric) < Metric::LastRealtime as usize { + let metric_index = metric; + tokio::spawn(async move { + globalMetrics.latency[metric_index].add(duration).await; + }); + } + }) + }) + } + + /// Time ILM action with versions - returns function that takes versions, then returns completion function + pub fn time_ilm(a: IlmAction) -> Box Box + Send + Sync> { + let a_clone = a as usize; + if a_clone == IlmAction::NoneAction as usize || a_clone >= IlmAction::ActionCount as usize { + return Box::new(move |_: u64| Box::new(move || {})); + } + let start = SystemTime::now(); + Box::new(move |versions: u64| { + Box::new(move || { + let duration = SystemTime::now().duration_since(start).unwrap_or(Duration::from_secs(0)); + tokio::spawn(async move { + globalMetrics.actions[a_clone].fetch_add(versions, Ordering::Relaxed); + globalMetrics.actions_latency[a_clone].add(duration).await; + }); + }) + }) + } + + /// Increment time with specific duration + pub async fn inc_time(metric: Metric, duration: Duration) { + let metric = metric as usize; + // Update operation count + globalMetrics.operations[metric].fetch_add(1, Ordering::Relaxed); + + // Update latency for realtime metrics + if (metric) < Metric::LastRealtime as usize { + globalMetrics.latency[metric].add(duration).await; + } + } + + /// Get lifetime operation count for a metric + pub fn lifetime(&self, metric: Metric) -> u64 { + let metric = metric as usize; + if (metric) >= Metric::Last as usize { + return 0; + } + self.operations[metric].load(Ordering::Relaxed) + } + + /// Get last minute statistics for a metric + pub async fn last_minute(&self, metric: Metric) -> AccElem { + let metric = metric as usize; + if (metric) >= Metric::LastRealtime as usize { + return AccElem::default(); + } + self.latency[metric].total().await + } + + /// Set current cycle information + pub async fn set_cycle(&self, cycle: Option) { + *self.cycle_info.write().await = cycle; + } + + /// Get current cycle information + pub async fn get_cycle(&self) -> Option { + self.cycle_info.read().await.clone() + } + + /// Get current active paths + pub async fn get_current_paths(&self) -> Vec { + let mut result = Vec::new(); + let paths = self.current_paths.read().await; + + for (disk, tracker) in paths.iter() { + let path = tracker.get_path().await; + result.push(format!("{disk}/{path}")); + } + + result + } + + /// Get number of active drives + pub async fn active_drives(&self) -> usize { + self.current_paths.read().await.len() + } + + /// Generate metrics report + pub async fn report(&self) -> M_ScannerMetrics { + let mut metrics = M_ScannerMetrics::default(); + + // Set cycle information + if let Some(cycle) = self.get_cycle().await { + metrics.current_cycle = cycle.current; + metrics.cycles_completed_at = cycle.cycle_completed; + metrics.current_started = cycle.started; + } + + metrics.collected_at = Utc::now(); + metrics.active_paths = self.get_current_paths().await; + + // Lifetime operations + for i in 0..Metric::Last as usize { + let count = self.operations[i].load(Ordering::Relaxed); + if count > 0 { + if let Some(metric) = Metric::from_index(i) { + metrics.life_time_ops.insert(metric.as_str().to_string(), count); + } + } + } + + // Last minute statistics for realtime metrics + for i in 0..Metric::LastRealtime as usize { + let last_min = self.latency[i].total().await; + if last_min.n > 0 { + if let Some(_metric) = Metric::from_index(i) { + // Convert to madmin TimedAction format if needed + // This would require implementing the conversion + } + } + } + + metrics + } +} + +// Type aliases for compatibility with existing code +pub type UpdateCurrentPathFn = Arc Pin + Send>> + Send + Sync>; +pub type CloseDiskFn = Arc Pin + Send>> + Send + Sync>; + +/// Create a current path updater for tracking scan progress +pub fn current_path_updater(disk: &str, initial: &str) -> (UpdateCurrentPathFn, CloseDiskFn) { + let tracker = Arc::new(CurrentPathTracker::new(initial.to_string())); + let disk_name = disk.to_string(); + + // Store the tracker in global metrics + let tracker_clone = Arc::clone(&tracker); + let disk_clone = disk_name.clone(); + tokio::spawn(async move { + globalMetrics.current_paths.write().await.insert(disk_clone, tracker_clone); + }); + + let update_fn = { + let tracker = Arc::clone(&tracker); + Arc::new(move |path: &str| -> Pin + Send>> { + let tracker = Arc::clone(&tracker); + let path = path.to_string(); + Box::pin(async move { + tracker.update_path(path).await; + }) + }) + }; + + let done_fn = { + let disk_name = disk_name.clone(); + Arc::new(move || -> Pin + Send>> { + let disk_name = disk_name.clone(); + Box::pin(async move { + globalMetrics.current_paths.write().await.remove(&disk_name); + }) + }) + }; + + (update_fn, done_fn) +} + +impl Default for Metrics { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/ecstore/src/bucket/lifecycle/bucket_lifecycle_ops.rs b/crates/ecstore/src/bucket/lifecycle/bucket_lifecycle_ops.rs index 14cfe9985..40af33df7 100644 --- a/crates/ecstore/src/bucket/lifecycle/bucket_lifecycle_ops.rs +++ b/crates/ecstore/src/bucket/lifecycle/bucket_lifecycle_ops.rs @@ -22,6 +22,7 @@ use async_channel::{Receiver as A_Receiver, Sender as A_Sender, bounded}; use futures::Future; use http::HeaderMap; use lazy_static::lazy_static; +use rustfs_common::metrics::{IlmAction, Metrics}; use s3s::Body; use sha2::{Digest, Sha256}; use std::any::Any; @@ -41,7 +42,7 @@ use xxhash_rust::xxh64; //use rustfs_notify::{BucketNotificationConfig, Event, EventName, LogLevel, NotificationError, init_logger}; //use rustfs_notify::{initialize, notification_system}; use super::bucket_lifecycle_audit::{LcAuditEvent, LcEventSrc}; -use super::lifecycle::{self, ExpirationOptions, IlmAction, Lifecycle, TransitionOptions}; +use super::lifecycle::{self, ExpirationOptions, Lifecycle, TransitionOptions}; use super::tier_last_day_stats::{DailyAllTierStats, LastDayTierStats}; use super::tier_sweeper::{Jentry, delete_object_from_remote_tier}; use crate::bucket::{metadata_sys::get_lifecycle_config, versioning_sys::BucketVersioningSys}; @@ -54,7 +55,6 @@ use crate::global::GLOBAL_LocalNodeName; use crate::global::{GLOBAL_LifecycleSys, GLOBAL_TierConfigMgr, get_global_deployment_id}; use crate::heal::{ data_scanner::{apply_expiry_on_non_transitioned_objects, apply_expiry_on_transitioned_object}, - data_scanner_metric::ScannerMetrics, data_usage_cache::TierStats, }; use crate::store::ECStore; @@ -631,7 +631,7 @@ pub async fn enqueue_transition_immediate(oi: &ObjectInfo, src: LcEventSrc) { if !lc.is_none() { let event = lc.expect("err").eval(&oi.to_lifecycle_opts()).await; match event.action { - lifecycle::IlmAction::TransitionAction | lifecycle::IlmAction::TransitionVersionAction => { + IlmAction::TransitionAction | IlmAction::TransitionVersionAction => { if oi.delete_marker || oi.is_dir { return; } @@ -728,7 +728,7 @@ pub fn gen_transition_objname(bucket: &str) -> Result { } pub async fn transition_object(api: Arc, oi: &ObjectInfo, lae: LcAuditEvent) -> Result<(), Error> { - let time_ilm = ScannerMetrics::time_ilm(lae.event.action); + let time_ilm = Metrics::time_ilm(lae.event.action); let opts = ObjectOptions { transition: TransitionOptions { diff --git a/crates/ecstore/src/bucket/lifecycle/lifecycle.rs b/crates/ecstore/src/bucket/lifecycle/lifecycle.rs index 2431d6ba8..3d232363c 100644 --- a/crates/ecstore/src/bucket/lifecycle/lifecycle.rs +++ b/crates/ecstore/src/bucket/lifecycle/lifecycle.rs @@ -43,49 +43,7 @@ const _ERR_XML_NOT_WELL_FORMED: &str = const ERR_LIFECYCLE_BUCKET_LOCKED: &str = "ExpiredObjectAllVersions element and DelMarkerExpiration action cannot be used on an retention bucket"; -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum IlmAction { - NoneAction = 0, - DeleteAction, - DeleteVersionAction, - TransitionAction, - TransitionVersionAction, - DeleteRestoredAction, - DeleteRestoredVersionAction, - DeleteAllVersionsAction, - DelMarkerDeleteAllVersionsAction, - ActionCount, -} - -impl IlmAction { - pub fn delete_restored(&self) -> bool { - *self == Self::DeleteRestoredAction || *self == Self::DeleteRestoredVersionAction - } - - pub fn delete_versioned(&self) -> bool { - *self == Self::DeleteVersionAction || *self == Self::DeleteRestoredVersionAction - } - - pub fn delete_all(&self) -> bool { - *self == Self::DeleteAllVersionsAction || *self == Self::DelMarkerDeleteAllVersionsAction - } - - pub fn delete(&self) -> bool { - if self.delete_restored() { - return true; - } - *self == Self::DeleteVersionAction - || *self == Self::DeleteAction - || *self == Self::DeleteAllVersionsAction - || *self == Self::DelMarkerDeleteAllVersionsAction - } -} - -impl Display for IlmAction { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) - } -} +pub use rustfs_common::metrics::IlmAction; #[async_trait::async_trait] pub trait RuleValidate { diff --git a/crates/ecstore/src/disk/local.rs b/crates/ecstore/src/disk/local.rs index 12214f3e0..649ca702a 100644 --- a/crates/ecstore/src/disk/local.rs +++ b/crates/ecstore/src/disk/local.rs @@ -39,13 +39,13 @@ use crate::global::{GLOBAL_IsErasureSD, GLOBAL_RootDiskThreshold}; use crate::heal::data_scanner::{ ScannerItem, ShouldSleepFn, SizeSummary, lc_has_active_rules, rep_has_active_rules, scan_data_folder, }; -use crate::heal::data_scanner_metric::{ScannerMetric, ScannerMetrics}; use crate::heal::data_usage_cache::{DataUsageCache, DataUsageEntry}; use crate::heal::error::{ERR_IGNORE_FILE_CONTRIB, ERR_SKIP_FILE}; use crate::heal::heal_commands::{HealScanMode, HealingTracker}; use crate::heal::heal_ops::HEALING_TRACKER_FILENAME; use crate::new_object_layer_fn; use crate::store_api::{ObjectInfo, StorageAPI}; +use rustfs_common::metrics::{Metric, Metrics}; use rustfs_utils::path::{ GLOBAL_DIR_SUFFIX, GLOBAL_DIR_SUFFIX_WITH_SLASH, SLASH_SEPARATOR, clean, decode_dir_object, encode_dir_object, has_suffix, path_join, path_join_buf, @@ -2323,9 +2323,9 @@ impl DiskAPI for LocalDisk { if !item.path.ends_with(&format!("{SLASH_SEPARATOR}{STORAGE_FORMAT_FILE}")) { return Err(Error::other(ERR_SKIP_FILE).into()); } - let stop_fn = ScannerMetrics::log(ScannerMetric::ScanObject); + let stop_fn = Metrics::log(Metric::ScanObject); let mut res = HashMap::new(); - let done_sz = ScannerMetrics::time_size(ScannerMetric::ReadMetadata); + let done_sz = Metrics::time_size(Metric::ReadMetadata); let buf = match disk.read_metadata(item.path.clone()).await { Ok(buf) => buf, Err(err) => { @@ -2351,7 +2351,7 @@ impl DiskAPI for LocalDisk { } }; let mut size_s = SizeSummary::default(); - let done = ScannerMetrics::time(ScannerMetric::ApplyAll); + let done = Metrics::time(Metric::ApplyAll); let obj_infos = match item.apply_versions_actions(&fivs.versions).await { Ok(obj_infos) => obj_infos, Err(err) => { @@ -2369,7 +2369,7 @@ impl DiskAPI for LocalDisk { let mut obj_deleted = false; for info in obj_infos.iter() { - let done = ScannerMetrics::time(ScannerMetric::ApplyVersion); + let done = Metrics::time(Metric::ApplyVersion); let sz: i64; (obj_deleted, sz) = item.apply_actions(info, &mut size_s).await; done(); @@ -2405,7 +2405,7 @@ impl DiskAPI for LocalDisk { &item.object_path().to_string_lossy(), versioned, ); - let done = ScannerMetrics::time(ScannerMetric::TierObjSweep); + let done = Metrics::time(Metric::TierObjSweep); done(); } diff --git a/crates/ecstore/src/heal/data_scanner.rs b/crates/ecstore/src/heal/data_scanner.rs index c5ea666c6..98a0924fd 100644 --- a/crates/ecstore/src/heal/data_scanner.rs +++ b/crates/ecstore/src/heal/data_scanner.rs @@ -29,7 +29,6 @@ use std::{ use time::{self, OffsetDateTime}; use super::{ - data_scanner_metric::{ScannerMetric, ScannerMetrics, globalScannerMetrics}, data_usage::{DATA_USAGE_BLOOM_NAME_PATH, DataUsageInfo, store_data_usage_in_backend}, data_usage_cache::{DataUsageCache, DataUsageEntry, DataUsageHash}, heal_commands::{HEAL_DEEP_SCAN, HEAL_NORMAL_SCAN, HealScanMode}, @@ -39,6 +38,7 @@ use crate::bucket::{ utils::is_meta_bucketname, }; use crate::cmd::bucket_replication::queue_replication_heal; +use crate::disk::local::LocalDisk; use crate::event::name::EventName; use crate::{ bucket::{ @@ -57,7 +57,7 @@ use crate::{ bucket::{versioning::VersioningApi, versioning_sys::BucketVersioningSys}, cmd::bucket_replication::ReplicationStatusType, disk, - heal::data_usage::DATA_USAGE_ROOT, + // heal::data_usage::DATA_USAGE_ROOT, }; use crate::{ cache_value::metacache_set::{ListPathRawOptions, list_path_raw}, @@ -66,7 +66,7 @@ use crate::{ heal::Config, }, disk::{DiskInfoOptions, DiskStore}, - global::{GLOBAL_BackgroundHealState, GLOBAL_IsErasure, GLOBAL_IsErasureSD}, + global::{GLOBAL_BackgroundHealState, GLOBAL_IsErasureSD}, heal::{ data_usage::BACKGROUND_HEAL_INFO_PATH, data_usage_cache::{DataUsageHashMap, hash_path}, @@ -83,7 +83,6 @@ use crate::{ disk::error::DiskError, error::{Error, Result}, }; -use crate::{disk::local::LocalDisk, heal::data_scanner_metric::current_path_updater}; use chrono::{DateTime, Utc}; use lazy_static::lazy_static; use rand::Rng; @@ -300,14 +299,14 @@ async fn run_data_scanner_cycle() { }; // Start metrics collection for this cycle - let stop_fn = ScannerMetrics::log(ScannerMetric::ScanCycle); + // let stop_fn = ScannerMetrics::log(ScannerMetric::ScanCycle); // Update cycle information cycle_info.current = cycle_info.next; cycle_info.started = Utc::now(); // Update global scanner metrics - globalScannerMetrics.set_cycle(Some(cycle_info.clone())).await; + // globalScannerMetrics.set_cycle(Some(cycle_info.clone())).await; // Read background healing information and determine scan mode let bg_heal_info = read_background_heal_info(store.clone()).await; @@ -357,7 +356,7 @@ async fn run_data_scanner_cycle() { } // Update global metrics with completion info - globalScannerMetrics.set_cycle(Some(cycle_info.clone())).await; + // globalScannerMetrics.set_cycle(Some(cycle_info.clone())).await; // Persist updated cycle information // ignore error, continue. @@ -379,7 +378,7 @@ async fn run_data_scanner_cycle() { } // Complete metrics collection for this cycle - stop_fn(&scan_result); + // stop_fn(&scan_result); } /// Execute namespace scan with cancellation support @@ -781,7 +780,7 @@ impl ScannerItem { } pub async fn apply_actions(&mut self, oi: &ObjectInfo, _size_s: &mut SizeSummary) -> (bool, i64) { - let done = ScannerMetrics::time(ScannerMetric::Ilm); + // let done = ScannerMetrics::time(ScannerMetric::Ilm); let (action, _size) = self.apply_lifecycle(oi).await; @@ -807,7 +806,7 @@ impl ScannerItem { self.heal_replication(&oi, _size_s).await; } - done(); + // done(); if action.delete_all() { return (true, 0); @@ -1572,68 +1571,69 @@ pub fn rep_has_active_rules(config: &ReplicationConfiguration, prefix: &str, rec pub type LocalDrive = Arc; pub async fn scan_data_folder( - disks: &[Option], - drive: LocalDrive, - cache: &DataUsageCache, - get_size_fn: GetSizeFn, - heal_scan_mode: HealScanMode, - should_sleep: ShouldSleepFn, + _disks: &[Option], + _drive: LocalDrive, + _cache: &DataUsageCache, + _get_size_fn: GetSizeFn, + _heal_scan_mode: HealScanMode, + _should_sleep: ShouldSleepFn, ) -> disk::error::Result { - if cache.info.name.is_empty() || cache.info.name == DATA_USAGE_ROOT { - return Err(DiskError::other("internal error: root scan attempted")); - } + // if cache.info.name.is_empty() || cache.info.name == DATA_USAGE_ROOT { + // return Err(DiskError::other("internal error: root scan attempted")); + // } - let base_path = drive.to_string(); - let (update_path, close_disk) = current_path_updater(&base_path, &cache.info.name); - let skip_heal = if *GLOBAL_IsErasure.read().await || cache.info.skip_healing { - AtomicBool::new(true) - } else { - AtomicBool::new(false) - }; - let mut s = FolderScanner { - root: base_path, - get_size: get_size_fn, - old_cache: cache.clone(), - new_cache: DataUsageCache { - info: cache.info.clone(), - ..Default::default() - }, - update_cache: DataUsageCache { - info: cache.info.clone(), - ..Default::default() - }, - data_usage_scanner_debug: false, - heal_object_select: 0, - scan_mode: heal_scan_mode, - updates: cache.info.updates.clone().unwrap(), - last_update: SystemTime::now(), - update_current_path: update_path, - disks: disks.to_vec(), - disks_quorum: disks.len() / 2, - skip_heal, - drive: drive.clone(), - we_sleep: should_sleep, - }; + // let base_path = drive.to_string(); + // // let (update_path, close_disk) = current_path_updater(&base_path, &cache.info.name); + // let skip_heal = if *GLOBAL_IsErasure.read().await || cache.info.skip_healing { + // AtomicBool::new(true) + // } else { + // AtomicBool::new(false) + // }; + // let mut s = FolderScanner { + // root: base_path, + // get_size: get_size_fn, + // old_cache: cache.clone(), + // new_cache: DataUsageCache { + // info: cache.info.clone(), + // ..Default::default() + // }, + // update_cache: DataUsageCache { + // info: cache.info.clone(), + // ..Default::default() + // }, + // data_usage_scanner_debug: false, + // heal_object_select: 0, + // scan_mode: heal_scan_mode, + // updates: cache.info.updates.clone().unwrap(), + // last_update: SystemTime::now(), + // update_current_path: update_path, + // disks: disks.to_vec(), + // disks_quorum: disks.len() / 2, + // skip_heal, + // drive: drive.clone(), + // we_sleep: should_sleep, + // }; - if *GLOBAL_IsErasure.read().await || !cache.info.skip_healing { - s.heal_object_select = HEAL_OBJECT_SELECT_PROB as u32; - } + // if *GLOBAL_IsErasure.read().await || !cache.info.skip_healing { + // s.heal_object_select = HEAL_OBJECT_SELECT_PROB as u32; + // } - let mut root = DataUsageEntry::default(); - let folder = CachedFolder { - name: cache.info.name.clone(), - object_heal_prob_div: 1, - parent: DataUsageHash("".to_string()), - }; + // let mut root = DataUsageEntry::default(); + // let folder = CachedFolder { + // name: cache.info.name.clone(), + // object_heal_prob_div: 1, + // parent: DataUsageHash("".to_string()), + // }; - if s.scan_folder(&folder, &mut root).await.is_err() { - close_disk().await; - } - s.new_cache.force_compact(DATA_SCANNER_COMPACT_AT_CHILDREN as usize); - s.new_cache.info.last_update = Some(SystemTime::now()); - s.new_cache.info.next_cycle = cache.info.next_cycle; - close_disk().await; - Ok(s.new_cache) + // if s.scan_folder(&folder, &mut root).await.is_err() { + // close_disk().await; + // } + // s.new_cache.force_compact(DATA_SCANNER_COMPACT_AT_CHILDREN as usize); + // s.new_cache.info.last_update = Some(SystemTime::now()); + // s.new_cache.info.next_cycle = cache.info.next_cycle; + // close_disk().await; + // Ok(s.new_cache) + todo!() } pub async fn eval_action_from_lifecycle( @@ -1695,11 +1695,11 @@ pub async fn apply_expiry_on_transitioned_object( lc_event: &lifecycle::Event, src: &LcEventSrc, ) -> bool { - let time_ilm = ScannerMetrics::time_ilm(lc_event.action.clone()); + // let time_ilm = ScannerMetrics::time_ilm(lc_event.action.clone()); if let Err(_err) = expire_transitioned_object(api, oi, lc_event, src).await { return false; } - let _ = time_ilm(1); + // let _ = time_ilm(1); true } @@ -1727,7 +1727,7 @@ pub async fn apply_expiry_on_non_transitioned_objects( opts.delete_prefix_object = true; } - let time_ilm = ScannerMetrics::time_ilm(lc_event.action.clone()); + // let time_ilm = ScannerMetrics::time_ilm(lc_event.action.clone()); let mut dobj = api .delete_object(&oi.bucket, &encode_dir_object(&oi.name), opts) @@ -1759,11 +1759,11 @@ pub async fn apply_expiry_on_non_transitioned_objects( }); if lc_event.action != lifecycle::IlmAction::NoneAction { - let mut num_versions = 1_u64; - if lc_event.action.delete_all() { - num_versions = oi.num_versions as u64; - } - let _ = time_ilm(num_versions); + // let mut num_versions = 1_u64; + // if lc_event.action.delete_all() { + // num_versions = oi.num_versions as u64; + // } + // let _ = time_ilm(num_versions); } true diff --git a/crates/ecstore/src/heal/mod.rs b/crates/ecstore/src/heal/mod.rs index a92bfc33b..0733d91d5 100644 --- a/crates/ecstore/src/heal/mod.rs +++ b/crates/ecstore/src/heal/mod.rs @@ -14,7 +14,7 @@ pub mod background_heal_ops; pub mod data_scanner; -pub mod data_scanner_metric; +// pub mod data_scanner_metric; pub mod data_usage; pub mod data_usage_cache; pub mod error; diff --git a/crates/ecstore/src/metrics_realtime.rs b/crates/ecstore/src/metrics_realtime.rs index 298ff846f..2363f1c0c 100644 --- a/crates/ecstore/src/metrics_realtime.rs +++ b/crates/ecstore/src/metrics_realtime.rs @@ -15,7 +15,10 @@ use std::collections::{HashMap, HashSet}; use chrono::Utc; -use rustfs_common::globals::{GLOBAL_Local_Node_Name, GLOBAL_Rustfs_Addr}; +use rustfs_common::{ + globals::{GLOBAL_Local_Node_Name, GLOBAL_Rustfs_Addr}, + metrics::globalMetrics, +}; use rustfs_madmin::metrics::{DiskIOStats, DiskMetric, RealtimeMetrics}; use rustfs_utils::os::get_drive_stats; use serde::{Deserialize, Serialize}; @@ -23,10 +26,7 @@ use tracing::info; use crate::{ admin_server_info::get_local_server_property, - heal::{ - data_scanner_metric::globalScannerMetrics, - heal_commands::{DRIVE_STATE_OK, DRIVE_STATE_UNFORMATTED}, - }, + heal::heal_commands::{DRIVE_STATE_OK, DRIVE_STATE_UNFORMATTED}, new_object_layer_fn, store_api::StorageAPI, // utils::os::get_drive_stats, @@ -108,7 +108,7 @@ pub async fn collect_local_metrics(types: MetricType, opts: &CollectMetricsOpts) if types.contains(&MetricType::SCANNER) { info!("start get scanner metrics"); - let metrics = globalScannerMetrics.report().await; + let metrics = globalMetrics.report().await; real_time_metrics.aggregated.scanner = Some(metrics); } diff --git a/rustfs/src/admin/mod.rs b/rustfs/src/admin/mod.rs index eb4ecf43d..4e24d7dc1 100644 --- a/rustfs/src/admin/mod.rs +++ b/rustfs/src/admin/mod.rs @@ -31,7 +31,8 @@ use router::{AdminOperation, S3Router}; use rpc::register_rpc_route; use s3s::route::S3Route; -const ADMIN_PREFIX: &str = "/minio/admin"; +const ADMIN_PREFIX: &str = "/rustfs/admin"; +// const ADMIN_PREFIX: &str = "/minio/admin"; pub fn make_admin_route(console_enabled: bool) -> std::io::Result { let mut r: S3Router = S3Router::new(console_enabled); From 3d3c6e4e06fd6b449697f45a37f99d71db244cc7 Mon Sep 17 00:00:00 2001 From: junxiang Mu <1948535941@qq.com> Date: Mon, 21 Jul 2025 18:16:29 +0800 Subject: [PATCH 13/29] chore(protos): update proto definitions, remove ns_scanner, fix codegen and formatting Signed-off-by: junxiang Mu <1948535941@qq.com> --- crates/protos/README.md | 6 ++ .../src/generated/proto_gen/node_service.rs | 72 ------------------- crates/protos/src/main.rs | 41 +++++++++-- crates/protos/src/node.proto | 14 ---- 4 files changed, 43 insertions(+), 90 deletions(-) diff --git a/crates/protos/README.md b/crates/protos/README.md index 73059598f..1dca836e0 100644 --- a/crates/protos/README.md +++ b/crates/protos/README.md @@ -28,6 +28,12 @@ - Type-safe message definitions - Code generation for multiple programming languages +## generate code + +``` +cargo run --bin gproto +``` + ## 📚 Documentation For comprehensive documentation, examples, and usage guides, please visit the main [RustFS repository](https://github.com/rustfs/rustfs). diff --git a/crates/protos/src/generated/proto_gen/node_service.rs b/crates/protos/src/generated/proto_gen/node_service.rs index 44f133792..b48cc3807 100644 --- a/crates/protos/src/generated/proto_gen/node_service.rs +++ b/crates/protos/src/generated/proto_gen/node_service.rs @@ -604,26 +604,6 @@ pub struct DiskInfoResponse { #[prost(message, optional, tag = "3")] pub error: ::core::option::Option, } -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct NsScannerRequest { - #[prost(string, tag = "1")] - pub disk: ::prost::alloc::string::String, - #[prost(string, tag = "2")] - pub cache: ::prost::alloc::string::String, - #[prost(uint64, tag = "3")] - pub scan_mode: u64, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct NsScannerResponse { - #[prost(bool, tag = "1")] - pub success: bool, - #[prost(string, tag = "2")] - pub update: ::prost::alloc::string::String, - #[prost(string, tag = "3")] - pub data_usage_cache: ::prost::alloc::string::String, - #[prost(message, optional, tag = "4")] - pub error: ::core::option::Option, -} /// lock api have same argument type #[derive(Clone, PartialEq, ::prost::Message)] pub struct GenerallyLockRequest { @@ -1660,21 +1640,6 @@ pub mod node_service_client { .insert(GrpcMethod::new("node_service.NodeService", "DiskInfo")); self.inner.unary(req, path, codec).await } - pub async fn ns_scanner( - &mut self, - request: impl tonic::IntoStreamingRequest, - ) -> std::result::Result>, tonic::Status> { - self.inner - .ready() - .await - .map_err(|e| tonic::Status::unknown(format!("Service was not ready: {}", e.into())))?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static("/node_service.NodeService/NsScanner"); - let mut req = request.into_streaming_request(); - req.extensions_mut() - .insert(GrpcMethod::new("node_service.NodeService", "NsScanner")); - self.inner.streaming(req, path, codec).await - } pub async fn lock( &mut self, request: impl tonic::IntoRequest, @@ -2466,14 +2431,6 @@ pub mod node_service_server { &self, request: tonic::Request, ) -> std::result::Result, tonic::Status>; - /// Server streaming response type for the NsScanner method. - type NsScannerStream: tonic::codegen::tokio_stream::Stream> - + std::marker::Send - + 'static; - async fn ns_scanner( - &self, - request: tonic::Request>, - ) -> std::result::Result, tonic::Status>; async fn lock( &self, request: tonic::Request, @@ -3670,35 +3627,6 @@ pub mod node_service_server { }; Box::pin(fut) } - "/node_service.NodeService/NsScanner" => { - #[allow(non_camel_case_types)] - struct NsScannerSvc(pub Arc); - impl tonic::server::StreamingService for NsScannerSvc { - type Response = super::NsScannerResponse; - type ResponseStream = T::NsScannerStream; - type Future = BoxFuture, tonic::Status>; - fn call(&mut self, request: tonic::Request>) -> Self::Future { - let inner = Arc::clone(&self.0); - let fut = async move { ::ns_scanner(&inner, request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let max_decoding_message_size = self.max_decoding_message_size; - let max_encoding_message_size = self.max_encoding_message_size; - let inner = self.inner.clone(); - let fut = async move { - let method = NsScannerSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec) - .apply_compression_config(accept_compression_encodings, send_compression_encodings) - .apply_max_message_size_config(max_decoding_message_size, max_encoding_message_size); - let res = grpc.streaming(method, req).await; - Ok(res) - }; - Box::pin(fut) - } "/node_service.NodeService/Lock" => { #[allow(non_camel_case_types)] struct LockSvc(pub Arc); diff --git a/crates/protos/src/main.rs b/crates/protos/src/main.rs index 889f28626..1a752bee4 100644 --- a/crates/protos/src/main.rs +++ b/crates/protos/src/main.rs @@ -16,7 +16,7 @@ use std::{cmp, env, fs, io::Write, path::Path, process::Command}; type AnyError = Box; -const VERSION_PROTOBUF: Version = Version(30, 2, 0); // 30.2.0 +const VERSION_PROTOBUF: Version = Version(27, 2, 0); // 27.2.0 const VERSION_FLATBUFFERS: Version = Version(24, 3, 25); // 24.3.25 /// Build protos if the major version of `flatc` or `protoc` is greater /// or lesser than the expected version. @@ -27,7 +27,7 @@ const ENV_FLATC_PATH: &str = "FLATC_PATH"; fn main() -> Result<(), AnyError> { let version = protobuf_compiler_version()?; let need_compile = match version.compare_ext(&VERSION_PROTOBUF) { - Ok(cmp::Ordering::Equal) => true, + 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."); @@ -47,6 +47,7 @@ fn main() -> Result<(), AnyError> { // path of proto file let project_root_dir = env::current_dir()?.join("crates/protos/src"); let proto_dir = project_root_dir.clone(); + println!("proto_dir: {proto_dir:?}"); let proto_files = &["node.proto"]; let proto_out_dir = project_root_dir.join("generated").join("proto_gen"); let flatbuffer_out_dir = project_root_dir.join("generated").join("flatbuffers_generated"); @@ -67,12 +68,44 @@ fn main() -> Result<(), AnyError> { let mut generated_mod_rs = fs::File::create(generated_mod_rs_path)?; writeln!(&mut generated_mod_rs, "pub mod node_service;")?; + writeln!( + &mut generated_mod_rs, + r#"// 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."# + )?; generated_mod_rs.flush()?; let generated_mod_rs_path = project_root_dir.join("generated").join("mod.rs"); let mut generated_mod_rs = fs::File::create(generated_mod_rs_path)?; writeln!(&mut generated_mod_rs, "#![allow(unused_imports)]")?; + writeln!( + &mut generated_mod_rs, + r#"// 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."# + )?; writeln!(&mut generated_mod_rs, "#![allow(clippy::all)]")?; writeln!(&mut generated_mod_rs, "pub mod proto_gen;")?; generated_mod_rs.flush()?; @@ -107,7 +140,7 @@ fn compile_flatbuffers_models, S: AsRef>( ) -> Result<(), AnyError> { let version = flatbuffers_compiler_version(flatc_path)?; let need_compile = match version.compare_ext(&VERSION_FLATBUFFERS) { - Ok(cmp::Ordering::Equal) => true, + 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."); @@ -217,7 +250,7 @@ impl Version { Ok(self.compare_major_version(expected_version)) } else { match self.compare_major_version(expected_version) { - cmp::Ordering::Equal => Ok(cmp::Ordering::Equal), + cmp::Ordering::Greater => Ok(cmp::Ordering::Greater), _ => Err(Self::build_error_message(self, expected_version).unwrap()), } } diff --git a/crates/protos/src/node.proto b/crates/protos/src/node.proto index b375f37bc..547e0e6be 100644 --- a/crates/protos/src/node.proto +++ b/crates/protos/src/node.proto @@ -423,19 +423,6 @@ message DiskInfoResponse { optional Error error = 3; } -message NsScannerRequest { - string disk = 1; - string cache = 2; - uint64 scan_mode = 3; -} - -message NsScannerResponse { - bool success = 1; - string update = 2; - string data_usage_cache = 3; - optional Error error = 4; -} - // lock api have same argument type message GenerallyLockRequest { string args = 1; @@ -805,7 +792,6 @@ service NodeService { rpc ReadMultiple(ReadMultipleRequest) returns (ReadMultipleResponse) {}; rpc DeleteVolume(DeleteVolumeRequest) returns (DeleteVolumeResponse) {}; rpc DiskInfo(DiskInfoRequest) returns (DiskInfoResponse) {}; - rpc NsScanner(stream NsScannerRequest) returns (stream NsScannerResponse) {}; /* -------------------------------lock service-------------------------- */ From ea210d52dc8b59702822a204fa2f20d6b6fa19d0 Mon Sep 17 00:00:00 2001 From: junxiang Mu <1948535941@qq.com> Date: Mon, 21 Jul 2025 18:16:42 +0800 Subject: [PATCH 14/29] refactor(heal): unify heal request interface, add disk field, update ahm/ecstore/common for erasure set healing Signed-off-by: junxiang Mu <1948535941@qq.com> --- Cargo.lock | 6 + crates/ahm/src/heal/channel.rs | 41 +- crates/ahm/src/heal/erasure_healer.rs | 10 +- crates/ahm/src/heal/manager.rs | 6 - crates/ahm/src/heal/storage.rs | 6 +- crates/ahm/src/heal/task.rs | 36 +- crates/ahm/src/scanner/data_scanner.rs | 17 +- crates/ahm/tests/heal_integration_test.rs | 14 +- crates/common/Cargo.toml | 6 + crates/common/src/data_usage.rs | 1281 +++++++++++++++++ crates/common/src/heal_channel.rs | 232 ++- crates/common/src/lib.rs | 1 + .../bucket/lifecycle/bucket_lifecycle_ops.rs | 169 ++- .../bucket/lifecycle/tier_last_day_stats.rs | 2 +- crates/ecstore/src/bucket/metadata_sys.rs | 2 +- crates/ecstore/src/data_usage.rs | 297 ++++ crates/ecstore/src/disk/mod.rs | 40 +- crates/ecstore/src/global.rs | 4 - crates/ecstore/src/heal/mod.rs | 16 +- crates/ecstore/src/lib.rs | 1 + crates/ecstore/src/metrics_realtime.rs | 4 +- crates/ecstore/src/pools.rs | 4 +- crates/ecstore/src/rpc/peer_rest_client.rs | 35 +- crates/ecstore/src/rpc/peer_s3_client.rs | 33 +- crates/ecstore/src/rpc/tonic_service.rs | 166 +-- crates/ecstore/src/sets.rs | 38 +- crates/ecstore/src/store.rs | 274 +--- crates/ecstore/src/store_api.rs | 8 +- crates/ecstore/src/store_init.rs | 9 +- 29 files changed, 2119 insertions(+), 639 deletions(-) create mode 100644 crates/common/src/data_usage.rs create mode 100644 crates/ecstore/src/data_usage.rs diff --git a/Cargo.lock b/Cargo.lock index b94029abf..683f5a05a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7966,8 +7966,14 @@ dependencies = [ name = "rustfs-common" version = "0.0.5" dependencies = [ + "async-trait", "chrono", + "path-clean", + "rmp-serde", + "rustfs-filemeta", "rustfs-madmin", + "s3s", + "serde", "tokio", "tonic", "uuid", diff --git a/crates/ahm/src/heal/channel.rs b/crates/ahm/src/heal/channel.rs index f9b59652b..ecfeae783 100644 --- a/crates/ahm/src/heal/channel.rs +++ b/crates/ahm/src/heal/channel.rs @@ -19,7 +19,7 @@ use crate::heal::{ }; use rustfs_common::heal_channel::{ - HealChannelCommand, HealChannelPriority, HealChannelReceiver, HealChannelRequest, HealChannelResponse, + HealChannelCommand, HealChannelPriority, HealChannelReceiver, HealChannelRequest, HealChannelResponse, HealScanMode, }; use std::sync::Arc; use tokio::sync::mpsc; @@ -173,15 +173,27 @@ impl HealChannelProcessor { /// Convert channel request to heal request fn convert_to_heal_request(&self, request: HealChannelRequest) -> Result { - let heal_type = match &request.object_prefix { - Some(prefix) if !prefix.is_empty() => HealType::Object { + let heal_type = if let Some(disk_id) = &request.disk { + HealType::ErasureSet { + buckets: vec![], + set_disk_id: disk_id.clone(), + } + } else if let Some(prefix) = &request.object_prefix { + if !prefix.is_empty() { + HealType::Object { + bucket: request.bucket.clone(), + object: prefix.clone(), + version_id: None, + } + } else { + HealType::Bucket { + bucket: request.bucket.clone(), + } + } + } else { + HealType::Bucket { bucket: request.bucket.clone(), - object: prefix.clone(), - version_id: None, - }, - _ => HealType::Bucket { - bucket: request.bucket.clone(), - }, + } }; let priority = match request.priority { @@ -191,18 +203,9 @@ impl HealChannelProcessor { HealChannelPriority::Critical => HealPriority::Urgent, }; - // Convert scan mode - let scan_mode = match request.scan_mode { - Some(rustfs_common::heal_channel::HealChannelScanMode::Normal) => { - rustfs_ecstore::heal::heal_commands::HEAL_NORMAL_SCAN - } - Some(rustfs_common::heal_channel::HealChannelScanMode::Deep) => rustfs_ecstore::heal::heal_commands::HEAL_DEEP_SCAN, - None => rustfs_ecstore::heal::heal_commands::HEAL_NORMAL_SCAN, - }; - // Build HealOptions with all available fields let mut options = HealOptions { - scan_mode, + scan_mode: request.scan_mode.unwrap_or(HealScanMode::Normal), remove_corrupted: request.remove_corrupted.unwrap_or(false), recreate_missing: request.recreate_missing.unwrap_or(true), update_parity: request.update_parity.unwrap_or(true), diff --git a/crates/ahm/src/heal/erasure_healer.rs b/crates/ahm/src/heal/erasure_healer.rs index 210648c41..f60d4afba 100644 --- a/crates/ahm/src/heal/erasure_healer.rs +++ b/crates/ahm/src/heal/erasure_healer.rs @@ -19,10 +19,8 @@ use crate::heal::{ storage::HealStorageAPI, }; use futures::future::join_all; -use rustfs_ecstore::{ - disk::DiskStore, - heal::heal_commands::{HealOpts, HEAL_NORMAL_SCAN}, -}; +use rustfs_common::heal_channel::{HealOpts, HealScanMode}; +use rustfs_ecstore::disk::DiskStore; use std::sync::Arc; use tokio::sync::RwLock; use tracing::{error, info, warn}; @@ -252,7 +250,7 @@ impl ErasureSetHealer { // heal object let heal_opts = HealOpts { - scan_mode: HEAL_NORMAL_SCAN, + scan_mode: HealScanMode::Normal, remove: true, recreate: true, ..Default::default() @@ -361,7 +359,7 @@ impl ErasureSetHealer { // 4. heal objects concurrently let heal_opts = HealOpts { - scan_mode: HEAL_NORMAL_SCAN, + scan_mode: HealScanMode::Normal, remove: true, // remove corrupted data recreate: true, // recreate missing data ..Default::default() diff --git a/crates/ahm/src/heal/manager.rs b/crates/ahm/src/heal/manager.rs index 180eaf665..358ad3668 100644 --- a/crates/ahm/src/heal/manager.rs +++ b/crates/ahm/src/heal/manager.rs @@ -288,12 +288,6 @@ impl HealManager { continue; } } - // disk currently healing and not finished - if let Some(h) = disk.healing().await { - if !h.finished { - endpoints.push(disk.endpoint()); - } - } } } diff --git a/crates/ahm/src/heal/storage.rs b/crates/ahm/src/heal/storage.rs index aa204307d..dcdf9781a 100644 --- a/crates/ahm/src/heal/storage.rs +++ b/crates/ahm/src/heal/storage.rs @@ -14,9 +14,9 @@ use crate::error::{Error, Result}; use async_trait::async_trait; +use rustfs_common::heal_channel::{HealOpts, HealScanMode}; use rustfs_ecstore::{ disk::{endpoint::Endpoint, DiskStore}, - heal::heal_commands::{HealOpts, HEAL_DEEP_SCAN, HEAL_NORMAL_SCAN}, store::ECStore, store_api::{BucketInfo, ObjectIO, StorageAPI}, }; @@ -238,7 +238,7 @@ impl HealStorageAPI for ECStoreHealStorage { dry_run: false, remove: false, recreate: true, - scan_mode: HEAL_DEEP_SCAN, + scan_mode: HealScanMode::Deep, update_parity: true, no_lock: false, pool: None, @@ -322,7 +322,7 @@ impl HealStorageAPI for ECStoreHealStorage { dry_run: false, remove: false, recreate: false, - scan_mode: HEAL_NORMAL_SCAN, + scan_mode: HealScanMode::Normal, update_parity: false, no_lock: false, pool: None, diff --git a/crates/ahm/src/heal/task.rs b/crates/ahm/src/heal/task.rs index 8e9d856d7..cf929b44e 100644 --- a/crates/ahm/src/heal/task.rs +++ b/crates/ahm/src/heal/task.rs @@ -13,9 +13,9 @@ // limitations under the License. use crate::error::{Error, Result}; -use crate::heal::{erasure_healer::ErasureSetHealer, progress::HealProgress, storage::HealStorageAPI}; -use rustfs_ecstore::heal::heal_commands::HealScanMode; -use rustfs_ecstore::heal::heal_commands::HEAL_NORMAL_SCAN; +use crate::heal::ErasureSetHealer; +use crate::heal::{progress::HealProgress, storage::HealStorageAPI}; +use rustfs_common::heal_channel::{HealOpts, HealScanMode}; use serde::{Deserialize, Serialize}; use std::sync::Arc; use std::time::{Duration, SystemTime}; @@ -93,7 +93,7 @@ pub struct HealOptions { impl Default for HealOptions { fn default() -> Self { Self { - scan_mode: HEAL_NORMAL_SCAN, + scan_mode: HealScanMode::Normal, remove_corrupted: false, recreate_missing: true, update_parity: true, @@ -324,7 +324,7 @@ impl HealTask { // Step 2: directly call ecstore to perform heal info!("Step 2: Performing heal using ecstore"); - let heal_opts = rustfs_ecstore::heal::heal_commands::HealOpts { + let heal_opts = HealOpts { recursive: self.options.recursive, dry_run: self.options.dry_run, remove: self.options.remove_corrupted, @@ -410,12 +410,12 @@ impl HealTask { info!("Attempting to recreate missing object: {}/{}", bucket, object); // Use ecstore's heal_object with recreate option - let heal_opts = rustfs_ecstore::heal::heal_commands::HealOpts { + let heal_opts = HealOpts { recursive: false, dry_run: self.options.dry_run, remove: false, recreate: true, - scan_mode: rustfs_ecstore::heal::heal_commands::HEAL_DEEP_SCAN, + scan_mode: HealScanMode::Deep, update_parity: true, no_lock: false, pool: None, @@ -476,7 +476,7 @@ impl HealTask { // Step 2: Perform bucket heal using ecstore info!("Step 2: Performing bucket heal using ecstore"); - let heal_opts = rustfs_ecstore::heal::heal_commands::HealOpts { + let heal_opts = HealOpts { recursive: self.options.recursive, dry_run: self.options.dry_run, remove: self.options.remove_corrupted, @@ -538,12 +538,12 @@ impl HealTask { // Step 2: Perform metadata heal using ecstore info!("Step 2: Performing metadata heal using ecstore"); - let heal_opts = rustfs_ecstore::heal::heal_commands::HealOpts { + let heal_opts = HealOpts { recursive: false, dry_run: self.options.dry_run, remove: false, recreate: false, - scan_mode: rustfs_ecstore::heal::heal_commands::HEAL_DEEP_SCAN, + scan_mode: HealScanMode::Deep, update_parity: false, no_lock: false, pool: self.options.pool_index, @@ -612,12 +612,12 @@ impl HealTask { // Step 1: Perform MRF heal using ecstore info!("Step 1: Performing MRF heal using ecstore"); - let heal_opts = rustfs_ecstore::heal::heal_commands::HealOpts { + let heal_opts = HealOpts { recursive: true, dry_run: self.options.dry_run, remove: self.options.remove_corrupted, recreate: self.options.recreate_missing, - scan_mode: rustfs_ecstore::heal::heal_commands::HEAL_DEEP_SCAN, + scan_mode: HealScanMode::Deep, update_parity: true, no_lock: false, pool: None, @@ -685,12 +685,12 @@ impl HealTask { // Step 2: Perform EC decode heal using ecstore info!("Step 2: Performing EC decode heal using ecstore"); - let heal_opts = rustfs_ecstore::heal::heal_commands::HealOpts { + let heal_opts = HealOpts { recursive: false, dry_run: self.options.dry_run, remove: false, recreate: true, - scan_mode: rustfs_ecstore::heal::heal_commands::HEAL_DEEP_SCAN, + scan_mode: HealScanMode::Deep, update_parity: true, no_lock: false, pool: None, @@ -748,6 +748,14 @@ impl HealTask { progress.update_progress(0, 4, 0, 0); } + let buckets = if buckets.is_empty() { + info!("No buckets specified, listing all buckets"); + let bucket_infos = self.storage.list_buckets().await?; + bucket_infos.into_iter().map(|info| info.name).collect() + } else { + buckets + }; + // Step 1: Perform disk format heal using ecstore info!("Step 1: Performing disk format heal using ecstore"); match self.storage.heal_format(self.options.dry_run).await { diff --git a/crates/ahm/src/scanner/data_scanner.rs b/crates/ahm/src/scanner/data_scanner.rs index a98e1f9f8..037879268 100644 --- a/crates/ahm/src/scanner/data_scanner.rs +++ b/crates/ahm/src/scanner/data_scanner.rs @@ -22,22 +22,22 @@ use ecstore::{ disk::{DiskAPI, DiskStore, WalkDirOptions}, set_disk::SetDisks, }; -use rustfs_ecstore::{self as ecstore, StorageAPI}; +use rustfs_ecstore::{self as ecstore, data_usage::store_data_usage_in_backend, StorageAPI}; use rustfs_filemeta::MetacacheReader; use tokio::sync::{Mutex, RwLock}; use tokio_util::sync::CancellationToken; use tracing::{debug, error, info, warn}; -use super::{ - data_usage::DataUsageInfo, - metrics::{BucketMetrics, DiskMetrics, MetricsCollector, ScannerMetrics}, -}; +use super::metrics::{BucketMetrics, DiskMetrics, MetricsCollector, ScannerMetrics}; use crate::heal::HealManager; use crate::{ error::{Error, Result}, get_ahm_services_cancel_token, HealRequest, }; -use rustfs_common::metrics::{globalMetrics, Metric, Metrics}; +use rustfs_common::{ + data_usage::DataUsageInfo, + metrics::{globalMetrics, Metric, Metrics}, +}; use rustfs_ecstore::disk::RUSTFS_META_BUCKET; @@ -1182,7 +1182,7 @@ impl Scanner { // Offload persistence to background task let data_clone = data_usage.clone(); tokio::spawn(async move { - if let Err(e) = super::data_usage::store_data_usage_in_backend(data_clone, store).await { + if let Err(e) = store_data_usage_in_backend(data_clone, store).await { error!("Failed to store data usage statistics to backend: {}", e); } else { info!("Successfully stored data usage statistics to backend"); @@ -1214,6 +1214,7 @@ impl Scanner { #[cfg(test)] mod tests { use super::*; + use rustfs_ecstore::data_usage::load_data_usage_from_backend; use rustfs_ecstore::disk::endpoint::Endpoint; use rustfs_ecstore::endpoints::{EndpointServerPools, Endpoints, PoolEndpoints}; use rustfs_ecstore::store::ECStore; @@ -1441,7 +1442,7 @@ mod tests { // verify correctness of persisted data tokio::time::sleep(std::time::Duration::from_millis(100)).await; - let persisted = crate::scanner::data_usage::load_data_usage_from_backend(ecstore.clone()) + let persisted = load_data_usage_from_backend(ecstore.clone()) .await .expect("load persisted usage"); assert_eq!(persisted.objects_total_count, du_after.objects_total_count); diff --git a/crates/ahm/tests/heal_integration_test.rs b/crates/ahm/tests/heal_integration_test.rs index f5dbb0c76..edda32dc4 100644 --- a/crates/ahm/tests/heal_integration_test.rs +++ b/crates/ahm/tests/heal_integration_test.rs @@ -3,10 +3,10 @@ use rustfs_ahm::heal::{ storage::{ECStoreHealStorage, HealStorageAPI}, task::{HealOptions, HealPriority, HealRequest, HealTaskStatus, HealType}, }; +use rustfs_common::heal_channel::{HealOpts, HealScanMode}; use rustfs_ecstore::{ disk::endpoint::Endpoint, endpoints::{EndpointServerPools, Endpoints, PoolEndpoints}, - heal::heal_commands::HEAL_NORMAL_SCAN, store::ECStore, store_api::{ObjectIO, ObjectOptions, PutObjReader, StorageAPI}, }; @@ -175,7 +175,7 @@ async fn test_heal_object_basic() { recursive: false, remove_corrupted: false, recreate_missing: true, - scan_mode: HEAL_NORMAL_SCAN, + scan_mode: HealScanMode::Normal, update_parity: true, timeout: Some(Duration::from_secs(300)), pool_index: None, @@ -240,7 +240,7 @@ async fn test_heal_bucket_basic() { recursive: true, remove_corrupted: false, recreate_missing: false, - scan_mode: HEAL_NORMAL_SCAN, + scan_mode: HealScanMode::Normal, update_parity: false, timeout: Some(Duration::from_secs(300)), pool_index: None, @@ -367,12 +367,12 @@ async fn test_heal_storage_api_direct() { let bucket_name = "test-bucket-direct"; create_test_bucket(&ecstore, bucket_name).await; - let heal_opts = rustfs_ecstore::heal::heal_commands::HealOpts { + let heal_opts = HealOpts { recursive: true, dry_run: true, remove: false, recreate: false, - scan_mode: rustfs_ecstore::heal::heal_commands::HEAL_NORMAL_SCAN, + scan_mode: HealScanMode::Normal, update_parity: false, no_lock: false, pool: None, @@ -388,12 +388,12 @@ async fn test_heal_storage_api_direct() { 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 = rustfs_ecstore::heal::heal_commands::HealOpts { + let object_heal_opts = HealOpts { recursive: false, dry_run: true, remove: false, recreate: false, - scan_mode: rustfs_ecstore::heal::heal_commands::HEAL_NORMAL_SCAN, + scan_mode: HealScanMode::Normal, update_parity: false, no_lock: false, pool: None, diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index daba9456e..88c8a3f4b 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -33,3 +33,9 @@ tonic = { workspace = true } uuid = { workspace = true } chrono = { workspace = true } rustfs-madmin = { workspace = true } +rustfs-filemeta = { workspace = true } +serde = { workspace = true } +path-clean = { workspace = true } +rmp-serde = { workspace = true } +async-trait = { workspace = true } +s3s = { workspace = true } diff --git a/crates/common/src/data_usage.rs b/crates/common/src/data_usage.rs new file mode 100644 index 000000000..b9b93b690 --- /dev/null +++ b/crates/common/src/data_usage.rs @@ -0,0 +1,1281 @@ +// 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 path_clean::PathClean; +use serde::{Deserialize, Serialize}; +use std::hash::{DefaultHasher, Hash, Hasher}; +use std::path::Path; +use std::{ + collections::{HashMap, HashSet}, + time::SystemTime, +}; + +#[derive(Clone, Copy, Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct TierStats { + pub total_size: u64, + pub num_versions: i32, + pub num_objects: i32, +} + +impl TierStats { + pub fn add(&self, u: &TierStats) -> TierStats { + TierStats { + total_size: self.total_size + u.total_size, + num_versions: self.num_versions + u.num_versions, + num_objects: self.num_objects + u.num_objects, + } + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct AllTierStats { + pub tiers: HashMap, +} + +impl AllTierStats { + pub fn new() -> Self { + Self { tiers: HashMap::new() } + } + + pub fn add_sizes(&mut self, tiers: HashMap) { + for (tier, st) in tiers { + self.tiers + .insert(tier.clone(), self.tiers.get(&tier).unwrap_or(&TierStats::default()).add(&st)); + } + } + + pub fn merge(&mut self, other: AllTierStats) { + for (tier, st) in other.tiers { + self.tiers + .insert(tier.clone(), self.tiers.get(&tier).unwrap_or(&TierStats::default()).add(&st)); + } + } + + pub fn populate_stats(&self, stats: &mut HashMap) { + for (tier, st) in &self.tiers { + stats.insert( + tier.clone(), + TierStats { + total_size: st.total_size, + num_versions: st.num_versions, + num_objects: st.num_objects, + }, + ); + } + } +} + +/// Bucket target usage info provides replication statistics +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct BucketTargetUsageInfo { + pub replication_pending_size: u64, + pub replication_failed_size: u64, + pub replicated_size: u64, + pub replica_size: u64, + pub replication_pending_count: u64, + pub replication_failed_count: u64, + pub replicated_count: u64, +} + +/// Bucket usage info provides bucket-level statistics +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct BucketUsageInfo { + pub size: u64, + // Following five fields suffixed with V1 are here for backward compatibility + // Total Size for objects that have not yet been replicated + pub replication_pending_size_v1: u64, + // Total size for objects that have witness one or more failures and will be retried + pub replication_failed_size_v1: u64, + // Total size for objects that have been replicated to destination + pub replicated_size_v1: u64, + // Total number of objects pending replication + pub replication_pending_count_v1: u64, + // Total number of objects that failed replication + pub replication_failed_count_v1: u64, + + pub objects_count: u64, + pub object_size_histogram: HashMap, + pub object_versions_histogram: HashMap, + pub versions_count: u64, + pub delete_markers_count: u64, + pub replica_size: u64, + pub replica_count: u64, + pub replication_info: HashMap, +} + +/// DataUsageInfo represents data usage stats of the underlying storage +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct DataUsageInfo { + /// Total capacity + pub total_capacity: u64, + /// Total used capacity + pub total_used_capacity: u64, + /// Total free capacity + pub total_free_capacity: u64, + + /// LastUpdate is the timestamp of when the data usage info was last updated + pub last_update: Option, + + /// Objects total count across all buckets + pub objects_total_count: u64, + /// Versions total count across all buckets + pub versions_total_count: u64, + /// Delete markers total count across all buckets + pub delete_markers_total_count: u64, + /// Objects total size across all buckets + pub objects_total_size: u64, + /// Replication info across all buckets + pub replication_info: HashMap, + + /// Total number of buckets in this cluster + pub buckets_count: u64, + /// Buckets usage info provides following information across all buckets + pub buckets_usage: HashMap, + /// Deprecated kept here for backward compatibility reasons + pub bucket_sizes: HashMap, +} + +/// Size summary for a single object or group of objects +#[derive(Debug, Default, Clone)] +pub struct SizeSummary { + /// Total size + pub total_size: usize, + /// Number of versions + pub versions: usize, + /// Number of delete markers + pub delete_markers: usize, + /// Replicated size + pub replicated_size: usize, + /// Replicated count + pub replicated_count: usize, + /// Pending size + pub pending_size: usize, + /// Failed size + pub failed_size: usize, + /// Replica size + pub replica_size: usize, + /// Replica count + pub replica_count: usize, + /// Pending count + pub pending_count: usize, + /// Failed count + pub failed_count: usize, + /// Replication target stats + pub repl_target_stats: HashMap, +} + +/// Replication target size summary +#[derive(Debug, Default, Clone)] +pub struct ReplTargetSizeSummary { + /// Replicated size + pub replicated_size: usize, + /// Replicated count + pub replicated_count: usize, + /// Pending size + pub pending_size: usize, + /// Failed size + pub failed_size: usize, + /// Pending count + pub pending_count: usize, + /// Failed count + pub failed_count: usize, +} + +// ===== 缓存相关数据结构 ===== + +/// Data usage hash for path-based caching +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct DataUsageHash(pub String); + +impl DataUsageHash { + pub fn string(&self) -> String { + self.0.clone() + } + + pub fn key(&self) -> String { + self.0.clone() + } + + pub fn mod_(&self, cycle: u32, cycles: u32) -> bool { + if cycles <= 1 { + return cycles == 1; + } + + let hash = self.calculate_hash(); + hash as u32 % cycles == cycle % cycles + } + + pub fn mod_alt(&self, cycle: u32, cycles: u32) -> bool { + if cycles <= 1 { + return cycles == 1; + } + + let hash = self.calculate_hash(); + (hash >> 32) as u32 % cycles == cycle % cycles + } + + fn calculate_hash(&self) -> u64 { + let mut hasher = DefaultHasher::new(); + self.0.hash(&mut hasher); + hasher.finish() + } +} + +/// Data usage hash map type +pub type DataUsageHashMap = HashSet; + +/// Size histogram for object size distribution +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SizeHistogram(Vec); + +impl Default for SizeHistogram { + fn default() -> Self { + Self(vec![0; 11]) // DATA_USAGE_BUCKET_LEN = 11 + } +} + +impl SizeHistogram { + pub fn add(&mut self, size: u64) { + let intervals = [ + (0, 1024), // LESS_THAN_1024_B + (1024, 64 * 1024 - 1), // BETWEEN_1024_B_AND_64_KB + (64 * 1024, 256 * 1024 - 1), // BETWEEN_64_KB_AND_256_KB + (256 * 1024, 512 * 1024 - 1), // BETWEEN_256_KB_AND_512_KB + (512 * 1024, 1024 * 1024 - 1), // BETWEEN_512_KB_AND_1_MB + (1024, 1024 * 1024 - 1), // BETWEEN_1024B_AND_1_MB + (1024 * 1024, 10 * 1024 * 1024 - 1), // BETWEEN_1_MB_AND_10_MB + (10 * 1024 * 1024, 64 * 1024 * 1024 - 1), // BETWEEN_10_MB_AND_64_MB + (64 * 1024 * 1024, 128 * 1024 * 1024 - 1), // BETWEEN_64_MB_AND_128_MB + (128 * 1024 * 1024, 512 * 1024 * 1024 - 1), // BETWEEN_128_MB_AND_512_MB + (512 * 1024 * 1024, u64::MAX), // GREATER_THAN_512_MB + ]; + + for (idx, (start, end)) in intervals.iter().enumerate() { + if size >= *start && size <= *end { + self.0[idx] += 1; + break; + } + } + } + + pub fn to_map(&self) -> HashMap { + let names = [ + "LESS_THAN_1024_B", + "BETWEEN_1024_B_AND_64_KB", + "BETWEEN_64_KB_AND_256_KB", + "BETWEEN_256_KB_AND_512_KB", + "BETWEEN_512_KB_AND_1_MB", + "BETWEEN_1024B_AND_1_MB", + "BETWEEN_1_MB_AND_10_MB", + "BETWEEN_10_MB_AND_64_MB", + "BETWEEN_64_MB_AND_128_MB", + "BETWEEN_128_MB_AND_512_MB", + "GREATER_THAN_512_MB", + ]; + + let mut res = HashMap::new(); + let mut spl_count = 0; + for (count, name) in self.0.iter().zip(names.iter()) { + if name == &"BETWEEN_1024B_AND_1_MB" { + res.insert(name.to_string(), spl_count); + } else if name.starts_with("BETWEEN_") && name.contains("_KB_") && name.contains("_MB") { + spl_count += count; + res.insert(name.to_string(), *count); + } else { + res.insert(name.to_string(), *count); + } + } + res + } +} + +/// Versions histogram for version count distribution +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct VersionsHistogram(Vec); + +impl Default for VersionsHistogram { + fn default() -> Self { + Self(vec![0; 7]) // DATA_USAGE_VERSION_LEN = 7 + } +} + +impl VersionsHistogram { + pub fn add(&mut self, count: u64) { + let intervals = [ + (0, 0), // UNVERSIONED + (1, 1), // SINGLE_VERSION + (2, 9), // BETWEEN_2_AND_10 + (10, 99), // BETWEEN_10_AND_100 + (100, 999), // BETWEEN_100_AND_1000 + (1000, 9999), // BETWEEN_1000_AND_10000 + (10000, u64::MAX), // GREATER_THAN_10000 + ]; + + for (idx, (start, end)) in intervals.iter().enumerate() { + if count >= *start && count <= *end { + self.0[idx] += 1; + break; + } + } + } + + pub fn to_map(&self) -> HashMap { + let names = [ + "UNVERSIONED", + "SINGLE_VERSION", + "BETWEEN_2_AND_10", + "BETWEEN_10_AND_100", + "BETWEEN_100_AND_1000", + "BETWEEN_1000_AND_10000", + "GREATER_THAN_10000", + ]; + + let mut res = HashMap::new(); + for (count, name) in self.0.iter().zip(names.iter()) { + res.insert(name.to_string(), *count); + } + res + } +} + +/// Replication statistics for a single target +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct ReplicationStats { + pub pending_size: u64, + pub replicated_size: u64, + pub failed_size: u64, + pub failed_count: u64, + pub pending_count: u64, + pub missed_threshold_size: u64, + pub after_threshold_size: u64, + pub missed_threshold_count: u64, + pub after_threshold_count: u64, + pub replicated_count: u64, +} + +impl ReplicationStats { + pub fn empty(&self) -> bool { + self.replicated_size == 0 && self.failed_size == 0 && self.failed_count == 0 + } +} + +/// Replication statistics for all targets +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct ReplicationAllStats { + pub targets: HashMap, + pub replica_size: u64, + pub replica_count: u64, +} + +impl ReplicationAllStats { + pub fn empty(&self) -> bool { + if self.replica_size != 0 && self.replica_count != 0 { + return false; + } + for (_, v) in self.targets.iter() { + if !v.empty() { + return false; + } + } + true + } +} + +/// Data usage cache entry +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct DataUsageEntry { + pub children: DataUsageHashMap, + // These fields do not include any children. + pub size: usize, + pub objects: usize, + pub versions: usize, + pub delete_markers: usize, + pub obj_sizes: SizeHistogram, + pub obj_versions: VersionsHistogram, + pub replication_stats: Option, + pub compacted: bool, +} + +impl DataUsageEntry { + pub fn add_child(&mut self, hash: &DataUsageHash) { + if self.children.contains(&hash.key()) { + return; + } + self.children.insert(hash.key()); + } + + pub fn add_sizes(&mut self, summary: &SizeSummary) { + self.size += summary.total_size; + self.versions += summary.versions; + self.delete_markers += summary.delete_markers; + self.obj_sizes.add(summary.total_size as u64); + self.obj_versions.add(summary.versions as u64); + + let replication_stats = if self.replication_stats.is_none() { + self.replication_stats = Some(ReplicationAllStats::default()); + self.replication_stats.as_mut().unwrap() + } else { + self.replication_stats.as_mut().unwrap() + }; + replication_stats.replica_size += summary.replica_size as u64; + replication_stats.replica_count += summary.replica_count as u64; + + for (arn, st) in &summary.repl_target_stats { + let tgt_stat = replication_stats + .targets + .entry(arn.to_string()) + .or_insert(ReplicationStats::default()); + tgt_stat.pending_size += st.pending_size as u64; + tgt_stat.failed_size += st.failed_size as u64; + tgt_stat.replicated_size += st.replicated_size as u64; + tgt_stat.replicated_count += st.replicated_count as u64; + tgt_stat.failed_count += st.failed_count as u64; + tgt_stat.pending_count += st.pending_count as u64; + } + } + + pub fn merge(&mut self, other: &DataUsageEntry) { + self.objects += other.objects; + self.versions += other.versions; + self.delete_markers += other.delete_markers; + self.size += other.size; + + if let Some(o_rep) = &other.replication_stats { + if self.replication_stats.is_none() { + self.replication_stats = Some(ReplicationAllStats::default()); + } + let s_rep = self.replication_stats.as_mut().unwrap(); + s_rep.targets.clear(); + s_rep.replica_size += o_rep.replica_size; + s_rep.replica_count += o_rep.replica_count; + for (arn, stat) in o_rep.targets.iter() { + let st = s_rep.targets.entry(arn.clone()).or_default(); + *st = ReplicationStats { + pending_size: stat.pending_size + st.pending_size, + failed_size: stat.failed_size + st.failed_size, + replicated_size: stat.replicated_size + st.replicated_size, + pending_count: stat.pending_count + st.pending_count, + failed_count: stat.failed_count + st.failed_count, + replicated_count: stat.replicated_count + st.replicated_count, + ..Default::default() + }; + } + } + + for (i, v) in other.obj_sizes.0.iter().enumerate() { + self.obj_sizes.0[i] += v; + } + + for (i, v) in other.obj_versions.0.iter().enumerate() { + self.obj_versions.0[i] += v; + } + } +} + +/// Data usage cache info +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct DataUsageCacheInfo { + pub name: String, + pub next_cycle: u32, + pub last_update: Option, + pub skip_healing: bool, +} + +/// Data usage cache +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct DataUsageCache { + pub info: DataUsageCacheInfo, + pub cache: HashMap, +} + +impl DataUsageCache { + pub fn replace(&mut self, path: &str, parent: &str, e: DataUsageEntry) { + let hash = hash_path(path); + self.cache.insert(hash.key(), e); + if !parent.is_empty() { + let phash = hash_path(parent); + let p = { + let p = self.cache.entry(phash.key()).or_default(); + p.add_child(&hash); + p.clone() + }; + self.cache.insert(phash.key(), p); + } + } + + pub fn replace_hashed(&mut self, hash: &DataUsageHash, parent: &Option, e: &DataUsageEntry) { + self.cache.insert(hash.key(), e.clone()); + if let Some(parent) = parent { + self.cache.entry(parent.key()).or_default().add_child(hash); + } + } + + pub fn find(&self, path: &str) -> Option { + self.cache.get(&hash_path(path).key()).cloned() + } + + pub fn find_children_copy(&mut self, h: DataUsageHash) -> DataUsageHashMap { + self.cache.entry(h.string()).or_default().children.clone() + } + + pub fn flatten(&self, root: &DataUsageEntry) -> DataUsageEntry { + let mut root = root.clone(); + for id in root.children.clone().iter() { + if let Some(e) = self.cache.get(id) { + let mut e = e.clone(); + if !e.children.is_empty() { + e = self.flatten(&e); + } + root.merge(&e); + } + } + root.children.clear(); + root + } + + pub fn copy_with_children(&mut self, src: &DataUsageCache, hash: &DataUsageHash, parent: &Option) { + if let Some(e) = src.cache.get(&hash.string()) { + self.cache.insert(hash.key(), e.clone()); + for ch in e.children.iter() { + if *ch == hash.key() { + return; + } + self.copy_with_children(src, &DataUsageHash(ch.to_string()), &Some(hash.clone())); + } + if let Some(parent) = parent { + let p = self.cache.entry(parent.key()).or_default(); + p.add_child(hash); + } + } + } + + pub fn delete_recursive(&mut self, hash: &DataUsageHash) { + let mut need_remove = Vec::new(); + if let Some(v) = self.cache.get(&hash.string()) { + for child in v.children.iter() { + need_remove.push(child.clone()); + } + } + self.cache.remove(&hash.string()); + need_remove.iter().for_each(|child| { + self.delete_recursive(&DataUsageHash(child.to_string())); + }); + } + + pub fn size_recursive(&self, path: &str) -> Option { + match self.find(path) { + Some(root) => { + if root.children.is_empty() { + return Some(root); + } + let mut flat = self.flatten(&root); + if flat.replication_stats.is_some() && flat.replication_stats.as_ref().unwrap().empty() { + flat.replication_stats = None; + } + Some(flat) + } + None => None, + } + } + + pub fn search_parent(&self, hash: &DataUsageHash) -> Option { + let want = hash.key(); + if let Some(last_index) = want.rfind('/') { + if let Some(v) = self.find(&want[0..last_index]) { + if v.children.contains(&want) { + let found = hash_path(&want[0..last_index]); + return Some(found); + } + } + } + + for (k, v) in self.cache.iter() { + if v.children.contains(&want) { + let found = DataUsageHash(k.clone()); + return Some(found); + } + } + None + } + + pub fn is_compacted(&self, hash: &DataUsageHash) -> bool { + match self.cache.get(&hash.key()) { + Some(due) => due.compacted, + None => false, + } + } + + pub fn force_compact(&mut self, limit: usize) { + if self.cache.len() < limit { + return; + } + let top = hash_path(&self.info.name).key(); + let top_e = match self.find(&top) { + Some(e) => e, + None => return, + }; + // Note: DATA_SCANNER_FORCE_COMPACT_AT_FOLDERS constant would need to be passed as parameter + // or defined in common crate if needed + if top_e.children.len() > 250_000 { + // DATA_SCANNER_FORCE_COMPACT_AT_FOLDERS + self.reduce_children_of(&hash_path(&self.info.name), limit, true); + } + if self.cache.len() <= limit { + return; + } + + let mut found = HashSet::new(); + found.insert(top); + mark(self, &top_e, &mut found); + self.cache.retain(|k, _| { + if !found.contains(k) { + return false; + } + true + }); + } + + pub fn reduce_children_of(&mut self, path: &DataUsageHash, limit: usize, compact_self: bool) { + let e = match self.cache.get(&path.key()) { + Some(e) => e, + None => return, + }; + + if e.compacted { + return; + } + + if e.children.len() > limit && compact_self { + let mut flat = self.size_recursive(&path.key()).unwrap_or_default(); + flat.compacted = true; + self.delete_recursive(path); + self.replace_hashed(path, &None, &flat); + return; + } + let total = self.total_children_rec(&path.key()); + if total < limit { + return; + } + + let mut leaves = Vec::new(); + let mut remove = total - limit; + add(self, path, &mut leaves); + leaves.sort_by(|a, b| a.objects.cmp(&b.objects)); + + while remove > 0 && !leaves.is_empty() { + let e = leaves.first().unwrap(); + let candidate = e.path.clone(); + if candidate == *path && !compact_self { + break; + } + let removing = self.total_children_rec(&candidate.key()); + let mut flat = match self.size_recursive(&candidate.key()) { + Some(flat) => flat, + None => { + leaves.remove(0); + continue; + } + }; + + flat.compacted = true; + self.delete_recursive(&candidate); + self.replace_hashed(&candidate, &None, &flat); + + remove -= removing; + leaves.remove(0); + } + } + + pub fn total_children_rec(&self, path: &str) -> usize { + let root = self.find(path); + + if root.is_none() { + return 0; + } + let root = root.unwrap(); + if root.children.is_empty() { + return 0; + } + + let mut n = root.children.len(); + for ch in root.children.iter() { + n += self.total_children_rec(ch); + } + n + } + + pub fn merge(&mut self, o: &DataUsageCache) { + let mut existing_root = self.root(); + let other_root = o.root(); + if existing_root.is_none() && other_root.is_none() { + return; + } + if other_root.is_none() { + return; + } + if existing_root.is_none() { + *self = o.clone(); + return; + } + if o.info.last_update.gt(&self.info.last_update) { + self.info.last_update = o.info.last_update; + } + + existing_root.as_mut().unwrap().merge(other_root.as_ref().unwrap()); + self.cache.insert(hash_path(&self.info.name).key(), existing_root.unwrap()); + let e_hash = self.root_hash(); + for key in other_root.as_ref().unwrap().children.iter() { + let entry = &o.cache[key]; + let flat = o.flatten(entry); + let mut existing = self.cache[key].clone(); + existing.merge(&flat); + self.replace_hashed(&DataUsageHash(key.clone()), &Some(e_hash.clone()), &existing); + } + } + + pub fn root_hash(&self) -> DataUsageHash { + hash_path(&self.info.name) + } + + pub fn root(&self) -> Option { + self.find(&self.info.name) + } + + /// Convert cache to DataUsageInfo for a specific path + pub fn dui(&self, path: &str, buckets: &[String]) -> DataUsageInfo { + let e = match self.find(path) { + Some(e) => e, + None => return DataUsageInfo::default(), + }; + let flat = self.flatten(&e); + + let mut buckets_usage = HashMap::new(); + for bucket_name in buckets.iter() { + let e = match self.find(bucket_name) { + Some(e) => e, + None => continue, + }; + let flat = self.flatten(&e); + let mut bui = BucketUsageInfo { + size: flat.size as u64, + versions_count: flat.versions as u64, + objects_count: flat.objects as u64, + delete_markers_count: flat.delete_markers as u64, + object_size_histogram: flat.obj_sizes.to_map(), + object_versions_histogram: flat.obj_versions.to_map(), + ..Default::default() + }; + + if let Some(rs) = &flat.replication_stats { + bui.replica_size = rs.replica_size; + bui.replica_count = rs.replica_count; + + for (arn, stat) in rs.targets.iter() { + bui.replication_info.insert( + arn.clone(), + BucketTargetUsageInfo { + replication_pending_size: stat.pending_size, + replicated_size: stat.replicated_size, + replication_failed_size: stat.failed_size, + replication_pending_count: stat.pending_count, + replication_failed_count: stat.failed_count, + replicated_count: stat.replicated_count, + ..Default::default() + }, + ); + } + } + buckets_usage.insert(bucket_name.clone(), bui); + } + + DataUsageInfo { + last_update: self.info.last_update, + objects_total_count: flat.objects as u64, + versions_total_count: flat.versions as u64, + delete_markers_total_count: flat.delete_markers as u64, + objects_total_size: flat.size as u64, + buckets_count: e.children.len() as u64, + buckets_usage, + ..Default::default() + } + } + + pub fn marshal_msg(&self) -> Result, Box> { + let mut buf = Vec::new(); + self.serialize(&mut rmp_serde::Serializer::new(&mut buf))?; + Ok(buf) + } + + pub fn unmarshal(buf: &[u8]) -> Result> { + let t: Self = rmp_serde::from_slice(buf)?; + Ok(t) + } + + // Note: load and save methods are storage-specific and should be implemented + // in the ecstore crate where storage access is available +} + +/// Trait for storage-specific operations on DataUsageCache +#[async_trait::async_trait] +pub trait DataUsageCacheStorage { + /// Load data usage cache from backend storage + async fn load(store: &dyn std::any::Any, name: &str) -> Result> + where + Self: Sized; + + /// Save data usage cache to backend storage + async fn save(&self, name: &str) -> Result<(), Box>; +} + +// Helper structs and functions for cache operations +#[derive(Default, Clone)] +struct Inner { + objects: usize, + path: DataUsageHash, +} + +fn add(data_usage_cache: &DataUsageCache, path: &DataUsageHash, leaves: &mut Vec) { + let e = match data_usage_cache.cache.get(&path.key()) { + Some(e) => e, + None => return, + }; + if !e.children.is_empty() { + return; + } + + let sz = data_usage_cache.size_recursive(&path.key()).unwrap_or_default(); + leaves.push(Inner { + objects: sz.objects, + path: path.clone(), + }); + for ch in e.children.iter() { + add(data_usage_cache, &DataUsageHash(ch.clone()), leaves); + } +} + +fn mark(duc: &DataUsageCache, entry: &DataUsageEntry, found: &mut HashSet) { + for k in entry.children.iter() { + found.insert(k.to_string()); + if let Some(ch) = duc.cache.get(k) { + mark(duc, ch, found); + } + } +} + +/// Hash a path for data usage caching +pub fn hash_path(data: &str) -> DataUsageHash { + DataUsageHash(Path::new(&data).clean().to_string_lossy().to_string()) +} + +impl DataUsageInfo { + /// Create a new DataUsageInfo + pub fn new() -> Self { + Self::default() + } + + /// Add object metadata to data usage statistics + pub fn add_object(&mut self, object_path: &str, meta_object: &rustfs_filemeta::MetaObject) { + // This method is kept for backward compatibility + // For accurate version counting, use add_object_from_file_meta instead + let bucket_name = match self.extract_bucket_from_path(object_path) { + Ok(name) => name, + Err(_) => return, + }; + + // Update bucket statistics + if let Some(bucket_usage) = self.buckets_usage.get_mut(&bucket_name) { + bucket_usage.size += meta_object.size as u64; + bucket_usage.objects_count += 1; + bucket_usage.versions_count += 1; // Simplified: assume 1 version per object + + // Update size histogram + let total_size = meta_object.size as u64; + let size_ranges = [ + ("0-1KB", 0, 1024), + ("1KB-1MB", 1024, 1024 * 1024), + ("1MB-10MB", 1024 * 1024, 10 * 1024 * 1024), + ("10MB-100MB", 10 * 1024 * 1024, 100 * 1024 * 1024), + ("100MB-1GB", 100 * 1024 * 1024, 1024 * 1024 * 1024), + ("1GB+", 1024 * 1024 * 1024, u64::MAX), + ]; + + for (range_name, min_size, max_size) in size_ranges { + if total_size >= min_size && total_size < max_size { + *bucket_usage.object_size_histogram.entry(range_name.to_string()).or_insert(0) += 1; + break; + } + } + + // Update version histogram (simplified - count as single version) + *bucket_usage + .object_versions_histogram + .entry("SINGLE_VERSION".to_string()) + .or_insert(0) += 1; + } else { + // Create new bucket usage + let mut bucket_usage = BucketUsageInfo { + size: meta_object.size as u64, + objects_count: 1, + versions_count: 1, + ..Default::default() + }; + bucket_usage.object_size_histogram.insert("0-1KB".to_string(), 1); + bucket_usage.object_versions_histogram.insert("SINGLE_VERSION".to_string(), 1); + self.buckets_usage.insert(bucket_name, bucket_usage); + } + + // Update global statistics + self.objects_total_size += meta_object.size as u64; + self.objects_total_count += 1; + self.versions_total_count += 1; + } + + /// Add object from FileMeta for accurate version counting + pub fn add_object_from_file_meta(&mut self, object_path: &str, file_meta: &rustfs_filemeta::FileMeta) { + let bucket_name = match self.extract_bucket_from_path(object_path) { + Ok(name) => name, + Err(_) => return, + }; + + // Calculate accurate statistics from all versions + let mut total_size = 0u64; + let mut versions_count = 0u64; + let mut delete_markers_count = 0u64; + let mut latest_object_size = 0u64; + + // Process all versions to get accurate counts + for version in &file_meta.versions { + match rustfs_filemeta::FileMetaVersion::try_from(version.clone()) { + Ok(ver) => { + if let Some(obj) = ver.object { + total_size += obj.size as u64; + versions_count += 1; + latest_object_size = obj.size as u64; // Keep track of latest object size + } else if ver.delete_marker.is_some() { + delete_markers_count += 1; + } + } + Err(_) => { + // Skip invalid versions + continue; + } + } + } + + // Update bucket statistics + if let Some(bucket_usage) = self.buckets_usage.get_mut(&bucket_name) { + bucket_usage.size += total_size; + bucket_usage.objects_count += 1; + bucket_usage.versions_count += versions_count; + bucket_usage.delete_markers_count += delete_markers_count; + + // Update size histogram based on latest object size + let size_ranges = [ + ("0-1KB", 0, 1024), + ("1KB-1MB", 1024, 1024 * 1024), + ("1MB-10MB", 1024 * 1024, 10 * 1024 * 1024), + ("10MB-100MB", 10 * 1024 * 1024, 100 * 1024 * 1024), + ("100MB-1GB", 100 * 1024 * 1024, 1024 * 1024 * 1024), + ("1GB+", 1024 * 1024 * 1024, u64::MAX), + ]; + + for (range_name, min_size, max_size) in size_ranges { + if latest_object_size >= min_size && latest_object_size < max_size { + *bucket_usage.object_size_histogram.entry(range_name.to_string()).or_insert(0) += 1; + break; + } + } + + // Update version histogram based on actual version count + let version_ranges = [ + ("1", 1, 1), + ("2-5", 2, 5), + ("6-10", 6, 10), + ("11-50", 11, 50), + ("51-100", 51, 100), + ("100+", 101, usize::MAX), + ]; + + for (range_name, min_versions, max_versions) in version_ranges { + if versions_count as usize >= min_versions && versions_count as usize <= max_versions { + *bucket_usage + .object_versions_histogram + .entry(range_name.to_string()) + .or_insert(0) += 1; + break; + } + } + } else { + // Create new bucket usage + let mut bucket_usage = BucketUsageInfo { + size: total_size, + objects_count: 1, + versions_count, + delete_markers_count, + ..Default::default() + }; + + // Set size histogram + let size_ranges = [ + ("0-1KB", 0, 1024), + ("1KB-1MB", 1024, 1024 * 1024), + ("1MB-10MB", 1024 * 1024, 10 * 1024 * 1024), + ("10MB-100MB", 10 * 1024 * 1024, 100 * 1024 * 1024), + ("100MB-1GB", 100 * 1024 * 1024, 1024 * 1024 * 1024), + ("1GB+", 1024 * 1024 * 1024, u64::MAX), + ]; + + for (range_name, min_size, max_size) in size_ranges { + if latest_object_size >= min_size && latest_object_size < max_size { + bucket_usage.object_size_histogram.insert(range_name.to_string(), 1); + break; + } + } + + // Set version histogram + let version_ranges = [ + ("1", 1, 1), + ("2-5", 2, 5), + ("6-10", 6, 10), + ("11-50", 11, 50), + ("51-100", 51, 100), + ("100+", 101, usize::MAX), + ]; + + for (range_name, min_versions, max_versions) in version_ranges { + if versions_count as usize >= min_versions && versions_count as usize <= max_versions { + bucket_usage.object_versions_histogram.insert(range_name.to_string(), 1); + break; + } + } + + self.buckets_usage.insert(bucket_name, bucket_usage); + // Update buckets count when adding new bucket + self.buckets_count = self.buckets_usage.len() as u64; + } + + // Update global statistics + self.objects_total_size += total_size; + self.objects_total_count += 1; + self.versions_total_count += versions_count; + self.delete_markers_total_count += delete_markers_count; + } + + /// Extract bucket name from object path + pub fn extract_bucket_from_path(&self, object_path: &str) -> Result> { + let parts: Vec<&str> = object_path.split('/').collect(); + if parts.is_empty() { + return Err("Invalid object path: empty".into()); + } + Ok(parts[0].to_string()) + } + + /// Update capacity information + pub fn update_capacity(&mut self, total: u64, used: u64, free: u64) { + self.total_capacity = total; + self.total_used_capacity = used; + self.total_free_capacity = free; + self.last_update = Some(SystemTime::now()); + } + + /// Add bucket usage info + pub fn add_bucket_usage(&mut self, bucket: String, usage: BucketUsageInfo) { + self.buckets_usage.insert(bucket.clone(), usage); + self.buckets_count = self.buckets_usage.len() as u64; + self.last_update = Some(SystemTime::now()); + } + + /// Get bucket usage info + pub fn get_bucket_usage(&self, bucket: &str) -> Option<&BucketUsageInfo> { + self.buckets_usage.get(bucket) + } + + /// Calculate total statistics from all buckets + pub fn calculate_totals(&mut self) { + self.objects_total_count = 0; + self.versions_total_count = 0; + self.delete_markers_total_count = 0; + self.objects_total_size = 0; + + for usage in self.buckets_usage.values() { + self.objects_total_count += usage.objects_count; + self.versions_total_count += usage.versions_count; + self.delete_markers_total_count += usage.delete_markers_count; + self.objects_total_size += usage.size; + } + } + + /// Merge another DataUsageInfo into this one + pub fn merge(&mut self, other: &DataUsageInfo) { + // Merge bucket usage + for (bucket, usage) in &other.buckets_usage { + if let Some(existing) = self.buckets_usage.get_mut(bucket) { + existing.merge(usage); + } else { + self.buckets_usage.insert(bucket.clone(), usage.clone()); + } + } + + // Recalculate totals + self.calculate_totals(); + + // Ensure buckets_count stays consistent with buckets_usage + self.buckets_count = self.buckets_usage.len() as u64; + + // Update last update time + if let Some(other_update) = other.last_update { + if self.last_update.is_none() || other_update > self.last_update.unwrap() { + self.last_update = Some(other_update); + } + } + } +} + +impl BucketUsageInfo { + /// Create a new BucketUsageInfo + pub fn new() -> Self { + Self::default() + } + + /// Add size summary to this bucket usage + pub fn add_size_summary(&mut self, summary: &SizeSummary) { + self.size += summary.total_size as u64; + self.versions_count += summary.versions as u64; + self.delete_markers_count += summary.delete_markers as u64; + self.replica_size += summary.replica_size as u64; + self.replica_count += summary.replica_count as u64; + } + + /// Merge another BucketUsageInfo into this one + pub fn merge(&mut self, other: &BucketUsageInfo) { + self.size += other.size; + self.objects_count += other.objects_count; + self.versions_count += other.versions_count; + self.delete_markers_count += other.delete_markers_count; + self.replica_size += other.replica_size; + self.replica_count += other.replica_count; + + // Merge histograms + for (key, value) in &other.object_size_histogram { + *self.object_size_histogram.entry(key.clone()).or_insert(0) += value; + } + + for (key, value) in &other.object_versions_histogram { + *self.object_versions_histogram.entry(key.clone()).or_insert(0) += value; + } + + // Merge replication info + for (target, info) in &other.replication_info { + let entry = self.replication_info.entry(target.clone()).or_default(); + entry.replicated_size += info.replicated_size; + entry.replica_size += info.replica_size; + entry.replication_pending_size += info.replication_pending_size; + entry.replication_failed_size += info.replication_failed_size; + entry.replication_pending_count += info.replication_pending_count; + entry.replication_failed_count += info.replication_failed_count; + entry.replicated_count += info.replicated_count; + } + + // Merge backward compatibility fields + self.replication_pending_size_v1 += other.replication_pending_size_v1; + self.replication_failed_size_v1 += other.replication_failed_size_v1; + self.replicated_size_v1 += other.replicated_size_v1; + self.replication_pending_count_v1 += other.replication_pending_count_v1; + self.replication_failed_count_v1 += other.replication_failed_count_v1; + } +} + +impl SizeSummary { + /// Create a new SizeSummary + pub fn new() -> Self { + Self::default() + } + + /// Add another SizeSummary to this one + pub fn add(&mut self, other: &SizeSummary) { + self.total_size += other.total_size; + self.versions += other.versions; + self.delete_markers += other.delete_markers; + self.replicated_size += other.replicated_size; + self.replicated_count += other.replicated_count; + self.pending_size += other.pending_size; + self.failed_size += other.failed_size; + self.replica_size += other.replica_size; + self.replica_count += other.replica_count; + self.pending_count += other.pending_count; + self.failed_count += other.failed_count; + + // Merge replication target stats + for (target, stats) in &other.repl_target_stats { + let entry = self.repl_target_stats.entry(target.clone()).or_default(); + entry.replicated_size += stats.replicated_size; + entry.replicated_count += stats.replicated_count; + entry.pending_size += stats.pending_size; + entry.failed_size += stats.failed_size; + entry.pending_count += stats.pending_count; + entry.failed_count += stats.failed_count; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_data_usage_info_creation() { + let mut info = DataUsageInfo::new(); + info.update_capacity(1000, 500, 500); + + assert_eq!(info.total_capacity, 1000); + assert_eq!(info.total_used_capacity, 500); + assert_eq!(info.total_free_capacity, 500); + assert!(info.last_update.is_some()); + } + + #[test] + fn test_bucket_usage_info_merge() { + let mut usage1 = BucketUsageInfo::new(); + usage1.size = 100; + usage1.objects_count = 10; + usage1.versions_count = 5; + + let mut usage2 = BucketUsageInfo::new(); + usage2.size = 200; + usage2.objects_count = 20; + usage2.versions_count = 10; + + usage1.merge(&usage2); + + assert_eq!(usage1.size, 300); + assert_eq!(usage1.objects_count, 30); + assert_eq!(usage1.versions_count, 15); + } + + #[test] + fn test_size_summary_add() { + let mut summary1 = SizeSummary::new(); + summary1.total_size = 100; + summary1.versions = 5; + + let mut summary2 = SizeSummary::new(); + summary2.total_size = 200; + summary2.versions = 10; + + summary1.add(&summary2); + + assert_eq!(summary1.total_size, 300); + assert_eq!(summary1.versions, 15); + } +} diff --git a/crates/common/src/heal_channel.rs b/crates/common/src/heal_channel.rs index 56399cb37..6988e06b8 100644 --- a/crates/common/src/heal_channel.rs +++ b/crates/common/src/heal_channel.rs @@ -12,10 +12,109 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::sync::OnceLock; +use s3s::dto::{BucketLifecycleConfiguration, ExpirationStatus, LifecycleRule, ReplicationConfiguration, ReplicationRuleStatus}; +use serde::{Deserialize, Serialize}; +use std::{ + fmt::{self, Display}, + sync::OnceLock, +}; use tokio::sync::mpsc; use uuid::Uuid; +pub const HEAL_DELETE_DANGLING: bool = true; +pub const RUSTFS_RESERVED_BUCKET: &str = "rustfs"; +pub const RUSTFS_RESERVED_BUCKET_PATH: &str = "/rustfs"; + +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +pub enum HealItemType { + Metadata, + Bucket, + BucketMetadata, + Object, +} + +impl HealItemType { + pub fn to_str(&self) -> &str { + match self { + HealItemType::Metadata => "metadata", + HealItemType::Bucket => "bucket", + HealItemType::BucketMetadata => "bucket-metadata", + HealItemType::Object => "object", + } + } +} + +impl Display for HealItemType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.to_str()) + } +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +pub enum DriveState { + Ok, + Offline, + Corrupt, + Missing, + PermissionDenied, + Faulty, + RootMount, + Unknown, + Unformatted, // only returned by disk +} + +impl DriveState { + pub fn to_str(&self) -> &str { + match self { + DriveState::Ok => "ok", + DriveState::Offline => "offline", + DriveState::Corrupt => "corrupt", + DriveState::Missing => "missing", + DriveState::PermissionDenied => "permission-denied", + DriveState::Faulty => "faulty", + DriveState::RootMount => "root-mount", + DriveState::Unknown => "unknown", + DriveState::Unformatted => "unformatted", + } + } +} + +impl Display for DriveState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.to_str()) + } +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub enum HealScanMode { + Unknown, + Normal, + Deep, +} + +impl Default for HealScanMode { + fn default() -> Self { + Self::Normal + } +} + +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)] +pub struct HealOpts { + pub recursive: bool, + #[serde(rename = "dryRun")] + pub dry_run: bool, + pub remove: bool, + pub recreate: bool, + #[serde(rename = "scanMode")] + pub scan_mode: HealScanMode, + #[serde(rename = "updateParity")] + pub update_parity: bool, + #[serde(rename = "nolock")] + pub no_lock: bool, + pub pool: Option, + pub set: Option, +} + /// Heal channel command type #[derive(Debug, Clone)] pub enum HealChannelCommand { @@ -32,6 +131,8 @@ pub enum HealChannelCommand { pub struct HealChannelRequest { /// Unique request ID pub id: String, + /// Disk ID for heal disk/erasure set task + pub disk: Option, /// Bucket name pub bucket: String, /// Object prefix (optional) @@ -45,7 +146,7 @@ pub struct HealChannelRequest { /// Set index (optional) pub set_index: Option, /// Scan mode (optional) - pub scan_mode: Option, + pub scan_mode: Option, /// Whether to remove corrupted data pub remove_corrupted: Option, /// Whether to recreate missing data @@ -164,6 +265,7 @@ pub fn create_heal_request( recursive: None, dry_run: None, timeout_seconds: None, + disk: None, } } @@ -203,11 +305,123 @@ pub fn create_heal_response( } } -/// Heal scan mode -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum HealChannelScanMode { - /// Normal scan - Normal, - /// Deep scan - Deep, +fn lc_get_prefix(rule: &LifecycleRule) -> String { + if let Some(p) = &rule.prefix { + return p.to_string(); + } else if let Some(filter) = &rule.filter { + if let Some(p) = &filter.prefix { + return p.to_string(); + } else if let Some(and) = &filter.and { + if let Some(p) = &and.prefix { + return p.to_string(); + } + } + } + + "".into() +} + +pub fn lc_has_active_rules(config: &BucketLifecycleConfiguration, prefix: &str) -> bool { + if config.rules.is_empty() { + return false; + } + + for rule in config.rules.iter() { + if rule.status == ExpirationStatus::from_static(ExpirationStatus::DISABLED) { + continue; + } + let rule_prefix = lc_get_prefix(rule); + if !prefix.is_empty() && !rule_prefix.is_empty() && !prefix.starts_with(&rule_prefix) && !rule_prefix.starts_with(prefix) + { + continue; + } + + if let Some(e) = &rule.noncurrent_version_expiration { + if let Some(true) = e.noncurrent_days.map(|d| d > 0) { + return true; + } + if let Some(true) = e.newer_noncurrent_versions.map(|d| d > 0) { + return true; + } + } + + if rule.noncurrent_version_transitions.is_some() { + return true; + } + if let Some(true) = rule.expiration.as_ref().map(|e| e.date.is_some()) { + return true; + } + + if let Some(true) = rule.expiration.as_ref().map(|e| e.days.is_some()) { + return true; + } + + if let Some(Some(true)) = rule.expiration.as_ref().map(|e| e.expired_object_delete_marker) { + return true; + } + + if let Some(true) = rule.transitions.as_ref().map(|t| !t.is_empty()) { + return true; + } + + if rule.transitions.is_some() { + return true; + } + } + false +} + +pub fn rep_has_active_rules(config: &ReplicationConfiguration, prefix: &str, recursive: bool) -> bool { + if config.rules.is_empty() { + return false; + } + + for rule in config.rules.iter() { + if rule + .status + .eq(&ReplicationRuleStatus::from_static(ReplicationRuleStatus::DISABLED)) + { + continue; + } + if !prefix.is_empty() { + if let Some(filter) = &rule.filter { + if let Some(r_prefix) = &filter.prefix { + if !r_prefix.is_empty() { + // incoming prefix must be in rule prefix + if !recursive && !prefix.starts_with(r_prefix) { + continue; + } + // If recursive, we can skip this rule if it doesn't match the tested prefix or level below prefix + // does not match + if recursive && !r_prefix.starts_with(prefix) && !prefix.starts_with(r_prefix) { + continue; + } + } + } + } + } + return true; + } + false +} + +pub async fn send_heal_disk(set_disk_id: String, priority: Option) -> Result<(), String> { + let req = HealChannelRequest { + id: Uuid::new_v4().to_string(), + bucket: "".to_string(), + object_prefix: None, + disk: Some(set_disk_id), + force_start: false, + priority: priority.unwrap_or_default(), + pool_index: None, + set_index: None, + scan_mode: None, + remove_corrupted: None, + recreate_missing: None, + update_parity: None, + recursive: None, + dry_run: None, + timeout_seconds: None, + }; + send_heal_request(req).await } diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 3a41462a5..09dc164a3 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -14,6 +14,7 @@ pub mod bucket_stats; // pub mod error; +pub mod data_usage; pub mod globals; pub mod heal_channel; pub mod last_minute; diff --git a/crates/ecstore/src/bucket/lifecycle/bucket_lifecycle_ops.rs b/crates/ecstore/src/bucket/lifecycle/bucket_lifecycle_ops.rs index 40af33df7..29b2097f6 100644 --- a/crates/ecstore/src/bucket/lifecycle/bucket_lifecycle_ops.rs +++ b/crates/ecstore/src/bucket/lifecycle/bucket_lifecycle_ops.rs @@ -22,7 +22,10 @@ use async_channel::{Receiver as A_Receiver, Sender as A_Sender, bounded}; use futures::Future; use http::HeaderMap; use lazy_static::lazy_static; +use rustfs_common::data_usage::TierStats; +use rustfs_common::heal_channel::rep_has_active_rules; use rustfs_common::metrics::{IlmAction, Metrics}; +use rustfs_utils::path::encode_dir_object; use s3s::Body; use sha2::{Digest, Sha256}; use std::any::Any; @@ -32,6 +35,7 @@ use std::io::Write; use std::pin::Pin; use std::sync::atomic::{AtomicI64, Ordering}; use std::sync::{Arc, Mutex}; +use time::OffsetDateTime; use tokio::select; use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::{RwLock, mpsc}; @@ -45,6 +49,7 @@ use super::bucket_lifecycle_audit::{LcAuditEvent, LcEventSrc}; use super::lifecycle::{self, ExpirationOptions, Lifecycle, TransitionOptions}; use super::tier_last_day_stats::{DailyAllTierStats, LastDayTierStats}; use super::tier_sweeper::{Jentry, delete_object_from_remote_tier}; +use crate::bucket::object_lock::objectlock_sys::enforce_retention_for_deletion; use crate::bucket::{metadata_sys::get_lifecycle_config, versioning_sys::BucketVersioningSys}; use crate::client::object_api_utils::new_getobjectreader; use crate::error::Error; @@ -53,15 +58,11 @@ use crate::event::name::EventName; use crate::event_notification::{EventArgs, send_event}; use crate::global::GLOBAL_LocalNodeName; use crate::global::{GLOBAL_LifecycleSys, GLOBAL_TierConfigMgr, get_global_deployment_id}; -use crate::heal::{ - data_scanner::{apply_expiry_on_non_transitioned_objects, apply_expiry_on_transitioned_object}, - data_usage_cache::TierStats, -}; use crate::store::ECStore; use crate::store_api::StorageAPI; use crate::store_api::{GetObjectReader, HTTPRangeSpec, ObjectInfo, ObjectOptions, ObjectToDelete}; use crate::tier::warm_backend::WarmBackendGetOpts; -use s3s::dto::BucketLifecycleConfiguration; +use s3s::dto::{BucketLifecycleConfiguration, DefaultRetention, ReplicationConfiguration}; pub type TimeFn = Arc Pin + Send>> + Send + Sync + 'static>; pub type TraceFn = @@ -842,3 +843,161 @@ pub struct RestoreObjectRequest { } const _MAX_RESTORE_OBJECT_REQUEST_SIZE: i64 = 2 << 20; + +pub async fn eval_action_from_lifecycle( + lc: &BucketLifecycleConfiguration, + lr: Option, + rcfg: Option<(ReplicationConfiguration, OffsetDateTime)>, + oi: &ObjectInfo, +) -> lifecycle::Event { + let event = lc.eval(&oi.to_lifecycle_opts()).await; + //if serverDebugLog { + info!("lifecycle: Secondary scan: {}", event.action); + //} + + let lock_enabled = if let Some(lr) = lr { lr.mode.is_some() } else { false }; + + match event.action { + lifecycle::IlmAction::DeleteAllVersionsAction | lifecycle::IlmAction::DelMarkerDeleteAllVersionsAction => { + if lock_enabled { + return lifecycle::Event::default(); + } + } + lifecycle::IlmAction::DeleteVersionAction | lifecycle::IlmAction::DeleteRestoredVersionAction => { + if oi.version_id.is_none() { + return lifecycle::Event::default(); + } + if lock_enabled && enforce_retention_for_deletion(oi) { + //if serverDebugLog { + if oi.version_id.is_some() { + info!("lifecycle: {} v({}) is locked, not deleting", oi.name, oi.version_id.expect("err")); + } else { + info!("lifecycle: {} is locked, not deleting", oi.name); + } + //} + return lifecycle::Event::default(); + } + if let Some(rcfg) = rcfg { + if rep_has_active_rules(&rcfg.0, &oi.name, true) { + return lifecycle::Event::default(); + } + } + } + _ => (), + } + + event +} + +async fn apply_transition_rule(event: &lifecycle::Event, src: &LcEventSrc, oi: &ObjectInfo) -> bool { + if oi.delete_marker || oi.is_dir { + return false; + } + GLOBAL_TransitionState.queue_transition_task(oi, event, src).await; + true +} + +pub async fn apply_expiry_on_transitioned_object( + api: Arc, + oi: &ObjectInfo, + lc_event: &lifecycle::Event, + src: &LcEventSrc, +) -> bool { + // let time_ilm = ScannerMetrics::time_ilm(lc_event.action.clone()); + if let Err(_err) = expire_transitioned_object(api, oi, lc_event, src).await { + return false; + } + // let _ = time_ilm(1); + + true +} + +pub async fn apply_expiry_on_non_transitioned_objects( + api: Arc, + oi: &ObjectInfo, + lc_event: &lifecycle::Event, + _src: &LcEventSrc, +) -> bool { + let mut opts = ObjectOptions { + expiration: ExpirationOptions { expire: true }, + ..Default::default() + }; + + if lc_event.action.delete_versioned() { + opts.version_id = Some(oi.version_id.expect("err").to_string()); + } + + opts.versioned = BucketVersioningSys::prefix_enabled(&oi.bucket, &oi.name).await; + opts.version_suspended = BucketVersioningSys::prefix_suspended(&oi.bucket, &oi.name).await; + + if lc_event.action.delete_all() { + opts.delete_prefix = true; + opts.delete_prefix_object = true; + } + + // let time_ilm = ScannerMetrics::time_ilm(lc_event.action.clone()); + + let mut dobj = api + .delete_object(&oi.bucket, &encode_dir_object(&oi.name), opts) + .await + .unwrap(); + if dobj.name.is_empty() { + dobj = oi.clone(); + } + + //let tags = LcAuditEvent::new(lc_event.clone(), src.clone()).tags(); + //tags["version-id"] = dobj.version_id; + + let mut event_name = EventName::ObjectRemovedDelete; + if oi.delete_marker { + event_name = EventName::ObjectRemovedDeleteMarkerCreated; + } + match lc_event.action { + lifecycle::IlmAction::DeleteAllVersionsAction => event_name = EventName::ObjectRemovedDeleteAllVersions, + lifecycle::IlmAction::DelMarkerDeleteAllVersionsAction => event_name = EventName::ILMDelMarkerExpirationDelete, + _ => (), + } + send_event(EventArgs { + event_name: event_name.as_ref().to_string(), + bucket_name: dobj.bucket.clone(), + object: dobj, + user_agent: "Internal: [ILM-Expiry]".to_string(), + host: GLOBAL_LocalNodeName.to_string(), + ..Default::default() + }); + + if lc_event.action != lifecycle::IlmAction::NoneAction { + // let mut num_versions = 1_u64; + // if lc_event.action.delete_all() { + // num_versions = oi.num_versions as u64; + // } + // let _ = time_ilm(num_versions); + } + + true +} + +async fn apply_expiry_rule(event: &lifecycle::Event, src: &LcEventSrc, oi: &ObjectInfo) -> bool { + let mut expiry_state = GLOBAL_ExpiryState.write().await; + expiry_state.enqueue_by_days(oi, event, src).await; + true +} + +pub async fn apply_lifecycle_action(event: &lifecycle::Event, src: &LcEventSrc, oi: &ObjectInfo) -> bool { + let mut success = false; + match event.action { + lifecycle::IlmAction::DeleteVersionAction + | lifecycle::IlmAction::DeleteAction + | lifecycle::IlmAction::DeleteRestoredAction + | lifecycle::IlmAction::DeleteRestoredVersionAction + | lifecycle::IlmAction::DeleteAllVersionsAction + | lifecycle::IlmAction::DelMarkerDeleteAllVersionsAction => { + success = apply_expiry_rule(event, src, oi).await; + } + lifecycle::IlmAction::TransitionAction | lifecycle::IlmAction::TransitionVersionAction => { + success = apply_transition_rule(event, src, oi).await; + } + _ => (), + } + success +} diff --git a/crates/ecstore/src/bucket/lifecycle/tier_last_day_stats.rs b/crates/ecstore/src/bucket/lifecycle/tier_last_day_stats.rs index 78e3e8ed0..557d6189d 100644 --- a/crates/ecstore/src/bucket/lifecycle/tier_last_day_stats.rs +++ b/crates/ecstore/src/bucket/lifecycle/tier_last_day_stats.rs @@ -25,7 +25,7 @@ use std::ops::Sub; use time::OffsetDateTime; use tracing::{error, warn}; -use crate::heal::data_usage_cache::TierStats; +use rustfs_common::data_usage::TierStats; pub type DailyAllTierStats = HashMap; diff --git a/crates/ecstore/src/bucket/metadata_sys.rs b/crates/ecstore/src/bucket/metadata_sys.rs index 791134da9..b192cd2f3 100644 --- a/crates/ecstore/src/bucket/metadata_sys.rs +++ b/crates/ecstore/src/bucket/metadata_sys.rs @@ -18,9 +18,9 @@ use crate::bucket::utils::{deserialize, is_meta_bucketname}; use crate::cmd::bucket_targets; use crate::error::{Error, Result, is_err_bucket_not_found}; use crate::global::{GLOBAL_Endpoints, is_dist_erasure, is_erasure, new_object_layer_fn}; -use crate::heal::heal_commands::HealOpts; use crate::store::ECStore; use futures::future::join_all; +use rustfs_common::heal_channel::HealOpts; use rustfs_policy::policy::BucketPolicy; use s3s::dto::{ BucketLifecycleConfiguration, NotificationConfiguration, ObjectLockConfiguration, ReplicationConfiguration, diff --git a/crates/ecstore/src/data_usage.rs b/crates/ecstore/src/data_usage.rs new file mode 100644 index 000000000..425081ef1 --- /dev/null +++ b/crates/ecstore/src/data_usage.rs @@ -0,0 +1,297 @@ +// 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::{collections::HashMap, sync::Arc}; + +use crate::{bucket::metadata_sys::get_replication_config, config::com::read_config, store::ECStore}; +use rustfs_common::data_usage::{BucketTargetUsageInfo, DataUsageCache, DataUsageEntry, DataUsageInfo, SizeSummary}; +use rustfs_utils::path::SLASH_SEPARATOR; +use tracing::{error, warn}; + +use crate::error::Error; + +// Data usage storage constants +pub const DATA_USAGE_ROOT: &str = SLASH_SEPARATOR; +const DATA_USAGE_OBJ_NAME: &str = ".usage.json"; +const DATA_USAGE_BLOOM_NAME: &str = ".bloomcycle.bin"; +pub const DATA_USAGE_CACHE_NAME: &str = ".usage-cache.bin"; + +// Data usage storage paths +lazy_static::lazy_static! { + pub static ref DATA_USAGE_BUCKET: String = format!("{}{}{}", + crate::disk::RUSTFS_META_BUCKET, + SLASH_SEPARATOR, + crate::disk::BUCKET_META_PREFIX + ); + pub static ref DATA_USAGE_OBJ_NAME_PATH: String = format!("{}{}{}", + crate::disk::BUCKET_META_PREFIX, + SLASH_SEPARATOR, + DATA_USAGE_OBJ_NAME + ); + pub static ref DATA_USAGE_BLOOM_NAME_PATH: String = format!("{}{}{}", + crate::disk::BUCKET_META_PREFIX, + SLASH_SEPARATOR, + DATA_USAGE_BLOOM_NAME + ); +} + +/// Store data usage info to backend storage +pub async fn store_data_usage_in_backend(data_usage_info: DataUsageInfo, store: Arc) -> Result<(), Error> { + let data = + serde_json::to_vec(&data_usage_info).map_err(|e| Error::other(format!("Failed to serialize data usage info: {e}")))?; + + // Save to backend using the same mechanism as original code + crate::config::com::save_config(store, &DATA_USAGE_OBJ_NAME_PATH, data) + .await + .map_err(Error::other)?; + + Ok(()) +} + +/// Load data usage info from backend storage +pub async fn load_data_usage_from_backend(store: Arc) -> Result { + let buf: Vec = match read_config(store, &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()); + } + return Err(Error::other(e)); + } + }; + + 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); + + // Handle backward compatibility like original code + if data_usage_info.buckets_usage.is_empty() { + data_usage_info.buckets_usage = data_usage_info + .bucket_sizes + .iter() + .map(|(bucket, &size)| { + ( + bucket.clone(), + rustfs_common::data_usage::BucketUsageInfo { + size, + ..Default::default() + }, + ) + }) + .collect(); + } + + if data_usage_info.bucket_sizes.is_empty() { + data_usage_info.bucket_sizes = data_usage_info + .buckets_usage + .iter() + .map(|(bucket, bui)| (bucket.clone(), bui.size)) + .collect(); + } + + for (bucket, bui) in &data_usage_info.buckets_usage { + if bui.replicated_size_v1 > 0 + || bui.replication_failed_count_v1 > 0 + || bui.replication_failed_size_v1 > 0 + || bui.replication_pending_count_v1 > 0 + { + if let Ok((cfg, _)) = get_replication_config(bucket).await { + if !cfg.role.is_empty() { + data_usage_info.replication_info.insert( + cfg.role.clone(), + BucketTargetUsageInfo { + replication_failed_size: bui.replication_failed_size_v1, + replication_failed_count: bui.replication_failed_count_v1, + replicated_size: bui.replicated_size_v1, + replication_pending_count: bui.replication_pending_count_v1, + replication_pending_size: bui.replication_pending_size_v1, + ..Default::default() + }, + ); + } + } + } + } + + 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(); + entry.add_sizes(summary); + entry +} + +/// Convert data usage cache to DataUsageInfo +pub fn cache_to_data_usage_info(cache: &DataUsageCache, path: &str, buckets: &[crate::store_api::BucketInfo]) -> DataUsageInfo { + let e = match cache.find(path) { + Some(e) => e, + None => return DataUsageInfo::default(), + }; + let flat = cache.flatten(&e); + + let mut buckets_usage = HashMap::new(); + for bucket in buckets.iter() { + let e = match cache.find(&bucket.name) { + Some(e) => e, + None => continue, + }; + let flat = cache.flatten(&e); + let mut bui = rustfs_common::data_usage::BucketUsageInfo { + size: flat.size as u64, + versions_count: flat.versions as u64, + objects_count: flat.objects as u64, + delete_markers_count: flat.delete_markers as u64, + object_size_histogram: flat.obj_sizes.to_map(), + object_versions_histogram: flat.obj_versions.to_map(), + ..Default::default() + }; + + if let Some(rs) = &flat.replication_stats { + bui.replica_size = rs.replica_size; + bui.replica_count = rs.replica_count; + + for (arn, stat) in rs.targets.iter() { + bui.replication_info.insert( + arn.clone(), + BucketTargetUsageInfo { + replication_pending_size: stat.pending_size, + replicated_size: stat.replicated_size, + replication_failed_size: stat.failed_size, + replication_pending_count: stat.pending_count, + replication_failed_count: stat.failed_count, + replicated_count: stat.replicated_count, + ..Default::default() + }, + ); + } + } + buckets_usage.insert(bucket.name.clone(), bui); + } + + DataUsageInfo { + last_update: cache.info.last_update, + objects_total_count: flat.objects as u64, + versions_total_count: flat.versions as u64, + delete_markers_total_count: flat.delete_markers as u64, + objects_total_size: flat.size as u64, + buckets_count: e.children.len() as u64, + buckets_usage, + ..Default::default() + } +} + +// Helper functions for DataUsageCache operations +pub async fn load_data_usage_cache(store: &crate::set_disk::SetDisks, name: &str) -> crate::error::Result { + use crate::disk::{BUCKET_META_PREFIX, RUSTFS_META_BUCKET}; + use crate::store_api::{ObjectIO, ObjectOptions}; + use http::HeaderMap; + use rand::Rng; + use std::path::Path; + use std::time::Duration; + use tokio::time::sleep; + + let mut d = DataUsageCache::default(); + let mut retries = 0; + while retries < 5 { + let path = Path::new(BUCKET_META_PREFIX).join(name); + match store + .get_object_reader( + RUSTFS_META_BUCKET, + path.to_str().unwrap(), + None, + HeaderMap::new(), + &ObjectOptions { + no_lock: true, + ..Default::default() + }, + ) + .await + { + Ok(mut reader) => { + if let Ok(info) = DataUsageCache::unmarshal(&reader.read_all().await?) { + d = info + } + break; + } + Err(err) => match err { + crate::error::Error::FileNotFound | crate::error::Error::VolumeNotFound => { + match store + .get_object_reader( + RUSTFS_META_BUCKET, + name, + None, + HeaderMap::new(), + &ObjectOptions { + no_lock: true, + ..Default::default() + }, + ) + .await + { + Ok(mut reader) => { + if let Ok(info) = DataUsageCache::unmarshal(&reader.read_all().await?) { + d = info + } + break; + } + Err(_) => match err { + crate::error::Error::FileNotFound | crate::error::Error::VolumeNotFound => { + break; + } + _ => {} + }, + } + } + _ => { + break; + } + }, + } + retries += 1; + let dur = { + let mut rng = rand::rng(); + rng.random_range(0..1_000) + }; + sleep(Duration::from_millis(dur)).await; + } + Ok(d) +} + +pub async fn save_data_usage_cache(cache: &DataUsageCache, name: &str) -> crate::error::Result<()> { + use crate::config::com::save_config; + use crate::disk::BUCKET_META_PREFIX; + use crate::new_object_layer_fn; + use std::path::Path; + + let Some(store) = new_object_layer_fn() else { + return Err(crate::error::Error::other("errServerNotInitialized")); + }; + let buf = cache.marshal_msg().map_err(crate::error::Error::other)?; + let buf_clone = buf.clone(); + + let store_clone = store.clone(); + + let name = Path::new(BUCKET_META_PREFIX).join(name).to_string_lossy().to_string(); + + let name_clone = name.clone(); + tokio::spawn(async move { + let _ = save_config(store_clone, &format!("{}{}", &name_clone, ".bkp"), buf_clone).await; + }); + save_config(store, &name, buf).await?; + Ok(()) +} diff --git a/crates/ecstore/src/disk/mod.rs b/crates/ecstore/src/disk/mod.rs index 1f918ee36..f680a42db 100644 --- a/crates/ecstore/src/disk/mod.rs +++ b/crates/ecstore/src/disk/mod.rs @@ -30,11 +30,6 @@ pub const FORMAT_CONFIG_FILE: &str = "format.json"; pub const STORAGE_FORMAT_FILE: &str = "xl.meta"; pub const STORAGE_FORMAT_FILE_BACKUP: &str = "xl.meta.bkp"; -use crate::heal::{ - data_scanner::ShouldSleepFn, - data_usage_cache::{DataUsageCache, DataUsageEntry}, - heal_commands::{HealScanMode, HealingTracker}, -}; use crate::rpc::RemoteDisk; use bytes::Bytes; use endpoint::Endpoint; @@ -46,10 +41,7 @@ use rustfs_madmin::info_commands::DiskMetrics; use serde::{Deserialize, Serialize}; use std::{fmt::Debug, path::PathBuf, sync::Arc}; use time::OffsetDateTime; -use tokio::{ - io::{AsyncRead, AsyncWrite}, - sync::mpsc::Sender, -}; +use tokio::io::{AsyncRead, AsyncWrite}; use uuid::Uuid; pub type DiskStore = Arc; @@ -406,28 +398,6 @@ impl DiskAPI for Disk { Disk::Remote(remote_disk) => remote_disk.disk_info(opts).await, } } - - #[tracing::instrument(skip(self, cache, we_sleep, scan_mode))] - async fn ns_scanner( - &self, - cache: &DataUsageCache, - updates: Sender, - scan_mode: HealScanMode, - we_sleep: ShouldSleepFn, - ) -> Result { - match self { - Disk::Local(local_disk) => local_disk.ns_scanner(cache, updates, scan_mode, we_sleep).await, - Disk::Remote(remote_disk) => remote_disk.ns_scanner(cache, updates, scan_mode, we_sleep).await, - } - } - - #[tracing::instrument(skip(self))] - async fn healing(&self) -> Option { - match self { - Disk::Local(local_disk) => local_disk.healing().await, - Disk::Remote(remote_disk) => remote_disk.healing().await, - } - } } pub async fn new_disk(ep: &Endpoint, opt: &DiskOption) -> Result { @@ -527,14 +497,6 @@ pub trait DiskAPI: Debug + Send + Sync + 'static { async fn write_all(&self, volume: &str, path: &str, data: Bytes) -> Result<()>; async fn read_all(&self, volume: &str, path: &str) -> Result; async fn disk_info(&self, opts: &DiskInfoOptions) -> Result; - async fn ns_scanner( - &self, - cache: &DataUsageCache, - updates: Sender, - scan_mode: HealScanMode, - we_sleep: ShouldSleepFn, - ) -> Result; - async fn healing(&self) -> Option; } #[derive(Debug, Default, Serialize, Deserialize)] diff --git a/crates/ecstore/src/global.rs b/crates/ecstore/src/global.rs index d8411fba1..533512266 100644 --- a/crates/ecstore/src/global.rs +++ b/crates/ecstore/src/global.rs @@ -17,7 +17,6 @@ use crate::{ disk::DiskStore, endpoints::{EndpointServerPools, PoolEndpoints, SetupType}, event_notification::EventNotifier, - heal::{background_heal_ops::HealRoutine, heal_ops::AllHealState}, store::ECStore, tier::tier::TierConfigMgr, }; @@ -50,9 +49,6 @@ pub static ref GLOBAL_LOCAL_DISK_MAP: Arc> = Arc::new(RwLock::new(Vec::new())); pub static ref GLOBAL_Endpoints: OnceLock = OnceLock::new(); pub static ref GLOBAL_RootDiskThreshold: RwLock = RwLock::new(0); -pub static ref GLOBAL_BackgroundHealRoutine: Arc = HealRoutine::new(); -pub static ref GLOBAL_BackgroundHealState: Arc = AllHealState::new(false); -// pub static ref GLOBAL_MRFState: Arc = Arc::new(MRFState::new()); pub static ref GLOBAL_TierConfigMgr: Arc> = TierConfigMgr::new(); pub static ref GLOBAL_LifecycleSys: Arc = LifecycleSys::new(); pub static ref GLOBAL_EventNotifier: Arc> = EventNotifier::new(); diff --git a/crates/ecstore/src/heal/mod.rs b/crates/ecstore/src/heal/mod.rs index 0733d91d5..6b34dc620 100644 --- a/crates/ecstore/src/heal/mod.rs +++ b/crates/ecstore/src/heal/mod.rs @@ -12,12 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub mod background_heal_ops; -pub mod data_scanner; +// pub mod background_heal_ops; +// pub mod data_scanner; // pub mod data_scanner_metric; -pub mod data_usage; -pub mod data_usage_cache; -pub mod error; -pub mod heal_commands; -pub mod heal_ops; -pub mod mrf; +// pub mod data_usage; +// pub mod data_usage_cache; +// pub mod error; +// pub mod heal_commands; +// pub mod heal_ops; +// pub mod mrf; diff --git a/crates/ecstore/src/lib.rs b/crates/ecstore/src/lib.rs index daf032595..177618892 100644 --- a/crates/ecstore/src/lib.rs +++ b/crates/ecstore/src/lib.rs @@ -23,6 +23,7 @@ mod chunk_stream; pub mod cmd; pub mod compress; pub mod config; +pub mod data_usage; pub mod disk; pub mod disks_layout; pub mod endpoints; diff --git a/crates/ecstore/src/metrics_realtime.rs b/crates/ecstore/src/metrics_realtime.rs index 2363f1c0c..a5f5f3a3a 100644 --- a/crates/ecstore/src/metrics_realtime.rs +++ b/crates/ecstore/src/metrics_realtime.rs @@ -17,6 +17,7 @@ use std::collections::{HashMap, HashSet}; use chrono::Utc; use rustfs_common::{ globals::{GLOBAL_Local_Node_Name, GLOBAL_Rustfs_Addr}, + heal_channel::DriveState, metrics::globalMetrics, }; use rustfs_madmin::metrics::{DiskIOStats, DiskMetric, RealtimeMetrics}; @@ -26,7 +27,6 @@ use tracing::info; use crate::{ admin_server_info::get_local_server_property, - heal::heal_commands::{DRIVE_STATE_OK, DRIVE_STATE_UNFORMATTED}, new_object_layer_fn, store_api::StorageAPI, // utils::os::get_drive_stats, @@ -147,7 +147,7 @@ async fn collect_local_disks_metrics(disks: &HashSet) -> HashMap Result { - let mut client = node_service_time_out_client(&self.grid_host) - .await - .map_err(|err| Error::other(err.to_string()))?; - let request = Request::new(BackgroundHealStatusRequest {}); - - let response = client.background_heal_status(request).await?.into_inner(); - if !response.success { - if let Some(msg) = response.error_info { - return Err(Error::other(msg)); - } - return Err(Error::other("")); - } - let data = response.bg_heal_state; - - let mut buf = Deserializer::new(Cursor::new(data)); - let bg_heal_state: BgHealState = Deserialize::deserialize(&mut buf)?; - - Ok(bg_heal_state) - } - pub async fn get_metacache_listing(&self) -> Result<()> { let _client = node_service_time_out_client(&self.grid_host) .await diff --git a/crates/ecstore/src/rpc/peer_s3_client.rs b/crates/ecstore/src/rpc/peer_s3_client.rs index a7789aabc..10a00e279 100644 --- a/crates/ecstore/src/rpc/peer_s3_client.rs +++ b/crates/ecstore/src/rpc/peer_s3_client.rs @@ -17,10 +17,6 @@ use crate::disk::error::{Error, Result}; use crate::disk::error_reduce::{BUCKET_OP_IGNORED_ERRS, is_all_buckets_not_found, reduce_write_quorum_errs}; use crate::disk::{DiskAPI, DiskStore}; use crate::global::GLOBAL_LOCAL_DISK_MAP; -use crate::heal::heal_commands::{ - DRIVE_STATE_CORRUPT, DRIVE_STATE_MISSING, DRIVE_STATE_OFFLINE, DRIVE_STATE_OK, HEAL_ITEM_BUCKET, HealOpts, -}; -use crate::heal::heal_ops::RUSTFS_RESERVED_BUCKET; use crate::store::all_local_disk; use crate::store_utils::is_reserved_or_invalid_bucket; use crate::{ @@ -30,6 +26,7 @@ use crate::{ }; use async_trait::async_trait; use futures::future::join_all; +use rustfs_common::heal_channel::{DriveState, HealItemType, HealOpts, RUSTFS_RESERVED_BUCKET}; use rustfs_madmin::heal_commands::{HealDriveInfo, HealResultItem}; use rustfs_protos::node_service_time_out_client; use rustfs_protos::proto_gen::node_service::{ @@ -542,7 +539,7 @@ impl PeerS3Client for RemotePeerS3Client { } Ok(HealResultItem { - heal_item_type: HEAL_ITEM_BUCKET.to_string(), + heal_item_type: HealItemType::Bucket.to_string(), bucket: bucket.to_string(), set_count: 0, ..Default::default() @@ -651,13 +648,13 @@ pub async fn heal_bucket_local(bucket: &str, opts: &HealOpts) -> Result disk, None => { - bs_clone.write().await[index] = DRIVE_STATE_OFFLINE.to_string(); - as_clone.write().await[index] = DRIVE_STATE_OFFLINE.to_string(); + bs_clone.write().await[index] = DriveState::Offline.to_string(); + as_clone.write().await[index] = DriveState::Offline.to_string(); return Some(Error::DiskNotFound); } }; - bs_clone.write().await[index] = DRIVE_STATE_OK.to_string(); - as_clone.write().await[index] = DRIVE_STATE_OK.to_string(); + bs_clone.write().await[index] = DriveState::Ok.to_string(); + as_clone.write().await[index] = DriveState::Ok.to_string(); if bucket == RUSTFS_RESERVED_BUCKET { return None; @@ -667,18 +664,18 @@ pub async fn heal_bucket_local(bucket: &str, opts: &HealOpts) -> Result None, Err(err) => match err { Error::DiskNotFound => { - bs_clone.write().await[index] = DRIVE_STATE_OFFLINE.to_string(); - as_clone.write().await[index] = DRIVE_STATE_OFFLINE.to_string(); + bs_clone.write().await[index] = DriveState::Offline.to_string(); + as_clone.write().await[index] = DriveState::Offline.to_string(); Some(err) } Error::VolumeNotFound => { - bs_clone.write().await[index] = DRIVE_STATE_MISSING.to_string(); - as_clone.write().await[index] = DRIVE_STATE_MISSING.to_string(); + bs_clone.write().await[index] = DriveState::Missing.to_string(); + as_clone.write().await[index] = DriveState::Missing.to_string(); Some(err) } _ => { - bs_clone.write().await[index] = DRIVE_STATE_CORRUPT.to_string(); - as_clone.write().await[index] = DRIVE_STATE_CORRUPT.to_string(); + bs_clone.write().await[index] = DriveState::Corrupt.to_string(); + as_clone.write().await[index] = DriveState::Corrupt.to_string(); Some(err) } }, @@ -687,7 +684,7 @@ pub async fn heal_bucket_local(bucket: &str, opts: &HealOpts) -> Result Result { - as_clone.write().await[idx] = DRIVE_STATE_OK.to_string(); + as_clone.write().await[idx] = DriveState::Ok.to_string(); return None; } Err(err) => { diff --git a/crates/ecstore/src/rpc/tonic_service.rs b/crates/ecstore/src/rpc/tonic_service.rs index ffcc3c0a5..ad6d58d91 100644 --- a/crates/ecstore/src/rpc/tonic_service.rs +++ b/crates/ecstore/src/rpc/tonic_service.rs @@ -22,21 +22,16 @@ use crate::{ DeleteOptions, DiskAPI, DiskInfoOptions, DiskStore, FileInfoVersions, ReadMultipleReq, ReadOptions, UpdateMetadataOpts, error::DiskError, }, - heal::{ - data_usage_cache::DataUsageCache, - heal_commands::{HealOpts, get_local_background_heal_status}, - }, metrics_realtime::{CollectMetricsOpts, MetricType, collect_local_metrics}, new_object_layer_fn, rpc::{LocalPeerS3Client, PeerS3Client}, store::{all_local_disk_path, find_local_disk}, store_api::{BucketOptions, DeleteBucketOptions, MakeBucketOptions, StorageAPI}, }; -use futures::{Stream, StreamExt}; +use futures::Stream; use futures_util::future::join_all; -use rustfs_common::globals::GLOBAL_Local_Node_Name; -use rustfs_lock::{LockClient, LockRequest}; +use rustfs_common::{globals::GLOBAL_Local_Node_Name, heal_channel::HealOpts}; use bytes::Bytes; use rmp_serde::{Deserializer, Serializer}; @@ -1439,120 +1434,22 @@ impl Node for NodeService { } } - type NsScannerStream = ResponseStream; - async fn ns_scanner(&self, request: Request>) -> Result, Status> { - info!("ns_scanner"); - - let mut in_stream = request.into_inner(); - let (tx, rx) = mpsc::channel(10); - - tokio::spawn(async move { - match in_stream.next().await { - Some(Ok(request)) => { - if let Some(disk) = find_local_disk(&request.disk).await { - let cache = match serde_json::from_str::(&request.cache) { - Ok(cache) => cache, - Err(err) => { - tx.send(Ok(NsScannerResponse { - success: false, - update: "".to_string(), - data_usage_cache: "".to_string(), - error: Some(DiskError::other(format!("decode DataUsageCache failed: {err}")).into()), - })) - .await - .expect("working rx"); - return; - } - }; - let (updates_tx, mut updates_rx) = mpsc::channel(100); - let tx_clone = tx.clone(); - let task = tokio::spawn(async move { - loop { - match updates_rx.recv().await { - Some(update) => { - let update = serde_json::to_string(&update).expect("encode failed"); - tx_clone - .send(Ok(NsScannerResponse { - success: true, - update, - data_usage_cache: "".to_string(), - error: None, - })) - .await - .expect("working rx"); - } - None => return, - } - } - }); - let data_usage_cache = disk.ns_scanner(&cache, updates_tx, request.scan_mode as usize, None).await; - let _ = task.await; - match data_usage_cache { - Ok(data_usage_cache) => { - let data_usage_cache = serde_json::to_string(&data_usage_cache).expect("encode failed"); - tx.send(Ok(NsScannerResponse { - success: true, - update: "".to_string(), - data_usage_cache, - error: None, - })) - .await - .expect("working rx"); - } - Err(err) => { - tx.send(Ok(NsScannerResponse { - success: false, - update: "".to_string(), - data_usage_cache: "".to_string(), - error: Some(err.into()), - })) - .await - .expect("working rx"); - } - } - } else { - tx.send(Ok(NsScannerResponse { - success: false, - update: "".to_string(), - data_usage_cache: "".to_string(), - error: Some(DiskError::other("can not find disk".to_string()).into()), - })) - .await - .expect("working rx"); - } - } - _ => todo!(), - } - }); - - let out_stream = ReceiverStream::new(rx); - Ok(tonic::Response::new(Box::pin(out_stream))) - } - async fn lock(&self, request: Request) -> Result, Status> { let request = request.into_inner(); - // Parse the request to extract resource and owner - let args: LockRequest = match serde_json::from_str(&request.args) { - Ok(args) => args, - Err(err) => { - return Ok(tonic::Response::new(GenerallyLockResponse { + match &serde_json::from_str::(&request.args) { + Ok(args) => match GLOBAL_LOCAL_SERVER.write().await.lock(args).await { + Ok(result) => Ok(tonic::Response::new(GenerallyLockResponse { + success: result, + error_info: None, + })), + Err(err) => Ok(tonic::Response::new(GenerallyLockResponse { success: false, - error_info: Some(format!("can not decode args, err: {err}")), - })); - } - }; - - match self.lock_manager.acquire_exclusive(&args).await { - Ok(result) => Ok(tonic::Response::new(GenerallyLockResponse { - success: result.success, - error_info: None, - })), + error_info: Some(format!("can not lock, args: {args}, err: {err}")), + })), + }, Err(err) => Ok(tonic::Response::new(GenerallyLockResponse { success: false, - error_info: Some(format!( - "can not lock, resource: {0}, owner: {1}, err: {2}", - args.resource, args.owner, err - )), + error_info: Some(format!("can not decode args, err: {err}")), })), } } @@ -2196,28 +2093,7 @@ impl Node for NodeService { &self, _request: Request, ) -> Result, Status> { - let (state, ok) = get_local_background_heal_status().await; - if !ok { - return Ok(tonic::Response::new(BackgroundHealStatusResponse { - success: false, - bg_heal_state: Bytes::new(), - error_info: Some("errServerNotInitialized".to_string()), - })); - } - - let mut buf = Vec::new(); - if let Err(err) = state.serialize(&mut Serializer::new(&mut buf)) { - return Ok(tonic::Response::new(BackgroundHealStatusResponse { - success: false, - bg_heal_state: Bytes::new(), - error_info: Some(err.to_string()), - })); - } - Ok(tonic::Response::new(BackgroundHealStatusResponse { - success: true, - bg_heal_state: buf.into(), - error_info: None, - })) + todo!() } async fn get_metacache_listing( @@ -3412,20 +3288,6 @@ mod tests { assert!(!proc_response.proc_info.is_empty()); } - #[tokio::test] - async fn test_background_heal_status() { - let service = create_test_node_service(); - - let request = Request::new(BackgroundHealStatusRequest {}); - - let response = service.background_heal_status(request).await; - assert!(response.is_ok()); - - let heal_response = response.unwrap().into_inner(); - // May fail if heal status is not available - assert!(heal_response.success || heal_response.error_info.is_some()); - } - #[tokio::test] async fn test_reload_pool_meta() { let service = create_test_node_service(); diff --git a/crates/ecstore/src/sets.rs b/crates/ecstore/src/sets.rs index 79a35684c..bea7d9232 100644 --- a/crates/ecstore/src/sets.rs +++ b/crates/ecstore/src/sets.rs @@ -28,9 +28,6 @@ use crate::{ endpoints::{Endpoints, PoolEndpoints}, error::StorageError, global::{GLOBAL_LOCAL_DISK_SET_DRIVES, is_dist_erasure}, - heal::heal_commands::{ - DRIVE_STATE_CORRUPT, DRIVE_STATE_MISSING, DRIVE_STATE_OFFLINE, DRIVE_STATE_OK, HEAL_ITEM_METADATA, HealOpts, - }, set_disk::SetDisks, store_api::{ BucketInfo, BucketOptions, CompletePart, DeleteBucketOptions, DeletedObject, GetObjectReader, HTTPRangeSpec, @@ -41,7 +38,11 @@ use crate::{ }; use futures::future::join_all; use http::HeaderMap; -use rustfs_common::globals::GLOBAL_Local_Node_Name; +use rustfs_common::heal_channel::HealOpts; +use rustfs_common::{ + globals::GLOBAL_Local_Node_Name, + heal_channel::{DriveState, HealItemType}, +}; use rustfs_filemeta::FileInfo; use rustfs_madmin::heal_commands::{HealDriveInfo, HealResultItem}; @@ -49,7 +50,6 @@ use rustfs_utils::{crc_hash, path::path_join_buf, sip_hash}; use tokio::sync::RwLock; use uuid::Uuid; -use crate::heal::heal_ops::HealSequence; use tokio::sync::broadcast::{Receiver, Sender}; use tokio::time::Duration; use tracing::warn; @@ -787,7 +787,7 @@ impl StorageAPI for Sets { Err(err) => return Ok((HealResultItem::default(), Some(err))), }; let mut res = HealResultItem { - heal_item_type: HEAL_ITEM_METADATA.to_string(), + heal_item_type: HealItemType::Metadata.to_string(), detail: "disk-format".to_string(), disk_count: self.set_count * self.set_drive_count, set_count: self.set_count, @@ -811,7 +811,6 @@ impl StorageAPI for Sets { // return Ok((res, Some(Error::new(DiskError::CorruptedFormat)))); // } - let format_op_id = Uuid::new_v4().to_string(); let (new_format_sets, _) = new_heal_format_sets(&ref_format, self.set_count, self.set_drive_count, &formats, &errs); if !dry_run { let mut tmp_new_formats = vec![None; self.set_count * self.set_drive_count]; @@ -819,14 +818,14 @@ impl StorageAPI for Sets { for (j, fm) in set.iter().enumerate() { if let Some(fm) = fm { res.after.drives[i * self.set_drive_count + j].uuid = fm.erasure.this.to_string(); - res.after.drives[i * self.set_drive_count + j].state = DRIVE_STATE_OK.to_string(); + res.after.drives[i * self.set_drive_count + j].state = DriveState::Ok.to_string(); tmp_new_formats[i * self.set_drive_count + j] = Some(fm.clone()); } } } // Save new formats `format.json` on unformatted disks. for (fm, disk) in tmp_new_formats.iter_mut().zip(disks.iter()) { - if fm.is_some() && disk.is_some() && save_format_file(disk, fm, &format_op_id).await.is_err() { + if fm.is_some() && disk.is_some() && save_format_file(disk, fm).await.is_err() { let _ = disk.as_ref().unwrap().close().await; *fm = None; } @@ -869,17 +868,6 @@ impl StorageAPI for Sets { .await } #[tracing::instrument(skip(self))] - async fn heal_objects( - &self, - _bucket: &str, - _prefix: &str, - _opts: &HealOpts, - _hs: Arc, - _is_meta: bool, - ) -> Result<()> { - unimplemented!() - } - #[tracing::instrument(skip(self))] async fn get_pool_and_set(&self, _id: &str) -> Result<(Option, Option, Option)> { unimplemented!() } @@ -957,17 +945,17 @@ fn formats_to_drives_info(endpoints: &Endpoints, formats: &[Option], e for (index, format) in formats.iter().enumerate() { let drive = endpoints.get_string(index); let state = if format.is_some() { - DRIVE_STATE_OK + DriveState::Ok.to_string() } else if let Some(Some(err)) = errs.get(index) { if *err == DiskError::UnformattedDisk { - DRIVE_STATE_MISSING + DriveState::Missing.to_string() } else if *err == DiskError::DiskNotFound { - DRIVE_STATE_OFFLINE + DriveState::Offline.to_string() } else { - DRIVE_STATE_CORRUPT + DriveState::Corrupt.to_string() } } else { - DRIVE_STATE_CORRUPT + DriveState::Corrupt.to_string() }; let uuid = if let Some(format) = format { diff --git a/crates/ecstore/src/store.rs b/crates/ecstore/src/store.rs index 193d3e061..765415fb8 100644 --- a/crates/ecstore/src/store.rs +++ b/crates/ecstore/src/store.rs @@ -30,11 +30,6 @@ use crate::global::{ GLOBAL_LOCAL_DISK_MAP, GLOBAL_LOCAL_DISK_SET_DRIVES, GLOBAL_TierConfigMgr, get_global_endpoints, is_dist_erasure, is_erasure_sd, set_global_deployment_id, set_object_layer, }; -use crate::heal::data_usage::{DATA_USAGE_ROOT, DataUsageInfo}; -use crate::heal::data_usage_cache::{DataUsageCache, DataUsageCacheInfo}; -use crate::heal::heal_commands::{HEAL_ITEM_METADATA, HealOpts, HealScanMode}; -use crate::heal::heal_ops::{HealEntryFn, HealSequence}; -use crate::new_object_layer_fn; use crate::notification_sys::get_global_notification_sys; use crate::pools::PoolMeta; use crate::rebalance::RebalanceMeta; @@ -54,13 +49,12 @@ use crate::{ store_init, }; use futures::future::join_all; -use glob::Pattern; use http::HeaderMap; use lazy_static::lazy_static; use rand::Rng as _; use rustfs_common::globals::{GLOBAL_Local_Node_Name, GLOBAL_Rustfs_Host, GLOBAL_Rustfs_Port}; +use rustfs_common::heal_channel::{HealItemType, HealOpts}; use rustfs_filemeta::FileInfo; -use rustfs_filemeta::MetaCacheEntry; use rustfs_madmin::heal_commands::HealResultItem; use rustfs_utils::crypto::base64_decode; use rustfs_utils::path::{SLASH_SEPARATOR, decode_dir_object, encode_dir_object, path_join_buf}; @@ -73,9 +67,8 @@ use std::time::SystemTime; use std::{collections::HashMap, sync::Arc, time::Duration}; use time::OffsetDateTime; use tokio::select; -use tokio::sync::mpsc::Sender; -use tokio::sync::{RwLock, broadcast, mpsc}; -use tokio::time::{interval, sleep}; +use tokio::sync::{RwLock, broadcast}; +use tokio::time::sleep; use tracing::{debug, info}; use tracing::{error, warn}; use uuid::Uuid; @@ -811,123 +804,6 @@ impl ECStore { errs } - pub async fn ns_scanner( - &self, - updates: Sender, - want_cycle: usize, - heal_scan_mode: HealScanMode, - ) -> Result<()> { - info!("ns_scanner updates - {}", want_cycle); - let all_buckets = self.list_bucket(&BucketOptions::default()).await?; - if all_buckets.is_empty() { - info!("No buckets found"); - let _ = updates.send(DataUsageInfo::default()).await; - return Ok(()); - } - - let mut total_results = 0; - let mut result_index = 0; - self.pools.iter().for_each(|pool| { - total_results += pool.disk_set.len(); - }); - let results = Arc::new(RwLock::new(vec![DataUsageCache::default(); total_results])); - let (cancel, _) = broadcast::channel(100); - let first_err = Arc::new(RwLock::new(None)); - let mut futures = Vec::new(); - for pool in self.pools.iter() { - for set in pool.disk_set.iter() { - let index = result_index; - let results_clone = results.clone(); - let first_err_clone = first_err.clone(); - let cancel_clone = cancel.clone(); - let all_buckets_clone = all_buckets.clone(); - futures.push(async move { - let (tx, mut rx) = mpsc::channel(1); - let task = tokio::spawn(async move { - loop { - match rx.recv().await { - Some(info) => { - results_clone.write().await[index] = info; - } - None => { - return; - } - } - } - }); - if let Err(err) = set - .clone() - .ns_scanner(&all_buckets_clone, want_cycle as u32, tx, heal_scan_mode) - .await - { - let mut f_w = first_err_clone.write().await; - if f_w.is_none() { - *f_w = Some(err); - } - let _ = cancel_clone.send(true); - return; - } - let _ = task.await; - }); - result_index += 1; - } - } - let (update_closer_tx, mut update_close_rx) = mpsc::channel(10); - let mut ctx_clone = cancel.subscribe(); - let all_buckets_clone = all_buckets.clone(); - // 新增:从环境变量读取 interval,默认 30 秒 - let ns_scanner_interval_secs = std::env::var("RUSTFS_NS_SCANNER_INTERVAL") - .ok() - .and_then(|v| v.parse::().ok()) - .unwrap_or(30); - - // 检查是否跳过后台任务 - let skip_background_task = std::env::var("RUSTFS_SKIP_BACKGROUND_TASK") - .ok() - .and_then(|v| v.parse::().ok()) - .unwrap_or(false); - - if skip_background_task { - info!("跳过后台任务执行:RUSTFS_SKIP_BACKGROUND_TASK=true"); - return Ok(()); - } - - let task = tokio::spawn(async move { - let mut last_update: Option = None; - let mut interval = interval(Duration::from_secs(ns_scanner_interval_secs)); - let all_merged = Arc::new(RwLock::new(DataUsageCache::default())); - loop { - select! { - _ = ctx_clone.recv() => { - return; - } - _ = update_close_rx.recv() => { - update_scan(all_merged.clone(), results.clone(), &mut last_update, all_buckets_clone.clone(), updates.clone()).await; - return; - } - _ = interval.tick() => { - update_scan(all_merged.clone(), results.clone(), &mut last_update, all_buckets_clone.clone(), updates.clone()).await; - } - } - } - }); - let _ = join_all(futures).await; - let mut ctx_closer = cancel.subscribe(); - select! { - _ = update_closer_tx.send(true) => { - - } - _ = ctx_closer.recv() => { - - } - } - let _ = task.await; - if let Some(err) = first_err.read().await.as_ref() { - return Err(err.clone()); - } - Ok(()) - } - async fn get_latest_object_info_with_idx( &self, bucket: &str, @@ -1068,34 +944,6 @@ impl ECStore { } } -#[tracing::instrument(level = "info", skip(all_buckets, updates))] -async fn update_scan( - all_merged: Arc>, - results: Arc>>, - last_update: &mut Option, - all_buckets: Vec, - updates: Sender, -) { - let mut w = all_merged.write().await; - *w = DataUsageCache { - info: DataUsageCacheInfo { - name: DATA_USAGE_ROOT.to_string(), - ..Default::default() - }, - ..Default::default() - }; - for info in results.read().await.iter() { - if info.info.last_update.is_none() { - return; - } - w.merge(info); - } - if (last_update.is_none() || w.info.last_update > *last_update) && w.root().is_some() { - let _ = updates.send(w.dui(&w.info.name, &all_buckets)).await; - *last_update = w.info.last_update; - } -} - pub async fn find_local_disk(disk_path: &String) -> Option { let disk_map = GLOBAL_LOCAL_DISK_MAP.read().await; @@ -2237,7 +2085,7 @@ impl StorageAPI for ECStore { async fn heal_format(&self, dry_run: bool) -> Result<(HealResultItem, Option)> { info!("heal_format"); let mut r = HealResultItem { - heal_item_type: HEAL_ITEM_METADATA.to_string(), + heal_item_type: HealItemType::Metadata.to_string(), detail: "disk-format".to_string(), ..Default::default() }; @@ -2351,120 +2199,6 @@ impl StorageAPI for ECStore { Ok((HealResultItem::default(), Some(Error::FileNotFound))) } - #[tracing::instrument(skip(self))] - async fn heal_objects( - &self, - bucket: &str, - prefix: &str, - opts: &HealOpts, - hs: Arc, - is_meta: bool, - ) -> Result<()> { - info!("heal objects"); - let opts_clone = *opts; - let heal_entry: HealEntryFn = Arc::new(move |bucket: String, entry: MetaCacheEntry, scan_mode: HealScanMode| { - let opts_clone = opts_clone; - let hs_clone = hs.clone(); - Box::pin(async move { - if entry.is_dir() { - return Ok(()); - } - - if bucket == RUSTFS_META_BUCKET - && Pattern::new("buckets/*/.metacache/*") - .map(|p| p.matches(&entry.name)) - .unwrap_or(false) - || Pattern::new("tmp/*").map(|p| p.matches(&entry.name)).unwrap_or(false) - || Pattern::new("multipart/*").map(|p| p.matches(&entry.name)).unwrap_or(false) - || Pattern::new("tmp-old/*").map(|p| p.matches(&entry.name)).unwrap_or(false) - { - return Ok(()); - } - let fivs = match entry.file_info_versions(&bucket) { - Ok(fivs) => fivs, - Err(_) => { - return if is_meta { - HealSequence::heal_meta_object(hs_clone.clone(), &bucket, &entry.name, "", scan_mode).await - } else { - HealSequence::heal_object(hs_clone.clone(), &bucket, &entry.name, "", scan_mode).await - }; - } - }; - - if opts_clone.remove && !opts_clone.dry_run { - let Some(store) = new_object_layer_fn() else { - return Err(Error::other("errServerNotInitialized")); - }; - - if let Err(err) = store.check_abandoned_parts(&bucket, &entry.name, &opts_clone).await { - info!("unable to check object {}/{} for abandoned data: {}", bucket, entry.name, err.to_string()); - } - } - for version in fivs.versions.iter() { - if is_meta { - if let Err(err) = HealSequence::heal_meta_object( - hs_clone.clone(), - &bucket, - &version.name, - &version.version_id.map(|v| v.to_string()).unwrap_or("".to_string()), - scan_mode, - ) - .await - { - match err { - Error::FileNotFound | Error::FileVersionNotFound => {} - _ => { - return Err(err); - } - } - } - } else if let Err(err) = HealSequence::heal_object( - hs_clone.clone(), - &bucket, - &version.name, - &version.version_id.map(|v| v.to_string()).unwrap_or("".to_string()), - scan_mode, - ) - .await - { - match err { - Error::FileNotFound | Error::FileVersionNotFound => {} - _ => { - return Err(err); - } - } - } - } - Ok(()) - }) - }); - let mut first_err = None; - for (idx, pool) in self.pools.iter().enumerate() { - if opts.pool.is_some() && opts.pool.unwrap() != idx { - continue; - } - //TODO: IsSuspended - - for (idx, set) in pool.disk_set.iter().enumerate() { - if opts.set.is_some() && opts.set.unwrap() != idx { - continue; - } - - if let Err(err) = set.list_and_heal(bucket, prefix, opts, heal_entry.clone()).await { - if first_err.is_none() { - first_err = Some(err) - } - } - } - } - - if first_err.is_some() { - return Err(first_err.unwrap()); - } - - Ok(()) - } - #[tracing::instrument(skip(self))] async fn get_pool_and_set(&self, id: &str) -> Result<(Option, Option, Option)> { for (pool_idx, pool) in self.pools.iter().enumerate() { diff --git a/crates/ecstore/src/store_api.rs b/crates/ecstore/src/store_api.rs index 7ca9cdb8f..5f1ea4e0d 100644 --- a/crates/ecstore/src/store_api.rs +++ b/crates/ecstore/src/store_api.rs @@ -15,16 +15,16 @@ use crate::bucket::metadata_sys::get_versioning_config; use crate::bucket::versioning::VersioningApi as _; use crate::cmd::bucket_replication::{ReplicationStatusType, VersionPurgeStatusType}; +use crate::disk::DiskStore; use crate::error::{Error, Result}; -use crate::heal::heal_ops::HealSequence; use crate::store_utils::clean_metadata; use crate::{ bucket::lifecycle::bucket_lifecycle_audit::LcAuditEvent, bucket::lifecycle::lifecycle::ExpirationOptions, bucket::lifecycle::{bucket_lifecycle_ops::TransitionedObject, lifecycle::TransitionOptions}, }; -use crate::{disk::DiskStore, heal::heal_commands::HealOpts}; use http::{HeaderMap, HeaderValue}; +use rustfs_common::heal_channel::HealOpts; use rustfs_filemeta::headers::RESERVED_METADATA_PREFIX_LOWER; use rustfs_filemeta::{FileInfo, MetaCacheEntriesSorted, ObjectPartInfo, headers::AMZ_OBJECT_TAGGING}; use rustfs_madmin::heal_commands::HealResultItem; @@ -1072,8 +1072,8 @@ pub trait StorageAPI: ObjectIO { version_id: &str, opts: &HealOpts, ) -> Result<(HealResultItem, Option)>; - async fn heal_objects(&self, bucket: &str, prefix: &str, opts: &HealOpts, hs: Arc, is_meta: bool) - -> Result<()>; + // async fn heal_objects(&self, bucket: &str, prefix: &str, opts: &HealOpts, hs: Arc, is_meta: bool) + // -> Result<()>; async fn get_pool_and_set(&self, id: &str) -> Result<(Option, Option, Option)>; async fn check_abandoned_parts(&self, bucket: &str, object: &str, opts: &HealOpts) -> Result<()>; } diff --git a/crates/ecstore/src/store_init.rs b/crates/ecstore/src/store_init.rs index 85ef58988..36bc0f33c 100644 --- a/crates/ecstore/src/store_init.rs +++ b/crates/ecstore/src/store_init.rs @@ -24,7 +24,6 @@ use crate::{ new_disk, }, endpoints::Endpoints, - heal::heal_commands::init_healing_tracker, }; use futures::future::join_all; use std::collections::{HashMap, hash_map::Entry}; @@ -288,7 +287,7 @@ async fn save_format_file_all(disks: &[Option], formats: &[Option], formats: &[Option, format: &Option, heal_id: &str) -> disk::error::Result<()> { +pub async fn save_format_file(disk: &Option, format: &Option) -> disk::error::Result<()> { if disk.is_none() { return Err(DiskError::DiskNotFound); } @@ -331,10 +330,6 @@ pub async fn save_format_file(disk: &Option, format: &Option Date: Mon, 21 Jul 2025 18:17:05 +0800 Subject: [PATCH 15/29] chore: update admin handlers, lockfile, and minor fixes Signed-off-by: junxiang Mu <1948535941@qq.com> --- crates/ecstore/src/admin_server_info.rs | 19 +++++++------------ rustfs/src/admin/handlers.rs | 6 +++--- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/crates/ecstore/src/admin_server_info.rs b/crates/ecstore/src/admin_server_info.rs index 0ee4b1b84..4ee7d94c4 100644 --- a/crates/ecstore/src/admin_server_info.rs +++ b/crates/ecstore/src/admin_server_info.rs @@ -12,23 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::data_usage::{DATA_USAGE_CACHE_NAME, DATA_USAGE_ROOT, load_data_usage_from_backend}; use crate::error::{Error, Result}; use crate::{ disk::endpoint::Endpoint, global::{GLOBAL_BOOT_TIME, GLOBAL_Endpoints}, - heal::{ - data_usage::{DATA_USAGE_CACHE_NAME, DATA_USAGE_ROOT, load_data_usage_from_backend}, - data_usage_cache::DataUsageCache, - heal_commands::{DRIVE_STATE_OK, DRIVE_STATE_UNFORMATTED}, - }, new_object_layer_fn, notification_sys::get_global_notification_sys, store_api::StorageAPI, }; -use rustfs_common::{ - // error::{Error, Result}, - globals::GLOBAL_Local_Node_Name, -}; + +use crate::data_usage::load_data_usage_cache; +use rustfs_common::{globals::GLOBAL_Local_Node_Name, heal_channel::DriveState}; use rustfs_madmin::{ BackendDisks, Disk, ErasureSetInfo, ITEM_INITIALIZING, ITEM_OFFLINE, ITEM_ONLINE, InfoMessage, ServerProperties, }; @@ -318,7 +313,7 @@ fn get_online_offline_disks_stats(disks_info: &[Disk]) -> (BackendDisks, Backend for disk in disks_info { let ep = &disk.endpoint; let state = &disk.state; - if *state != DRIVE_STATE_OK && *state != DRIVE_STATE_UNFORMATTED { + if *state != DriveState::Ok.to_string() && *state != DriveState::Unformatted.to_string() { *offline_disks.get_mut(ep).unwrap() += 1; continue; } @@ -359,13 +354,13 @@ async fn get_pools_info(all_disks: &[Disk]) -> Result::new()); erasure_set.objects_count = data_usage_info.objects_total_count; erasure_set.versions_count = data_usage_info.versions_total_count; erasure_set.delete_markers_count = data_usage_info.delete_markers_total_count; diff --git a/rustfs/src/admin/handlers.rs b/rustfs/src/admin/handlers.rs index ab3a083cd..3923fc636 100644 --- a/rustfs/src/admin/handlers.rs +++ b/rustfs/src/admin/handlers.rs @@ -23,15 +23,15 @@ use http::{HeaderMap, Uri}; use hyper::StatusCode; use matchit::Params; use percent_encoding::{AsciiSet, CONTROLS, percent_encode}; +use rustfs_common::heal_channel::HealOpts; use rustfs_ecstore::admin_server_info::get_server_info; use rustfs_ecstore::bucket::metadata_sys::{self, get_replication_config}; use rustfs_ecstore::bucket::target::BucketTarget; use rustfs_ecstore::bucket::versioning_sys::BucketVersioningSys; use rustfs_ecstore::cmd::bucket_targets::{self, GLOBAL_Bucket_Target_Sys}; +use rustfs_ecstore::data_usage::load_data_usage_from_backend; use rustfs_ecstore::error::StorageError; use rustfs_ecstore::global::get_global_action_cred; -use rustfs_ecstore::heal::data_usage::load_data_usage_from_backend; -use rustfs_ecstore::heal::heal_commands::HealOpts; use rustfs_ecstore::metrics_realtime::{CollectMetricsOpts, MetricType, collect_local_metrics}; use rustfs_ecstore::new_object_layer_fn; use rustfs_ecstore::pools::{get_total_usable_capacity, get_total_usable_capacity_free}; @@ -1095,7 +1095,7 @@ impl Operation for RemoveRemoteTargetHandler { #[cfg(test)] mod test { - use rustfs_ecstore::heal::heal_commands::HealOpts; + use rustfs_common::heal_channel::HealOpts; #[ignore] // FIXME: failed in github actions #[test] From b907f4e61b2b1b91654f3c9679178ec41d5309d6 Mon Sep 17 00:00:00 2001 From: junxiang Mu <1948535941@qq.com> Date: Mon, 21 Jul 2025 18:17:48 +0800 Subject: [PATCH 16/29] refactor(ahm): remove obsolete scanner/data_usage.rs after data usage refactor Signed-off-by: junxiang Mu <1948535941@qq.com> --- crates/ahm/src/scanner/data_usage.rs | 671 --------------------------- 1 file changed, 671 deletions(-) delete mode 100644 crates/ahm/src/scanner/data_usage.rs diff --git a/crates/ahm/src/scanner/data_usage.rs b/crates/ahm/src/scanner/data_usage.rs deleted file mode 100644 index 2ab97bff4..000000000 --- a/crates/ahm/src/scanner/data_usage.rs +++ /dev/null @@ -1,671 +0,0 @@ -// 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::{collections::HashMap, sync::Arc, time::SystemTime}; - -use rustfs_ecstore::{bucket::metadata_sys::get_replication_config, config::com::read_config, store::ECStore}; -use rustfs_utils::path::SLASH_SEPARATOR; -use serde::{Deserialize, Serialize}; -use tracing::{error, info, warn}; - -use crate::error::{Error, Result}; - -// Data usage storage constants -pub const DATA_USAGE_ROOT: &str = SLASH_SEPARATOR; -const DATA_USAGE_OBJ_NAME: &str = ".usage.json"; -const DATA_USAGE_BLOOM_NAME: &str = ".bloomcycle.bin"; -pub const DATA_USAGE_CACHE_NAME: &str = ".usage-cache.bin"; - -// Data usage storage paths -lazy_static::lazy_static! { - pub static ref DATA_USAGE_BUCKET: String = format!("{}{}{}", - rustfs_ecstore::disk::RUSTFS_META_BUCKET, - SLASH_SEPARATOR, - rustfs_ecstore::disk::BUCKET_META_PREFIX - ); - pub static ref DATA_USAGE_OBJ_NAME_PATH: String = format!("{}{}{}", - rustfs_ecstore::disk::BUCKET_META_PREFIX, - SLASH_SEPARATOR, - DATA_USAGE_OBJ_NAME - ); - pub static ref DATA_USAGE_BLOOM_NAME_PATH: String = format!("{}{}{}", - rustfs_ecstore::disk::BUCKET_META_PREFIX, - SLASH_SEPARATOR, - DATA_USAGE_BLOOM_NAME - ); -} - -/// Bucket target usage info provides replication statistics -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct BucketTargetUsageInfo { - pub replication_pending_size: u64, - pub replication_failed_size: u64, - pub replicated_size: u64, - pub replica_size: u64, - pub replication_pending_count: u64, - pub replication_failed_count: u64, - pub replicated_count: u64, -} - -/// Bucket usage info provides bucket-level statistics -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct BucketUsageInfo { - pub size: u64, - // Following five fields suffixed with V1 are here for backward compatibility - // Total Size for objects that have not yet been replicated - pub replication_pending_size_v1: u64, - // Total size for objects that have witness one or more failures and will be retried - pub replication_failed_size_v1: u64, - // Total size for objects that have been replicated to destination - pub replicated_size_v1: u64, - // Total number of objects pending replication - pub replication_pending_count_v1: u64, - // Total number of objects that failed replication - pub replication_failed_count_v1: u64, - - pub objects_count: u64, - pub object_size_histogram: HashMap, - pub object_versions_histogram: HashMap, - pub versions_count: u64, - pub delete_markers_count: u64, - pub replica_size: u64, - pub replica_count: u64, - pub replication_info: HashMap, -} - -/// DataUsageInfo represents data usage stats of the underlying storage -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct DataUsageInfo { - /// Total capacity - pub total_capacity: u64, - /// Total used capacity - pub total_used_capacity: u64, - /// Total free capacity - pub total_free_capacity: u64, - - /// LastUpdate is the timestamp of when the data usage info was last updated - pub last_update: Option, - - /// Objects total count across all buckets - pub objects_total_count: u64, - /// Versions total count across all buckets - pub versions_total_count: u64, - /// Delete markers total count across all buckets - pub delete_markers_total_count: u64, - /// Objects total size across all buckets - pub objects_total_size: u64, - /// Replication info across all buckets - pub replication_info: HashMap, - - /// Total number of buckets in this cluster - pub buckets_count: u64, - /// Buckets usage info provides following information across all buckets - pub buckets_usage: HashMap, - /// Deprecated kept here for backward compatibility reasons - pub bucket_sizes: HashMap, -} - -/// Size summary for a single object or group of objects -#[derive(Debug, Default, Clone)] -pub struct SizeSummary { - /// Total size - pub total_size: usize, - /// Number of versions - pub versions: usize, - /// Number of delete markers - pub delete_markers: usize, - /// Replicated size - pub replicated_size: usize, - /// Replicated count - pub replicated_count: usize, - /// Pending size - pub pending_size: usize, - /// Failed size - pub failed_size: usize, - /// Replica size - pub replica_size: usize, - /// Replica count - pub replica_count: usize, - /// Pending count - pub pending_count: usize, - /// Failed count - pub failed_count: usize, - /// Replication target stats - pub repl_target_stats: HashMap, -} - -/// Replication target size summary -#[derive(Debug, Default, Clone)] -pub struct ReplTargetSizeSummary { - /// Replicated size - pub replicated_size: usize, - /// Replicated count - pub replicated_count: usize, - /// Pending size - pub pending_size: usize, - /// Failed size - pub failed_size: usize, - /// Pending count - pub pending_count: usize, - /// Failed count - pub failed_count: usize, -} - -impl DataUsageInfo { - /// Create a new DataUsageInfo - pub fn new() -> Self { - Self::default() - } - - /// Add object metadata to data usage statistics - pub fn add_object(&mut self, object_path: &str, meta_object: &rustfs_filemeta::MetaObject) { - // This method is kept for backward compatibility - // For accurate version counting, use add_object_from_file_meta instead - let bucket_name = match self.extract_bucket_from_path(object_path) { - Ok(name) => name, - Err(_) => return, - }; - - // Update bucket statistics - if let Some(bucket_usage) = self.buckets_usage.get_mut(&bucket_name) { - bucket_usage.size += meta_object.size as u64; - bucket_usage.objects_count += 1; - bucket_usage.versions_count += 1; // Simplified: assume 1 version per object - - // Update size histogram - let total_size = meta_object.size as u64; - let size_ranges = [ - ("0-1KB", 0, 1024), - ("1KB-1MB", 1024, 1024 * 1024), - ("1MB-10MB", 1024 * 1024, 10 * 1024 * 1024), - ("10MB-100MB", 10 * 1024 * 1024, 100 * 1024 * 1024), - ("100MB-1GB", 100 * 1024 * 1024, 1024 * 1024 * 1024), - ("1GB+", 1024 * 1024 * 1024, u64::MAX), - ]; - - for (range_name, min_size, max_size) in size_ranges { - if total_size >= min_size && total_size < max_size { - *bucket_usage.object_size_histogram.entry(range_name.to_string()).or_insert(0) += 1; - break; - } - } - - // Update version histogram (simplified - count as single version) - *bucket_usage - .object_versions_histogram - .entry("SINGLE_VERSION".to_string()) - .or_insert(0) += 1; - } else { - // Create new bucket usage - let mut bucket_usage = BucketUsageInfo { - size: meta_object.size as u64, - objects_count: 1, - versions_count: 1, - ..Default::default() - }; - bucket_usage.object_size_histogram.insert("0-1KB".to_string(), 1); - bucket_usage.object_versions_histogram.insert("SINGLE_VERSION".to_string(), 1); - self.buckets_usage.insert(bucket_name, bucket_usage); - } - - // Update global statistics - self.objects_total_size += meta_object.size as u64; - self.objects_total_count += 1; - self.versions_total_count += 1; - } - - /// Add object from FileMeta for accurate version counting - pub fn add_object_from_file_meta(&mut self, object_path: &str, file_meta: &rustfs_filemeta::FileMeta) { - let bucket_name = match self.extract_bucket_from_path(object_path) { - Ok(name) => name, - Err(_) => return, - }; - - // Calculate accurate statistics from all versions - let mut total_size = 0u64; - let mut versions_count = 0u64; - let mut delete_markers_count = 0u64; - let mut latest_object_size = 0u64; - - // Process all versions to get accurate counts - for version in &file_meta.versions { - match rustfs_filemeta::FileMetaVersion::try_from(version.clone()) { - Ok(ver) => { - if let Some(obj) = ver.object { - total_size += obj.size as u64; - versions_count += 1; - latest_object_size = obj.size as u64; // Keep track of latest object size - } else if ver.delete_marker.is_some() { - delete_markers_count += 1; - } - } - Err(_) => { - // Skip invalid versions - continue; - } - } - } - - // Update bucket statistics - if let Some(bucket_usage) = self.buckets_usage.get_mut(&bucket_name) { - bucket_usage.size += total_size; - bucket_usage.objects_count += 1; - bucket_usage.versions_count += versions_count; - bucket_usage.delete_markers_count += delete_markers_count; - - // Update size histogram based on latest object size - let size_ranges = [ - ("0-1KB", 0, 1024), - ("1KB-1MB", 1024, 1024 * 1024), - ("1MB-10MB", 1024 * 1024, 10 * 1024 * 1024), - ("10MB-100MB", 10 * 1024 * 1024, 100 * 1024 * 1024), - ("100MB-1GB", 100 * 1024 * 1024, 1024 * 1024 * 1024), - ("1GB+", 1024 * 1024 * 1024, u64::MAX), - ]; - - for (range_name, min_size, max_size) in size_ranges { - if latest_object_size >= min_size && latest_object_size < max_size { - *bucket_usage.object_size_histogram.entry(range_name.to_string()).or_insert(0) += 1; - break; - } - } - - // Update version histogram based on actual version count - let version_ranges = [ - ("1", 1, 1), - ("2-5", 2, 5), - ("6-10", 6, 10), - ("11-50", 11, 50), - ("51-100", 51, 100), - ("100+", 101, usize::MAX), - ]; - - for (range_name, min_versions, max_versions) in version_ranges { - if versions_count as usize >= min_versions && versions_count as usize <= max_versions { - *bucket_usage - .object_versions_histogram - .entry(range_name.to_string()) - .or_insert(0) += 1; - break; - } - } - } else { - // Create new bucket usage - let mut bucket_usage = BucketUsageInfo { - size: total_size, - objects_count: 1, - versions_count, - delete_markers_count, - ..Default::default() - }; - - // Set size histogram - let size_ranges = [ - ("0-1KB", 0, 1024), - ("1KB-1MB", 1024, 1024 * 1024), - ("1MB-10MB", 1024 * 1024, 10 * 1024 * 1024), - ("10MB-100MB", 10 * 1024 * 1024, 100 * 1024 * 1024), - ("100MB-1GB", 100 * 1024 * 1024, 1024 * 1024 * 1024), - ("1GB+", 1024 * 1024 * 1024, u64::MAX), - ]; - - for (range_name, min_size, max_size) in size_ranges { - if latest_object_size >= min_size && latest_object_size < max_size { - bucket_usage.object_size_histogram.insert(range_name.to_string(), 1); - break; - } - } - - // Set version histogram - let version_ranges = [ - ("1", 1, 1), - ("2-5", 2, 5), - ("6-10", 6, 10), - ("11-50", 11, 50), - ("51-100", 51, 100), - ("100+", 101, usize::MAX), - ]; - - for (range_name, min_versions, max_versions) in version_ranges { - if versions_count as usize >= min_versions && versions_count as usize <= max_versions { - bucket_usage.object_versions_histogram.insert(range_name.to_string(), 1); - break; - } - } - - self.buckets_usage.insert(bucket_name, bucket_usage); - // Update buckets count when adding new bucket - self.buckets_count = self.buckets_usage.len() as u64; - } - - // Update global statistics - self.objects_total_size += total_size; - self.objects_total_count += 1; - self.versions_total_count += versions_count; - self.delete_markers_total_count += delete_markers_count; - } - - /// Extract bucket name from object path - fn extract_bucket_from_path(&self, object_path: &str) -> Result { - let parts: Vec<&str> = object_path.split('/').collect(); - if parts.is_empty() { - return Err(Error::Scanner("Invalid object path: empty".to_string())); - } - Ok(parts[0].to_string()) - } - - /// Update capacity information - pub fn update_capacity(&mut self, total: u64, used: u64, free: u64) { - self.total_capacity = total; - self.total_used_capacity = used; - self.total_free_capacity = free; - self.last_update = Some(SystemTime::now()); - } - - /// Add bucket usage info - pub fn add_bucket_usage(&mut self, bucket: String, usage: BucketUsageInfo) { - self.buckets_usage.insert(bucket.clone(), usage); - self.buckets_count = self.buckets_usage.len() as u64; - self.last_update = Some(SystemTime::now()); - } - - /// Get bucket usage info - pub fn get_bucket_usage(&self, bucket: &str) -> Option<&BucketUsageInfo> { - self.buckets_usage.get(bucket) - } - - /// Calculate total statistics from all buckets - pub fn calculate_totals(&mut self) { - self.objects_total_count = 0; - self.versions_total_count = 0; - self.delete_markers_total_count = 0; - self.objects_total_size = 0; - - for usage in self.buckets_usage.values() { - self.objects_total_count += usage.objects_count; - self.versions_total_count += usage.versions_count; - self.delete_markers_total_count += usage.delete_markers_count; - self.objects_total_size += usage.size; - } - } - - /// Merge another DataUsageInfo into this one - pub fn merge(&mut self, other: &DataUsageInfo) { - // Merge bucket usage - for (bucket, usage) in &other.buckets_usage { - if let Some(existing) = self.buckets_usage.get_mut(bucket) { - existing.merge(usage); - } else { - self.buckets_usage.insert(bucket.clone(), usage.clone()); - } - } - - // Recalculate totals - self.calculate_totals(); - - // Ensure buckets_count stays consistent with buckets_usage - self.buckets_count = self.buckets_usage.len() as u64; - - // Update last update time - if let Some(other_update) = other.last_update { - if self.last_update.is_none() || other_update > self.last_update.unwrap() { - self.last_update = Some(other_update); - } - } - } -} - -impl BucketUsageInfo { - /// Create a new BucketUsageInfo - pub fn new() -> Self { - Self::default() - } - - /// Add size summary to this bucket usage - pub fn add_size_summary(&mut self, summary: &SizeSummary) { - self.size += summary.total_size as u64; - self.versions_count += summary.versions as u64; - self.delete_markers_count += summary.delete_markers as u64; - self.replica_size += summary.replica_size as u64; - self.replica_count += summary.replica_count as u64; - } - - /// Merge another BucketUsageInfo into this one - pub fn merge(&mut self, other: &BucketUsageInfo) { - self.size += other.size; - self.objects_count += other.objects_count; - self.versions_count += other.versions_count; - self.delete_markers_count += other.delete_markers_count; - self.replica_size += other.replica_size; - self.replica_count += other.replica_count; - - // Merge histograms - for (key, value) in &other.object_size_histogram { - *self.object_size_histogram.entry(key.clone()).or_insert(0) += value; - } - - for (key, value) in &other.object_versions_histogram { - *self.object_versions_histogram.entry(key.clone()).or_insert(0) += value; - } - - // Merge replication info - for (target, info) in &other.replication_info { - let entry = self.replication_info.entry(target.clone()).or_default(); - entry.replicated_size += info.replicated_size; - entry.replica_size += info.replica_size; - entry.replication_pending_size += info.replication_pending_size; - entry.replication_failed_size += info.replication_failed_size; - entry.replication_pending_count += info.replication_pending_count; - entry.replication_failed_count += info.replication_failed_count; - entry.replicated_count += info.replicated_count; - } - - // Merge backward compatibility fields - self.replication_pending_size_v1 += other.replication_pending_size_v1; - self.replication_failed_size_v1 += other.replication_failed_size_v1; - self.replicated_size_v1 += other.replicated_size_v1; - self.replication_pending_count_v1 += other.replication_pending_count_v1; - self.replication_failed_count_v1 += other.replication_failed_count_v1; - } -} - -impl SizeSummary { - /// Create a new SizeSummary - pub fn new() -> Self { - Self::default() - } - - /// Add another SizeSummary to this one - pub fn add(&mut self, other: &SizeSummary) { - self.total_size += other.total_size; - self.versions += other.versions; - self.delete_markers += other.delete_markers; - self.replicated_size += other.replicated_size; - self.replicated_count += other.replicated_count; - self.pending_size += other.pending_size; - self.failed_size += other.failed_size; - self.replica_size += other.replica_size; - self.replica_count += other.replica_count; - self.pending_count += other.pending_count; - self.failed_count += other.failed_count; - - // Merge replication target stats - for (target, stats) in &other.repl_target_stats { - let entry = self.repl_target_stats.entry(target.clone()).or_default(); - entry.replicated_size += stats.replicated_size; - entry.replicated_count += stats.replicated_count; - entry.pending_size += stats.pending_size; - entry.failed_size += stats.failed_size; - entry.pending_count += stats.pending_count; - entry.failed_count += stats.failed_count; - } - } -} - -/// Store data usage info to backend storage -pub async fn store_data_usage_in_backend(data_usage_info: DataUsageInfo, store: Arc) -> Result<()> { - let data = - serde_json::to_vec(&data_usage_info).map_err(|e| Error::Config(format!("Failed to serialize data usage info: {e}")))?; - - // Save to backend using the same mechanism as original code - rustfs_ecstore::config::com::save_config(store, &DATA_USAGE_OBJ_NAME_PATH, data) - .await - .map_err(Error::Storage)?; - - Ok(()) -} - -/// Load data usage info from backend storage -pub async fn load_data_usage_from_backend(store: Arc) -> Result { - let buf = match read_config(store, &DATA_USAGE_OBJ_NAME_PATH).await { - Ok(data) => data, - Err(e) => { - error!("Failed to read data usage info from backend: {}", e); - if e == rustfs_ecstore::error::Error::ConfigNotFound { - return Ok(DataUsageInfo::default()); - } - return Err(Error::Storage(e)); - } - }; - - let mut data_usage_info: DataUsageInfo = - serde_json::from_slice(&buf).map_err(|e| Error::Config(format!("Failed to deserialize data usage info: {e}")))?; - - warn!("Loaded data usage info from backend {:?}", &data_usage_info); - - // Handle backward compatibility like original code - if data_usage_info.buckets_usage.is_empty() { - data_usage_info.buckets_usage = data_usage_info - .bucket_sizes - .iter() - .map(|(bucket, &size)| { - ( - bucket.clone(), - BucketUsageInfo { - size, - ..Default::default() - }, - ) - }) - .collect(); - } - - if data_usage_info.bucket_sizes.is_empty() { - data_usage_info.bucket_sizes = data_usage_info - .buckets_usage - .iter() - .map(|(bucket, bui)| (bucket.clone(), bui.size)) - .collect(); - } - - for (bucket, bui) in &data_usage_info.buckets_usage { - if bui.replicated_size_v1 > 0 - || bui.replication_failed_count_v1 > 0 - || bui.replication_failed_size_v1 > 0 - || bui.replication_pending_count_v1 > 0 - { - if let Ok((cfg, _)) = get_replication_config(bucket).await { - if !cfg.role.is_empty() { - data_usage_info.replication_info.insert( - cfg.role.clone(), - BucketTargetUsageInfo { - replication_failed_size: bui.replication_failed_size_v1, - replication_failed_count: bui.replication_failed_count_v1, - replicated_size: bui.replicated_size_v1, - replication_pending_count: bui.replication_pending_count_v1, - replication_pending_size: bui.replication_pending_size_v1, - ..Default::default() - }, - ); - } - } - } - } - - Ok(data_usage_info) -} - -/// Example function showing how to use AHM data usage functionality -/// This demonstrates the integration pattern for DataUsageInfoHandler -pub async fn example_data_usage_integration() -> Result<()> { - // Get the global storage instance - let Some(store) = rustfs_ecstore::new_object_layer_fn() else { - return Err(Error::Config("Storage not initialized".to_string())); - }; - - // Load data usage from backend (this replaces the original load_data_usage_from_backend) - let data_usage = load_data_usage_from_backend(store).await?; - - info!( - "Loaded data usage info: {} buckets, {} total objects", - data_usage.buckets_count, data_usage.objects_total_count - ); - - // Example: Store updated data usage back to backend - // This would typically be called by the scanner after collecting new statistics - // store_data_usage_in_backend(data_usage, store).await?; - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_data_usage_info_creation() { - let mut info = DataUsageInfo::new(); - info.update_capacity(1000, 500, 500); - - assert_eq!(info.total_capacity, 1000); - assert_eq!(info.total_used_capacity, 500); - assert_eq!(info.total_free_capacity, 500); - assert!(info.last_update.is_some()); - } - - #[test] - fn test_bucket_usage_info_merge() { - let mut usage1 = BucketUsageInfo::new(); - usage1.size = 100; - usage1.objects_count = 10; - usage1.versions_count = 5; - - let mut usage2 = BucketUsageInfo::new(); - usage2.size = 200; - usage2.objects_count = 20; - usage2.versions_count = 10; - - usage1.merge(&usage2); - - assert_eq!(usage1.size, 300); - assert_eq!(usage1.objects_count, 30); - assert_eq!(usage1.versions_count, 15); - } - - #[test] - fn test_size_summary_add() { - let mut summary1 = SizeSummary::new(); - summary1.total_size = 100; - summary1.versions = 5; - - let mut summary2 = SizeSummary::new(); - summary2.total_size = 200; - summary2.versions = 10; - - summary1.add(&summary2); - - assert_eq!(summary1.total_size, 300); - assert_eq!(summary1.versions, 15); - } -} From 0854e6b9219f5c6451ee047eac9ebbd335751e00 Mon Sep 17 00:00:00 2001 From: junxiang Mu <1948535941@qq.com> Date: Tue, 22 Jul 2025 09:52:57 +0800 Subject: [PATCH 17/29] Chore: rename init_heal_manager_with_channel Signed-off-by: junxiang Mu <1948535941@qq.com> --- crates/ahm/src/lib.rs | 2 +- rustfs/src/main.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/ahm/src/lib.rs b/crates/ahm/src/lib.rs index 207973e99..ae1307146 100644 --- a/crates/ahm/src/lib.rs +++ b/crates/ahm/src/lib.rs @@ -63,7 +63,7 @@ static GLOBAL_HEAL_MANAGER: OnceLock> = OnceLock::new(); static GLOBAL_HEAL_CHANNEL_PROCESSOR: OnceLock>> = OnceLock::new(); /// Initialize and start heal manager with channel processor -pub async fn init_heal_manager_with_channel( +pub async fn init_heal_manager( storage: Arc, config: Option, ) -> Result<()> { diff --git a/rustfs/src/main.rs b/rustfs/src/main.rs index b7cc0a32f..c40658328 100644 --- a/rustfs/src/main.rs +++ b/rustfs/src/main.rs @@ -30,7 +30,7 @@ use clap::Parser; use license::init_license; use rustfs_ahm::scanner::data_scanner::ScannerConfig; use rustfs_ahm::{ - Scanner, create_ahm_services_cancel_token, heal::storage::ECStoreHealStorage, init_heal_manager_with_channel, + Scanner, create_ahm_services_cancel_token, heal::storage::ECStoreHealStorage, init_heal_manager, shutdown_ahm_services, }; use rustfs_common::globals::set_global_addr; @@ -191,7 +191,7 @@ async fn run(opt: config::Opt) -> Result<()> { // Initialize heal manager with channel processor let heal_storage = Arc::new(ECStoreHealStorage::new(store.clone())); - init_heal_manager_with_channel(heal_storage, None).await?; + init_heal_manager(heal_storage, None).await?; let scanner = Scanner::new(Some(ScannerConfig::default()), None); scanner.start().await?; From 2bfd1efb9b5c6d6f2a962c566763cbf10815e4e2 Mon Sep 17 00:00:00 2001 From: junxiang Mu <1948535941@qq.com> Date: Tue, 22 Jul 2025 10:12:09 +0800 Subject: [PATCH 18/29] Fix: fix add heal_manager into scanner when scanner start Signed-off-by: junxiang Mu <1948535941@qq.com> --- crates/ahm/src/lib.rs | 6 +++--- rustfs/src/main.rs | 7 +++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/ahm/src/lib.rs b/crates/ahm/src/lib.rs index ae1307146..17a6b71bc 100644 --- a/crates/ahm/src/lib.rs +++ b/crates/ahm/src/lib.rs @@ -66,7 +66,7 @@ static GLOBAL_HEAL_CHANNEL_PROCESSOR: OnceLock, config: Option, -) -> Result<()> { +) -> Result> { // Create heal manager let heal_manager = Arc::new(HealManager::new(storage, config)); @@ -82,7 +82,7 @@ pub async fn init_heal_manager( let channel_receiver = rustfs_common::heal_channel::init_heal_channel(); // Create channel processor - let channel_processor = HealChannelProcessor::new(heal_manager); + let channel_processor = HealChannelProcessor::new(heal_manager.clone()); // Store channel processor instance first GLOBAL_HEAL_CHANNEL_PROCESSOR @@ -101,7 +101,7 @@ pub async fn init_heal_manager( }); info!("Heal manager with channel processor initialized successfully"); - Ok(()) + Ok(heal_manager) } /// Get global heal manager instance diff --git a/rustfs/src/main.rs b/rustfs/src/main.rs index c40658328..c6677ed9e 100644 --- a/rustfs/src/main.rs +++ b/rustfs/src/main.rs @@ -30,8 +30,7 @@ use clap::Parser; use license::init_license; use rustfs_ahm::scanner::data_scanner::ScannerConfig; use rustfs_ahm::{ - Scanner, create_ahm_services_cancel_token, heal::storage::ECStoreHealStorage, init_heal_manager, - shutdown_ahm_services, + Scanner, create_ahm_services_cancel_token, heal::storage::ECStoreHealStorage, init_heal_manager, shutdown_ahm_services, }; use rustfs_common::globals::set_global_addr; use rustfs_config::DEFAULT_DELIMITER; @@ -191,9 +190,9 @@ async fn run(opt: config::Opt) -> Result<()> { // Initialize heal manager with channel processor let heal_storage = Arc::new(ECStoreHealStorage::new(store.clone())); - init_heal_manager(heal_storage, None).await?; + let heal_manager = init_heal_manager(heal_storage, None).await?; - let scanner = Scanner::new(Some(ScannerConfig::default()), None); + let scanner = Scanner::new(Some(ScannerConfig::default()), Some(heal_manager)); scanner.start().await?; print_server_info(); init_bucket_replication_pool().await; From 69b0c828c94c3a6bb75a16dc8817225837199b55 Mon Sep 17 00:00:00 2001 From: junxiang Mu <1948535941@qq.com> Date: Tue, 22 Jul 2025 11:08:01 +0800 Subject: [PATCH 19/29] fix: scanner add heal bucket Signed-off-by: junxiang Mu <1948535941@qq.com> --- crates/ahm/src/scanner/data_scanner.rs | 154 +++++++++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/crates/ahm/src/scanner/data_scanner.rs b/crates/ahm/src/scanner/data_scanner.rs index 037879268..68f66d8a2 100644 --- a/crates/ahm/src/scanner/data_scanner.rs +++ b/crates/ahm/src/scanner/data_scanner.rs @@ -429,9 +429,16 @@ impl Scanner { info!("Scanning EC set {} in pool {}", set_index, pool_index); + // list all bucket for heal bucket + // Get online disks from this EC set let (disks, _) = set_disks.get_online_disks_with_healing(false).await; + // Check volume consistency across disks and heal missing buckets + if !disks.is_empty() { + self.check_and_heal_missing_volumes(&disks, set_index, pool_index).await?; + } + if disks.is_empty() { warn!("No online disks available for EC set {} in pool {}", set_index, pool_index); return Ok(Vec::new()); @@ -1196,6 +1203,91 @@ impl Scanner { Ok(()) } + /// Check volume consistency across disks and heal missing buckets + async fn check_and_heal_missing_volumes(&self, disks: &[DiskStore], set_index: usize, pool_index: usize) -> Result<()> { + info!("Checking volume consistency for EC set {} in pool {}", set_index, pool_index); + + // Step 1: Collect bucket lists from all online disks + let mut disk_bucket_lists = Vec::new(); + let mut all_buckets = std::collections::HashSet::new(); + + for (disk_idx, disk) in disks.iter().enumerate() { + match disk.list_volumes().await { + Ok(volumes) => { + let bucket_names: Vec = volumes.iter().map(|v| v.name.clone()).collect(); + for bucket in &bucket_names { + all_buckets.insert(bucket.clone()); + } + disk_bucket_lists.push((disk_idx, bucket_names)); + debug!("Disk {} has {} buckets", disk_idx, volumes.len()); + } + Err(e) => { + warn!("Failed to list volumes on disk {}: {}", disk_idx, e); + disk_bucket_lists.push((disk_idx, Vec::new())); + } + } + } + + // Step 2: Find missing buckets on each disk + let mut missing_buckets_count = 0; + for (disk_idx, disk_buckets) in &disk_bucket_lists { + let disk_bucket_set: std::collections::HashSet<_> = disk_buckets.iter().collect(); + let missing_buckets: Vec<_> = all_buckets + .iter() + .filter(|bucket| !disk_bucket_set.contains(bucket)) + .collect(); + + if !missing_buckets.is_empty() { + missing_buckets_count += missing_buckets.len(); + warn!("Disk {} is missing {} buckets: {:?}", disk_idx, missing_buckets.len(), missing_buckets); + + // Step 3: Submit heal tasks for missing buckets + let enable_healing = self.config.read().await.enable_healing; + if enable_healing { + if let Some(heal_manager) = &self.heal_manager { + for bucket in missing_buckets { + let req = crate::heal::HealRequest::bucket(bucket.clone()); + match heal_manager.submit_heal_request(req).await { + Ok(task_id) => { + info!( + "Submitted bucket heal task {} for missing bucket '{}' on disk {}", + task_id, bucket, disk_idx + ); + } + Err(e) => { + error!("Failed to submit bucket heal task for '{}' on disk {}: {}", bucket, disk_idx, e); + } + } + } + } else { + warn!("Healing is enabled but no heal manager available"); + } + } else { + info!("Healing is disabled, skipping bucket heal tasks"); + } + } + } + + if missing_buckets_count > 0 { + warn!( + "Found {} missing bucket instances across {} disks in EC set {} (pool {})", + missing_buckets_count, + disks.len(), + set_index, + pool_index + ); + } else { + info!( + "All buckets are consistent across {} disks in EC set {} (pool {})", + disks.len(), + set_index, + pool_index + ); + } + + Ok(()) + } + /// Clone scanner for background tasks fn clone_for_background(&self) -> Self { Self { @@ -1460,4 +1552,66 @@ mod tests { // clean up temp dir let _ = std::fs::remove_dir_all(std::path::Path::new(TEST_DIR_USAGE_STATS)); } + + #[tokio::test(flavor = "multi_thread")] + #[serial] + async fn test_volume_healing_functionality() { + const TEST_DIR_VOLUME_HEAL: &str = "/tmp/rustfs_ahm_test_volume_heal"; + let (disk_paths, ecstore) = prepare_test_env(Some(TEST_DIR_VOLUME_HEAL), Some(9003)).await; + + // Create test buckets + let bucket1 = "test-bucket-1"; + let bucket2 = "test-bucket-2"; + + ecstore.make_bucket(bucket1, &Default::default()).await.unwrap(); + ecstore.make_bucket(bucket2, &Default::default()).await.unwrap(); + + // Add some test objects + let mut pr1 = PutObjReader::from_vec(b"test data 1".to_vec()); + ecstore + .put_object(bucket1, "obj1", &mut pr1, &Default::default()) + .await + .unwrap(); + + let mut pr2 = PutObjReader::from_vec(b"test data 2".to_vec()); + ecstore + .put_object(bucket2, "obj2", &mut pr2, &Default::default()) + .await + .unwrap(); + + // Simulate missing bucket on one disk by removing bucket directory + let disk1_bucket1_path = disk_paths[0].join(bucket1); + if disk1_bucket1_path.exists() { + println!("Removing bucket directory to simulate missing volume: {disk1_bucket1_path:?}"); + match fs::remove_dir_all(&disk1_bucket1_path) { + Ok(_) => println!("Successfully removed bucket directory from disk 0"), + Err(e) => println!("Failed to remove bucket directory: {e}"), + } + } + + // Create scanner without heal manager for now (testing the detection logic) + let scanner = Scanner::new(None, None); + + // Enable healing in config + { + let mut config = scanner.config.write().await; + config.enable_healing = true; + } + + println!("=== Testing volume healing functionality ==="); + + // Run scan cycle which should detect missing volume + // The new check_and_heal_missing_volumes function should be called + let scan_result = scanner.scan_cycle().await; + assert!(scan_result.is_ok(), "Scan cycle should succeed"); + + // Get metrics to verify scan completed + let metrics = scanner.get_metrics().await; + assert!(metrics.total_cycles > 0, "Should have completed scan cycles"); + println!("Volume healing detection test completed successfully"); + println!("Scan metrics: {metrics:?}"); + + // Clean up + let _ = std::fs::remove_dir_all(std::path::Path::new(TEST_DIR_VOLUME_HEAL)); + } } From d562620e9995810f160803815405a4c3607f9583 Mon Sep 17 00:00:00 2001 From: junxiang Mu <1948535941@qq.com> Date: Tue, 22 Jul 2025 15:39:59 +0800 Subject: [PATCH 20/29] fix: implement uses_data_dir method Signed-off-by: junxiang Mu <1948535941@qq.com> --- crates/filemeta/src/filemeta.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/filemeta/src/filemeta.rs b/crates/filemeta/src/filemeta.rs index 8ab72cbc9..fd961d2d5 100644 --- a/crates/filemeta/src/filemeta.rs +++ b/crates/filemeta/src/filemeta.rs @@ -1523,8 +1523,7 @@ impl MetaObject { } pub fn uses_data_dir(&self) -> bool { - // TODO: when use inlinedata - true + !self.inlinedata() } pub fn inlinedata(&self) -> bool { From bdaee228db4e764463d1e34eaa22d8cbc7ed6ca1 Mon Sep 17 00:00:00 2001 From: junxiang Mu <1948535941@qq.com> Date: Wed, 23 Jul 2025 10:20:06 +0800 Subject: [PATCH 21/29] fix(ahm): adjust test expectations for missing xl.meta recovery scenario Signed-off-by: junxiang Mu <1948535941@qq.com> --- crates/ahm/src/scanner/data_scanner.rs | 780 +++++++++++++++++++++---- 1 file changed, 657 insertions(+), 123 deletions(-) diff --git a/crates/ahm/src/scanner/data_scanner.rs b/crates/ahm/src/scanner/data_scanner.rs index 68f66d8a2..ec20a0b95 100644 --- a/crates/ahm/src/scanner/data_scanner.rs +++ b/crates/ahm/src/scanner/data_scanner.rs @@ -419,6 +419,206 @@ impl Scanner { Ok(()) } + /// Verify object integrity and trigger healing if necessary + async fn verify_object_integrity(&self, bucket: &str, object: &str) -> Result<()> { + debug!("Starting verify_object_integrity for {}/{}", bucket, object); + + let config = self.config.read().await; + if !config.enable_healing || config.scan_mode != ScanMode::Deep { + debug!("Healing disabled or not in deep scan mode, skipping verification"); + return Ok(()); + } + + if let Some(ecstore) = rustfs_ecstore::new_object_layer_fn() { + // First try the standard integrity check + let object_opts = ecstore::store_api::ObjectOptions::default(); + let mut integrity_failed = false; + + debug!("Running standard object verification for {}/{}", bucket, object); + match ecstore.verify_object_integrity(bucket, object, &object_opts).await { + Ok(_) => { + debug!("Standard verification passed for {}/{}", bucket, object); + // Standard verification passed, now check for missing data parts + match self.check_data_parts_integrity(bucket, object).await { + Ok(_) => { + // Object is completely healthy + debug!("Data parts integrity check passed for {}/{}", bucket, object); + self.metrics.increment_healthy_objects(); + } + Err(e) => { + // Data parts are missing or corrupt + debug!("Data parts integrity check failed for {}/{}: {}", bucket, object, e); + warn!("Data parts integrity check failed for {}/{}: {}. Triggering heal.", bucket, object, e); + integrity_failed = true; + } + } + } + Err(e) => { + // Standard object verification failed + debug!("Standard verification failed for {}/{}: {}", bucket, object, e); + warn!("Object verification failed for {}/{}: {}. Triggering heal.", bucket, object, e); + integrity_failed = true; + } + } + + debug!("integrity_failed = {} for {}/{}", integrity_failed, bucket, object); + if integrity_failed { + self.metrics.increment_corrupted_objects(); + + if let Some(heal_manager) = &self.heal_manager { + debug!("Submitting heal request for {}/{}", bucket, object); + let heal_request = HealRequest::object(bucket.to_string(), object.to_string(), None); + if let Err(e) = heal_manager.submit_heal_request(heal_request).await { + error!("Failed to submit heal task for {}/{}: {}", bucket, object, e); + } else { + debug!("Successfully submitted heal request for {}/{}", bucket, object); + } + } else { + debug!("No heal manager available for {}/{}", bucket, object); + } + } + } else { + debug!("No ECStore available for {}/{}", bucket, object); + } + + debug!("Completed verify_object_integrity for {}/{}", bucket, object); + Ok(()) + } + + /// Check data parts integrity by verifying all parts exist on disks + async fn check_data_parts_integrity(&self, bucket: &str, object: &str) -> Result<()> { + debug!("Checking data parts integrity for {}/{}", bucket, object); + + if let Some(ecstore) = rustfs_ecstore::new_object_layer_fn() { + // Get object info + let object_info = match ecstore.get_object_info(bucket, object, &Default::default()).await { + Ok(info) => info, + Err(e) => { + return Err(Error::Other(format!("Failed to get object info: {}", e))); + } + }; + + debug!( + "Object info for {}/{}: data_blocks={}, parity_blocks={}, parts={}", + bucket, + object, + object_info.data_blocks, + object_info.parity_blocks, + object_info.parts.len() + ); + + // Create FileInfo from ObjectInfo + let file_info = rustfs_filemeta::FileInfo { + volume: bucket.to_string(), + name: object.to_string(), + version_id: object_info.version_id, + is_latest: object_info.is_latest, + deleted: object_info.delete_marker, + size: object_info.size, + mod_time: object_info.mod_time, + parts: object_info + .parts + .iter() + .map(|p| rustfs_filemeta::ObjectPartInfo { + etag: p.etag.clone(), + number: 0, // Will be set by erasure info + size: p.size, + actual_size: p.actual_size, + mod_time: p.mod_time, + index: p.index.clone(), + checksums: p.checksums.clone(), + }) + .collect(), + erasure: rustfs_filemeta::ErasureInfo { + algorithm: "ReedSolomon".to_string(), + data_blocks: object_info.data_blocks, + parity_blocks: object_info.parity_blocks, + block_size: 0, // Default value + index: 1, // Default index + distribution: (1..=object_info.data_blocks + object_info.parity_blocks).collect(), + checksums: vec![], + }, + ..Default::default() + }; + + // Get all disks from ECStore's disk_map + let mut has_missing_parts = false; + let mut total_disks_checked = 0; + let mut disks_with_errors = 0; + + debug!("Checking {} pools in disk_map", ecstore.disk_map.len()); + + for (pool_idx, pool_disks) in &ecstore.disk_map { + debug!("Checking pool {}, {} disks", pool_idx, pool_disks.len()); + + for (disk_idx, disk_option) in pool_disks.iter().enumerate() { + if let Some(disk) = disk_option { + total_disks_checked += 1; + debug!("Checking disk {} in pool {}: {}", disk_idx, pool_idx, disk.path().display()); + + match disk.check_parts(bucket, object, &file_info).await { + Ok(check_result) => { + debug!( + "check_parts returned {} results for disk {}", + check_result.results.len(), + disk.path().display() + ); + + // Check if any parts are missing or corrupt + for (part_idx, &result) in check_result.results.iter().enumerate() { + debug!("Part {} result: {} on disk {}", part_idx, result, disk.path().display()); + + if result == 4 || result == 5 { + // CHECK_PART_FILE_NOT_FOUND or CHECK_PART_FILE_CORRUPT + has_missing_parts = true; + disks_with_errors += 1; + warn!( + "Found missing or corrupt part {} for object {}/{} on disk {} (pool {}): result={}", + part_idx, + bucket, + object, + disk.path().display(), + pool_idx, + result + ); + break; + } + } + } + Err(e) => { + disks_with_errors += 1; + warn!("Failed to check parts on disk {}: {}", disk.path().display(), e); + // Continue checking other disks + } + } + + if has_missing_parts { + break; // No need to check other disks if we found missing parts + } + } else { + debug!("Disk {} in pool {} is None", disk_idx, pool_idx); + } + } + + if has_missing_parts { + break; // No need to check other pools if we found missing parts + } + } + + debug!( + "Data parts check completed for {}/{}: total_disks={}, disks_with_errors={}, has_missing_parts={}", + bucket, object, total_disks_checked, disks_with_errors, has_missing_parts + ); + + if has_missing_parts { + return Err(Error::Other(format!("Object has missing or corrupt data parts: {}/{}", bucket, object))); + } + } + + debug!("Data parts integrity verified for {}/{}", bucket, object); + Ok(()) + } + /// Scan a single SetDisks (EC set) async fn scan_set_disks( &self, @@ -899,7 +1099,6 @@ impl Scanner { objects_needing_heal += 1; let missing_disks: Vec = (0..disks.len()).filter(|&i| !locations.contains(&i)).collect(); warn!("Object {}/{} missing from disks: {:?}", bucket, object_name, missing_disks); - println!("Object {bucket}/{object_name} missing from disks: {missing_disks:?}"); // submit heal task let enable_healing = self.config.read().await.enable_healing; @@ -933,20 +1132,9 @@ impl Scanner { // Step 3: Deep scan EC verification let config = self.config.read().await; if config.scan_mode == ScanMode::Deep { - // Find the first disk that has this object to get metadata - if let Some(&first_disk_idx) = locations.first() { - if let Some(file_meta) = all_disk_objects[first_disk_idx] - .get(bucket) - .and_then(|objects| objects.get(object_name)) - { - if let Err(e) = self - .verify_ec_decode_with_locations(bucket, object_name, file_meta, locations, disks) - .await - { - objects_with_ec_issues += 1; - warn!("EC decode verification failed for object {}/{}: {}", bucket, object_name, e); - } - } + if let Err(e) = self.verify_object_integrity(bucket, object_name).await { + objects_with_ec_issues += 1; + warn!("Object integrity verification failed for object {}/{}: {}", bucket, object_name, e); } } } @@ -969,114 +1157,6 @@ impl Scanner { Ok(()) } - /// Verify EC decode capability for an object using known disk locations - /// - /// This method is optimized to use the known locations of object copies - /// instead of scanning all disks. - async fn verify_ec_decode_with_locations( - &self, - bucket: &str, - object: &str, - file_meta: &rustfs_filemeta::FileMeta, - locations: &[usize], - all_disks: &[DiskStore], - ) -> Result<()> { - // Get EC parameters from the latest version - let (data_blocks, _parity_blocks) = if let Some(latest_version) = file_meta.versions.last() { - if let Ok(version) = rustfs_filemeta::FileMetaVersion::try_from(latest_version.clone()) { - if let Some(obj) = version.object { - (obj.erasure_m, obj.erasure_n) - } else { - // Not an object version, skip EC verification - return Ok(()); - } - } else { - // Cannot parse version, skip EC verification - return Ok(()); - } - } else { - // No versions, skip EC verification - return Ok(()); - }; - - let read_quorum = data_blocks; // Need at least data_blocks to decode - - if locations.len() < read_quorum { - return Err(Error::Scanner(format!( - "Insufficient object copies for EC decode: need {}, have {}", - read_quorum, - locations.len() - ))); - } - - // Try to read object metadata from the known locations - let mut successful_reads = 0; - let mut errors = Vec::new(); - - for &disk_idx in locations { - if successful_reads >= read_quorum { - break; // We have enough copies for EC decode - } - - let disk = &all_disks[disk_idx]; - match disk.read_xl(bucket, object, false).await { - Ok(_) => { - successful_reads += 1; - debug!( - "Successfully read object {}/{} from disk {} (index: {})", - bucket, - object, - disk.to_string(), - disk_idx - ); - } - Err(e) => { - let error_msg = format!("{e}"); - errors.push(error_msg); - debug!( - "Failed to read object {}/{} from disk {} (index: {}): {}", - bucket, - object, - disk.to_string(), - disk_idx, - e - ); - } - } - } - - if successful_reads >= read_quorum { - debug!( - "EC decode verification passed for object {}/{} ({} successful reads from {} locations)", - bucket, - object, - successful_reads, - locations.len() - ); - Ok(()) - } else { - // submit heal task - let enable_healing = self.config.read().await.enable_healing; - if enable_healing { - if let Some(heal_manager) = &self.heal_manager { - use crate::heal::HealRequest; - let req = HealRequest::ec_decode(bucket.to_string(), object.to_string(), None); - match heal_manager.submit_heal_request(req).await { - Ok(task_id) => { - warn!("EC decode failed, submit heal task: {} {} / {}", task_id, bucket, object); - } - Err(e) => { - error!("EC decode failed, submit heal task failed: {} / {} {}", bucket, object, e); - } - } - } - } - Err(Error::Scanner(format!( - "EC decode verification failed for object {bucket}/{object}: need {read_quorum} reads, got {successful_reads} (errors: {errors:?})" - ))) - } - } - /// Background scan loop with graceful shutdown async fn scan_loop(self) -> Result<()> { let config = self.config.read().await; @@ -1306,6 +1386,7 @@ impl Scanner { #[cfg(test)] mod tests { use super::*; + use crate::heal::manager::HealConfig; use rustfs_ecstore::data_usage::load_data_usage_from_backend; use rustfs_ecstore::disk::endpoint::Endpoint; use rustfs_ecstore::endpoints::{EndpointServerPools, Endpoints, PoolEndpoints}; @@ -1614,4 +1695,457 @@ mod tests { // Clean up let _ = std::fs::remove_dir_all(std::path::Path::new(TEST_DIR_VOLUME_HEAL)); } + + #[tokio::test(flavor = "multi_thread")] + #[serial] + async fn test_scanner_detect_missing_data_parts() { + const TEST_DIR_MISSING_PARTS: &str = "/tmp/rustfs_ahm_test_missing_parts"; + let (disk_paths, ecstore) = prepare_test_env(Some(TEST_DIR_MISSING_PARTS), Some(9004)).await; + + // Create test bucket + let bucket_name = "test-bucket-parts"; + let object_name = "large-object-20mb"; + + ecstore.make_bucket(bucket_name, &Default::default()).await.unwrap(); + + // Create a 20MB object to ensure it has multiple parts (MIN_PART_SIZE is 16MB) + let large_data = vec![b'A'; 20 * 1024 * 1024]; // 20MB of 'A' characters + let mut put_reader = PutObjReader::from_vec(large_data); + let object_opts = rustfs_ecstore::store_api::ObjectOptions::default(); + + println!("=== Creating 20MB object ==="); + ecstore + .put_object(bucket_name, object_name, &mut put_reader, &object_opts) + .await + .expect("put_object failed for large object"); + + // Verify object was created and get its info + let obj_info = ecstore + .get_object_info(bucket_name, object_name, &object_opts) + .await + .expect("get_object_info failed"); + + println!( + "Object info: size={}, parts={}, inlined={}", + obj_info.size, + obj_info.parts.len(), + obj_info.inlined + ); + assert!(!obj_info.inlined, "20MB object should not be inlined"); + // Note: Even 20MB might be stored as single part depending on configuration + println!("Object has {} parts", obj_info.parts.len()); + + // Create HealManager and Scanner with shorter heal interval for testing + let heal_storage = Arc::new(crate::heal::storage::ECStoreHealStorage::new(ecstore.clone())); + let heal_config = HealConfig { + enable_auto_heal: true, + heal_interval: Duration::from_millis(100), // 100ms for faster testing + max_concurrent_heals: 4, + task_timeout: Duration::from_secs(300), + queue_size: 1000, + }; + let heal_manager = Arc::new(crate::heal::HealManager::new(heal_storage, Some(heal_config))); + heal_manager.start().await.unwrap(); + let scanner = Scanner::new(None, Some(heal_manager.clone())); + + // Enable healing to detect missing parts + { + let mut config = scanner.config.write().await; + config.enable_healing = true; + config.scan_mode = ScanMode::Deep; + } + + println!("=== Initial scan (all parts present) ==="); + let initial_scan = scanner.scan_cycle().await; + assert!(initial_scan.is_ok(), "Initial scan should succeed"); + + let initial_metrics = scanner.get_metrics().await; + println!("Initial scan metrics: objects_scanned={}", initial_metrics.objects_scanned); + + // Simulate data part loss by deleting part files from some disks + println!("=== Simulating data part loss ==="); + let mut deleted_parts = 0; + let mut deleted_part_paths = Vec::new(); // Track deleted file paths for later verification + + for (disk_idx, disk_path) in disk_paths.iter().enumerate() { + if disk_idx > 0 { + // Only delete from first two disks + break; + } + let bucket_path = disk_path.join(bucket_name); + let object_path = bucket_path.join(object_name); + + if !object_path.exists() { + continue; + } + + // Find the data directory (UUID) + if let Ok(entries) = fs::read_dir(&object_path) { + for entry in entries.flatten() { + let entry_path = entry.path(); + if entry_path.is_dir() { + // This is likely the data_dir, look for part files inside + let part_file_path = entry_path.join("part.1"); + if part_file_path.exists() { + match fs::remove_file(&part_file_path) { + Ok(_) => { + println!("Deleted part file: {:?}", part_file_path); + deleted_part_paths.push(part_file_path); // Store path for verification + deleted_parts += 1; + } + Err(e) => { + println!("Failed to delete part file {:?}: {}", part_file_path, e); + } + } + } + } + } + } + } + + println!("Deleted {} part files to simulate data loss", deleted_parts); + assert!(deleted_parts > 0, "Should have deleted some part files"); + + // Scan again to detect missing parts + println!("=== Scan after data deletion (should detect missing data) ==="); + let scan_after_deletion = scanner.scan_cycle().await; + + // Wait a bit for the heal manager to process the queue + tokio::time::sleep(Duration::from_millis(200)).await; + + // Add debug information + println!("=== Debug: Checking heal manager state ==="); + let tasks_count = heal_manager.get_active_tasks_count().await; + println!("Active heal tasks count: {}", tasks_count); + + // Check heal statistics to see if any tasks were submitted + let heal_stats = heal_manager.get_statistics().await; + println!("Heal statistics:"); + println!(" - total_tasks: {}", heal_stats.total_tasks); + println!(" - successful_tasks: {}", heal_stats.successful_tasks); + println!(" - failed_tasks: {}", heal_stats.failed_tasks); + println!(" - running_tasks: {}", heal_stats.running_tasks); + + // Get scanner metrics to see what was scanned + let final_metrics = scanner.get_metrics().await; + println!("Scanner metrics after deletion scan:"); + println!(" - objects_scanned: {}", final_metrics.objects_scanned); + println!(" - healthy_objects: {}", final_metrics.healthy_objects); + println!(" - corrupted_objects: {}", final_metrics.corrupted_objects); + println!(" - objects_with_issues: {}", final_metrics.objects_with_issues); + + // Try to manually verify the object to see what happens + println!("=== Manual object verification ==="); + if let Some(ecstore) = rustfs_ecstore::new_object_layer_fn() { + match ecstore.verify_object_integrity(bucket_name, object_name, &object_opts).await { + Ok(_) => println!("Manual verification: Object is healthy"), + Err(e) => println!("Manual verification: Object verification failed: {}", e), + } + } + + // Check if a heal task was submitted (check total tasks instead of active tasks) + assert!(heal_stats.total_tasks > 0, "Heal task should have been submitted"); + println!("{} heal tasks submitted in total", heal_stats.total_tasks); + + // Scanner should handle missing parts gracefully but may detect errors + match scan_after_deletion { + Ok(_) => { + println!("Scanner completed successfully despite missing data"); + } + Err(e) => { + println!("Scanner detected errors (expected): {}", e); + // This is acceptable - scanner may report errors when data is missing + } + } + + let final_metrics = scanner.get_metrics().await; + println!("Final scan metrics: objects_scanned={}", final_metrics.objects_scanned); + + // Verify that scanner completed additional cycles + assert!( + final_metrics.total_cycles > initial_metrics.total_cycles, + "Should have completed additional scan cycles" + ); + + // Test object retrieval after data loss + println!("=== Testing object retrieval after data loss ==="); + let get_result = ecstore.get_object_info(bucket_name, object_name, &object_opts).await; + match get_result { + Ok(info) => { + println!("Object still accessible: size={}", info.size); + // EC should allow recovery if enough shards remain + } + Err(e) => { + println!("Object not accessible due to missing data: {}", e); + // This is expected if too many shards are missing + } + } + + println!("=== Test completed ==="); + println!("Scanner successfully handled missing data scenario"); + + // Verify that deleted part files have been restored by the healing process + println!("=== Verifying file recovery ==="); + let mut recovered_files = 0; + for deleted_path in &deleted_part_paths { + assert!(deleted_path.exists(), "Deleted file should have been recovered"); + println!("Recovered file: {:?}", deleted_path); + recovered_files += 1; + } + + // Assert that at least some files have been recovered + // Note: In a real scenario, healing might take longer, but our test setup should allow recovery + if heal_stats.successful_tasks > 0 { + assert!( + recovered_files == deleted_part_paths.len(), + "Expected at least some deleted files to be recovered by healing process. \ + Deleted {} files, recovered {} files, successful heal tasks: {}", + deleted_part_paths.len(), + recovered_files, + heal_stats.successful_tasks + ); + println!("Successfully recovered {}/{} deleted files", recovered_files, deleted_part_paths.len()); + } else { + println!("No successful heal tasks completed yet - healing may still be in progress"); + } + + // Clean up + let _ = std::fs::remove_dir_all(std::path::Path::new(TEST_DIR_MISSING_PARTS)); + } + + #[tokio::test(flavor = "multi_thread")] + #[serial] + async fn test_scanner_detect_missing_xl_meta() { + const TEST_DIR_MISSING_META: &str = "/tmp/rustfs_ahm_test_missing_meta"; + let (disk_paths, ecstore) = prepare_test_env(Some(TEST_DIR_MISSING_META), Some(9005)).await; + + // Create test bucket + let bucket_name = "test-bucket-meta"; + let object_name = "test-object-meta"; + + ecstore.make_bucket(bucket_name, &Default::default()).await.unwrap(); + + // Create a test object + let test_data = vec![b'B'; 5 * 1024 * 1024]; // 5MB of 'B' characters + let mut put_reader = PutObjReader::from_vec(test_data); + let object_opts = rustfs_ecstore::store_api::ObjectOptions::default(); + + println!("=== Creating test object ==="); + ecstore + .put_object(bucket_name, object_name, &mut put_reader, &object_opts) + .await + .expect("put_object failed"); + + // Verify object was created and get its info + let obj_info = ecstore + .get_object_info(bucket_name, object_name, &object_opts) + .await + .expect("get_object_info failed"); + + println!("Object info: size={}, parts={}", obj_info.size, obj_info.parts.len()); + + // Create HealManager and Scanner with shorter heal interval for testing + let heal_storage = Arc::new(crate::heal::storage::ECStoreHealStorage::new(ecstore.clone())); + let heal_config = HealConfig { + enable_auto_heal: true, + heal_interval: Duration::from_millis(100), // 100ms for faster testing + max_concurrent_heals: 4, + task_timeout: Duration::from_secs(300), + queue_size: 1000, + }; + let heal_manager = Arc::new(crate::heal::HealManager::new(heal_storage, Some(heal_config))); + heal_manager.start().await.unwrap(); + let scanner = Scanner::new(None, Some(heal_manager.clone())); + + // Enable healing to detect missing metadata + { + let mut config = scanner.config.write().await; + config.enable_healing = true; + config.scan_mode = ScanMode::Deep; + } + + println!("=== Initial scan (all metadata present) ==="); + let initial_scan = scanner.scan_cycle().await; + assert!(initial_scan.is_ok(), "Initial scan should succeed"); + + let initial_metrics = scanner.get_metrics().await; + println!("Initial scan metrics: objects_scanned={}", initial_metrics.objects_scanned); + + // Simulate xl.meta file loss by deleting xl.meta files from some disks + println!("=== Simulating xl.meta file loss ==="); + let mut deleted_meta_files = 0; + let mut deleted_meta_paths = Vec::new(); // Track deleted file paths for later verification + + for (disk_idx, disk_path) in disk_paths.iter().enumerate() { + if disk_idx >= 2 { + // Only delete from first two disks to ensure some copies remain for recovery + break; + } + let bucket_path = disk_path.join(bucket_name); + let object_path = bucket_path.join(object_name); + + if !object_path.exists() { + continue; + } + + // Delete xl.meta file + let xl_meta_path = object_path.join("xl.meta"); + if xl_meta_path.exists() { + match fs::remove_file(&xl_meta_path) { + Ok(_) => { + println!("Deleted xl.meta file: {:?}", xl_meta_path); + deleted_meta_paths.push(xl_meta_path); + deleted_meta_files += 1; + } + Err(e) => { + println!("Failed to delete xl.meta file {:?}: {}", xl_meta_path, e); + } + } + } + } + + println!("Deleted {} xl.meta files to simulate metadata loss", deleted_meta_files); + assert!(deleted_meta_files > 0, "Should have deleted some xl.meta files"); + + // Scan again to detect missing metadata + println!("=== Scan after xl.meta deletion (should detect missing metadata) ==="); + let scan_after_deletion = scanner.scan_cycle().await; + + // Wait a bit for the heal manager to process the queue + tokio::time::sleep(Duration::from_millis(500)).await; + + // Add debug information + println!("=== Debug: Checking heal manager state ==="); + let tasks_count = heal_manager.get_active_tasks_count().await; + println!("Active heal tasks count: {}", tasks_count); + + // Check heal statistics to see if any tasks were submitted + let heal_stats = heal_manager.get_statistics().await; + println!("Heal statistics:"); + println!(" - total_tasks: {}", heal_stats.total_tasks); + println!(" - successful_tasks: {}", heal_stats.successful_tasks); + println!(" - failed_tasks: {}", heal_stats.failed_tasks); + println!(" - running_tasks: {}", heal_stats.running_tasks); + + // Get scanner metrics to see what was scanned + let final_metrics = scanner.get_metrics().await; + println!("Scanner metrics after deletion scan:"); + println!(" - objects_scanned: {}", final_metrics.objects_scanned); + println!(" - healthy_objects: {}", final_metrics.healthy_objects); + println!(" - corrupted_objects: {}", final_metrics.corrupted_objects); + println!(" - objects_with_issues: {}", final_metrics.objects_with_issues); + + // Try to manually verify the object to see what happens + println!("=== Manual object verification ==="); + if let Some(ecstore) = rustfs_ecstore::new_object_layer_fn() { + match ecstore.verify_object_integrity(bucket_name, object_name, &object_opts).await { + Ok(_) => println!("Manual verification: Object is healthy"), + Err(e) => println!("Manual verification: Object verification failed: {}", e), + } + } + + // Check if a heal task was submitted for metadata recovery + assert!(heal_stats.total_tasks > 0, "Heal task should have been submitted for missing xl.meta"); + println!("{} heal tasks submitted in total", heal_stats.total_tasks); + + // Scanner should handle missing metadata gracefully but may detect errors + match scan_after_deletion { + Ok(_) => { + println!("Scanner completed successfully despite missing metadata"); + } + Err(e) => { + println!("Scanner detected errors (expected): {}", e); + // This is acceptable - scanner may report errors when metadata is missing + } + } + + let final_metrics = scanner.get_metrics().await; + println!("Final scan metrics: objects_scanned={}", final_metrics.objects_scanned); + + // Verify that scanner completed additional cycles + assert!( + final_metrics.total_cycles > initial_metrics.total_cycles, + "Should have completed additional scan cycles" + ); + + // Test object retrieval after metadata loss + println!("=== Testing object retrieval after metadata loss ==="); + let get_result = ecstore.get_object_info(bucket_name, object_name, &object_opts).await; + match get_result { + Ok(info) => { + println!("Object still accessible: size={}", info.size); + // Object should still be accessible if enough metadata copies remain + } + Err(e) => { + println!("Object not accessible due to missing metadata: {}", e); + // This might happen if too many metadata files are missing + } + } + + // Wait a bit more for healing to complete + tokio::time::sleep(Duration::from_millis(1000)).await; + + // Check heal statistics again after waiting + let final_heal_stats = heal_manager.get_statistics().await; + println!("Final heal statistics:"); + println!(" - total_tasks: {}", final_heal_stats.total_tasks); + println!(" - successful_tasks: {}", final_heal_stats.successful_tasks); + println!(" - failed_tasks: {}", final_heal_stats.failed_tasks); + + // Verify that deleted xl.meta files have been restored by the healing process + println!("=== Verifying xl.meta file recovery ==="); + let mut recovered_files = 0; + for deleted_path in &deleted_meta_paths { + if deleted_path.exists() { + println!("Recovered xl.meta file: {:?}", deleted_path); + recovered_files += 1; + } else { + println!("xl.meta file still missing: {:?}", deleted_path); + } + } + + // Assert that healing was attempted + assert!( + final_heal_stats.total_tasks > 0, + "Heal tasks should have been submitted for missing xl.meta files" + ); + + // Check if any heal tasks were successful + if final_heal_stats.successful_tasks > 0 { + println!("Healing completed successfully, checking file recovery..."); + if recovered_files > 0 { + println!("Successfully recovered {}/{} deleted xl.meta files", recovered_files, deleted_meta_paths.len()); + } else { + println!("No xl.meta files recovered yet - healing may have recreated metadata elsewhere"); + } + } else { + println!("No successful heal tasks completed yet - healing may still be in progress or failed"); + + // If healing failed, this is acceptable for this test scenario + // The important thing is that the scanner detected the issue and submitted heal tasks + if final_heal_stats.failed_tasks > 0 { + println!("Heal tasks failed - this is acceptable for missing xl.meta scenario"); + println!("The scanner correctly detected missing metadata and submitted heal requests"); + } + } + + // The key success criteria for this test is that: + // 1. Scanner detected missing xl.meta files + // 2. Scanner submitted heal tasks for the missing metadata + // 3. Scanner handled the situation gracefully without crashing + println!("=== Test completed ==="); + println!("Scanner successfully handled missing xl.meta scenario"); + println!("Key achievements:"); + println!(" - Scanner detected missing xl.meta files"); + println!(" - Scanner submitted {} heal tasks", final_heal_stats.total_tasks); + println!(" - Scanner handled the situation gracefully"); + if recovered_files > 0 { + println!(" - Successfully recovered {}/{} xl.meta files", recovered_files, deleted_meta_paths.len()); + } else { + println!(" - Note: xl.meta file recovery may require additional time or manual intervention"); + } + + // Clean up + let _ = std::fs::remove_dir_all(std::path::Path::new(TEST_DIR_MISSING_META)); + } } From b8b5511b68d3472d9bde0e7dd7a20944dd479675 Mon Sep 17 00:00:00 2001 From: junxiang Mu <1948535941@qq.com> Date: Wed, 23 Jul 2025 10:35:34 +0800 Subject: [PATCH 22/29] fix: heal data part lose Signed-off-by: junxiang Mu <1948535941@qq.com> --- crates/ahm/src/heal/manager.rs | 4 ++++ crates/ahm/src/scanner/data_scanner.rs | 14 +++++++++++--- crates/ahm/src/scanner/metrics.rs | 22 ++++++++++++++++++++++ crates/ecstore/src/set_disk.rs | 8 ++++++++ crates/ecstore/src/sets.rs | 7 +++++++ crates/ecstore/src/store.rs | 7 +++++++ 6 files changed, 59 insertions(+), 3 deletions(-) diff --git a/crates/ahm/src/heal/manager.rs b/crates/ahm/src/heal/manager.rs index 358ad3668..cb2418838 100644 --- a/crates/ahm/src/heal/manager.rs +++ b/crates/ahm/src/heal/manager.rs @@ -188,6 +188,10 @@ impl HealManager { } /// Get task progress + pub async fn get_active_tasks_count(&self) -> usize { + self.active_heals.lock().await.len() + } + pub async fn get_task_progress(&self, task_id: &str) -> Result { let active_heals = self.active_heals.lock().await; if let Some(task) = active_heals.get(task_id) { diff --git a/crates/ahm/src/scanner/data_scanner.rs b/crates/ahm/src/scanner/data_scanner.rs index ec20a0b95..98f810b90 100644 --- a/crates/ahm/src/scanner/data_scanner.rs +++ b/crates/ahm/src/scanner/data_scanner.rs @@ -2114,13 +2114,17 @@ mod tests { if final_heal_stats.successful_tasks > 0 { println!("Healing completed successfully, checking file recovery..."); if recovered_files > 0 { - println!("Successfully recovered {}/{} deleted xl.meta files", recovered_files, deleted_meta_paths.len()); + println!( + "Successfully recovered {}/{} deleted xl.meta files", + recovered_files, + deleted_meta_paths.len() + ); } else { println!("No xl.meta files recovered yet - healing may have recreated metadata elsewhere"); } } else { println!("No successful heal tasks completed yet - healing may still be in progress or failed"); - + // If healing failed, this is acceptable for this test scenario // The important thing is that the scanner detected the issue and submitted heal tasks if final_heal_stats.failed_tasks > 0 { @@ -2140,7 +2144,11 @@ mod tests { println!(" - Scanner submitted {} heal tasks", final_heal_stats.total_tasks); println!(" - Scanner handled the situation gracefully"); if recovered_files > 0 { - println!(" - Successfully recovered {}/{} xl.meta files", recovered_files, deleted_meta_paths.len()); + println!( + " - Successfully recovered {}/{} xl.meta files", + recovered_files, + deleted_meta_paths.len() + ); } else { println!(" - Note: xl.meta file recovery may require additional time or manual intervention"); } diff --git a/crates/ahm/src/scanner/metrics.rs b/crates/ahm/src/scanner/metrics.rs index 10d010058..f5d7b73bd 100644 --- a/crates/ahm/src/scanner/metrics.rs +++ b/crates/ahm/src/scanner/metrics.rs @@ -42,6 +42,10 @@ pub struct ScannerMetrics { pub heal_tasks_completed: u64, /// Total heal tasks failed pub heal_tasks_failed: u64, + /// Total healthy objects found + pub healthy_objects: u64, + /// Total corrupted objects found + pub corrupted_objects: u64, /// Last scan activity time pub last_activity: Option, /// Current scan cycle @@ -122,6 +126,8 @@ pub struct MetricsCollector { heal_tasks_failed: AtomicU64, current_cycle: AtomicU64, total_cycles: AtomicU64, + healthy_objects: AtomicU64, + corrupted_objects: AtomicU64, } impl MetricsCollector { @@ -139,6 +145,8 @@ impl MetricsCollector { heal_tasks_failed: AtomicU64::new(0), current_cycle: AtomicU64::new(0), total_cycles: AtomicU64::new(0), + healthy_objects: AtomicU64::new(0), + corrupted_objects: AtomicU64::new(0), } } @@ -197,6 +205,16 @@ impl MetricsCollector { self.total_cycles.fetch_add(1, Ordering::Relaxed); } + /// Increment healthy objects count + pub fn increment_healthy_objects(&self) { + self.healthy_objects.fetch_add(1, Ordering::Relaxed); + } + + /// Increment corrupted objects count + pub fn increment_corrupted_objects(&self) { + self.corrupted_objects.fetch_add(1, Ordering::Relaxed); + } + /// Get current metrics snapshot pub fn get_metrics(&self) -> ScannerMetrics { ScannerMetrics { @@ -209,6 +227,8 @@ impl MetricsCollector { heal_tasks_queued: self.heal_tasks_queued.load(Ordering::Relaxed), heal_tasks_completed: self.heal_tasks_completed.load(Ordering::Relaxed), heal_tasks_failed: self.heal_tasks_failed.load(Ordering::Relaxed), + healthy_objects: self.healthy_objects.load(Ordering::Relaxed), + corrupted_objects: self.corrupted_objects.load(Ordering::Relaxed), last_activity: Some(SystemTime::now()), current_cycle: self.current_cycle.load(Ordering::Relaxed), total_cycles: self.total_cycles.load(Ordering::Relaxed), @@ -234,6 +254,8 @@ impl MetricsCollector { self.heal_tasks_failed.store(0, Ordering::Relaxed); self.current_cycle.store(0, Ordering::Relaxed); self.total_cycles.store(0, Ordering::Relaxed); + self.healthy_objects.store(0, Ordering::Relaxed); + self.corrupted_objects.store(0, Ordering::Relaxed); info!("Scanner metrics reset"); } diff --git a/crates/ecstore/src/set_disk.rs b/crates/ecstore/src/set_disk.rs index 678480d47..d00ead5c2 100644 --- a/crates/ecstore/src/set_disk.rs +++ b/crates/ecstore/src/set_disk.rs @@ -5950,6 +5950,14 @@ impl StorageAPI for SetDisks { async fn check_abandoned_parts(&self, _bucket: &str, _object: &str, _opts: &HealOpts) -> Result<()> { unimplemented!() } + + #[tracing::instrument(skip(self))] + async fn verify_object_integrity(&self, bucket: &str, object: &str, opts: &ObjectOptions) -> Result<()> { + let mut get_object_reader = + ::get_object_reader(self, bucket, object, None, HeaderMap::new(), opts).await?; + let _ = get_object_reader.read_all().await?; + Ok(()) + } } #[derive(Debug, PartialEq, Eq)] diff --git a/crates/ecstore/src/sets.rs b/crates/ecstore/src/sets.rs index bea7d9232..40e692ae9 100644 --- a/crates/ecstore/src/sets.rs +++ b/crates/ecstore/src/sets.rs @@ -875,6 +875,13 @@ impl StorageAPI for Sets { async fn check_abandoned_parts(&self, _bucket: &str, _object: &str, _opts: &HealOpts) -> Result<()> { unimplemented!() } + + #[tracing::instrument(skip(self))] + async fn verify_object_integrity(&self, bucket: &str, object: &str, opts: &ObjectOptions) -> Result<()> { + self.get_disks_by_key(object) + .verify_object_integrity(bucket, object, opts) + .await + } } async fn _close_storage_disks(disks: &[Option]) { diff --git a/crates/ecstore/src/store.rs b/crates/ecstore/src/store.rs index 765415fb8..88cffaeca 100644 --- a/crates/ecstore/src/store.rs +++ b/crates/ecstore/src/store.rs @@ -2235,6 +2235,13 @@ impl StorageAPI for ECStore { Ok(()) } + + async fn verify_object_integrity(&self, bucket: &str, object: &str, opts: &ObjectOptions) -> Result<()> { + let mut get_object_reader = + ::get_object_reader(self, bucket, object, None, HeaderMap::new(), opts).await?; + let _ = get_object_reader.read_all().await?; + Ok(()) + } } async fn init_local_peer(endpoint_pools: &EndpointServerPools, host: &String, port: &String) { From 2a8c46874d09d7fa734952b289268b4fe016786f Mon Sep 17 00:00:00 2001 From: junxiang Mu <1948535941@qq.com> Date: Wed, 23 Jul 2025 16:41:23 +0800 Subject: [PATCH 23/29] fix: auto heal when xl.meta lose Signed-off-by: junxiang Mu <1948535941@qq.com> --- crates/ahm/src/scanner/data_scanner.rs | 43 ++++++++++++-------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/crates/ahm/src/scanner/data_scanner.rs b/crates/ahm/src/scanner/data_scanner.rs index 98f810b90..ab19595c9 100644 --- a/crates/ahm/src/scanner/data_scanner.rs +++ b/crates/ahm/src/scanner/data_scanner.rs @@ -494,7 +494,7 @@ impl Scanner { let object_info = match ecstore.get_object_info(bucket, object, &Default::default()).await { Ok(info) => info, Err(e) => { - return Err(Error::Other(format!("Failed to get object info: {}", e))); + return Err(Error::Other(format!("Failed to get object info: {e}"))); } }; @@ -611,7 +611,7 @@ impl Scanner { ); if has_missing_parts { - return Err(Error::Other(format!("Object has missing or corrupt data parts: {}/{}", bucket, object))); + return Err(Error::Other(format!("Object has missing or corrupt data parts: {bucket}/{object}"))); } } @@ -1789,12 +1789,12 @@ mod tests { if part_file_path.exists() { match fs::remove_file(&part_file_path) { Ok(_) => { - println!("Deleted part file: {:?}", part_file_path); + println!("Deleted part file: {part_file_path:?}"); deleted_part_paths.push(part_file_path); // Store path for verification deleted_parts += 1; } Err(e) => { - println!("Failed to delete part file {:?}: {}", part_file_path, e); + println!("Failed to delete part file {part_file_path:?}: {e}"); } } } @@ -1803,7 +1803,7 @@ mod tests { } } - println!("Deleted {} part files to simulate data loss", deleted_parts); + println!("Deleted {deleted_parts} part files to simulate data loss"); assert!(deleted_parts > 0, "Should have deleted some part files"); // Scan again to detect missing parts @@ -1816,7 +1816,7 @@ mod tests { // Add debug information println!("=== Debug: Checking heal manager state ==="); let tasks_count = heal_manager.get_active_tasks_count().await; - println!("Active heal tasks count: {}", tasks_count); + println!("Active heal tasks count: {tasks_count}"); // Check heal statistics to see if any tasks were submitted let heal_stats = heal_manager.get_statistics().await; @@ -1839,7 +1839,7 @@ mod tests { if let Some(ecstore) = rustfs_ecstore::new_object_layer_fn() { match ecstore.verify_object_integrity(bucket_name, object_name, &object_opts).await { Ok(_) => println!("Manual verification: Object is healthy"), - Err(e) => println!("Manual verification: Object verification failed: {}", e), + Err(e) => println!("Manual verification: Object verification failed: {e}"), } } @@ -1853,7 +1853,7 @@ mod tests { println!("Scanner completed successfully despite missing data"); } Err(e) => { - println!("Scanner detected errors (expected): {}", e); + println!("Scanner detected errors (expected): {e}"); // This is acceptable - scanner may report errors when data is missing } } @@ -1876,7 +1876,7 @@ mod tests { // EC should allow recovery if enough shards remain } Err(e) => { - println!("Object not accessible due to missing data: {}", e); + println!("Object not accessible due to missing data: {e}"); // This is expected if too many shards are missing } } @@ -1889,7 +1889,7 @@ mod tests { let mut recovered_files = 0; for deleted_path in &deleted_part_paths { assert!(deleted_path.exists(), "Deleted file should have been recovered"); - println!("Recovered file: {:?}", deleted_path); + println!("Recovered file: {deleted_path:?}"); recovered_files += 1; } @@ -1993,18 +1993,18 @@ mod tests { if xl_meta_path.exists() { match fs::remove_file(&xl_meta_path) { Ok(_) => { - println!("Deleted xl.meta file: {:?}", xl_meta_path); + println!("Deleted xl.meta file: {xl_meta_path:?}"); deleted_meta_paths.push(xl_meta_path); deleted_meta_files += 1; } Err(e) => { - println!("Failed to delete xl.meta file {:?}: {}", xl_meta_path, e); + println!("Failed to delete xl.meta file {xl_meta_path:?}: {e}"); } } } } - println!("Deleted {} xl.meta files to simulate metadata loss", deleted_meta_files); + println!("Deleted {deleted_meta_files} xl.meta files to simulate metadata loss"); assert!(deleted_meta_files > 0, "Should have deleted some xl.meta files"); // Scan again to detect missing metadata @@ -2017,7 +2017,7 @@ mod tests { // Add debug information println!("=== Debug: Checking heal manager state ==="); let tasks_count = heal_manager.get_active_tasks_count().await; - println!("Active heal tasks count: {}", tasks_count); + println!("Active heal tasks count: {tasks_count}"); // Check heal statistics to see if any tasks were submitted let heal_stats = heal_manager.get_statistics().await; @@ -2040,7 +2040,7 @@ mod tests { if let Some(ecstore) = rustfs_ecstore::new_object_layer_fn() { match ecstore.verify_object_integrity(bucket_name, object_name, &object_opts).await { Ok(_) => println!("Manual verification: Object is healthy"), - Err(e) => println!("Manual verification: Object verification failed: {}", e), + Err(e) => println!("Manual verification: Object verification failed: {e}"), } } @@ -2054,7 +2054,7 @@ mod tests { println!("Scanner completed successfully despite missing metadata"); } Err(e) => { - println!("Scanner detected errors (expected): {}", e); + println!("Scanner detected errors (expected): {e}"); // This is acceptable - scanner may report errors when metadata is missing } } @@ -2077,7 +2077,7 @@ mod tests { // Object should still be accessible if enough metadata copies remain } Err(e) => { - println!("Object not accessible due to missing metadata: {}", e); + println!("Object not accessible due to missing metadata: {e}"); // This might happen if too many metadata files are missing } } @@ -2096,12 +2096,9 @@ mod tests { println!("=== Verifying xl.meta file recovery ==="); let mut recovered_files = 0; for deleted_path in &deleted_meta_paths { - if deleted_path.exists() { - println!("Recovered xl.meta file: {:?}", deleted_path); - recovered_files += 1; - } else { - println!("xl.meta file still missing: {:?}", deleted_path); - } + assert!(deleted_path.exists(), "Deleted xl.meta file should exist after healing"); + recovered_files += 1; + println!("Recovered xl.meta file: {deleted_path:?}"); } // Assert that healing was attempted From 4fefd63a5ba1711c653cfa7c03e740a3ed385abe Mon Sep 17 00:00:00 2001 From: junxiang Mu <1948535941@qq.com> Date: Thu, 24 Jul 2025 11:08:32 +0800 Subject: [PATCH 24/29] rebase Signed-off-by: junxiang Mu <1948535941@qq.com> --- Cargo.lock | 6 + crates/ahm/Cargo.toml | 19 +- crates/ahm/src/lib.rs | 5 +- crates/ahm/src/scanner/data_scanner.rs | 3 +- crates/ahm/src/scanner/histogram.rs | 473 ++++++----- crates/ahm/src/scanner/mod.rs | 5 - crates/common/Cargo.toml | 3 +- crates/ecstore/src/disk/local.rs | 197 +---- crates/ecstore/src/rpc/remote_disk.rs | 70 +- crates/ecstore/src/set_disk.rs | 1023 +++--------------------- crates/ecstore/src/store_api.rs | 1 + rustfs/src/main.rs | 1 + 12 files changed, 408 insertions(+), 1398 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 683f5a05a..3098046bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7933,6 +7933,7 @@ dependencies = [ "chrono", "futures", "lazy_static", + "once_cell", "rmp-serde", "rustfs-common", "rustfs-ecstore", @@ -7942,14 +7943,18 @@ dependencies = [ "rustfs-utils", "serde", "serde_json", + "serial_test", + "tempfile", "thiserror 2.0.12", "time", "tokio", "tokio-test", "tokio-util", "tracing", + "tracing-subscriber", "url", "uuid", + "walkdir", ] [[package]] @@ -7968,6 +7973,7 @@ version = "0.0.5" dependencies = [ "async-trait", "chrono", + "lazy_static", "path-clean", "rmp-serde", "rustfs-filemeta", diff --git a/crates/ahm/Cargo.toml b/crates/ahm/Cargo.toml index 5442fb71f..817e3019e 100644 --- a/crates/ahm/Cargo.toml +++ b/crates/ahm/Cargo.toml @@ -1,16 +1,10 @@ [package] name = "rustfs-ahm" -version.workspace = true -edition.workspace = true +version = "0.0.5" +edition = "2021" authors = ["RustFS Team"] -license.workspace = true +license = "Apache-2.0" description = "RustFS AHM (Automatic Health Management) Scanner" -repository.workspace = true -rust-version.workspace = true -homepage.workspace = true -documentation = "https://docs.rs/rustfs-ahm/latest/rustfs_ahm/" -keywords = ["RustFS", "AHM", "health-management", "scanner", "Minio"] -categories = ["web-programming", "development-tools", "filesystem"] [dependencies] rustfs-ecstore = { workspace = true } @@ -38,5 +32,10 @@ chrono = { workspace = true } [dev-dependencies] rmp-serde = { workspace = true } -tokio-test = { workspace = true } +tokio-test = "0.4" serde_json = { workspace = true } +serial_test = "3.2.0" +once_cell = { workspace = true } +tracing-subscriber = { workspace = true } +walkdir = "2.5.0" +tempfile = "3.10" \ No newline at end of file diff --git a/crates/ahm/src/lib.rs b/crates/ahm/src/lib.rs index 17a6b71bc..7bc1b689c 100644 --- a/crates/ahm/src/lib.rs +++ b/crates/ahm/src/lib.rs @@ -22,10 +22,7 @@ pub mod scanner; pub use error::{Error, Result}; pub use heal::{channel::HealChannelProcessor, HealManager, HealOptions, HealPriority, HealRequest, HealType}; -pub use scanner::{ - BucketTargetUsageInfo, BucketUsageInfo, DataUsageInfo, Scanner, ScannerMetrics, load_data_usage_from_backend, - store_data_usage_in_backend, -}; +pub use scanner::Scanner; // Global cancellation token for AHM services (scanner and other background tasks) static GLOBAL_AHM_SERVICES_CANCEL_TOKEN: OnceLock = OnceLock::new(); diff --git a/crates/ahm/src/scanner/data_scanner.rs b/crates/ahm/src/scanner/data_scanner.rs index ab19595c9..5a28186ae 100644 --- a/crates/ahm/src/scanner/data_scanner.rs +++ b/crates/ahm/src/scanner/data_scanner.rs @@ -527,6 +527,7 @@ impl Scanner { mod_time: p.mod_time, index: p.index.clone(), checksums: p.checksums.clone(), + error: None, }) .collect(), erasure: rustfs_filemeta::ErasureInfo { @@ -1392,8 +1393,8 @@ mod tests { use rustfs_ecstore::endpoints::{EndpointServerPools, Endpoints, PoolEndpoints}; use rustfs_ecstore::store::ECStore; use rustfs_ecstore::{ - StorageAPI, store_api::{MakeBucketOptions, ObjectIO, PutObjReader}, + StorageAPI, }; use serial_test::serial; use std::fs; diff --git a/crates/ahm/src/scanner/histogram.rs b/crates/ahm/src/scanner/histogram.rs index 778569ced..f5d7b73bd 100644 --- a/crates/ahm/src/scanner/histogram.rs +++ b/crates/ahm/src/scanner/histogram.rs @@ -12,197 +12,258 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::HashMap; +use std::{ + collections::HashMap, + sync::atomic::{AtomicU64, Ordering}, + time::{Duration, SystemTime}, +}; -/// Size interval for object size histogram -#[derive(Debug, Clone)] -pub struct SizeInterval { - pub start: u64, - pub end: u64, - pub name: &'static str, +use serde::{Deserialize, Serialize}; +use tracing::info; + +/// Scanner metrics +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct ScannerMetrics { + /// Total objects scanned since server start + pub objects_scanned: u64, + /// Total object versions scanned since server start + pub versions_scanned: u64, + /// Total directories scanned since server start + pub directories_scanned: u64, + /// Total bucket scans started since server start + pub bucket_scans_started: u64, + /// Total bucket scans finished since server start + pub bucket_scans_finished: u64, + /// Total objects with health issues found + pub objects_with_issues: u64, + /// Total heal tasks queued + pub heal_tasks_queued: u64, + /// Total heal tasks completed + pub heal_tasks_completed: u64, + /// Total heal tasks failed + pub heal_tasks_failed: u64, + /// Total healthy objects found + pub healthy_objects: u64, + /// Total corrupted objects found + pub corrupted_objects: u64, + /// Last scan activity time + pub last_activity: Option, + /// Current scan cycle + pub current_cycle: u64, + /// Total scan cycles completed + pub total_cycles: u64, + /// Current scan duration + pub current_scan_duration: Option, + /// Average scan duration + pub avg_scan_duration: Duration, + /// Objects scanned per second + pub objects_per_second: f64, + /// Buckets scanned per second + pub buckets_per_second: f64, + /// Storage metrics by bucket + pub bucket_metrics: HashMap, + /// Disk metrics + pub disk_metrics: HashMap, } -/// Version interval for object versions histogram -#[derive(Debug, Clone)] -pub struct VersionInterval { - pub start: u64, - pub end: u64, - pub name: &'static str, +/// Bucket-specific metrics +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct BucketMetrics { + /// Bucket name + pub bucket: String, + /// Total objects in bucket + pub total_objects: u64, + /// Total size of objects in bucket (bytes) + pub total_size: u64, + /// Objects with health issues + pub objects_with_issues: u64, + /// Last scan time + pub last_scan_time: Option, + /// Scan duration + pub scan_duration: Option, + /// Heal tasks queued for this bucket + pub heal_tasks_queued: u64, + /// Heal tasks completed for this bucket + pub heal_tasks_completed: u64, + /// Heal tasks failed for this bucket + pub heal_tasks_failed: u64, } -/// Object size histogram intervals -pub const OBJECTS_HISTOGRAM_INTERVALS: &[SizeInterval] = &[ - SizeInterval { - start: 0, - end: 1024 - 1, - name: "LESS_THAN_1_KiB", - }, - SizeInterval { - start: 1024, - end: 1024 * 1024 - 1, - name: "1_KiB_TO_1_MiB", - }, - SizeInterval { - start: 1024 * 1024, - end: 10 * 1024 * 1024 - 1, - name: "1_MiB_TO_10_MiB", - }, - SizeInterval { - start: 10 * 1024 * 1024, - end: 64 * 1024 * 1024 - 1, - name: "10_MiB_TO_64_MiB", - }, - SizeInterval { - start: 64 * 1024 * 1024, - end: 128 * 1024 * 1024 - 1, - name: "64_MiB_TO_128_MiB", - }, - SizeInterval { - start: 128 * 1024 * 1024, - end: 512 * 1024 * 1024 - 1, - name: "128_MiB_TO_512_MiB", - }, - SizeInterval { - start: 512 * 1024 * 1024, - end: u64::MAX, - name: "MORE_THAN_512_MiB", - }, -]; - -/// Object version count histogram intervals -pub const OBJECTS_VERSION_COUNT_INTERVALS: &[VersionInterval] = &[ - VersionInterval { - start: 1, - end: 1, - name: "1_VERSION", - }, - VersionInterval { - start: 2, - end: 10, - name: "2_TO_10_VERSIONS", - }, - VersionInterval { - start: 11, - end: 100, - name: "11_TO_100_VERSIONS", - }, - VersionInterval { - start: 101, - end: 1000, - name: "101_TO_1000_VERSIONS", - }, - VersionInterval { - start: 1001, - end: u64::MAX, - name: "MORE_THAN_1000_VERSIONS", - }, -]; - -/// Size histogram for object size distribution -#[derive(Debug, Clone, Default)] -pub struct SizeHistogram { - counts: Vec, +/// Disk-specific metrics +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct DiskMetrics { + /// Disk path + pub disk_path: String, + /// Total disk space (bytes) + pub total_space: u64, + /// Used disk space (bytes) + pub used_space: u64, + /// Free disk space (bytes) + pub free_space: u64, + /// Objects scanned on this disk + pub objects_scanned: u64, + /// Objects with issues on this disk + pub objects_with_issues: u64, + /// Last scan time + pub last_scan_time: Option, + /// Whether disk is online + pub is_online: bool, + /// Whether disk is being scanned + pub is_scanning: bool, } -/// Versions histogram for object version count distribution -#[derive(Debug, Clone, Default)] -pub struct VersionsHistogram { - counts: Vec, +/// Thread-safe metrics collector +pub struct MetricsCollector { + /// Atomic counters for real-time metrics + objects_scanned: AtomicU64, + versions_scanned: AtomicU64, + directories_scanned: AtomicU64, + bucket_scans_started: AtomicU64, + bucket_scans_finished: AtomicU64, + objects_with_issues: AtomicU64, + heal_tasks_queued: AtomicU64, + heal_tasks_completed: AtomicU64, + heal_tasks_failed: AtomicU64, + current_cycle: AtomicU64, + total_cycles: AtomicU64, + healthy_objects: AtomicU64, + corrupted_objects: AtomicU64, } -impl SizeHistogram { - /// Create a new size histogram +impl MetricsCollector { + /// Create a new metrics collector pub fn new() -> Self { Self { - counts: vec![0; OBJECTS_HISTOGRAM_INTERVALS.len()], + objects_scanned: AtomicU64::new(0), + versions_scanned: AtomicU64::new(0), + directories_scanned: AtomicU64::new(0), + bucket_scans_started: AtomicU64::new(0), + bucket_scans_finished: AtomicU64::new(0), + objects_with_issues: AtomicU64::new(0), + heal_tasks_queued: AtomicU64::new(0), + heal_tasks_completed: AtomicU64::new(0), + heal_tasks_failed: AtomicU64::new(0), + current_cycle: AtomicU64::new(0), + total_cycles: AtomicU64::new(0), + healthy_objects: AtomicU64::new(0), + corrupted_objects: AtomicU64::new(0), } } - /// Add a size to the histogram - pub fn add(&mut self, size: u64) { - for (idx, interval) in OBJECTS_HISTOGRAM_INTERVALS.iter().enumerate() { - if size >= interval.start && size <= interval.end { - self.counts[idx] += 1; - break; - } + /// Increment objects scanned count + pub fn increment_objects_scanned(&self, count: u64) { + self.objects_scanned.fetch_add(count, Ordering::Relaxed); + } + + /// Increment versions scanned count + pub fn increment_versions_scanned(&self, count: u64) { + self.versions_scanned.fetch_add(count, Ordering::Relaxed); + } + + /// Increment directories scanned count + pub fn increment_directories_scanned(&self, count: u64) { + self.directories_scanned.fetch_add(count, Ordering::Relaxed); + } + + /// Increment bucket scans started count + pub fn increment_bucket_scans_started(&self, count: u64) { + self.bucket_scans_started.fetch_add(count, Ordering::Relaxed); + } + + /// Increment bucket scans finished count + pub fn increment_bucket_scans_finished(&self, count: u64) { + self.bucket_scans_finished.fetch_add(count, Ordering::Relaxed); + } + + /// Increment objects with issues count + pub fn increment_objects_with_issues(&self, count: u64) { + self.objects_with_issues.fetch_add(count, Ordering::Relaxed); + } + + /// Increment heal tasks queued count + pub fn increment_heal_tasks_queued(&self, count: u64) { + self.heal_tasks_queued.fetch_add(count, Ordering::Relaxed); + } + + /// Increment heal tasks completed count + pub fn increment_heal_tasks_completed(&self, count: u64) { + self.heal_tasks_completed.fetch_add(count, Ordering::Relaxed); + } + + /// Increment heal tasks failed count + pub fn increment_heal_tasks_failed(&self, count: u64) { + self.heal_tasks_failed.fetch_add(count, Ordering::Relaxed); + } + + /// Set current cycle + pub fn set_current_cycle(&self, cycle: u64) { + self.current_cycle.store(cycle, Ordering::Relaxed); + } + + /// Increment total cycles + pub fn increment_total_cycles(&self) { + self.total_cycles.fetch_add(1, Ordering::Relaxed); + } + + /// Increment healthy objects count + pub fn increment_healthy_objects(&self) { + self.healthy_objects.fetch_add(1, Ordering::Relaxed); + } + + /// Increment corrupted objects count + pub fn increment_corrupted_objects(&self) { + self.corrupted_objects.fetch_add(1, Ordering::Relaxed); + } + + /// Get current metrics snapshot + pub fn get_metrics(&self) -> ScannerMetrics { + ScannerMetrics { + objects_scanned: self.objects_scanned.load(Ordering::Relaxed), + versions_scanned: self.versions_scanned.load(Ordering::Relaxed), + directories_scanned: self.directories_scanned.load(Ordering::Relaxed), + bucket_scans_started: self.bucket_scans_started.load(Ordering::Relaxed), + bucket_scans_finished: self.bucket_scans_finished.load(Ordering::Relaxed), + objects_with_issues: self.objects_with_issues.load(Ordering::Relaxed), + heal_tasks_queued: self.heal_tasks_queued.load(Ordering::Relaxed), + heal_tasks_completed: self.heal_tasks_completed.load(Ordering::Relaxed), + heal_tasks_failed: self.heal_tasks_failed.load(Ordering::Relaxed), + healthy_objects: self.healthy_objects.load(Ordering::Relaxed), + corrupted_objects: self.corrupted_objects.load(Ordering::Relaxed), + last_activity: Some(SystemTime::now()), + current_cycle: self.current_cycle.load(Ordering::Relaxed), + total_cycles: self.total_cycles.load(Ordering::Relaxed), + current_scan_duration: None, // Will be set by scanner + avg_scan_duration: Duration::ZERO, // Will be calculated + objects_per_second: 0.0, // Will be calculated + buckets_per_second: 0.0, // Will be calculated + bucket_metrics: HashMap::new(), // Will be populated by scanner + disk_metrics: HashMap::new(), // Will be populated by scanner } } - /// Get the histogram as a map - pub fn to_map(&self) -> HashMap { - let mut result = HashMap::new(); - for (idx, count) in self.counts.iter().enumerate() { - let interval = &OBJECTS_HISTOGRAM_INTERVALS[idx]; - result.insert(interval.name.to_string(), *count); - } - result - } + /// Reset all metrics + pub fn reset(&self) { + self.objects_scanned.store(0, Ordering::Relaxed); + self.versions_scanned.store(0, Ordering::Relaxed); + self.directories_scanned.store(0, Ordering::Relaxed); + self.bucket_scans_started.store(0, Ordering::Relaxed); + self.bucket_scans_finished.store(0, Ordering::Relaxed); + self.objects_with_issues.store(0, Ordering::Relaxed); + self.heal_tasks_queued.store(0, Ordering::Relaxed); + self.heal_tasks_completed.store(0, Ordering::Relaxed); + self.heal_tasks_failed.store(0, Ordering::Relaxed); + self.current_cycle.store(0, Ordering::Relaxed); + self.total_cycles.store(0, Ordering::Relaxed); + self.healthy_objects.store(0, Ordering::Relaxed); + self.corrupted_objects.store(0, Ordering::Relaxed); - /// Merge another histogram into this one - pub fn merge(&mut self, other: &SizeHistogram) { - for (idx, count) in other.counts.iter().enumerate() { - self.counts[idx] += count; - } - } - - /// Get total count - pub fn total_count(&self) -> u64 { - self.counts.iter().sum() - } - - /// Reset the histogram - pub fn reset(&mut self) { - for count in &mut self.counts { - *count = 0; - } + info!("Scanner metrics reset"); } } -impl VersionsHistogram { - /// Create a new versions histogram - pub fn new() -> Self { - Self { - counts: vec![0; OBJECTS_VERSION_COUNT_INTERVALS.len()], - } - } - - /// Add a version count to the histogram - pub fn add(&mut self, versions: u64) { - for (idx, interval) in OBJECTS_VERSION_COUNT_INTERVALS.iter().enumerate() { - if versions >= interval.start && versions <= interval.end { - self.counts[idx] += 1; - break; - } - } - } - - /// Get the histogram as a map - pub fn to_map(&self) -> HashMap { - let mut result = HashMap::new(); - for (idx, count) in self.counts.iter().enumerate() { - let interval = &OBJECTS_VERSION_COUNT_INTERVALS[idx]; - result.insert(interval.name.to_string(), *count); - } - result - } - - /// Merge another histogram into this one - pub fn merge(&mut self, other: &VersionsHistogram) { - for (idx, count) in other.counts.iter().enumerate() { - self.counts[idx] += count; - } - } - - /// Get total count - pub fn total_count(&self) -> u64 { - self.counts.iter().sum() - } - - /// Reset the histogram - pub fn reset(&mut self) { - for count in &mut self.counts { - *count = 0; - } +impl Default for MetricsCollector { + fn default() -> Self { + Self::new() } } @@ -211,67 +272,35 @@ mod tests { use super::*; #[test] - fn test_size_histogram() { - let mut histogram = SizeHistogram::new(); - - // Add some sizes - histogram.add(512); // LESS_THAN_1_KiB - histogram.add(1024); // 1_KiB_TO_1_MiB - histogram.add(1024 * 1024); // 1_MiB_TO_10_MiB - histogram.add(5 * 1024 * 1024); // 1_MiB_TO_10_MiB - - let map = histogram.to_map(); - - assert_eq!(map.get("LESS_THAN_1_KiB"), Some(&1)); - assert_eq!(map.get("1_KiB_TO_1_MiB"), Some(&1)); - assert_eq!(map.get("1_MiB_TO_10_MiB"), Some(&2)); - assert_eq!(map.get("10_MiB_TO_64_MiB"), Some(&0)); + fn test_metrics_collector_creation() { + let collector = MetricsCollector::new(); + let metrics = collector.get_metrics(); + assert_eq!(metrics.objects_scanned, 0); + assert_eq!(metrics.versions_scanned, 0); } #[test] - fn test_versions_histogram() { - let mut histogram = VersionsHistogram::new(); + fn test_metrics_increment() { + let collector = MetricsCollector::new(); - // Add some version counts - histogram.add(1); // 1_VERSION - histogram.add(5); // 2_TO_10_VERSIONS - histogram.add(50); // 11_TO_100_VERSIONS - histogram.add(500); // 101_TO_1000_VERSIONS + collector.increment_objects_scanned(10); + collector.increment_versions_scanned(5); + collector.increment_objects_with_issues(2); - let map = histogram.to_map(); - - assert_eq!(map.get("1_VERSION"), Some(&1)); - assert_eq!(map.get("2_TO_10_VERSIONS"), Some(&1)); - assert_eq!(map.get("11_TO_100_VERSIONS"), Some(&1)); - assert_eq!(map.get("101_TO_1000_VERSIONS"), Some(&1)); + let metrics = collector.get_metrics(); + assert_eq!(metrics.objects_scanned, 10); + assert_eq!(metrics.versions_scanned, 5); + assert_eq!(metrics.objects_with_issues, 2); } #[test] - fn test_histogram_merge() { - let mut histogram1 = SizeHistogram::new(); - histogram1.add(1024); - histogram1.add(1024 * 1024); + fn test_metrics_reset() { + let collector = MetricsCollector::new(); - let mut histogram2 = SizeHistogram::new(); - histogram2.add(1024); - histogram2.add(5 * 1024 * 1024); + collector.increment_objects_scanned(10); + collector.reset(); - histogram1.merge(&histogram2); - - let map = histogram1.to_map(); - assert_eq!(map.get("1_KiB_TO_1_MiB"), Some(&2)); // 1 from histogram1 + 1 from histogram2 - assert_eq!(map.get("1_MiB_TO_10_MiB"), Some(&2)); // 1 from histogram1 + 1 from histogram2 - } - - #[test] - fn test_histogram_reset() { - let mut histogram = SizeHistogram::new(); - histogram.add(1024); - histogram.add(1024 * 1024); - - assert_eq!(histogram.total_count(), 2); - - histogram.reset(); - assert_eq!(histogram.total_count(), 0); + let metrics = collector.get_metrics(); + assert_eq!(metrics.objects_scanned, 0); } } diff --git a/crates/ahm/src/scanner/mod.rs b/crates/ahm/src/scanner/mod.rs index 025bbb385..d299c1432 100644 --- a/crates/ahm/src/scanner/mod.rs +++ b/crates/ahm/src/scanner/mod.rs @@ -13,13 +13,8 @@ // limitations under the License. pub mod data_scanner; -pub mod data_usage; pub mod histogram; pub mod metrics; -// Re-export main types for convenience pub use data_scanner::Scanner; -pub use data_usage::{ - BucketTargetUsageInfo, BucketUsageInfo, DataUsageInfo, load_data_usage_from_backend, store_data_usage_in_backend, -}; pub use metrics::ScannerMetrics; diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 88c8a3f4b..a2b57c663 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -28,7 +28,8 @@ categories = ["web-programming", "development-tools", "data-structures"] workspace = true [dependencies] -tokio.workspace = true +lazy_static = { workspace = true} +tokio = { workspace = true } tonic = { workspace = true } uuid = { workspace = true } chrono = { workspace = true } diff --git a/crates/ecstore/src/disk/local.rs b/crates/ecstore/src/disk/local.rs index 649ca702a..18aafecab 100644 --- a/crates/ecstore/src/disk/local.rs +++ b/crates/ecstore/src/disk/local.rs @@ -21,9 +21,6 @@ use super::{ }; use super::{endpoint::Endpoint, error::DiskError, format::FormatV3}; -use crate::bucket::metadata_sys::{self}; -use crate::bucket::versioning::VersioningApi; -use crate::bucket::versioning_sys::BucketVersioningSys; use crate::disk::error::FileAccessDeniedWithContext; use crate::disk::error_conv::{to_access_error, to_file_error, to_unformatted_disk_error, to_volume_error}; use crate::disk::fs::{ @@ -36,16 +33,6 @@ use crate::disk::{ }; use crate::disk::{FileWriter, STORAGE_FORMAT_FILE}; use crate::global::{GLOBAL_IsErasureSD, GLOBAL_RootDiskThreshold}; -use crate::heal::data_scanner::{ - ScannerItem, ShouldSleepFn, SizeSummary, lc_has_active_rules, rep_has_active_rules, scan_data_folder, -}; -use crate::heal::data_usage_cache::{DataUsageCache, DataUsageEntry}; -use crate::heal::error::{ERR_IGNORE_FILE_CONTRIB, ERR_SKIP_FILE}; -use crate::heal::heal_commands::{HealScanMode, HealingTracker}; -use crate::heal::heal_ops::HEALING_TRACKER_FILENAME; -use crate::new_object_layer_fn; -use crate::store_api::{ObjectInfo, StorageAPI}; -use rustfs_common::metrics::{Metric, Metrics}; use rustfs_utils::path::{ GLOBAL_DIR_SUFFIX, GLOBAL_DIR_SUFFIX_WITH_SLASH, SLASH_SEPARATOR, clean, decode_dir_object, encode_dir_object, has_suffix, path_join, path_join_buf, @@ -55,19 +42,18 @@ use tokio::time::interval; use crate::erasure_coding::bitrot_verify; use bytes::Bytes; use path_absolutize::Absolutize; -use rustfs_common::defer; use rustfs_filemeta::{ Cache, FileInfo, FileInfoOpts, FileMeta, MetaCacheEntry, MetacacheWriter, ObjectPartInfo, Opts, RawFileInfo, UpdateFn, get_file_info, read_xl_meta_no_data, }; use rustfs_utils::HashAlgorithm; use rustfs_utils::os::get_info; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use std::fmt::Debug; use std::io::SeekFrom; use std::sync::Arc; use std::sync::atomic::{AtomicU32, Ordering}; -use std::time::{Duration, SystemTime}; +use std::time::Duration; use std::{ fs::Metadata, path::{Path, PathBuf}, @@ -76,7 +62,6 @@ use time::OffsetDateTime; use tokio::fs::{self, File}; use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWrite, AsyncWriteExt, ErrorKind}; use tokio::sync::RwLock; -use tokio::sync::mpsc::Sender; use tracing::{debug, error, info, warn}; use uuid::Uuid; @@ -2268,184 +2253,6 @@ impl DiskAPI for LocalDisk { Ok(info) } - - #[tracing::instrument(level = "info", skip_all)] - async fn ns_scanner( - &self, - cache: &DataUsageCache, - updates: Sender, - scan_mode: HealScanMode, - we_sleep: ShouldSleepFn, - ) -> Result { - self.scanning.fetch_add(1, Ordering::SeqCst); - defer!(|| { self.scanning.fetch_sub(1, Ordering::SeqCst) }); - - // must before metadata_sys - let Some(store) = new_object_layer_fn() else { - return Err(Error::other("errServerNotInitialized")); - }; - - let mut cache = cache.clone(); - // Check if the current bucket has a configured lifecycle policy - if let Ok((lc, _)) = metadata_sys::get_lifecycle_config(&cache.info.name).await { - if lc_has_active_rules(&lc, "") { - cache.info.lifecycle = Some(lc); - } - } - - // Check if the current bucket has replication configuration - if let Ok((rcfg, _)) = metadata_sys::get_replication_config(&cache.info.name).await { - if rep_has_active_rules(&rcfg, "", true) { - // TODO: globalBucketTargetSys - } - } - - let vcfg = BucketVersioningSys::get(&cache.info.name).await.ok(); - - let loc = self.get_disk_location(); - // TODO: 这里需要处理错误 - let disks = store - .get_disks(loc.pool_idx.unwrap(), loc.disk_idx.unwrap()) - .await - .map_err(|e| Error::other(e.to_string()))?; - let disk = Arc::new(LocalDisk::new(&self.endpoint(), false).await?); - let disk_clone = disk.clone(); - cache.info.updates = Some(updates.clone()); - let mut data_usage_info = scan_data_folder( - &disks, - disk, - &cache, - Box::new(move |item: &ScannerItem| { - let mut item = item.clone(); - let disk = disk_clone.clone(); - let vcfg = vcfg.clone(); - Box::pin(async move { - if !item.path.ends_with(&format!("{SLASH_SEPARATOR}{STORAGE_FORMAT_FILE}")) { - return Err(Error::other(ERR_SKIP_FILE).into()); - } - let stop_fn = Metrics::log(Metric::ScanObject); - let mut res = HashMap::new(); - let done_sz = Metrics::time_size(Metric::ReadMetadata); - let buf = match disk.read_metadata(item.path.clone()).await { - Ok(buf) => buf, - Err(err) => { - res.insert("err".to_string(), err.to_string()); - stop_fn(&res); - return Err(Error::other(ERR_SKIP_FILE).into()); - } - }; - done_sz(buf.len() as u64); - res.insert("metasize".to_string(), buf.len().to_string()); - item.transform_meta_dir(); - let meta_cache = MetaCacheEntry { - name: item.object_path().to_string_lossy().to_string(), - metadata: buf, - ..Default::default() - }; - let fivs = match meta_cache.file_info_versions(&item.bucket) { - Ok(fivs) => fivs, - Err(err) => { - res.insert("err".to_string(), err.to_string()); - stop_fn(&res); - return Err(Error::other(ERR_SKIP_FILE).into()); - } - }; - let mut size_s = SizeSummary::default(); - let done = Metrics::time(Metric::ApplyAll); - let obj_infos = match item.apply_versions_actions(&fivs.versions).await { - Ok(obj_infos) => obj_infos, - Err(err) => { - res.insert("err".to_string(), err.to_string()); - stop_fn(&res); - return Err(Error::other(ERR_SKIP_FILE).into()); - } - }; - - let versioned = if let Some(vcfg) = vcfg.as_ref() { - vcfg.versioned(item.object_path().to_str().unwrap_or_default()) - } else { - false - }; - - let mut obj_deleted = false; - for info in obj_infos.iter() { - let done = Metrics::time(Metric::ApplyVersion); - let sz: i64; - (obj_deleted, sz) = item.apply_actions(info, &mut size_s).await; - done(); - - if obj_deleted { - break; - } - - let actual_sz = match info.get_actual_size() { - Ok(size) => size, - Err(_) => continue, - }; - - if info.delete_marker { - size_s.delete_markers += 1; - } - - if info.version_id.is_some() && sz == actual_sz { - size_s.versions += 1; - } - - size_s.total_size += sz as usize; - - if info.delete_marker { - continue; - } - } - - for free_version in fivs.free_versions.iter() { - let _obj_info = ObjectInfo::from_file_info( - free_version, - &item.bucket, - &item.object_path().to_string_lossy(), - versioned, - ); - let done = Metrics::time(Metric::TierObjSweep); - done(); - } - - // todo: global trace - if obj_deleted { - return Err(Error::other(ERR_IGNORE_FILE_CONTRIB).into()); - } - done(); - Ok(size_s) - }) - }), - scan_mode, - we_sleep, - ) - .await?; - data_usage_info.info.last_update = Some(SystemTime::now()); - debug!("ns_scanner completed: {data_usage_info:?}"); - Ok(data_usage_info) - } - - #[tracing::instrument(skip(self))] - async fn healing(&self) -> Option { - let healing_file = path_join(&[ - self.path(), - PathBuf::from(RUSTFS_META_BUCKET), - PathBuf::from(BUCKET_META_PREFIX), - PathBuf::from(HEALING_TRACKER_FILENAME), - ]); - let b = match fs::read(healing_file).await { - Ok(b) => b, - Err(_) => return None, - }; - if b.is_empty() { - return None; - } - match HealingTracker::unmarshal_msg(&b) { - Ok(h) => Some(h), - Err(_) => Some(HealingTracker::default()), - } - } } async fn get_disk_info(drive_path: PathBuf) -> Result<(rustfs_utils::os::DiskInfo, bool)> { diff --git a/crates/ecstore/src/rpc/remote_disk.rs b/crates/ecstore/src/rpc/remote_disk.rs index e45c9e63a..5f2f078a7 100644 --- a/crates/ecstore/src/rpc/remote_disk.rs +++ b/crates/ecstore/src/rpc/remote_disk.rs @@ -21,9 +21,9 @@ use rustfs_protos::{ node_service_time_out_client, proto_gen::node_service::{ CheckPartsRequest, DeletePathsRequest, DeleteRequest, DeleteVersionRequest, DeleteVersionsRequest, DeleteVolumeRequest, - DiskInfoRequest, ListDirRequest, ListVolumesRequest, MakeVolumeRequest, MakeVolumesRequest, NsScannerRequest, - ReadAllRequest, ReadMultipleRequest, ReadPartsRequest, ReadVersionRequest, ReadXlRequest, RenameDataRequest, - RenameFileRequest, StatVolumeRequest, UpdateMetadataRequest, VerifyFileRequest, WriteAllRequest, WriteMetadataRequest, + DiskInfoRequest, ListDirRequest, ListVolumesRequest, MakeVolumeRequest, MakeVolumesRequest, ReadAllRequest, + ReadMultipleRequest, ReadPartsRequest, ReadVersionRequest, ReadXlRequest, RenameDataRequest, RenameFileRequest, + StatVolumeRequest, UpdateMetadataRequest, VerifyFileRequest, WriteAllRequest, WriteMetadataRequest, }, }; @@ -32,26 +32,15 @@ use crate::disk::{ ReadMultipleReq, ReadMultipleResp, ReadOptions, RenameDataResp, UpdateMetadataOpts, VolumeInfo, WalkDirOptions, endpoint::Endpoint, }; +use crate::disk::{FileReader, FileWriter}; use crate::{ disk::error::{Error, Result}, rpc::build_auth_headers, }; -use crate::{ - disk::{FileReader, FileWriter}, - heal::{ - data_scanner::ShouldSleepFn, - data_usage_cache::{DataUsageCache, DataUsageEntry}, - heal_commands::{HealScanMode, HealingTracker}, - }, -}; use rustfs_filemeta::{FileInfo, ObjectPartInfo, RawFileInfo}; use rustfs_protos::proto_gen::node_service::RenamePartRequest; use rustfs_rio::{HttpReader, HttpWriter}; -use tokio::{ - io::AsyncWrite, - sync::mpsc::{self, Sender}, -}; -use tokio_stream::{StreamExt, wrappers::ReceiverStream}; +use tokio::io::AsyncWrite; use tonic::Request; use tracing::info; use uuid::Uuid; @@ -927,55 +916,6 @@ impl DiskAPI for RemoteDisk { Ok(disk_info) } - - #[tracing::instrument(skip(self, cache, scan_mode, _we_sleep))] - async fn ns_scanner( - &self, - cache: &DataUsageCache, - updates: Sender, - scan_mode: HealScanMode, - _we_sleep: ShouldSleepFn, - ) -> Result { - info!("ns_scanner"); - let cache = serde_json::to_string(cache)?; - let mut client = node_service_time_out_client(&self.addr) - .await - .map_err(|err| Error::other(format!("can not get client, err: {err}")))?; - - let (tx, rx) = mpsc::channel(10); - let in_stream = ReceiverStream::new(rx); - let mut response = client.ns_scanner(in_stream).await?.into_inner(); - let request = NsScannerRequest { - disk: self.endpoint.to_string(), - cache, - scan_mode: scan_mode as u64, - }; - tx.send(request) - .await - .map_err(|err| Error::other(format!("can not send request, err: {err}")))?; - - loop { - match response.next().await { - Some(Ok(resp)) => { - if !resp.update.is_empty() { - let data_usage_cache = serde_json::from_str::(&resp.update)?; - let _ = updates.send(data_usage_cache).await; - } else if !resp.data_usage_cache.is_empty() { - let data_usage_cache = serde_json::from_str::(&resp.data_usage_cache)?; - return Ok(data_usage_cache); - } else { - return Err(Error::other("scan was interrupted")); - } - } - _ => return Err(Error::other("scan was interrupted")), - } - } - } - - #[tracing::instrument(skip(self))] - async fn healing(&self) -> Option { - None - } } #[cfg(test)] diff --git a/crates/ecstore/src/set_disk.rs b/crates/ecstore/src/set_disk.rs index d00ead5c2..4519d5205 100644 --- a/crates/ecstore/src/set_disk.rs +++ b/crates/ecstore/src/set_disk.rs @@ -17,6 +17,7 @@ use crate::bitrot::{create_bitrot_reader, create_bitrot_writer}; use crate::bucket::lifecycle::lifecycle::TRANSITION_COMPLETE; use crate::client::{object_api_utils::extract_etag, transition_api::ReaderImpl}; +use crate::disk::STORAGE_FORMAT_FILE; use crate::disk::error_reduce::{OBJECT_OP_IGNORED_ERRS, reduce_read_quorum_errs, reduce_write_quorum_errs}; use crate::disk::{ self, CHECK_PART_DISK_NOT_FOUND, CHECK_PART_FILE_CORRUPT, CHECK_PART_FILE_NOT_FOUND, CHECK_PART_SUCCESS, @@ -26,10 +27,8 @@ use crate::erasure_coding; use crate::erasure_coding::bitrot_verify; use crate::error::{Error, Result}; use crate::error::{ObjectApiError, is_err_object_not_found}; -use crate::global::GLOBAL_MRFState; use crate::global::{GLOBAL_LocalNodeName, GLOBAL_TierConfigMgr}; -use crate::heal::data_usage_cache::DataUsageCache; -use crate::heal::heal_ops::{HealEntryFn, HealSequence}; +use crate::store_api::ListObjectVersionsInfo; use crate::store_api::{ListPartsInfo, ObjectToDelete}; use crate::{ bucket::lifecycle::bucket_lifecycle_ops::{gen_transition_objname, get_transitioned_object_reader, put_restore_opts}, @@ -43,19 +42,7 @@ use crate::{ error::{StorageError, to_object_err}, event::name::EventName, event_notification::{EventArgs, send_event}, - global::{ - GLOBAL_BackgroundHealState, GLOBAL_LOCAL_DISK_MAP, GLOBAL_LOCAL_DISK_SET_DRIVES, get_global_deployment_id, - is_dist_erasure, - }, - heal::{ - data_usage::{DATA_USAGE_CACHE_NAME, DATA_USAGE_ROOT}, - data_usage_cache::{DataUsageCacheInfo, DataUsageEntry, DataUsageEntryInfo}, - heal_commands::{ - DRIVE_STATE_CORRUPT, DRIVE_STATE_MISSING, DRIVE_STATE_OFFLINE, DRIVE_STATE_OK, HEAL_DEEP_SCAN, HEAL_ITEM_OBJECT, - HEAL_NORMAL_SCAN, HealOpts, HealScanMode, HealingTracker, - }, - heal_ops::BG_HEALING_UUID, - }, + global::{GLOBAL_LOCAL_DISK_MAP, GLOBAL_LOCAL_DISK_SET_DRIVES, get_global_deployment_id, is_dist_erasure}, store_api::{ BucketInfo, BucketOptions, CompletePart, DeleteBucketOptions, DeletedObject, GetObjectReader, HTTPRangeSpec, ListMultipartsInfo, ListObjectsV2Info, MakeBucketOptions, MultipartInfo, MultipartUploadResult, ObjectIO, ObjectInfo, @@ -63,11 +50,6 @@ use crate::{ }, store_init::load_format_erasure, }; -use crate::{disk::STORAGE_FORMAT_FILE, heal::mrf::PartialOperation}; -use crate::{ - heal::data_scanner::{HEAL_DELETE_DANGLING, globalHealConfig}, - store_api::ListObjectVersionsInfo, -}; use bytes::Bytes; use bytesize::ByteSize; use chrono::Utc; @@ -76,6 +58,7 @@ use glob::Pattern; use http::HeaderMap; use md5::{Digest as Md5Digest, Md5}; use rand::{Rng, seq::SliceRandom}; +use rustfs_common::heal_channel::{DriveState, HealChannelPriority, HealItemType, HealOpts, HealScanMode, send_heal_disk}; use rustfs_filemeta::headers::RESERVED_METADATA_PREFIX_LOWER; use rustfs_filemeta::{ FileInfo, FileMeta, FileMetaShallowVersion, MetaCacheEntries, MetaCacheEntry, MetadataResolutionParams, ObjectPartInfo, @@ -1653,21 +1636,14 @@ impl SetDisks { Err(e) => { warn!("connect_endpoint err {:?}", &e); if ep.is_local && e == DiskError::UnformattedDisk { - info!("unformatteddisk will push_heal_local_disks, {:?}", ep); - GLOBAL_BackgroundHealState.push_heal_local_disks(&[ep.clone()]).await; + info!("unformatteddisk will trigger heal_disk, {:?}", ep); + let set_disk_id = format!("pool_{}_set_{}", ep.pool_idx, ep.set_idx); + let _ = send_heal_disk(set_disk_id, Some(HealChannelPriority::Normal)).await; } return; } }; - if new_disk.is_local() { - if let Some(h) = new_disk.healing().await { - if !h.finished { - GLOBAL_BackgroundHealState.push_heal_local_disks(&[new_disk.endpoint()]).await; - } - } - } - let (set_idx, disk_idx) = match self.find_disk_index(&fm) { Ok(res) => res, Err(e) => { @@ -2033,23 +2009,16 @@ impl SetDisks { let fi = Self::pick_valid_fileinfo(&parts_metadata, mot_time, etag, read_quorum as usize)?; if errs.iter().any(|err| err.is_some()) { - let _ = rustfs_common::heal_channel::send_heal_request( - rustfs_common::heal_channel::create_heal_request_with_options( - fi.volume.to_string(), // bucket - Some(fi.name.to_string()), // object_prefix - false, // force_start + let _ = + rustfs_common::heal_channel::send_heal_request(rustfs_common::heal_channel::create_heal_request_with_options( + fi.volume.to_string(), // bucket + Some(fi.name.to_string()), // object_prefix + false, // force_start Some(rustfs_common::heal_channel::HealChannelPriority::Normal), // priority - Some(self.pool_index), // pool_index - Some(self.set_index), // set_index - None, // scan_mode - None, // remove_corrupted - None, // recreate_missing - None, // update_parity - None, // recursive - None, // dry_run - None, // timeout_seconds - ) - ).await; + Some(self.pool_index), // pool_index + Some(self.set_index), // set_index + )) + .await; } // debug!("get_object_fileinfo pick fi {:?}", &fi); @@ -2188,15 +2157,9 @@ impl SetDisks { Some(rustfs_common::heal_channel::HealChannelPriority::Normal), Some(pool_index), Some(set_index), - None, - None, - None, - None, - None, - None, - None, - ) - ).await; + ), + ) + .await; has_err = false; } _ => {} @@ -2275,99 +2238,6 @@ impl SetDisks { Ok(()) } - pub async fn list_and_heal(&self, bucket: &str, prefix: &str, opts: &HealOpts, heal_entry: HealEntryFn) -> Result<()> { - let bucket = bucket.to_string(); - let (disks, _) = self.get_online_disk_with_healing(false).await?; - if disks.is_empty() { - return Err(Error::other("listAndHeal: No non-healing drives found")); - } - - let expected_disks = disks.len() / 2 + 1; - let fallback_disks = &disks[expected_disks..]; - let disks = &disks[..expected_disks]; - let resolver = MetadataResolutionParams { - dir_quorum: 1, - obj_quorum: 1, - bucket: bucket.clone(), - strict: false, - ..Default::default() - }; - let path = Path::new(prefix).parent().map_or("", |p| p.to_str().unwrap()); - let filter_prefix = prefix.trim_start_matches(path).trim_matches('/'); - let opts_clone = *opts; - let bucket_agreed = bucket.clone(); - let bucket_partial = bucket.to_string(); - let (tx, rx) = broadcast::channel(1); - let tx_agreed = tx.clone(); - let tx_partial = tx.clone(); - let func_agreed = heal_entry.clone(); - let func_partial = heal_entry.clone(); - let lopts = ListPathRawOptions { - disks: disks.to_vec(), - fallback_disks: fallback_disks.to_vec(), - bucket: bucket.to_string(), - path: path.to_string(), - filter_prefix: { - if filter_prefix.is_empty() { - None - } else { - Some(filter_prefix.to_string()) - } - }, - recursive: true, - forward_to: None, - min_disks: 1, - report_not_found: false, - per_disk_limit: 0, - agreed: Some(Box::new(move |entry: MetaCacheEntry| { - let heal_entry = func_agreed.clone(); - let tx_agreed = tx_agreed.clone(); - - Box::pin({ - let bucket_agreed = bucket_agreed.clone(); - async move { - if heal_entry(bucket_agreed.clone(), entry.clone(), opts_clone.scan_mode) - .await - .is_err() - { - let _ = tx_agreed.send(true); - } - } - }) - })), - partial: Some(Box::new(move |entries: MetaCacheEntries, _: &[Option]| { - let heal_entry = func_partial.clone(); - let tx_partial = tx_partial.clone(); - - Box::pin({ - let resolver_partial = resolver.clone(); - let bucket_partial = bucket_partial.clone(); - async move { - let entry = match entries.resolve(resolver_partial) { - Some(entry) => entry, - _ => match entries.first_found() { - (Some(entry), _) => entry, - _ => return, - }, - }; - - if heal_entry(bucket_partial.clone(), entry.clone(), opts_clone.scan_mode) - .await - .is_err() - { - let _ = tx_partial.send(true); - } - } - }) - })), - finished: None, - }; - - _ = list_path_raw(rx, lopts) - .await - .map_err(|err| Error::other(format!("listPathRaw returned {err}: bucket: {bucket}, path: {path}"))); - Ok(()) - } async fn get_online_disk_with_healing(&self, incl_healing: bool) -> Result<(Vec>, bool)> { let (new_disks, _, healing) = self.get_online_disk_with_healing_and_info(incl_healing).await?; @@ -2436,7 +2306,7 @@ impl SetDisks { ) -> disk::error::Result<(HealResultItem, Option)> { info!("SetDisks heal_object"); let mut result = HealResultItem { - heal_item_type: HEAL_ITEM_OBJECT.to_string(), + heal_item_type: HealItemType::Object.to_string(), bucket: bucket.to_string(), object: object.to_string(), version_id: version_id.to_string(), @@ -2542,15 +2412,15 @@ impl SetDisks { let drive_state = match reason { Some(err) => match err { - DiskError::DiskNotFound => DRIVE_STATE_OFFLINE, + DiskError::DiskNotFound => DriveState::Offline.to_string(), DiskError::FileNotFound | DiskError::FileVersionNotFound | DiskError::VolumeNotFound | DiskError::PartMissingOrCorrupt - | DiskError::OutdatedXLMeta => DRIVE_STATE_MISSING, - _ => DRIVE_STATE_CORRUPT, + | DiskError::OutdatedXLMeta => DriveState::Missing.to_string(), + _ => DriveState::Corrupt.to_string(), }, - None => DRIVE_STATE_OK, + None => DriveState::Ok.to_string(), }; result.before.drives.push(HealDriveInfo { uuid: "".to_string(), @@ -2894,37 +2764,82 @@ impl SetDisks { "rename temp data, src_volume: {}, src_path: {}, dst_volume: {}, dst_path: {}", RUSTFS_META_TMP_BUCKET, tmp_id, bucket, object ); - if let Err(err) = disk + let rename_result = disk .rename_data(RUSTFS_META_TMP_BUCKET, &tmp_id, parts_metadata[index].clone(), bucket, object) - .await - { - info!("rename temp data err: {}", err.to_string()); - // self.delete_all(RUSTFS_META_TMP_BUCKET, &tmp_id).await?; - return Ok((result, Some(err))); - } + .await; - info!("remove temp object, volume: {}, path: {}", RUSTFS_META_TMP_BUCKET, tmp_id); - self.delete_all(RUSTFS_META_TMP_BUCKET, &tmp_id) - .await - .map_err(DiskError::other)?; - if parts_metadata[index].is_remote() { - let rm_data_dir = parts_metadata[index].data_dir.unwrap().to_string(); - let d_path = Path::new(&encode_dir_object(object)).join(rm_data_dir); - disk.delete( - bucket, - d_path.to_str().unwrap(), - DeleteOptions { - immediate: true, - recursive: true, - ..Default::default() - }, - ) - .await?; - } + if let Err(err) = &rename_result { + info!( + "rename temp data err: {}. Try fallback to direct xl.meta overwrite...", + err.to_string() + ); - for (i, v) in result.before.drives.iter().enumerate() { - if v.endpoint == disk.endpoint().to_string() { - result.after.drives[i].state = DRIVE_STATE_OK.to_string(); + let healthy_index = latest_disks.iter().position(|d| d.is_some()).unwrap_or(0); + + if let Some(healthy_disk) = &latest_disks[healthy_index] { + let xlmeta_path = format!("{object}/xl.meta"); + + match healthy_disk.read_all(bucket, &xlmeta_path).await { + Ok(xlmeta_bytes) => { + if let Err(e) = disk.write_all(bucket, &xlmeta_path, xlmeta_bytes).await { + info!("fallback xl.meta overwrite failed: {}", e.to_string()); + + return Ok(( + result, + Some(DiskError::other(format!("fallback xl.meta overwrite failed: {e}"))), + )); + } else { + info!("fallback xl.meta overwrite succeeded for disk {}", disk.to_string()); + } + } + + Err(e) => { + info!("read healthy xl.meta failed: {}", e.to_string()); + + return Ok(( + result, + Some(DiskError::other(format!("read healthy xl.meta failed: {e}"))), + )); + } + } + } else { + info!("no healthy disk found for xl.meta fallback overwrite"); + + return Ok(( + result, + Some(DiskError::other("no healthy disk found for xl.meta fallback overwrite")), + )); + } + } else { + info!("remove temp object, volume: {}, path: {}", RUSTFS_META_TMP_BUCKET, tmp_id); + + self.delete_all(RUSTFS_META_TMP_BUCKET, &tmp_id) + .await + .map_err(DiskError::other)?; + + if parts_metadata[index].is_remote() { + let rm_data_dir = parts_metadata[index].data_dir.unwrap().to_string(); + + let d_path = Path::new(&encode_dir_object(object)).join(rm_data_dir); + + disk.delete( + bucket, + d_path.to_str().unwrap(), + DeleteOptions { + immediate: true, + + recursive: true, + + ..Default::default() + }, + ) + .await?; + } + + for (i, v) in result.before.drives.iter().enumerate() { + if v.endpoint == disk.endpoint().to_string() { + result.after.drives[i].state = DriveState::Ok.to_string(); + } } } } @@ -2984,7 +2899,7 @@ impl SetDisks { disks.clone() }; let mut result = HealResultItem { - heal_item_type: HEAL_ITEM_OBJECT.to_string(), + heal_item_type: HealItemType::Object.to_string(), bucket: bucket.to_string(), object: object.to_string(), disk_count: self.disks.read().await.len(), @@ -3028,11 +2943,11 @@ impl SetDisks { let endpoint = drive.to_string(); let drive_state = match err { Some(err) => match err { - DiskError::DiskNotFound => DRIVE_STATE_OFFLINE, - DiskError::FileNotFound | DiskError::VolumeNotFound => DRIVE_STATE_MISSING, - _ => DRIVE_STATE_CORRUPT, + DiskError::DiskNotFound => DriveState::Offline.to_string(), + DiskError::FileNotFound | DiskError::VolumeNotFound => DriveState::Missing.to_string(), + _ => DriveState::Corrupt.to_string(), }, - None => DRIVE_STATE_OK, + None => DriveState::Ok.to_string(), }; result.before.drives.push(HealDriveInfo { uuid: "".to_string(), @@ -3059,11 +2974,11 @@ impl SetDisks { if let (Some(DiskError::VolumeNotFound | DiskError::FileNotFound), Some(disk)) = (err, disk) { let vol_path = Path::new(bucket).join(object); let drive_state = match disk.make_volume(vol_path.to_str().unwrap()).await { - Ok(_) => DRIVE_STATE_OK, + Ok(_) => DriveState::Ok.to_string(), Err(merr) => match merr { - DiskError::VolumeExists => DRIVE_STATE_OK, - DiskError::DiskNotFound => DRIVE_STATE_OFFLINE, - _ => DRIVE_STATE_CORRUPT, + DiskError::VolumeExists => DriveState::Ok.to_string(), + DiskError::DiskNotFound => DriveState::Offline.to_string(), + _ => DriveState::Corrupt.to_string(), }, }; result.after.drives[index].state = drive_state.to_string(); @@ -3083,7 +2998,7 @@ impl SetDisks { ) -> HealResultItem { let disk_len = { self.disks.read().await.len() }; let mut result = HealResultItem { - heal_item_type: HEAL_ITEM_OBJECT.to_string(), + heal_item_type: HealItemType::Object.to_string(), bucket: bucket.to_string(), object: object.to_string(), object_size: lfi.size as usize, @@ -3105,23 +3020,23 @@ impl SetDisks { result.before.drives.push(HealDriveInfo { uuid: "".to_string(), endpoint: self.set_endpoints[index].to_string(), - state: DRIVE_STATE_OFFLINE.to_string(), + state: DriveState::Offline.to_string(), }); result.after.drives.push(HealDriveInfo { uuid: "".to_string(), endpoint: self.set_endpoints[index].to_string(), - state: DRIVE_STATE_OFFLINE.to_string(), + state: DriveState::Offline.to_string(), }); } - let mut drive_state = DRIVE_STATE_CORRUPT; + let mut drive_state = DriveState::Corrupt; if let Some(err) = &errs[index] { if err == &DiskError::FileNotFound || err == &DiskError::VolumeNotFound { - drive_state = DRIVE_STATE_MISSING; + drive_state = DriveState::Missing; } } else { - drive_state = DRIVE_STATE_OK; + drive_state = DriveState::Ok; } result.before.drives.push(HealDriveInfo { @@ -3213,661 +3128,6 @@ impl SetDisks { } } - pub async fn ns_scanner( - self: Arc, - buckets: &[BucketInfo], - want_cycle: u32, - updates: Sender, - heal_scan_mode: HealScanMode, - ) -> Result<()> { - info!("ns_scanner"); - if buckets.is_empty() { - info!("data-scanner: no buckets to scan, skipping scanner cycle"); - return Ok(()); - } - - let (mut disks, healing) = self.get_online_disk_with_healing(false).await?; - if disks.is_empty() { - info!("data-scanner: all drives are offline or being healed, skipping scanner cycle"); - return Ok(()); - } - - let old_cache = DataUsageCache::load(&self, DATA_USAGE_CACHE_NAME).await?; - let mut cache = DataUsageCache { - info: DataUsageCacheInfo { - name: DATA_USAGE_ROOT.to_string(), - next_cycle: old_cache.info.next_cycle, - ..Default::default() - }, - cache: HashMap::new(), - }; - - // Put all buckets into channel. - let (bucket_tx, bucket_rx) = mpsc::channel(buckets.len()); - // Shuffle buckets to ensure total randomness of buckets, being scanned. - // Otherwise, same set of buckets get scanned across erasure sets always. - // at any given point in time. This allows different buckets to be scanned - // in different order per erasure set, this wider spread is needed when - // there are lots of buckets with different order of objects in them. - let permutes = { - let mut rng = rand::rng(); - let mut permutes: Vec = (0..buckets.len()).collect(); - permutes.shuffle(&mut rng); - permutes - }; - - // Add new buckets first - for idx in permutes.iter() { - let b = buckets[*idx].clone(); - match old_cache.find(&b.name) { - Some(e) => { - cache.replace(&b.name, DATA_USAGE_ROOT, e); - let _ = bucket_tx.send(b).await; - } - None => { - let _ = bucket_tx.send(b).await; - } - } - } - - let (buckets_results_tx, mut buckets_results_rx) = mpsc::channel::(disks.len()); - // 新增:从环境变量读取基础间隔,默认 30 秒 - let set_disk_update_interval_secs = std::env::var("RUSTFS_NS_SCANNER_INTERVAL") - .ok() - .and_then(|v| v.parse::().ok()) - .unwrap_or(30); - let update_time = { - let mut rng = rand::rng(); - Duration::from_secs(set_disk_update_interval_secs) + Duration::from_secs_f64(10.0 * rng.random_range(0.0..1.0)) - }; - let mut ticker = interval(update_time); - - // 检查是否需要运行后台任务 - let skip_background_task = std::env::var("RUSTFS_SKIP_BACKGROUND_TASK") - .ok() - .and_then(|v| v.parse::().ok()) - .unwrap_or(false); - - let task = if !skip_background_task { - Some(tokio::spawn(async move { - let last_save = Some(SystemTime::now()); - let mut need_loop = true; - while need_loop { - select! { - _ = ticker.tick() => { - if !cache.info.last_update.eq(&last_save) { - let _ = cache.save(DATA_USAGE_CACHE_NAME).await; - let _ = updates.send(cache.clone()).await; - } - } - result = buckets_results_rx.recv() => { - match result { - Some(result) => { - cache.replace(&result.name, &result.parent, result.entry); - cache.info.last_update = Some(SystemTime::now()); - }, - None => { - need_loop = false; - cache.info.next_cycle = want_cycle; - cache.info.last_update = Some(SystemTime::now()); - let _ = cache.save(DATA_USAGE_CACHE_NAME).await; - let _ = updates.send(cache.clone()).await; - } - } - } - } - } - })) - } else { - None - }; - - // Restrict parallelism for disk usage scanner - let max_procs = num_cpus::get(); - if max_procs < disks.len() { - disks = disks[0..max_procs].to_vec(); - } - - let mut futures = Vec::new(); - let bucket_rx = Arc::new(RwLock::new(bucket_rx)); - for disk in disks.iter() { - let disk = match disk { - Some(disk) => disk.clone(), - None => continue, - }; - let self_clone = Arc::clone(&self); - let bucket_rx_clone = bucket_rx.clone(); - let buckets_results_tx_clone = buckets_results_tx.clone(); - futures.push(async move { - loop { - match bucket_rx_clone.write().await.try_recv() { - Err(_) => return, - Ok(bucket_info) => { - let cache_name = Path::new(&bucket_info.name).join(DATA_USAGE_CACHE_NAME); - let mut cache = match DataUsageCache::load(&self_clone, &cache_name.to_string_lossy()).await { - Ok(cache) => cache, - Err(_) => continue, - }; - if cache.info.name.is_empty() { - cache.info.name = bucket_info.name.clone(); - } - cache.info.skip_healing = healing; - cache.info.next_cycle = want_cycle; - if cache.info.name != bucket_info.name { - cache.info = DataUsageCacheInfo { - name: bucket_info.name, - last_update: Some(SystemTime::now()), - next_cycle: want_cycle, - ..Default::default() - }; - } - - // Collect updates. - let (tx, mut rx) = mpsc::channel(1); - let buckets_results_tx_inner_clone = buckets_results_tx_clone.clone(); - let name = cache.info.name.clone(); - let task = tokio::spawn(async move { - loop { - match rx.recv().await { - Some(entry) => { - let _ = buckets_results_tx_inner_clone - .send(DataUsageEntryInfo { - name: name.clone(), - parent: DATA_USAGE_ROOT.to_string(), - entry, - }) - .await; - } - None => return, - } - } - }); - - // Calc usage - let before = cache.info.last_update; - let mut cache = match disk.ns_scanner(&cache, tx, heal_scan_mode, None).await { - Ok(cache) => cache, - Err(_) => { - if cache.info.last_update > before { - let _ = cache.save(&cache_name.to_string_lossy()).await; - } - let _ = task.await; - continue; - } - }; - - cache.info.updates = None; - let _ = task.await; - let mut root = DataUsageEntry::default(); - if let Some(r) = cache.root() { - root = cache.flatten(&r); - if let Some(r) = &root.replication_stats { - if r.empty() { - root.replication_stats = None; - } - } - } - let _ = buckets_results_tx_clone - .send(DataUsageEntryInfo { - name: cache.info.name.clone(), - parent: DATA_USAGE_ROOT.to_string(), - entry: root, - }) - .await; - let _ = cache.save(&cache_name.to_string_lossy()).await; - } - } - info!("continue scanner"); - } - }); - } - - info!("ns_scanner start"); - let _ = join_all(futures).await; - if let Some(task) = task { - let _ = task.await; - } - info!("ns_scanner completed"); - Ok(()) - } - - pub async fn heal_erasure_set(self: Arc, buckets: &[String], tracker: Arc>) -> Result<()> { - let (bg_seq, found) = GLOBAL_BackgroundHealState.get_heal_sequence_by_token(BG_HEALING_UUID).await; - if !found { - return Err(Error::other("no local healing sequence initialized, unable to heal the drive")); - } - let bg_seq = bg_seq.unwrap(); - let scan_mode = HEAL_NORMAL_SCAN; - - let tracker_defer = tracker.clone(); - let defer = async move { - let mut w = tracker_defer.write().await; - w.set_object("").await; - w.set_bucket("").await; - let _ = w.update().await; - }; - - for bucket in buckets.iter() { - if let Err(err) = HealSequence::heal_bucket(bg_seq.clone(), bucket, true).await { - info!("{}", err.to_string()); - } - } - - let info = match tracker - .read() - .await - .disk - .as_ref() - .unwrap() - .disk_info(&DiskInfoOptions::default()) - .await - { - Ok(info) => info, - Err(err) => { - defer.await; - return Err(Error::other(format!("unable to get disk information before healing it: {err}"))); - } - }; - let num_cores = num_cpus::get(); // use num_cpus crate to get the number of cores - let mut num_healers: usize; - - if info.nr_requests as usize > num_cores { - num_healers = num_cores / 4; - } else { - num_healers = (info.nr_requests / 4) as usize; - } - - if num_healers < 4 { - num_healers = 4; - } - - let v = globalHealConfig.read().await.get_workers(); - if v > 0 { - num_healers = v; - } - info!( - "Healing drive '{}' - use {} parallel workers.", - tracker.read().await.disk.as_ref().unwrap().to_string(), - num_healers - ); - - let jt = rustfs_workers::workers::Workers::new(num_healers).map_err(|err| Error::other(err.to_string()))?; - - let heal_entry_done = |name: String| HealEntryResult { - entry_done: true, - name, - ..Default::default() - }; - - let heal_entry_success = |sz: usize| HealEntryResult { - bytes: sz, - success: true, - ..Default::default() - }; - - let heal_entry_failure = |sz: usize| HealEntryResult { - bytes: sz, - ..Default::default() - }; - - let heal_entry_skipped = |sz: usize| HealEntryResult { - bytes: sz, - skipped: true, - ..Default::default() - }; - - let (result_tx, mut result_rx) = mpsc::channel::(1000); - let tracker_task = tracker.clone(); - let task = tokio::spawn(async move { - loop { - match result_rx.recv().await { - Some(entry) => { - if entry.entry_done { - tracker_task.write().await.set_object(entry.name.as_str()).await; - if let Some(last_update) = tracker_task.read().await.get_last_update().await { - if SystemTime::now().duration_since(last_update).unwrap() > Duration::from_secs(60) { - if let Err(err) = tracker_task.write().await.update().await { - info!("tracker update failed, err: {}", err.to_string()); - } - } - } - continue; - } - - tracker_task - .write() - .await - .update_progress(entry.success, entry.skipped, entry.bytes as u64) - .await; - } - None => { - if let Err(err) = tracker_task.write().await.update().await { - info!("tracker update failed, err: {}", err.to_string()); - } - return; - } - } - } - }); - - let started = tracker.read().await.started; - let mut ret_err = None; - for bucket in buckets.iter() { - if tracker.read().await.is_healed(bucket).await { - info!("bucket{} was healed", bucket); - continue; - } - - let mut forward_to = None; - let b = tracker.read().await.get_bucket().await; - if b == *bucket { - forward_to = Some(tracker.read().await.get_object().await); - } - - if !b.is_empty() { - tracker.write().await.resume().await; - } - - tracker.write().await.set_object("").await; - tracker.write().await.set_bucket("").await; - - if let Err(err) = HealSequence::heal_bucket(bg_seq.clone(), bucket, true).await { - info!("heal bucket failed: {}", err.to_string()); - ret_err = Some(err); - continue; - } - - let (mut disks, _, healing) = self.get_online_disk_with_healing_and_info(true).await?; - if disks.len() == healing { - info!("all drives are in healing state, aborting.."); - defer.await; - return Ok(()); - } - - disks = disks[0..disks.len() - healing].to_vec(); - if disks.len() < self.set_drive_count / 2 { - defer.await; - return Err(Error::other(format!( - "not enough drives (found={}, healing={}, total={}) are available to heal `{}`", - disks.len(), - healing, - self.set_drive_count, - tracker.read().await.disk.as_ref().unwrap().to_string() - ))); - } - - { - let mut rng = rand::rng(); - - // 随机洗牌 - disks.shuffle(&mut rng); - } - - let expected_disk = disks.len() / 2 + 1; - let fallback_disks = disks[expected_disk..].to_vec(); - disks = disks[..expected_disk].to_vec(); - - let result_tx_send = result_tx.clone(); - let bg_seq_send = bg_seq.clone(); - let send = Box::new(move |result: HealEntryResult| { - let result_tx_send = result_tx_send.clone(); - let bg_seq_send = bg_seq_send.clone(); - Box::pin(async move { - let _ = result_tx_send.send(result).await; - bg_seq_send.count_scanned(HEAL_ITEM_OBJECT.to_string()).await; - true - }) - }); - - let jt_clone = jt.clone(); - let self_clone = self.clone(); - let started_clone = started; - let tracker_heal = tracker.clone(); - let bg_seq_clone = bg_seq.clone(); - let send_clone = send.clone(); - let heal_entry = Arc::new(move |bucket: String, entry: MetaCacheEntry| { - info!("heal entry, bucket: {}, entry: {:?}", bucket, entry); - let jt_clone = jt_clone.clone(); - let self_clone = self_clone.clone(); - let started = started_clone; - let tracker_heal = tracker_heal.clone(); - let bg_seq = bg_seq_clone.clone(); - let send = send_clone.clone(); - Box::pin(async move { - let defer = async { - jt_clone.give().await; - }; - if entry.name.is_empty() && entry.metadata.is_empty() { - defer.await; - return; - } - if entry.is_dir() { - defer.await; - return; - } - if bucket == RUSTFS_META_BUCKET - && (Pattern::new("buckets/*/.metacache/*") - .map(|p| p.matches(&entry.name)) - .unwrap_or(false) - || Pattern::new("tmp/.trash/*").map(|p| p.matches(&entry.name)).unwrap_or(false) - || Pattern::new("multipart/*").map(|p| p.matches(&entry.name)).unwrap_or(false)) - { - defer.await; - return; - } - let encoded_entry_name = encode_dir_object(entry.name.as_str()); - let mut result: HealEntryResult; - let fivs = match entry.file_info_versions(bucket.as_str()) { - Ok(fivs) => fivs, - Err(err) => { - match self_clone - .heal_object( - &bucket, - &encoded_entry_name, - "", - &HealOpts { - scan_mode, - remove: HEAL_DELETE_DANGLING, - ..Default::default() - }, - ) - .await - { - Ok((res, None)) => { - bg_seq.count_healed(HEAL_ITEM_OBJECT.to_string()).await; - result = heal_entry_success(res.object_size); - } - Ok((_, Some(err))) => { - if DiskError::is_err_object_not_found(&err) || DiskError::is_err_version_not_found(&err) { - defer.await; - return; - } - - result = heal_entry_failure(0); - bg_seq.count_failed(HEAL_ITEM_OBJECT.to_string()).await; - info!("unable to heal object {}/{}: {}", bucket, entry.name, err.to_string()); - } - Err(_) => { - result = heal_entry_failure(0); - bg_seq.count_failed(HEAL_ITEM_OBJECT.to_string()).await; - info!("unable to heal object {}/{}: {}", bucket, entry.name, err.to_string()); - } - } - send(result.clone()).await; - defer.await; - return; - } - }; - let mut version_not_found = 0; - for version in fivs.versions.iter() { - if let (Some(started), Some(mod_time)) = (started, version.mod_time) { - if mod_time > started { - version_not_found += 1; - if send(heal_entry_skipped(version.size as usize)).await { - defer.await; - return; - } - continue; - } - } - - let mut version_healed = false; - match self_clone - .heal_object( - &bucket, - &encoded_entry_name, - version - .version_id - .as_ref() - .map(|v| v.to_string()) - .unwrap_or("".to_string()) - .as_str(), - &HealOpts { - scan_mode, - remove: HEAL_DELETE_DANGLING, - ..Default::default() - }, - ) - .await - { - Ok((res, None)) => { - if res.after.drives[tracker_heal.read().await.disk_index.unwrap()].state == DRIVE_STATE_OK { - version_healed = true; - } - } - Ok((_, Some(err))) => match err { - DiskError::FileNotFound | DiskError::FileVersionNotFound => { - version_not_found += 1; - continue; - } - _ => {} - }, - Err(_) => {} - } - - if version_healed { - bg_seq.count_healed(HEAL_ITEM_OBJECT.to_string()).await; - result = heal_entry_success(version.size as usize); - } else { - bg_seq.count_failed(HEAL_ITEM_OBJECT.to_string()).await; - result = heal_entry_failure(version.size as usize); - match version.version_id { - Some(version_id) => { - info!("unable to heal object {}/{}-v({})", bucket, version.name, version_id); - } - None => { - info!("unable to heal object {}/{}", bucket, version.name); - } - } - } - - if !send(result).await { - defer.await; - return; - } - } - if version_not_found == fivs.versions.len() { - defer.await; - return; - } - send(heal_entry_done(entry.name.clone())).await; - defer.await; - }) - }); - let resolver = MetadataResolutionParams { - dir_quorum: 1, - obj_quorum: 1, - bucket: bucket.clone(), - ..Default::default() - }; - let (_, rx) = broadcast::channel(1); - let jt_agree = jt.clone(); - let jt_partial = jt.clone(); - let bucket_agree = bucket.clone(); - let bucket_partial = bucket.clone(); - let heal_entry_agree = heal_entry.clone(); - let heal_entry_partial = heal_entry.clone(); - if let Err(err) = list_path_raw( - rx, - ListPathRawOptions { - disks, - fallback_disks, - bucket: bucket.clone(), - recursive: true, - forward_to, - min_disks: 1, - report_not_found: false, - agreed: Some(Box::new(move |entry: MetaCacheEntry| { - let jt = jt_agree.clone(); - let bucket = bucket_agree.clone(); - let heal_entry = heal_entry_agree.clone(); - Box::pin(async move { - jt.take().await; - let bucket = bucket.clone(); - tokio::spawn(async move { - heal_entry(bucket, entry).await; - }); - }) - })), - partial: Some(Box::new(move |entries: MetaCacheEntries, _: &[Option]| { - let jt = jt_partial.clone(); - let bucket = bucket_partial.clone(); - let heal_entry = heal_entry_partial.clone(); - Box::pin({ - let heal_entry = heal_entry.clone(); - let resolver = resolver.clone(); - async move { - let entry = if let Some(entry) = entries.resolve(resolver) { - entry - } else if let (Some(entry), _) = entries.first_found() { - entry - } else { - return; - }; - jt.take().await; - let bucket = bucket.clone(); - let heal_entry = heal_entry.clone(); - tokio::spawn(async move { - heal_entry(bucket, entry).await; - }); - } - }) - })), - finished: None, - ..Default::default() - }, - ) - .await - { - ret_err = Some(err.into()); - } - - jt.wait().await; - if let Some(err) = ret_err.as_ref() { - info!("listing failed with: {} on bucket: {}", err.to_string(), bucket); - continue; - } - tracker.write().await.bucket_done(bucket).await; - if let Err(err) = tracker.write().await.update().await { - info!("tracker update failed, err: {}", err.to_string()); - } - } - - if let Some(err) = ret_err.as_ref() { - return Err(err.clone()); - } - if !tracker.read().await.queue_buckets.is_empty() { - return Err(Error::other(format!( - "not all buckets were healed: {:?}", - tracker.read().await.queue_buckets - ))); - } - drop(result_tx); - let _ = task.await; - defer.await; - Ok(()) - } - async fn delete_prefix(&self, bucket: &str, prefix: &str) -> disk::error::Result<()> { let disks = self.get_disks_internal().await; let write_quorum = disks.len() / 2 + 1; @@ -4655,23 +3915,15 @@ impl StorageAPI for SetDisks { #[tracing::instrument(skip(self))] async fn add_partial(&self, bucket: &str, object: &str, version_id: &str) -> Result<()> { - let _ = rustfs_common::heal_channel::send_heal_request( - rustfs_common::heal_channel::create_heal_request_with_options( - bucket.to_string(), - Some(object.to_string()), - false, - Some(rustfs_common::heal_channel::HealChannelPriority::Normal), - Some(self.pool_index), - Some(self.set_index), - None, - None, - None, - None, - None, - None, - None, - ) - ).await; + let _ = rustfs_common::heal_channel::send_heal_request(rustfs_common::heal_channel::create_heal_request_with_options( + bucket.to_string(), + Some(object.to_string()), + false, + Some(rustfs_common::heal_channel::HealChannelPriority::Normal), + Some(self.pool_index), + Some(self.set_index), + )) + .await; Ok(()) } @@ -5813,23 +5065,16 @@ impl StorageAPI for SetDisks { .await?; } if let Some(versions) = versions { - let _ = rustfs_common::heal_channel::send_heal_request( - rustfs_common::heal_channel::create_heal_request_with_options( + let _ = + rustfs_common::heal_channel::send_heal_request(rustfs_common::heal_channel::create_heal_request_with_options( bucket.to_string(), Some(object.to_string()), false, Some(rustfs_common::heal_channel::HealChannelPriority::Normal), Some(self.pool_index), Some(self.set_index), - None, - None, - None, - None, - None, - None, - None, - ) - ).await; + )) + .await; } let upload_id_path = upload_id_path.clone(); @@ -5915,11 +5160,11 @@ impl StorageAPI for SetDisks { let (result, err) = self.heal_object(bucket, object, version_id, opts).await?; if let Some(err) = err.as_ref() { match err { - &DiskError::FileCorrupt if opts.scan_mode != HEAL_DEEP_SCAN => { + &DiskError::FileCorrupt if opts.scan_mode != HealScanMode::Deep => { // Instead of returning an error when a bitrot error is detected // during a normal heal scan, heal again with bitrot flag enabled. let mut opts = *opts; - opts.scan_mode = HEAL_DEEP_SCAN; + opts.scan_mode = HealScanMode::Deep; let (result, err) = self.heal_object(bucket, object, version_id, &opts).await?; return Ok((result, err.map(|e| e.into()))); } @@ -5929,18 +5174,6 @@ impl StorageAPI for SetDisks { Ok((result, err.map(|e| e.into()))) } - #[tracing::instrument(skip(self))] - async fn heal_objects( - &self, - _bucket: &str, - _prefix: &str, - _opts: &HealOpts, - _hs: Arc, - _is_meta: bool, - ) -> Result<()> { - unimplemented!() - } - #[tracing::instrument(skip(self))] async fn get_pool_and_set(&self, _id: &str) -> Result<(Option, Option, Option)> { unimplemented!() @@ -6236,7 +5469,7 @@ async fn disks_with_all_parts( let mut verify_resp = CheckPartsResp::default(); let mut verify_err = None; meta.data_dir = latest_meta.data_dir; - if scan_mode == HEAL_DEEP_SCAN { + if scan_mode == HealScanMode::Deep { // disk has a valid xl.meta but may not have all the // parts. This is considered an outdated disk, since // it needs healing too. diff --git a/crates/ecstore/src/store_api.rs b/crates/ecstore/src/store_api.rs index 5f1ea4e0d..5a582a528 100644 --- a/crates/ecstore/src/store_api.rs +++ b/crates/ecstore/src/store_api.rs @@ -970,6 +970,7 @@ pub trait StorageAPI: ObjectIO { // Walk TODO: async fn get_object_info(&self, bucket: &str, object: &str, opts: &ObjectOptions) -> Result; + async fn verify_object_integrity(&self, bucket: &str, object: &str, opts: &ObjectOptions) -> Result<()>; async fn copy_object( &self, src_bucket: &str, diff --git a/rustfs/src/main.rs b/rustfs/src/main.rs index c6677ed9e..0aa2b5736 100644 --- a/rustfs/src/main.rs +++ b/rustfs/src/main.rs @@ -54,6 +54,7 @@ use rustfs_iam::init_iam_sys; use rustfs_obs::{init_obs, set_global_guard}; use rustfs_utils::net::parse_and_resolve_address; use std::io::{Error, Result}; +use std::sync::Arc; use tracing::{debug, error, info, instrument, warn}; #[cfg(all(target_os = "linux", target_env = "gnu"))] From 2ce7e01f55731a5f755053156823adf21b907f70 Mon Sep 17 00:00:00 2001 From: junxiang Mu <1948535941@qq.com> Date: Thu, 24 Jul 2025 11:15:00 +0800 Subject: [PATCH 25/29] Chore: remove dirty file(heal) Signed-off-by: junxiang Mu <1948535941@qq.com> --- .../ecstore/src/heal/background_heal_ops.rs | 512 ----- crates/ecstore/src/heal/data_scanner.rs | 1826 ----------------- .../ecstore/src/heal/data_scanner_metric.rs | 486 ----- crates/ecstore/src/heal/data_usage.rs | 221 -- crates/ecstore/src/heal/data_usage_cache.rs | 928 --------- crates/ecstore/src/heal/error.rs | 19 - crates/ecstore/src/heal/heal_commands.rs | 544 ----- crates/ecstore/src/heal/heal_ops.rs | 842 -------- crates/ecstore/src/heal/mod.rs | 23 - crates/ecstore/src/heal/mrf.rs | 183 -- crates/ecstore/src/lib.rs | 1 - 11 files changed, 5585 deletions(-) delete mode 100644 crates/ecstore/src/heal/background_heal_ops.rs delete mode 100644 crates/ecstore/src/heal/data_scanner.rs delete mode 100644 crates/ecstore/src/heal/data_scanner_metric.rs delete mode 100644 crates/ecstore/src/heal/data_usage.rs delete mode 100644 crates/ecstore/src/heal/data_usage_cache.rs delete mode 100644 crates/ecstore/src/heal/error.rs delete mode 100644 crates/ecstore/src/heal/heal_commands.rs delete mode 100644 crates/ecstore/src/heal/heal_ops.rs delete mode 100644 crates/ecstore/src/heal/mod.rs delete mode 100644 crates/ecstore/src/heal/mrf.rs diff --git a/crates/ecstore/src/heal/background_heal_ops.rs b/crates/ecstore/src/heal/background_heal_ops.rs deleted file mode 100644 index 59abc3db9..000000000 --- a/crates/ecstore/src/heal/background_heal_ops.rs +++ /dev/null @@ -1,512 +0,0 @@ -// 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 futures::future::join_all; -use rustfs_madmin::heal_commands::HealResultItem; -use rustfs_utils::path::{SLASH_SEPARATOR, path_join}; -use std::{cmp::Ordering, env, path::PathBuf, sync::Arc, time::Duration}; -use tokio::{ - spawn, - sync::{ - RwLock, - mpsc::{self, Receiver, Sender}, - }, - time::interval, -}; -use tokio_util::sync::CancellationToken; -use tracing::{error, info}; -use uuid::Uuid; - -use super::{ - heal_commands::HealOpts, - heal_ops::{HealSequence, new_bg_heal_sequence}, -}; -use crate::error::{Error, Result}; -use crate::global::get_background_services_cancel_token; -use crate::heal::error::ERR_RETRY_HEALING; -use crate::heal::heal_commands::{HEAL_ITEM_BUCKET, HealScanMode}; -use crate::heal::heal_ops::{BG_HEALING_UUID, HealSource}; -use crate::{ - config::RUSTFS_CONFIG_PREFIX, - disk::{BUCKET_META_PREFIX, DiskAPI, DiskInfoOptions, RUSTFS_META_BUCKET, endpoint::Endpoint, error::DiskError}, - global::{GLOBAL_BackgroundHealRoutine, GLOBAL_BackgroundHealState, GLOBAL_LOCAL_DISK_MAP}, - heal::{ - data_usage::{DATA_USAGE_CACHE_NAME, DATA_USAGE_ROOT}, - data_usage_cache::DataUsageCache, - heal_commands::{init_healing_tracker, load_healing_tracker}, - heal_ops::NOP_HEAL, - }, - new_object_layer_fn, - store::get_disk_via_endpoint, - store_api::{BucketInfo, BucketOptions, StorageAPI}, -}; - -pub static DEFAULT_MONITOR_NEW_DISK_INTERVAL: Duration = Duration::from_secs(10); - -pub async fn init_auto_heal() { - info!("Initializing auto heal background task"); - - let Some(cancel_token) = get_background_services_cancel_token() else { - error!("Background services cancel token not initialized"); - return; - }; - - init_background_healing().await; - let v = env::var("_RUSTFS_AUTO_DRIVE_HEALING").unwrap_or("on".to_string()); - if v == "on" { - info!("start monitor local disks and heal"); - GLOBAL_BackgroundHealState - .push_heal_local_disks(&get_local_disks_to_heal().await) - .await; - - let cancel_clone = cancel_token.clone(); - spawn(async move { - monitor_local_disks_and_heal(cancel_clone).await; - }); - } - - // let cancel_clone = cancel_token.clone(); - // spawn(async move { - // GLOBAL_MRFState.heal_routine_with_cancel(cancel_clone).await; - // }); -} - -async fn init_background_healing() { - let bg_seq = Arc::new(new_bg_heal_sequence()); - for _ in 0..GLOBAL_BackgroundHealRoutine.workers { - let bg_seq_clone = bg_seq.clone(); - spawn(async { - GLOBAL_BackgroundHealRoutine.add_worker(bg_seq_clone).await; - }); - } - let _ = GLOBAL_BackgroundHealState.launch_new_heal_sequence(bg_seq).await; -} - -pub async fn get_local_disks_to_heal() -> Vec { - let mut disks_to_heal = Vec::new(); - for (_, disk) in GLOBAL_LOCAL_DISK_MAP.read().await.iter() { - if let Some(disk) = disk { - if let Err(err) = disk.disk_info(&DiskInfoOptions::default()).await { - if err == DiskError::UnformattedDisk { - info!("get_local_disks_to_heal, disk is unformatted: {}", err); - disks_to_heal.push(disk.endpoint()); - } - } - let h = disk.healing().await; - if let Some(h) = h { - if !h.finished { - info!("get_local_disks_to_heal, disk healing not finished"); - disks_to_heal.push(disk.endpoint()); - } - } - } - } - - // todo - // if disks_to_heal.len() == GLOBAL_Endpoints.read().await.n { - - // } - disks_to_heal -} - -async fn monitor_local_disks_and_heal(cancel_token: CancellationToken) { - info!("Auto heal monitor started"); - let mut interval = interval(DEFAULT_MONITOR_NEW_DISK_INTERVAL); - - loop { - tokio::select! { - _ = cancel_token.cancelled() => { - info!("Auto heal monitor received shutdown signal, exiting gracefully"); - break; - } - _ = interval.tick() => { - let heal_disks = GLOBAL_BackgroundHealState.get_heal_local_disk_endpoints().await; - if heal_disks.is_empty() { - info!("heal local disks is empty"); - interval.reset(); - continue; - } - - info!("heal local disks: {:?}", heal_disks); - - let store = new_object_layer_fn().expect("errServerNotInitialized"); - if let (_result, Some(err)) = store.heal_format(false).await.expect("heal format failed") { - error!("heal local disk format error: {}", err); - if err == Error::NoHealRequired { - } else { - info!("heal format err: {}", err.to_string()); - interval.reset(); - continue; - } - } - - let mut futures = Vec::new(); - for disk in heal_disks.into_ref().iter() { - let disk_clone = disk.clone(); - let cancel_clone = cancel_token.clone(); - futures.push(async move { - let disk_for_cancel = disk_clone.clone(); - tokio::select! { - _ = cancel_clone.cancelled() => { - info!("Disk healing task cancelled for disk: {}", disk_for_cancel); - } - _ = async { - GLOBAL_BackgroundHealState - .set_disk_healing_status(disk_clone.clone(), true) - .await; - if heal_fresh_disk(&disk_clone).await.is_err() { - info!("heal_fresh_disk is err"); - GLOBAL_BackgroundHealState - .set_disk_healing_status(disk_clone.clone(), false) - .await; - } - GLOBAL_BackgroundHealState.pop_heal_local_disks(&[disk_clone]).await; - } => {} - } - }); - } - let _ = join_all(futures).await; - interval.reset(); - } - } - } -} - -async fn heal_fresh_disk(endpoint: &Endpoint) -> Result<()> { - let (pool_idx, set_idx) = (endpoint.pool_idx as usize, endpoint.set_idx as usize); - let disk = match get_disk_via_endpoint(endpoint).await { - Some(disk) => disk, - None => { - return Err(Error::other(format!( - "Unexpected error disk must be initialized by now after formatting: {endpoint}" - ))); - } - }; - - if let Err(err) = disk.disk_info(&DiskInfoOptions::default()).await { - match err { - DiskError::DriveIsRoot => { - return Ok(()); - } - DiskError::UnformattedDisk => {} - _ => { - return Err(err.into()); - } - } - } - - let mut tracker = match load_healing_tracker(&Some(disk.clone())).await { - Ok(tracker) => tracker, - Err(err) => { - match err { - DiskError::FileNotFound => { - return Ok(()); - } - _ => { - info!( - "Unable to load healing tracker on '{}': {}, re-initializing..", - disk.to_string(), - err.to_string() - ); - } - } - init_healing_tracker(disk.clone(), &Uuid::new_v4().to_string()).await? - } - }; - - info!( - "Healing drive '{}' - 'mc admin heal alias/ --verbose' to check the current status.", - endpoint.to_string() - ); - - let Some(store) = new_object_layer_fn() else { - return Err(Error::other("errServerNotInitialized")); - }; - - let mut buckets = store.list_bucket(&BucketOptions::default()).await?; - buckets.push(BucketInfo { - name: path_join(&[PathBuf::from(RUSTFS_META_BUCKET), PathBuf::from(RUSTFS_CONFIG_PREFIX)]) - .to_string_lossy() - .to_string(), - ..Default::default() - }); - buckets.push(BucketInfo { - name: path_join(&[PathBuf::from(RUSTFS_META_BUCKET), PathBuf::from(BUCKET_META_PREFIX)]) - .to_string_lossy() - .to_string(), - ..Default::default() - }); - - buckets.sort_by(|a, b| { - let a_has_prefix = a.name.starts_with(RUSTFS_META_BUCKET); - let b_has_prefix = b.name.starts_with(RUSTFS_META_BUCKET); - - match (a_has_prefix, b_has_prefix) { - (true, false) => Ordering::Less, - (false, true) => Ordering::Greater, - _ => b.created.cmp(&a.created), - } - }); - - if let Ok(cache) = DataUsageCache::load(&store.pools[pool_idx].disk_set[set_idx], DATA_USAGE_CACHE_NAME).await { - let data_usage_info = cache.dui(DATA_USAGE_ROOT, &Vec::new()); - tracker.objects_total_count = data_usage_info.objects_total_count; - tracker.objects_total_size = data_usage_info.objects_total_size; - }; - - tracker.set_queue_buckets(&buckets).await; - tracker.save().await?; - - let tracker = Arc::new(RwLock::new(tracker)); - let qb = tracker.read().await.queue_buckets.clone(); - store.pools[pool_idx].disk_set[set_idx] - .clone() - .heal_erasure_set(&qb, tracker.clone()) - .await?; - let mut tracker_w = tracker.write().await; - if tracker_w.items_failed > 0 && tracker_w.retry_attempts < 4 { - tracker_w.retry_attempts += 1; - tracker_w.reset_healing().await; - if let Err(err) = tracker_w.update().await { - info!("update tracker failed: {}", err.to_string()); - } - return Err(Error::other(ERR_RETRY_HEALING)); - } - - if tracker_w.items_failed > 0 { - info!( - "Healing of drive '{}' is incomplete, retried {} times (healed: {}, skipped: {}, failed: {}).", - disk.to_string(), - tracker_w.retry_attempts, - tracker_w.items_healed, - tracker_w.item_skipped, - tracker_w.items_failed - ); - } else if tracker_w.retry_attempts > 0 { - info!( - "Healing of drive '{}' is incomplete, retried {} times (healed: {}, skipped: {}).", - disk.to_string(), - tracker_w.retry_attempts, - tracker_w.items_healed, - tracker_w.item_skipped - ); - } else { - info!( - "Healing of drive '{}' is finished (healed: {}, skipped: {}).", - disk.to_string(), - tracker_w.items_healed, - tracker_w.item_skipped - ); - } - - if tracker_w.heal_id.is_empty() { - if let Err(err) = tracker_w.delete().await { - error!("delete tracker failed: {}", err.to_string()); - } - } - let Some(store) = new_object_layer_fn() else { - return Err(Error::other("errServerNotInitialized")); - }; - let disks = store.get_disks(pool_idx, set_idx).await?; - for disk in disks.into_iter() { - if disk.is_none() { - continue; - } - let mut tracker = match load_healing_tracker(&disk).await { - Ok(tracker) => tracker, - Err(err) => { - match err { - DiskError::FileNotFound => {} - _ => { - info!("Unable to load healing tracker on '{:?}': {}, re-initializing..", disk, err.to_string()); - } - } - continue; - } - }; - if tracker.heal_id == tracker_w.heal_id { - tracker.finished = true; - tracker.update().await?; - } - } - Ok(()) -} - -#[derive(Debug)] -pub struct HealTask { - pub bucket: String, - pub object: String, - pub version_id: String, - pub opts: HealOpts, - pub resp_tx: Option>, - pub resp_rx: Option>, -} - -impl HealTask { - pub fn new(bucket: &str, object: &str, version_id: &str, opts: &HealOpts) -> Self { - Self { - bucket: bucket.to_string(), - object: object.to_string(), - version_id: version_id.to_string(), - opts: *opts, - resp_tx: None, - resp_rx: None, - } - } -} - -#[derive(Debug)] -pub struct HealResult { - pub result: HealResultItem, - pub err: Option, -} - -pub struct HealRoutine { - pub tasks_tx: Sender, - tasks_rx: RwLock>, - workers: usize, -} - -impl HealRoutine { - pub fn new() -> Arc { - let mut workers = num_cpus::get() / 2; - if let Ok(env_heal_workers) = env::var("_RUSTFS_HEAL_WORKERS") { - if let Ok(num_healers) = env_heal_workers.parse::() { - workers = num_healers; - } - } - - if workers == 0 { - workers = 4; - } - - let (tx, rx) = mpsc::channel(100); - Arc::new(Self { - tasks_tx: tx, - tasks_rx: RwLock::new(rx), - workers, - }) - } - - pub async fn add_worker(&self, bgseq: Arc) { - loop { - let mut d_res = HealResultItem::default(); - let d_err: Option; - match self.tasks_rx.write().await.recv().await { - Some(task) => { - info!("got task: {:?}", task); - if task.bucket == NOP_HEAL { - d_err = Some(Error::other("skip file")); - } else if task.bucket == SLASH_SEPARATOR { - match heal_disk_format(task.opts).await { - Ok((res, err)) => { - d_res = res; - d_err = err; - } - Err(err) => d_err = Some(err), - } - } else { - let store = new_object_layer_fn().expect("errServerNotInitialized"); - if task.object.is_empty() { - match store.heal_bucket(&task.bucket, &task.opts).await { - Ok(res) => { - d_res = res; - d_err = None; - } - Err(err) => d_err = Some(err), - } - } else { - match store - .heal_object(&task.bucket, &task.object, &task.version_id, &task.opts) - .await - { - Ok((res, err)) => { - d_res = res; - d_err = err; - } - Err(err) => d_err = Some(err), - } - } - } - info!("task finished, task: {:?}", task); - if let Some(resp_tx) = task.resp_tx { - let _ = resp_tx - .send(HealResult { - result: d_res, - err: d_err, - }) - .await; - } else { - // when respCh is not set caller is not waiting but we - // update the relevant metrics for them - if d_err.is_none() { - bgseq.count_healed(d_res.heal_item_type).await; - } else { - bgseq.count_failed(d_res.heal_item_type).await; - } - } - } - None => { - info!("add_worker, tasks_rx was closed, return"); - return; - } - } - } - } -} - -// pub fn active_listeners() -> Result { - -// } - -async fn heal_disk_format(opts: HealOpts) -> Result<(HealResultItem, Option)> { - let Some(store) = new_object_layer_fn() else { - return Err(Error::other("errServerNotInitialized")); - }; - - let (res, err) = store.heal_format(opts.dry_run).await?; - // return any error, ignore error returned when disks have - // already healed. - if err.is_some() { - return Ok((HealResultItem::default(), err)); - } - Ok((res, err)) -} - -pub(crate) async fn heal_bucket(bucket: &str) -> Result<()> { - let (bg_seq, ok) = GLOBAL_BackgroundHealState.get_heal_sequence_by_token(BG_HEALING_UUID).await; - if ok { - // bg_seq must be Some when ok is true - return bg_seq - .unwrap() - .queue_heal_task( - HealSource { - bucket: bucket.to_string(), - ..Default::default() - }, - HEAL_ITEM_BUCKET.to_string(), - ) - .await; - } - Ok(()) -} - -pub(crate) async fn heal_object(bucket: &str, object: &str, version_id: &str, scan_mode: HealScanMode) -> Result<()> { - let (bg_seq, ok) = GLOBAL_BackgroundHealState.get_heal_sequence_by_token(BG_HEALING_UUID).await; - if ok { - // bg_seq must be Some when ok is true - return HealSequence::heal_object(bg_seq.unwrap(), bucket, object, version_id, scan_mode).await; - } - Ok(()) -} diff --git a/crates/ecstore/src/heal/data_scanner.rs b/crates/ecstore/src/heal/data_scanner.rs deleted file mode 100644 index 98a0924fd..000000000 --- a/crates/ecstore/src/heal/data_scanner.rs +++ /dev/null @@ -1,1826 +0,0 @@ -// 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::{ - collections::{HashMap, HashSet}, - fs, - future::Future, - io::{Cursor, Read}, - path::{Path, PathBuf}, - pin::Pin, - sync::{ - Arc, - atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}, - }, - time::{Duration, SystemTime}, -}; - -use time::{self, OffsetDateTime}; - -use super::{ - data_usage::{DATA_USAGE_BLOOM_NAME_PATH, DataUsageInfo, store_data_usage_in_backend}, - data_usage_cache::{DataUsageCache, DataUsageEntry, DataUsageHash}, - heal_commands::{HEAL_DEEP_SCAN, HEAL_NORMAL_SCAN, HealScanMode}, -}; -use crate::bucket::{ - object_lock::objectlock_sys::{BucketObjectLockSys, enforce_retention_for_deletion}, - utils::is_meta_bucketname, -}; -use crate::cmd::bucket_replication::queue_replication_heal; -use crate::disk::local::LocalDisk; -use crate::event::name::EventName; -use crate::{ - bucket::{ - lifecycle::{ - bucket_lifecycle_audit::LcEventSrc, - bucket_lifecycle_ops::{GLOBAL_ExpiryState, GLOBAL_TransitionState, LifecycleOps, expire_transitioned_object}, - lifecycle::{self, ExpirationOptions, Lifecycle}, - }, - metadata_sys, - }, - event_notification::{EventArgs, send_event}, - global::{GLOBAL_LocalNodeName, get_background_services_cancel_token}, - store_api::{ObjectOptions, ObjectToDelete, StorageAPI}, -}; -use crate::{ - bucket::{versioning::VersioningApi, versioning_sys::BucketVersioningSys}, - cmd::bucket_replication::ReplicationStatusType, - disk, - // heal::data_usage::DATA_USAGE_ROOT, -}; -use crate::{ - cache_value::metacache_set::{ListPathRawOptions, list_path_raw}, - config::{ - com::{read_config, save_config}, - heal::Config, - }, - disk::{DiskInfoOptions, DiskStore}, - global::{GLOBAL_BackgroundHealState, GLOBAL_IsErasureSD}, - heal::{ - data_usage::BACKGROUND_HEAL_INFO_PATH, - data_usage_cache::{DataUsageHashMap, hash_path}, - error::ERR_IGNORE_FILE_CONTRIB, - heal_commands::{HEAL_ITEM_BUCKET, HEAL_ITEM_OBJECT}, - heal_ops::{BG_HEALING_UUID, HealSource}, - }, - new_object_layer_fn, - store::ECStore, - store_utils::is_reserved_or_invalid_bucket, -}; -use crate::{disk::DiskAPI, store_api::ObjectInfo}; -use crate::{ - disk::error::DiskError, - error::{Error, Result}, -}; -use chrono::{DateTime, Utc}; -use lazy_static::lazy_static; -use rand::Rng; -use rmp_serde::{Deserializer, Serializer}; -use rustfs_filemeta::{FileInfo, MetaCacheEntries, MetaCacheEntry, MetadataResolutionParams}; -use rustfs_utils::path::encode_dir_object; -use rustfs_utils::path::{SLASH_SEPARATOR, path_join, path_to_bucket_object, path_to_bucket_object_with_base_path}; -use s3s::dto::{ - BucketLifecycleConfiguration, DefaultRetention, ExpirationStatus, LifecycleRule, ReplicationConfiguration, - ReplicationRuleStatus, -}; -use serde::{Deserialize, Serialize}; -use tokio::{ - sync::{ - RwLock, broadcast, - mpsc::{self, Sender}, - }, - time::sleep, -}; -use tracing::{debug, error, info}; - -const DATA_SCANNER_SLEEP_PER_FOLDER: Duration = Duration::from_millis(1); // Time to wait between folders. -const DATA_USAGE_UPDATE_DIR_CYCLES: u32 = 16; // Visit all folders every n cycles. -const DATA_SCANNER_COMPACT_LEAST_OBJECT: u64 = 500; // Compact when there are less than this many objects in a branch. -const DATA_SCANNER_COMPACT_AT_CHILDREN: u64 = 10000; // Compact when there are this many children in a branch. -const DATA_SCANNER_COMPACT_AT_FOLDERS: u64 = DATA_SCANNER_COMPACT_AT_CHILDREN / 4; // Compact when this many subfolders in a single folder. -pub const DATA_SCANNER_FORCE_COMPACT_AT_FOLDERS: u64 = 250_000; // Compact when this many subfolders in a single folder (even top level). -const DATA_SCANNER_START_DELAY: Duration = Duration::from_secs(60); // Time to wait on startup and between cycles. - -pub const HEAL_DELETE_DANGLING: bool = true; -const HEAL_OBJECT_SELECT_PROB: u64 = 1024; // Overall probability of a file being scanned; one in n. - -static SCANNER_CYCLE: AtomicU64 = AtomicU64::new(DATA_SCANNER_START_DELAY.as_secs()); -static _SCANNER_IDLE_MODE: AtomicU32 = AtomicU32::new(0); // default is throttled when idle -static SCANNER_EXCESS_OBJECT_VERSIONS: AtomicU64 = AtomicU64::new(100); -static SCANNER_EXCESS_OBJECT_VERSIONS_TOTAL_SIZE: AtomicU64 = AtomicU64::new(1024 * 1024 * 1024 * 1024); // 1 TB -static SCANNER_EXCESS_FOLDERS: AtomicU64 = AtomicU64::new(50_000); - -lazy_static! { - static ref SCANNER_SLEEPER: RwLock = RwLock::new(new_dynamic_sleeper(2.0, Duration::from_secs(1), true)); - pub static ref globalHealConfig: Arc> = Arc::new(RwLock::new(Config::default())); -} - -struct DynamicSleeper { - factor: f64, - max_sleep: Duration, - min_sleep: Duration, - _is_scanner: bool, -} - -type TimerFn = Pin + Send>>; -impl DynamicSleeper { - fn timer() -> TimerFn { - let t = SystemTime::now(); - Box::pin(async move { - let done_at = SystemTime::now().duration_since(t).unwrap_or_default(); - SCANNER_SLEEPER.read().await.sleep(done_at).await; - }) - } - - async fn sleep(&self, base: Duration) { - let (min_wait, max_wait) = (self.min_sleep, self.max_sleep); - let factor = self.factor; - - let want_sleep = { - let tmp = base.mul_f64(factor); - if tmp < min_wait { - return; - } - - if max_wait > Duration::from_secs(0) && tmp > max_wait { - max_wait - } else { - tmp - } - }; - sleep(want_sleep).await; - } - - fn _update(&mut self, factor: f64, max_wait: Duration) -> Result<()> { - if (self.factor - factor).abs() < 1e-10 && self.max_sleep == max_wait { - return Ok(()); - } - - self.factor = factor; - self.max_sleep = max_wait; - - Ok(()) - } -} - -fn new_dynamic_sleeper(factor: f64, max_wait: Duration, is_scanner: bool) -> DynamicSleeper { - DynamicSleeper { - factor, - max_sleep: max_wait, - min_sleep: Duration::from_micros(100), - _is_scanner: is_scanner, - } -} - -/// Initialize and start the data scanner in the background -/// -/// This function starts a background task that continuously runs the data scanner -/// with randomized intervals between cycles to avoid resource contention. -/// -/// # Features -/// - Graceful shutdown support via cancellation token -/// - Randomized sleep intervals to prevent synchronized scanning across nodes -/// - Minimum sleep duration to avoid excessive CPU usage -/// - Proper error handling and logging -/// -/// # Architecture -/// 1. Initialize with random seed for sleep intervals -/// 2. Run scanner cycles in a loop -/// 3. Use randomized sleep between cycles to avoid thundering herd -/// 4. Ensure minimum sleep duration to prevent CPU thrashing -pub async fn init_data_scanner() { - info!("Initializing data scanner background task"); - - let Some(cancel_token) = get_background_services_cancel_token() else { - error!("Background services cancel token not initialized"); - return; - }; - - let cancel_clone = cancel_token.clone(); - tokio::spawn(async move { - info!("Data scanner background task started"); - - loop { - tokio::select! { - _ = cancel_clone.cancelled() => { - info!("Data scanner received shutdown signal, exiting gracefully"); - break; - } - _ = run_data_scanner_cycle() => { - // Calculate randomized sleep duration - let random_factor = { - let mut rng = rand::rng(); - rng.random_range(1.0..10.0) - }; - let base_cycle_duration = SCANNER_CYCLE.load(Ordering::SeqCst) as f64; - let sleep_duration_secs = random_factor * base_cycle_duration; - - let sleep_duration = Duration::from_secs_f64(sleep_duration_secs); - - debug!( - duration_secs = sleep_duration.as_secs(), - "Data scanner sleeping before next cycle" - ); - - // Interruptible sleep - tokio::select! { - _ = cancel_clone.cancelled() => { - info!("Data scanner received shutdown signal during sleep, exiting"); - break; - } - _ = sleep(sleep_duration) => { - // Continue to next cycle - } - } - } - } - } - - info!("Data scanner background task stopped gracefully"); - }); -} - -/// Run a single data scanner cycle -/// -/// This function performs one complete scan cycle, including: -/// - Loading and updating cycle information -/// - Determining scan mode based on healing configuration -/// - Running the namespace scanner -/// - Saving cycle completion state -/// -/// # Error Handling -/// - Gracefully handles missing object layer -/// - Continues operation even if individual steps fail -/// - Logs errors appropriately without terminating the scanner -async fn run_data_scanner_cycle() { - debug!("Starting data scanner cycle"); - - // Get the object layer, return early if not available - let Some(store) = new_object_layer_fn() else { - error!("Object layer not initialized, skipping scanner cycle"); - return; - }; - - // Check for cancellation before starting expensive operations - if let Some(token) = get_background_services_cancel_token() { - if token.is_cancelled() { - debug!("Scanner cancelled before starting cycle"); - return; - } - } - - // Load current cycle information from persistent storage - let buf = read_config(store.clone(), &DATA_USAGE_BLOOM_NAME_PATH) - .await - .unwrap_or_else(|err| { - info!(error = %err, "Failed to read cycle info, starting fresh"); - Vec::new() - }); - - let mut cycle_info = if buf.is_empty() { - CurrentScannerCycle::default() - } else { - let mut buf_cursor = Deserializer::new(Cursor::new(buf)); - Deserialize::deserialize(&mut buf_cursor).unwrap_or_else(|err| { - error!(error = %err, "Failed to deserialize cycle info, using default"); - CurrentScannerCycle::default() - }) - }; - - // Start metrics collection for this cycle - // let stop_fn = ScannerMetrics::log(ScannerMetric::ScanCycle); - - // Update cycle information - cycle_info.current = cycle_info.next; - cycle_info.started = Utc::now(); - - // Update global scanner metrics - // globalScannerMetrics.set_cycle(Some(cycle_info.clone())).await; - - // Read background healing information and determine scan mode - let bg_heal_info = read_background_heal_info(store.clone()).await; - let scan_mode = - get_cycle_scan_mode(cycle_info.current, bg_heal_info.bitrot_start_cycle, bg_heal_info.bitrot_start_time).await; - - // Update healing info if scan mode changed - if bg_heal_info.current_scan_mode != scan_mode { - let mut new_heal_info = bg_heal_info; - new_heal_info.current_scan_mode = scan_mode; - if scan_mode == HEAL_DEEP_SCAN { - new_heal_info.bitrot_start_time = SystemTime::now(); - new_heal_info.bitrot_start_cycle = cycle_info.current; - } - save_background_heal_info(store.clone(), &new_heal_info).await; - } - - // Set up data usage storage channel - let (tx, rx) = mpsc::channel::(100); - tokio::spawn(async move { - let _ = store_data_usage_in_backend(rx).await; - }); - - // Prepare result tracking - let mut scan_result = HashMap::new(); - scan_result.insert("cycle".to_string(), cycle_info.current.to_string()); - - info!( - cycle = cycle_info.current, - scan_mode = ?scan_mode, - "Starting namespace scanner" - ); - - // Run the namespace scanner with cancellation support - match execute_namespace_scan(&store, tx, cycle_info.current, scan_mode).await { - Ok(_) => { - info!(cycle = cycle_info.current, "Namespace scanner completed successfully"); - - // Update cycle completion information - cycle_info.next += 1; - cycle_info.current = 0; - cycle_info.cycle_completed.push(Utc::now()); - - // Maintain cycle completion history (keep only recent cycles) - if cycle_info.cycle_completed.len() > DATA_USAGE_UPDATE_DIR_CYCLES as usize { - let _ = cycle_info.cycle_completed.remove(0); - } - - // Update global metrics with completion info - // globalScannerMetrics.set_cycle(Some(cycle_info.clone())).await; - - // Persist updated cycle information - // ignore error, continue. - let mut serialized_data = Vec::new(); - if let Err(err) = cycle_info.serialize(&mut Serializer::new(&mut serialized_data)) { - error!(error = %err, "Failed to serialize cycle info"); - } else if let Err(err) = save_config(store.clone(), &DATA_USAGE_BLOOM_NAME_PATH, serialized_data).await { - error!(error = %err, "Failed to save cycle info to storage"); - } - } - Err(err) => { - error!( - cycle = cycle_info.current, - error = %err, - "Namespace scanner failed" - ); - scan_result.insert("error".to_string(), err.to_string()); - } - } - - // Complete metrics collection for this cycle - // stop_fn(&scan_result); -} - -/// Execute namespace scan with cancellation support -async fn execute_namespace_scan( - store: &Arc, - tx: Sender, - cycle: u64, - scan_mode: HealScanMode, -) -> Result<()> { - let cancel_token = - get_background_services_cancel_token().ok_or_else(|| Error::other("Background services not initialized"))?; - - tokio::select! { - result = store.ns_scanner(tx, cycle as usize, scan_mode) => { - result.map_err(|e| Error::other(format!("Namespace scan failed: {e}"))) - } - _ = cancel_token.cancelled() => { - info!("Namespace scan cancelled"); - Err(Error::other("Scan cancelled")) - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -struct BackgroundHealInfo { - bitrot_start_time: SystemTime, - bitrot_start_cycle: u64, - current_scan_mode: HealScanMode, -} - -impl Default for BackgroundHealInfo { - fn default() -> Self { - Self { - bitrot_start_time: SystemTime::now(), - bitrot_start_cycle: Default::default(), - current_scan_mode: Default::default(), - } - } -} - -async fn read_background_heal_info(store: Arc) -> BackgroundHealInfo { - if *GLOBAL_IsErasureSD.read().await { - return BackgroundHealInfo::default(); - } - - let buf = read_config(store, &BACKGROUND_HEAL_INFO_PATH) - .await - .map_or(Vec::new(), |buf| buf); - if buf.is_empty() { - return BackgroundHealInfo::default(); - } - serde_json::from_slice::(&buf).map_or(BackgroundHealInfo::default(), |b| b) -} - -async fn save_background_heal_info(store: Arc, info: &BackgroundHealInfo) { - if *GLOBAL_IsErasureSD.read().await { - return; - } - let b = match serde_json::to_vec(info) { - Ok(info) => info, - Err(_) => return, - }; - let _ = save_config(store, &BACKGROUND_HEAL_INFO_PATH, b).await; -} - -async fn get_cycle_scan_mode(current_cycle: u64, bitrot_start_cycle: u64, bitrot_start_time: SystemTime) -> HealScanMode { - let bitrot_cycle = globalHealConfig.read().await.bitrot_scan_cycle(); - let v = bitrot_cycle.as_secs_f64(); - if v == -1.0 { - return HEAL_NORMAL_SCAN; - } else if v == 0.0 { - return HEAL_DEEP_SCAN; - } - - if current_cycle - bitrot_start_cycle < HEAL_OBJECT_SELECT_PROB { - return HEAL_DEEP_SCAN; - } - - if SystemTime::now().duration_since(bitrot_start_time).unwrap_or_default() > bitrot_cycle { - return HEAL_DEEP_SCAN; - } - - HEAL_NORMAL_SCAN -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct CurrentScannerCycle { - pub current: u64, - pub next: u64, - pub started: DateTime, - pub cycle_completed: Vec>, -} - -impl Default for CurrentScannerCycle { - fn default() -> Self { - Self { - current: Default::default(), - next: Default::default(), - started: Utc::now(), - cycle_completed: Default::default(), - } - } -} - -impl CurrentScannerCycle { - pub fn marshal_msg(&self, next_buf: &[u8]) -> Result> { - let len: u32 = 4; - let mut wr = Vec::new(); - - // 字段数量 - rmp::encode::write_map_len(&mut wr, len)?; - - // write "current" - rmp::encode::write_str(&mut wr, "current")?; - rmp::encode::write_uint(&mut wr, self.current)?; - - // write "next" - rmp::encode::write_str(&mut wr, "next")?; - rmp::encode::write_uint(&mut wr, self.next)?; - - // write "started" - rmp::encode::write_str(&mut wr, "started")?; - rmp::encode::write_sint(&mut wr, system_time_to_timestamp(&self.started))?; - - // write "cycle_completed" - rmp::encode::write_str(&mut wr, "cycle_completed")?; - let mut buf = Vec::new(); - self.cycle_completed - .serialize(&mut Serializer::new(&mut buf)) - .expect("Serialization failed"); - rmp::encode::write_bin(&mut wr, &buf)?; - let mut result = next_buf.to_vec(); - result.extend(wr.iter()); - Ok(result) - } - - pub fn unmarshal_msg(&mut self, buf: &[u8]) -> Result { - let mut cur = Cursor::new(buf); - - let mut fields_len = rmp::decode::read_map_len(&mut cur)?; - - while fields_len > 0 { - fields_len -= 1; - - let str_len = rmp::decode::read_str_len(&mut cur)?; - - // !!!Vec::with_capacity(str_len) 失败,vec! 正常 - let mut field_buff = vec![0u8; str_len as usize]; - - cur.read_exact(&mut field_buff)?; - - let field = String::from_utf8(field_buff)?; - - match field.as_str() { - "current" => { - let u: u64 = rmp::decode::read_int(&mut cur)?; - self.current = u; - } - - // "next" => { - // let u: u64 = rmp::decode::read_int(&mut cur)?; - // self.next = u; - // } - "started" => { - let u: i64 = rmp::decode::read_int(&mut cur)?; - let started = timestamp_to_system_time(u); - self.started = started; - } - "cycleCompleted" => { - let mut buf = Vec::new(); - let _ = cur.read_to_end(&mut buf)?; - let u: Vec> = - Deserialize::deserialize(&mut Deserializer::new(&buf[..])).expect("Deserialization failed"); - self.cycle_completed = u; - } - name => return Err(Error::other(format!("not support field name {name}"))), - } - } - - Ok(cur.position()) - } -} - -// Convert `SystemTime` to timestamp -fn system_time_to_timestamp(time: &DateTime) -> i64 { - time.timestamp_micros() -} - -// Convert timestamp to `SystemTime` -fn timestamp_to_system_time(timestamp: i64) -> DateTime { - DateTime::from_timestamp_micros(timestamp).unwrap_or_default() -} - -#[derive(Clone, Debug, Default)] -pub struct Heal { - enabled: bool, - bitrot: bool, -} - -#[derive(Clone)] -pub struct ScannerItem { - pub path: String, - pub bucket: String, - pub prefix: String, - pub object_name: String, - pub replication: Option, - pub lifecycle: Option, - // typ: fs::Permissions, - pub heal: Heal, - pub debug: bool, -} - -impl ScannerItem { - pub fn transform_meta_dir(&mut self) { - let split = self.prefix.split(SLASH_SEPARATOR).map(PathBuf::from).collect::>(); - if split.len() > 1 { - self.prefix = path_join(&split[0..split.len() - 1]).to_string_lossy().to_string(); - } else { - self.prefix = "".to_string(); - } - self.object_name = split.last().map_or("".to_string(), |v| v.to_string_lossy().to_string()); - } - - pub fn object_path(&self) -> PathBuf { - path_join(&[PathBuf::from(self.prefix.clone()), PathBuf::from(self.object_name.clone())]) - } - - async fn apply_lifecycle(&self, oi: &ObjectInfo) -> (lifecycle::IlmAction, i64) { - let mut size = oi.get_actual_size().expect("err!"); - if self.debug { - info!("apply_lifecycle debug"); - } - if self.lifecycle.is_none() { - return (lifecycle::IlmAction::NoneAction, size); - } - - let version_id = oi.version_id; - - let mut vc = None; - let mut lr = None; - let mut rcfg = None; - if !is_meta_bucketname(&self.bucket) { - vc = Some(BucketVersioningSys::get(&self.bucket).await.unwrap()); - lr = BucketObjectLockSys::get(&self.bucket).await; - rcfg = (metadata_sys::get_replication_config(&self.bucket).await).ok(); - } - - let lc_evt = eval_action_from_lifecycle(self.lifecycle.as_ref().expect("err"), lr, rcfg, oi).await; - if self.debug { - if version_id.is_some() { - info!( - "lifecycle: {} (version-id={}), Initial scan: {}", - self.object_path().to_string_lossy().to_string(), - version_id.expect("err"), - lc_evt.action - ); - } else { - info!( - "lifecycle: {} Initial scan: {}", - self.object_path().to_string_lossy().to_string(), - lc_evt.action - ); - } - } - - match lc_evt.action { - lifecycle::IlmAction::DeleteVersionAction - | lifecycle::IlmAction::DeleteAllVersionsAction - | lifecycle::IlmAction::DelMarkerDeleteAllVersionsAction => { - size = 0; - } - lifecycle::IlmAction::DeleteAction => { - if !vc.unwrap().prefix_enabled(&oi.name) { - size = 0 - } - } - _ => (), - } - - apply_lifecycle_action(&lc_evt, &LcEventSrc::Scanner, oi).await; - (lc_evt.action, size) - } - - pub async fn apply_versions_actions(&self, fivs: &[FileInfo]) -> Result> { - let obj_infos = self.apply_newer_noncurrent_version_limit(fivs).await?; - if obj_infos.len() >= SCANNER_EXCESS_OBJECT_VERSIONS.load(Ordering::SeqCst) as usize { - // todo - } - - let mut cumulative_size = 0; - for obj_info in obj_infos.iter() { - cumulative_size += obj_info.size; - } - - if cumulative_size >= SCANNER_EXCESS_OBJECT_VERSIONS_TOTAL_SIZE.load(Ordering::SeqCst) as i64 { - //todo - } - - Ok(obj_infos) - } - - pub async fn apply_newer_noncurrent_version_limit(&self, fivs: &[FileInfo]) -> Result> { - // let done = ScannerMetrics::time(ScannerMetric::ApplyNonCurrent); - - let lock_enabled = if let Some(rcfg) = BucketObjectLockSys::get(&self.bucket).await { - rcfg.mode.is_some() - } else { - false - }; - let _vcfg = BucketVersioningSys::get(&self.bucket).await?; - - let versioned = match BucketVersioningSys::get(&self.bucket).await { - Ok(vcfg) => vcfg.versioned(self.object_path().to_str().unwrap_or_default()), - Err(_) => false, - }; - let mut object_infos = Vec::with_capacity(fivs.len()); - - if self.lifecycle.is_none() { - for info in fivs.iter() { - object_infos.push(ObjectInfo::from_file_info( - info, - &self.bucket, - &self.object_path().to_string_lossy(), - versioned, - )); - } - return Ok(object_infos); - } - - let event = self - .lifecycle - .as_ref() - .expect("lifecycle err.") - .noncurrent_versions_expiration_limit(&lifecycle::ObjectOpts { - name: self.object_path().to_string_lossy().to_string(), - ..Default::default() - }) - .await; - let lim = event.newer_noncurrent_versions; - if lim == 0 || fivs.len() <= lim + 1 { - for fi in fivs.iter() { - object_infos.push(ObjectInfo::from_file_info( - fi, - &self.bucket, - &self.object_path().to_string_lossy(), - versioned, - )); - } - return Ok(object_infos); - } - - let overflow_versions = &fivs[lim + 1..]; - for fi in fivs[..lim + 1].iter() { - object_infos.push(ObjectInfo::from_file_info( - fi, - &self.bucket, - &self.object_path().to_string_lossy(), - versioned, - )); - } - - let mut to_del = Vec::::with_capacity(overflow_versions.len()); - for fi in overflow_versions.iter() { - let obj = ObjectInfo::from_file_info(fi, &self.bucket, &self.object_path().to_string_lossy(), versioned); - if lock_enabled && enforce_retention_for_deletion(&obj) { - //if enforce_retention_for_deletion(&obj) { - if self.debug { - if obj.version_id.is_some() { - info!("lifecycle: {} v({}) is locked, not deleting\n", obj.name, obj.version_id.expect("err")); - } else { - info!("lifecycle: {} is locked, not deleting\n", obj.name); - } - } - object_infos.push(obj); - continue; - } - - if OffsetDateTime::now_utc().unix_timestamp() - < lifecycle::expected_expiry_time(obj.successor_mod_time.expect("err"), event.noncurrent_days as i32) - .unix_timestamp() - { - object_infos.push(obj); - continue; - } - - to_del.push(ObjectToDelete { - object_name: obj.name, - version_id: obj.version_id, - }); - } - - if !to_del.is_empty() { - let mut expiry_state = GLOBAL_ExpiryState.write().await; - expiry_state.enqueue_by_newer_noncurrent(&self.bucket, to_del, event).await; - } - // done().await; - - Ok(object_infos) - } - - pub async fn apply_actions(&mut self, oi: &ObjectInfo, _size_s: &mut SizeSummary) -> (bool, i64) { - // let done = ScannerMetrics::time(ScannerMetric::Ilm); - - let (action, _size) = self.apply_lifecycle(oi).await; - - info!( - "apply_actions {} {} {:?} {:?}", - oi.bucket.clone(), - oi.name.clone(), - oi.version_id.clone(), - oi.user_defined.clone() - ); - - // Create a mutable clone if you need to modify fields - let mut oi = oi.clone(); - - let versioned = BucketVersioningSys::prefix_enabled(&oi.bucket, &oi.name).await; - if versioned { - oi.replication_status = ReplicationStatusType::from( - oi.user_defined - .get("x-amz-bucket-replication-status") - .unwrap_or(&"PENDING".to_string()), - ); - debug!("apply status is: {:?}", oi.replication_status); - self.heal_replication(&oi, _size_s).await; - } - - // done(); - - if action.delete_all() { - return (true, 0); - } - - (false, oi.size) - } - - pub async fn heal_replication(&mut self, oi: &ObjectInfo, size_s: &mut SizeSummary) { - if oi.version_id.is_none() { - error!( - "heal_replication: no version_id or replication config {} {} {}", - oi.bucket, - oi.name, - oi.version_id.is_none() - ); - return; - } - - //let config = s3s::dto::ReplicationConfiguration{ role: todo!(), rules: todo!() }; - // Use the provided variable instead of borrowing self mutably. - let replication = match metadata_sys::get_replication_config(&oi.bucket).await { - Ok((replication, _)) => replication, - Err(_) => { - error!( - "heal_replication: failed to get replication config for bucket: {} and object name: {}", - oi.bucket, oi.name - ); - return; - } - }; - if replication.rules.is_empty() { - error!("heal_replication: no replication rules for bucket {} {}", oi.bucket, oi.name); - return; - } - if replication.role.is_empty() { - // error!("heal_replication: no replication role for bucket {} {}", oi.bucket, oi.name); - // return; - } - - //if oi.delete_marker || !oi.version_purge_status.is_empty() { - if oi.delete_marker { - error!( - "heal_replication: delete marker or version purge status {} {} {:?} {} {:?}", - oi.bucket, oi.name, oi.version_id, oi.delete_marker, oi.version_purge_status - ); - return; - } - - if oi.replication_status == ReplicationStatusType::Completed { - return; - } - - info!("replication status is: {:?} and user define {:?}", oi.replication_status, oi.user_defined); - - let roi = queue_replication_heal(&oi.bucket, oi, &replication, 3).await; - - if roi.is_none() { - info!("not need heal {} {} {:?}", oi.bucket, oi.name, oi.version_id); - return; - } - - for (arn, tgt_status) in &roi.unwrap().target_statuses { - let tgt_size_s = size_s.repl_target_stats.entry(arn.clone()).or_default(); - - match tgt_status { - ReplicationStatusType::Pending => { - tgt_size_s.pending_count += 1; - tgt_size_s.pending_size += oi.size as usize; - size_s.pending_count += 1; - size_s.pending_size += oi.size as usize; - } - ReplicationStatusType::Failed => { - tgt_size_s.failed_count += 1; - tgt_size_s.failed_size += oi.size as usize; - size_s.failed_count += 1; - size_s.failed_size += oi.size as usize; - } - ReplicationStatusType::Completed | ReplicationStatusType::CompletedLegacy => { - tgt_size_s.replicated_count += 1; - tgt_size_s.replicated_size += oi.size as usize; - size_s.replicated_count += 1; - size_s.replicated_size += oi.size as usize; - } - _ => {} - } - } - - if matches!(oi.replication_status, ReplicationStatusType::Replica) { - size_s.replica_count += 1; - size_s.replica_size += oi.size as usize; - } - } -} - -#[derive(Debug, Default)] -pub struct SizeSummary { - pub total_size: usize, - pub versions: usize, - pub delete_markers: usize, - pub replicated_size: usize, - pub replicated_count: usize, - pub pending_size: usize, - pub failed_size: usize, - pub replica_size: usize, - pub replica_count: usize, - pub pending_count: usize, - pub failed_count: usize, - pub repl_target_stats: HashMap, - // Todo: tires -} - -#[derive(Debug, Default)] -pub struct ReplTargetSizeSummary { - pub replicated_size: usize, - pub replicated_count: usize, - pub pending_size: usize, - pub failed_size: usize, - pub pending_count: usize, - pub failed_count: usize, -} - -#[derive(Debug, Clone)] -struct CachedFolder { - name: String, - parent: DataUsageHash, - object_heal_prob_div: u32, -} - -pub type GetSizeFn = - Box Pin> + Send>> + Send + Sync + 'static>; -pub type UpdateCurrentPathFn = Arc Pin + Send>> + Send + Sync + 'static>; -pub type ShouldSleepFn = Option bool + Send + Sync + 'static>>; - -struct FolderScanner { - root: String, - get_size: GetSizeFn, - old_cache: DataUsageCache, - new_cache: DataUsageCache, - update_cache: DataUsageCache, - data_usage_scanner_debug: bool, - heal_object_select: u32, - scan_mode: HealScanMode, - disks: Vec>, - disks_quorum: usize, - updates: Sender, - last_update: SystemTime, - update_current_path: UpdateCurrentPathFn, - skip_heal: AtomicBool, - drive: LocalDrive, - we_sleep: ShouldSleepFn, -} - -impl FolderScanner { - async fn should_heal(&self) -> bool { - if self.skip_heal.load(Ordering::SeqCst) { - return false; - } - if self.heal_object_select == 0 { - return false; - } - if let Ok(info) = self.drive.disk_info(&DiskInfoOptions::default()).await { - if info.healing { - self.skip_heal.store(true, Ordering::SeqCst); - return false; - } - } - true - } - - #[tracing::instrument(level = "info", skip_all)] - async fn scan_folder(&mut self, folder: &CachedFolder, into: &mut DataUsageEntry) -> Result<()> { - let this_hash = hash_path(&folder.name); - let was_compacted = into.compacted; - - 'outer: { - let mut abandoned_children: DataUsageHashMap = if !into.compacted { - self.old_cache.find_children_copy(this_hash.clone()) - } else { - HashSet::new() - }; - - let (_, prefix) = path_to_bucket_object_with_base_path(&self.root, &folder.name); - // Todo: lifeCycle - let active_life_cycle = if let Some(lc) = self.old_cache.info.lifecycle.as_ref() { - if lc_has_active_rules(lc, &prefix) { - self.old_cache.info.lifecycle.clone() - } else { - None - } - } else { - None - }; - - let replication_cfg = if self.old_cache.info.replication.is_some() - && rep_has_active_rules(self.old_cache.info.replication.as_ref().unwrap(), &prefix, true) - { - self.old_cache.info.replication.clone() - } else { - None - }; - - if let Some(should_sleep) = &self.we_sleep { - if should_sleep() { - SCANNER_SLEEPER.read().await.sleep(DATA_SCANNER_SLEEP_PER_FOLDER).await; - } - } - - let mut existing_folders = Vec::new(); - let mut new_folders = Vec::new(); - let mut found_objects: bool = false; - - let path = Path::new(&self.root).join(&folder.name); - if path.is_dir() { - for entry in fs::read_dir(path)? { - let entry = entry?; - let sub_path = entry.path(); - let ent_name = Path::new(&folder.name).join(&sub_path); - let (bucket, prefix) = path_to_bucket_object_with_base_path(&self.root, ent_name.to_str().unwrap()); - if bucket.is_empty() { - continue; - } - if is_reserved_or_invalid_bucket(&bucket, false) { - continue; - } - - if sub_path.is_dir() { - let h = hash_path(ent_name.to_str().unwrap()); - if h == this_hash { - continue; - } - let this = CachedFolder { - name: ent_name.to_string_lossy().to_string(), - parent: this_hash.clone(), - object_heal_prob_div: folder.object_heal_prob_div, - }; - abandoned_children.remove(&h.key()); - if self.old_cache.cache.contains_key(&h.key()) { - existing_folders.push(this); - self.update_cache - .copy_with_children(&self.old_cache, &h, &Some(this_hash.clone())); - } else { - new_folders.push(this); - } - continue; - } - - let _wait = if let Some(should_sleep) = &self.we_sleep { - if should_sleep() { - DynamicSleeper::timer() - } else { - Box::pin(async {}) - } - } else { - Box::pin(async {}) - }; - - let mut item = ScannerItem { - path: Path::new(&self.root).join(&ent_name).to_string_lossy().to_string(), - bucket, - prefix: Path::new(&prefix) - .parent() - .unwrap_or(Path::new("")) - .to_string_lossy() - .to_string(), - object_name: ent_name - .file_name() - .map(|name| name.to_string_lossy().into_owned()) - .unwrap_or_default(), - debug: self.data_usage_scanner_debug, - replication: replication_cfg.clone(), - lifecycle: active_life_cycle.clone(), - heal: Heal::default(), - }; - - item.heal.enabled = this_hash.mod_alt( - self.old_cache.info.next_cycle / folder.object_heal_prob_div, - self.heal_object_select / folder.object_heal_prob_div, - ) && self.should_heal().await; - item.heal.bitrot = self.scan_mode == HEAL_DEEP_SCAN; - - let (sz, err) = match (self.get_size)(&item).await { - Ok(sz) => (sz, None), - Err(err) => { - if err.to_string() != ERR_IGNORE_FILE_CONTRIB { - continue; - } - (SizeSummary::default(), Some(err)) - } - }; - // successfully read means we have a valid object. - found_objects = true; - // Remove filename i.e is the meta file to construct object name - item.transform_meta_dir(); - // Object already accounted for, remove from heal map, - // simply because getSize() function already heals the - // object. - abandoned_children.remove( - &path_join(&[PathBuf::from(item.bucket.clone()), item.object_path()]) - .to_string_lossy() - .to_string(), - ); - - if err.is_none() || err.unwrap().to_string() != ERR_IGNORE_FILE_CONTRIB { - into.add_sizes(&sz); - into.objects += 1; - } - } - } - // if found_objects && *GLOBAL_IsErasure.read().await { - if found_objects { - break 'outer; - } - - let should_compact = self.new_cache.info.name != folder.name - && (existing_folders.len() + new_folders.len() >= DATA_SCANNER_COMPACT_AT_FOLDERS as usize - || existing_folders.len() + new_folders.len() >= DATA_SCANNER_FORCE_COMPACT_AT_FOLDERS as usize); - - let total_folders = existing_folders.len() + new_folders.len(); - if total_folders > SCANNER_EXCESS_FOLDERS.load(Ordering::SeqCst) as usize { - let _prefix_name = format!("{}/", folder.name.trim_end_matches('/')); - // todo: notification - } - - if !into.compacted && should_compact { - into.compacted = true; - new_folders.extend(existing_folders.clone()); - existing_folders.clear(); - } - - // Transfer existing - if !into.compacted { - for folder in existing_folders.iter() { - let h = hash_path(&folder.name); - self.update_cache - .copy_with_children(&self.old_cache, &h, &Some(folder.parent.clone())); - } - } - - // Scan new... - for folder in new_folders.iter() { - let h = hash_path(&folder.name); - if !into.compacted { - let mut found_any = false; - let mut parent = this_hash.clone(); - while parent != hash_path(&self.update_cache.info.name) { - let e = self.update_cache.find(&parent.key()); - if e.is_none() || e.as_ref().unwrap().compacted { - found_any = true; - break; - } - match self.update_cache.search_parent(&parent) { - Some(next) => { - parent = next; - } - None => { - found_any = true; - break; - } - } - } - if !found_any { - self.update_cache - .replace_hashed(&h, &Some(this_hash.clone()), &DataUsageEntry::default()); - } - } - (self.update_current_path)(&folder.name).await; - scan(folder, into, self).await; - // Add new folders if this is new and we don't have existing. - if !into.compacted { - if let Some(parent) = self.update_cache.find(&this_hash.key()) { - if !parent.compacted { - self.update_cache.delete_recursive(&h); - self.update_cache - .copy_with_children(&self.new_cache, &h, &Some(this_hash.clone())); - } - } - } - } - - // Scan existing... - for folder in existing_folders.iter() { - let h = hash_path(&folder.name); - if !into.compacted - && self.old_cache.is_compacted(&h) - && !h.mod_(self.old_cache.info.next_cycle, DATA_USAGE_UPDATE_DIR_CYCLES) - { - self.new_cache - .copy_with_children(&self.old_cache, &h, &Some(folder.parent.clone())); - into.add_child(&h); - continue; - } - (self.update_current_path)(&folder.name).await; - scan(folder, into, self).await; - } - - // Scan for healing - if abandoned_children.is_empty() || !self.should_heal().await { - break 'outer; - } - - if self.disks.is_empty() || self.disks_quorum == 0 { - break 'outer; - } - - let (bg_seq, found) = GLOBAL_BackgroundHealState.get_heal_sequence_by_token(BG_HEALING_UUID).await; - if !found { - break 'outer; - } - let bg_seq = bg_seq.unwrap(); - - let mut resolver = MetadataResolutionParams { - dir_quorum: self.disks_quorum, - obj_quorum: self.disks_quorum, - bucket: "".to_string(), - strict: false, - ..Default::default() - }; - - for k in abandoned_children.iter() { - if !self.should_heal().await { - break; - } - - let (bucket, prefix) = path_to_bucket_object(k); - (self.update_current_path)(k).await; - - if bucket != resolver.bucket { - bg_seq - .clone() - .queue_heal_task( - HealSource { - bucket: bucket.clone(), - ..Default::default() - }, - HEAL_ITEM_BUCKET.to_owned(), - ) - .await?; - } - - resolver.bucket = bucket.clone(); - let found_objs = Arc::new(RwLock::new(false)); - let found_objs_clone = found_objs.clone(); - let (tx, _rx) = broadcast::channel(1); - // let tx_partial = tx.clone(); - let tx_finished = tx.clone(); - let update_current_path_agreed = self.update_current_path.clone(); - let update_current_path_partial = self.update_current_path.clone(); - let resolver_clone = resolver.clone(); - let bg_seq_clone = bg_seq.clone(); - let lopts = ListPathRawOptions { - disks: self.disks.clone(), - bucket: bucket.clone(), - path: prefix.clone(), - recursive: true, - report_not_found: true, - min_disks: self.disks_quorum, - agreed: Some(Box::new(move |entry: MetaCacheEntry| { - Box::pin({ - let update_current_path_agreed = update_current_path_agreed.clone(); - async move { - update_current_path_agreed(&entry.name).await; - } - }) - })), - partial: Some(Box::new(move |entries: MetaCacheEntries, _: &[Option]| { - Box::pin({ - let update_current_path_partial = update_current_path_partial.clone(); - // let tx_partial = tx_partial.clone(); - let resolver_partial = resolver_clone.clone(); - let bucket_partial = bucket.clone(); - let found_objs_clone = found_objs_clone.clone(); - let bg_seq_partial = bg_seq_clone.clone(); - async move { - // Todo - // if !fs.should_heal().await { - // let _ = tx_partial.send(true); - // return; - // } - let entry = match entries.resolve(resolver_partial) { - Some(entry) => entry, - _ => match entries.first_found() { - (Some(entry), _) => entry, - _ => return, - }, - }; - - update_current_path_partial(&entry.name).await; - let mut custom = HashMap::new(); - if entry.is_dir() { - return; - } - - // We got an entry which we should be able to heal. - let fiv = match entry.file_info_versions(&bucket_partial) { - Ok(fiv) => fiv, - Err(_) => { - if let Err(err) = bg_seq_partial - .queue_heal_task( - HealSource { - bucket: bucket_partial.clone(), - object: entry.name.clone(), - version_id: "".to_string(), - ..Default::default() - }, - HEAL_ITEM_OBJECT.to_string(), - ) - .await - { - match err { - Error::FileNotFound | Error::FileVersionNotFound => {} - _ => { - info!("{}", err.to_string()); - } - } - } else { - let mut w = found_objs_clone.write().await; - *w = true; - } - return; - } - }; - - custom.insert("versions", fiv.versions.len().to_string()); - let (mut success_versions, mut fail_versions) = (0, 0); - for ver in fiv.versions.iter() { - match bg_seq_partial - .queue_heal_task( - HealSource { - bucket: bucket_partial.clone(), - object: fiv.name.clone(), - version_id: ver.version_id.map_or("".to_string(), |ver_id| ver_id.to_string()), - ..Default::default() - }, - HEAL_ITEM_OBJECT.to_string(), - ) - .await - { - Ok(_) => { - success_versions += 1; - - let mut w = found_objs_clone.write().await; - *w = true; - } - Err(_) => { - fail_versions += 1; - } - } - } - custom.insert("success_versions", success_versions.to_string()); - custom.insert("failed_versions", fail_versions.to_string()); - } - }) - })), - finished: Some(Box::new(move |_: &[Option]| { - Box::pin({ - let tx_finished = tx_finished.clone(); - async move { - let _ = tx_finished.send(true); - } - }) - })), - ..Default::default() - }; - let _ = list_path_raw(lopts).await; - - if *found_objs.read().await { - let this: CachedFolder = CachedFolder { - name: k.clone(), - parent: this_hash.clone(), - object_heal_prob_div: 1, - }; - scan(&this, into, self).await; - } - } - } - if !was_compacted { - self.new_cache.replace_hashed(&this_hash, &Some(folder.parent.clone()), into); - } - - if !into.compacted && self.new_cache.info.name != folder.name { - let mut flat = self.new_cache.size_recursive(&this_hash.key()).unwrap_or_default(); - flat.compacted = true; - let compact = if flat.objects < DATA_SCANNER_COMPACT_LEAST_OBJECT as usize { - true - } else { - // Compact if we only have objects as children... - let mut compact = true; - for k in into.children.iter() { - if let Some(v) = self.new_cache.cache.get(k) { - if !v.children.is_empty() || v.objects > 1 { - compact = false; - break; - } - } - } - compact - }; - if compact { - self.new_cache.delete_recursive(&this_hash); - self.new_cache.replace_hashed(&this_hash, &Some(folder.parent.clone()), &flat); - let mut total: HashMap = HashMap::new(); - total.insert("objects".to_string(), flat.objects.to_string()); - total.insert("size".to_string(), flat.size.to_string()); - if flat.versions > 0 { - total.insert("versions".to_string(), flat.versions.to_string()); - } - } - } - // Compact if too many children... - if !into.compacted { - self.new_cache.reduce_children_of( - &this_hash, - DATA_SCANNER_COMPACT_AT_CHILDREN as usize, - self.new_cache.info.name != folder.name, - ); - } - if self.update_cache.cache.contains_key(&this_hash.key()) && !was_compacted { - // Replace if existed before. - if let Some(flat) = self.new_cache.size_recursive(&this_hash.key()) { - self.update_cache.delete_recursive(&this_hash); - self.update_cache - .replace_hashed(&this_hash, &Some(folder.parent.clone()), &flat); - } - } - Ok(()) - } - - #[tracing::instrument(level = "info", skip_all)] - async fn send_update(&mut self) { - if SystemTime::now().duration_since(self.last_update).unwrap() < Duration::from_secs(60) { - return; - } - if let Some(flat) = self.update_cache.size_recursive(&self.new_cache.info.name) { - let _ = self.updates.send(flat).await; - self.last_update = SystemTime::now(); - } - } -} - -#[tracing::instrument(level = "info", skip(into, folder_scanner))] -async fn scan(folder: &CachedFolder, into: &mut DataUsageEntry, folder_scanner: &mut FolderScanner) { - let mut dst = if !into.compacted { - DataUsageEntry::default() - } else { - into.clone() - }; - - if Box::pin(folder_scanner.scan_folder(folder, &mut dst)).await.is_err() { - return; - } - if !into.compacted { - let h = DataUsageHash(folder.name.clone()); - into.add_child(&h); - folder_scanner.update_cache.delete_recursive(&h); - folder_scanner - .update_cache - .copy_with_children(&folder_scanner.new_cache, &h, &Some(folder.parent.clone())); - folder_scanner.send_update().await; - } -} - -fn lc_get_prefix(rule: &LifecycleRule) -> String { - if let Some(p) = &rule.prefix { - return p.to_string(); - } else if let Some(filter) = &rule.filter { - if let Some(p) = &filter.prefix { - return p.to_string(); - } else if let Some(and) = &filter.and { - if let Some(p) = &and.prefix { - return p.to_string(); - } - } - } - - "".into() -} - -pub fn lc_has_active_rules(config: &BucketLifecycleConfiguration, prefix: &str) -> bool { - if config.rules.is_empty() { - return false; - } - - for rule in config.rules.iter() { - if rule.status == ExpirationStatus::from_static(ExpirationStatus::DISABLED) { - continue; - } - let rule_prefix = lc_get_prefix(rule); - if !prefix.is_empty() && !rule_prefix.is_empty() && !prefix.starts_with(&rule_prefix) && !rule_prefix.starts_with(prefix) - { - continue; - } - - if let Some(e) = &rule.noncurrent_version_expiration { - if let Some(true) = e.noncurrent_days.map(|d| d > 0) { - return true; - } - if let Some(true) = e.newer_noncurrent_versions.map(|d| d > 0) { - return true; - } - } - - if rule.noncurrent_version_transitions.is_some() { - return true; - } - if let Some(true) = rule.expiration.as_ref().map(|e| e.date.is_some()) { - return true; - } - - if let Some(true) = rule.expiration.as_ref().map(|e| e.days.is_some()) { - return true; - } - - if let Some(Some(true)) = rule.expiration.as_ref().map(|e| e.expired_object_delete_marker) { - return true; - } - - if let Some(true) = rule.transitions.as_ref().map(|t| !t.is_empty()) { - return true; - } - - if rule.transitions.is_some() { - return true; - } - } - false -} - -pub fn rep_has_active_rules(config: &ReplicationConfiguration, prefix: &str, recursive: bool) -> bool { - if config.rules.is_empty() { - return false; - } - - for rule in config.rules.iter() { - if rule - .status - .eq(&ReplicationRuleStatus::from_static(ReplicationRuleStatus::DISABLED)) - { - continue; - } - if !prefix.is_empty() { - if let Some(filter) = &rule.filter { - if let Some(r_prefix) = &filter.prefix { - if !r_prefix.is_empty() { - // incoming prefix must be in rule prefix - if !recursive && !prefix.starts_with(r_prefix) { - continue; - } - // If recursive, we can skip this rule if it doesn't match the tested prefix or level below prefix - // does not match - if recursive && !r_prefix.starts_with(prefix) && !prefix.starts_with(r_prefix) { - continue; - } - } - } - } - } - return true; - } - false -} - -pub type LocalDrive = Arc; -pub async fn scan_data_folder( - _disks: &[Option], - _drive: LocalDrive, - _cache: &DataUsageCache, - _get_size_fn: GetSizeFn, - _heal_scan_mode: HealScanMode, - _should_sleep: ShouldSleepFn, -) -> disk::error::Result { - // if cache.info.name.is_empty() || cache.info.name == DATA_USAGE_ROOT { - // return Err(DiskError::other("internal error: root scan attempted")); - // } - - // let base_path = drive.to_string(); - // // let (update_path, close_disk) = current_path_updater(&base_path, &cache.info.name); - // let skip_heal = if *GLOBAL_IsErasure.read().await || cache.info.skip_healing { - // AtomicBool::new(true) - // } else { - // AtomicBool::new(false) - // }; - // let mut s = FolderScanner { - // root: base_path, - // get_size: get_size_fn, - // old_cache: cache.clone(), - // new_cache: DataUsageCache { - // info: cache.info.clone(), - // ..Default::default() - // }, - // update_cache: DataUsageCache { - // info: cache.info.clone(), - // ..Default::default() - // }, - // data_usage_scanner_debug: false, - // heal_object_select: 0, - // scan_mode: heal_scan_mode, - // updates: cache.info.updates.clone().unwrap(), - // last_update: SystemTime::now(), - // update_current_path: update_path, - // disks: disks.to_vec(), - // disks_quorum: disks.len() / 2, - // skip_heal, - // drive: drive.clone(), - // we_sleep: should_sleep, - // }; - - // if *GLOBAL_IsErasure.read().await || !cache.info.skip_healing { - // s.heal_object_select = HEAL_OBJECT_SELECT_PROB as u32; - // } - - // let mut root = DataUsageEntry::default(); - // let folder = CachedFolder { - // name: cache.info.name.clone(), - // object_heal_prob_div: 1, - // parent: DataUsageHash("".to_string()), - // }; - - // if s.scan_folder(&folder, &mut root).await.is_err() { - // close_disk().await; - // } - // s.new_cache.force_compact(DATA_SCANNER_COMPACT_AT_CHILDREN as usize); - // s.new_cache.info.last_update = Some(SystemTime::now()); - // s.new_cache.info.next_cycle = cache.info.next_cycle; - // close_disk().await; - // Ok(s.new_cache) - todo!() -} - -pub async fn eval_action_from_lifecycle( - lc: &BucketLifecycleConfiguration, - lr: Option, - rcfg: Option<(ReplicationConfiguration, OffsetDateTime)>, - oi: &ObjectInfo, -) -> lifecycle::Event { - let event = lc.eval(&oi.to_lifecycle_opts()).await; - //if serverDebugLog { - info!("lifecycle: Secondary scan: {}", event.action); - //} - - let lock_enabled = if let Some(lr) = lr { lr.mode.is_some() } else { false }; - - match event.action { - lifecycle::IlmAction::DeleteAllVersionsAction | lifecycle::IlmAction::DelMarkerDeleteAllVersionsAction => { - if lock_enabled { - return lifecycle::Event::default(); - } - } - lifecycle::IlmAction::DeleteVersionAction | lifecycle::IlmAction::DeleteRestoredVersionAction => { - if oi.version_id.is_none() { - return lifecycle::Event::default(); - } - if lock_enabled && enforce_retention_for_deletion(oi) { - //if serverDebugLog { - if oi.version_id.is_some() { - info!("lifecycle: {} v({}) is locked, not deleting", oi.name, oi.version_id.expect("err")); - } else { - info!("lifecycle: {} is locked, not deleting", oi.name); - } - //} - return lifecycle::Event::default(); - } - if let Some(rcfg) = rcfg { - if rep_has_active_rules(&rcfg.0, &oi.name, true) { - return lifecycle::Event::default(); - } - } - } - _ => (), - } - - event -} - -async fn apply_transition_rule(event: &lifecycle::Event, src: &LcEventSrc, oi: &ObjectInfo) -> bool { - if oi.delete_marker || oi.is_dir { - return false; - } - GLOBAL_TransitionState.queue_transition_task(oi, event, src).await; - true -} - -pub async fn apply_expiry_on_transitioned_object( - api: Arc, - oi: &ObjectInfo, - lc_event: &lifecycle::Event, - src: &LcEventSrc, -) -> bool { - // let time_ilm = ScannerMetrics::time_ilm(lc_event.action.clone()); - if let Err(_err) = expire_transitioned_object(api, oi, lc_event, src).await { - return false; - } - // let _ = time_ilm(1); - - true -} - -pub async fn apply_expiry_on_non_transitioned_objects( - api: Arc, - oi: &ObjectInfo, - lc_event: &lifecycle::Event, - _src: &LcEventSrc, -) -> bool { - let mut opts = ObjectOptions { - expiration: ExpirationOptions { expire: true }, - ..Default::default() - }; - - if lc_event.action.delete_versioned() { - opts.version_id = Some(oi.version_id.expect("err").to_string()); - } - - opts.versioned = BucketVersioningSys::prefix_enabled(&oi.bucket, &oi.name).await; - opts.version_suspended = BucketVersioningSys::prefix_suspended(&oi.bucket, &oi.name).await; - - if lc_event.action.delete_all() { - opts.delete_prefix = true; - opts.delete_prefix_object = true; - } - - // let time_ilm = ScannerMetrics::time_ilm(lc_event.action.clone()); - - let mut dobj = api - .delete_object(&oi.bucket, &encode_dir_object(&oi.name), opts) - .await - .unwrap(); - if dobj.name.is_empty() { - dobj = oi.clone(); - } - - //let tags = LcAuditEvent::new(lc_event.clone(), src.clone()).tags(); - //tags["version-id"] = dobj.version_id; - - let mut event_name = EventName::ObjectRemovedDelete; - if oi.delete_marker { - event_name = EventName::ObjectRemovedDeleteMarkerCreated; - } - match lc_event.action { - lifecycle::IlmAction::DeleteAllVersionsAction => event_name = EventName::ObjectRemovedDeleteAllVersions, - lifecycle::IlmAction::DelMarkerDeleteAllVersionsAction => event_name = EventName::ILMDelMarkerExpirationDelete, - _ => (), - } - send_event(EventArgs { - event_name: event_name.as_ref().to_string(), - bucket_name: dobj.bucket.clone(), - object: dobj, - user_agent: "Internal: [ILM-Expiry]".to_string(), - host: GLOBAL_LocalNodeName.to_string(), - ..Default::default() - }); - - if lc_event.action != lifecycle::IlmAction::NoneAction { - // let mut num_versions = 1_u64; - // if lc_event.action.delete_all() { - // num_versions = oi.num_versions as u64; - // } - // let _ = time_ilm(num_versions); - } - - true -} - -async fn apply_expiry_rule(event: &lifecycle::Event, src: &LcEventSrc, oi: &ObjectInfo) -> bool { - let mut expiry_state = GLOBAL_ExpiryState.write().await; - expiry_state.enqueue_by_days(oi, event, src).await; - true -} - -pub async fn apply_lifecycle_action(event: &lifecycle::Event, src: &LcEventSrc, oi: &ObjectInfo) -> bool { - let mut success = false; - match event.action { - lifecycle::IlmAction::DeleteVersionAction - | lifecycle::IlmAction::DeleteAction - | lifecycle::IlmAction::DeleteRestoredAction - | lifecycle::IlmAction::DeleteRestoredVersionAction - | lifecycle::IlmAction::DeleteAllVersionsAction - | lifecycle::IlmAction::DelMarkerDeleteAllVersionsAction => { - success = apply_expiry_rule(event, src, oi).await; - } - lifecycle::IlmAction::TransitionAction | lifecycle::IlmAction::TransitionVersionAction => { - success = apply_transition_rule(event, src, oi).await; - } - _ => (), - } - success -} - -#[cfg(test)] -mod tests { - use std::io::Cursor; - - use chrono::Utc; - use rmp_serde::{Deserializer, Serializer}; - use serde::{Deserialize, Serialize}; - - use super::CurrentScannerCycle; - - #[test] - fn test_current_cycle() { - let cycle_info = CurrentScannerCycle { - current: 0, - next: 1, - started: Utc::now(), - cycle_completed: vec![Utc::now(), Utc::now()], - }; - - println!("{cycle_info:?}"); - - let mut wr = Vec::new(); - cycle_info.serialize(&mut Serializer::new(&mut wr)).unwrap(); - - let mut buf_t = Deserializer::new(Cursor::new(wr)); - let c: CurrentScannerCycle = Deserialize::deserialize(&mut buf_t).unwrap(); - - println!("{c:?}"); - } -} diff --git a/crates/ecstore/src/heal/data_scanner_metric.rs b/crates/ecstore/src/heal/data_scanner_metric.rs deleted file mode 100644 index 8e0b8ccc7..000000000 --- a/crates/ecstore/src/heal/data_scanner_metric.rs +++ /dev/null @@ -1,486 +0,0 @@ -// 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 super::data_scanner::CurrentScannerCycle; -use crate::bucket::lifecycle::lifecycle; -use chrono::Utc; -use lazy_static::lazy_static; -use rustfs_common::last_minute::{AccElem, LastMinuteLatency}; -use rustfs_madmin::metrics::ScannerMetrics as M_ScannerMetrics; -use std::{ - collections::HashMap, - pin::Pin, - sync::{ - Arc, - atomic::{AtomicU64, Ordering}, - }, - time::{Duration, SystemTime}, -}; -use tokio::sync::{Mutex, RwLock}; - -lazy_static! { - pub static ref globalScannerMetrics: Arc = Arc::new(ScannerMetrics::new()); -} - -#[derive(Clone, Debug, PartialEq, PartialOrd)] -pub enum ScannerMetric { - // START Realtime metrics, that only records - // last minute latencies and total operation count. - ReadMetadata = 0, - CheckMissing, - SaveUsage, - ApplyAll, - ApplyVersion, - TierObjSweep, - HealCheck, - Ilm, - CheckReplication, - Yield, - CleanAbandoned, - ApplyNonCurrent, - HealAbandonedVersion, - - // START Trace metrics: - StartTrace, - ScanObject, // Scan object. All operations included. - HealAbandonedObject, - - // END realtime metrics: - LastRealtime, - - // Trace only metrics: - ScanFolder, // Scan a folder on disk, recursively. - ScanCycle, // Full cycle, cluster global. - ScanBucketDrive, // Single bucket on one drive. - CompactFolder, // Folder compacted. - - // Must be last: - Last, -} - -impl ScannerMetric { - /// Convert to string representation for metrics - pub fn as_str(self) -> &'static str { - match self { - Self::ReadMetadata => "read_metadata", - Self::CheckMissing => "check_missing", - Self::SaveUsage => "save_usage", - Self::ApplyAll => "apply_all", - Self::ApplyVersion => "apply_version", - Self::TierObjSweep => "tier_obj_sweep", - Self::HealCheck => "heal_check", - Self::Ilm => "ilm", - Self::CheckReplication => "check_replication", - Self::Yield => "yield", - Self::CleanAbandoned => "clean_abandoned", - Self::ApplyNonCurrent => "apply_non_current", - Self::HealAbandonedVersion => "heal_abandoned_version", - Self::StartTrace => "start_trace", - Self::ScanObject => "scan_object", - Self::HealAbandonedObject => "heal_abandoned_object", - Self::LastRealtime => "last_realtime", - Self::ScanFolder => "scan_folder", - Self::ScanCycle => "scan_cycle", - Self::ScanBucketDrive => "scan_bucket_drive", - Self::CompactFolder => "compact_folder", - Self::Last => "last", - } - } - - /// Convert from index back to enum (safe version) - pub fn from_index(index: usize) -> Option { - if index >= Self::Last as usize { - return None; - } - // Safe conversion using match instead of unsafe transmute - match index { - 0 => Some(Self::ReadMetadata), - 1 => Some(Self::CheckMissing), - 2 => Some(Self::SaveUsage), - 3 => Some(Self::ApplyAll), - 4 => Some(Self::ApplyVersion), - 5 => Some(Self::TierObjSweep), - 6 => Some(Self::HealCheck), - 7 => Some(Self::Ilm), - 8 => Some(Self::CheckReplication), - 9 => Some(Self::Yield), - 10 => Some(Self::CleanAbandoned), - 11 => Some(Self::ApplyNonCurrent), - 12 => Some(Self::HealAbandonedVersion), - 13 => Some(Self::StartTrace), - 14 => Some(Self::ScanObject), - 15 => Some(Self::HealAbandonedObject), - 16 => Some(Self::LastRealtime), - 17 => Some(Self::ScanFolder), - 18 => Some(Self::ScanCycle), - 19 => Some(Self::ScanBucketDrive), - 20 => Some(Self::CompactFolder), - 21 => Some(Self::Last), - _ => None, - } - } -} - -/// Thread-safe wrapper for LastMinuteLatency with atomic operations -#[derive(Default)] -pub struct LockedLastMinuteLatency { - latency: Arc>, -} - -impl Clone for LockedLastMinuteLatency { - fn clone(&self) -> Self { - Self { - latency: Arc::clone(&self.latency), - } - } -} - -impl LockedLastMinuteLatency { - pub fn new() -> Self { - Self { - latency: Arc::new(Mutex::new(LastMinuteLatency::default())), - } - } - - /// Add a duration measurement - pub async fn add(&self, duration: Duration) { - self.add_size(duration, 0).await; - } - - /// Add a duration measurement with size - pub async fn add_size(&self, duration: Duration, size: u64) { - let mut latency = self.latency.lock().await; - let now = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); - - let elem = AccElem { - n: 1, - total: duration.as_secs(), - size, - }; - latency.add_all(now, &elem); - } - - /// Get total accumulated metrics for the last minute - pub async fn total(&self) -> AccElem { - let mut latency = self.latency.lock().await; - latency.get_total() - } -} - -/// Current path tracker for monitoring active scan paths -struct CurrentPathTracker { - current_path: Arc>, -} - -impl CurrentPathTracker { - fn new(initial_path: String) -> Self { - Self { - current_path: Arc::new(RwLock::new(initial_path)), - } - } - - async fn update_path(&self, path: String) { - *self.current_path.write().await = path; - } - - async fn get_path(&self) -> String { - self.current_path.read().await.clone() - } -} - -/// Main scanner metrics structure -pub struct ScannerMetrics { - // All fields must be accessed atomically and aligned. - operations: Vec, - latency: Vec, - actions: Vec, - actions_latency: Vec, - // Current paths contains disk -> tracker mappings - current_paths: Arc>>>, - - // Cycle information - cycle_info: Arc>>, -} - -impl ScannerMetrics { - pub fn new() -> Self { - let operations = (0..ScannerMetric::Last as usize).map(|_| AtomicU64::new(0)).collect(); - - let latency = (0..ScannerMetric::LastRealtime as usize) - .map(|_| LockedLastMinuteLatency::new()) - .collect(); - - Self { - operations, - latency, - actions: (0..ScannerMetric::Last as usize).map(|_| AtomicU64::new(0)).collect(), - actions_latency: vec![LockedLastMinuteLatency::default(); ScannerMetric::LastRealtime as usize], - current_paths: Arc::new(RwLock::new(HashMap::new())), - cycle_info: Arc::new(RwLock::new(None)), - } - } - - /// Log scanner action with custom metadata - compatible with existing usage - pub fn log(metric: ScannerMetric) -> impl Fn(&HashMap) { - let metric = metric as usize; - let start_time = SystemTime::now(); - move |_custom: &HashMap| { - let duration = SystemTime::now().duration_since(start_time).unwrap_or_default(); - - // Update operation count - globalScannerMetrics.operations[metric].fetch_add(1, Ordering::Relaxed); - - // Update latency for realtime metrics (spawn async task for this) - if (metric) < ScannerMetric::LastRealtime as usize { - let metric_index = metric; - tokio::spawn(async move { - globalScannerMetrics.latency[metric_index].add(duration).await; - }); - } - - // Log trace metrics - if metric as u8 > ScannerMetric::StartTrace as u8 { - //debug!(metric = metric.as_str(), duration_ms = duration.as_millis(), "Scanner trace metric"); - } - } - } - - /// Time scanner action with size - returns function that takes size - pub fn time_size(metric: ScannerMetric) -> impl Fn(u64) { - let metric = metric as usize; - let start_time = SystemTime::now(); - move |size: u64| { - let duration = SystemTime::now().duration_since(start_time).unwrap_or_default(); - - // Update operation count - globalScannerMetrics.operations[metric].fetch_add(1, Ordering::Relaxed); - - // Update latency for realtime metrics with size (spawn async task) - if (metric) < ScannerMetric::LastRealtime as usize { - let metric_index = metric; - tokio::spawn(async move { - globalScannerMetrics.latency[metric_index].add_size(duration, size).await; - }); - } - } - } - - /// Time a scanner action - returns a closure to call when done - pub fn time(metric: ScannerMetric) -> impl Fn() { - let metric = metric as usize; - let start_time = SystemTime::now(); - move || { - let duration = SystemTime::now().duration_since(start_time).unwrap_or_default(); - - // Update operation count - globalScannerMetrics.operations[metric].fetch_add(1, Ordering::Relaxed); - - // Update latency for realtime metrics (spawn async task) - if (metric) < ScannerMetric::LastRealtime as usize { - let metric_index = metric; - tokio::spawn(async move { - globalScannerMetrics.latency[metric_index].add(duration).await; - }); - } - } - } - - /// Time N scanner actions - returns function that takes count, then returns completion function - pub fn time_n(metric: ScannerMetric) -> Box Box + Send + Sync> { - let metric = metric as usize; - let start_time = SystemTime::now(); - Box::new(move |count: usize| { - Box::new(move || { - let duration = SystemTime::now().duration_since(start_time).unwrap_or_default(); - - // Update operation count - globalScannerMetrics.operations[metric].fetch_add(count as u64, Ordering::Relaxed); - - // Update latency for realtime metrics (spawn async task) - if (metric) < ScannerMetric::LastRealtime as usize { - let metric_index = metric; - tokio::spawn(async move { - globalScannerMetrics.latency[metric_index].add(duration).await; - }); - } - }) - }) - } - - pub fn time_ilm(a: lifecycle::IlmAction) -> Box Box + Send + Sync> { - let a_clone = a as usize; - if a_clone == lifecycle::IlmAction::NoneAction as usize || a_clone >= lifecycle::IlmAction::ActionCount as usize { - return Box::new(move |_: u64| Box::new(move || {})); - } - let start = SystemTime::now(); - Box::new(move |versions: u64| { - Box::new(move || { - let duration = SystemTime::now().duration_since(start).unwrap_or(Duration::from_secs(0)); - tokio::spawn(async move { - globalScannerMetrics.actions[a_clone].fetch_add(versions, Ordering::Relaxed); - globalScannerMetrics.actions_latency[a_clone].add(duration).await; - }); - }) - }) - } - - /// Increment time with specific duration - pub async fn inc_time(metric: ScannerMetric, duration: Duration) { - let metric = metric as usize; - // Update operation count - globalScannerMetrics.operations[metric].fetch_add(1, Ordering::Relaxed); - - // Update latency for realtime metrics - if (metric) < ScannerMetric::LastRealtime as usize { - globalScannerMetrics.latency[metric].add(duration).await; - } - } - - /// Get lifetime operation count for a metric - pub fn lifetime(&self, metric: ScannerMetric) -> u64 { - let metric = metric as usize; - if (metric) >= ScannerMetric::Last as usize { - return 0; - } - self.operations[metric].load(Ordering::Relaxed) - } - - /// Get last minute statistics for a metric - pub async fn last_minute(&self, metric: ScannerMetric) -> AccElem { - let metric = metric as usize; - if (metric) >= ScannerMetric::LastRealtime as usize { - return AccElem::default(); - } - self.latency[metric].total().await - } - - /// Set current cycle information - pub async fn set_cycle(&self, cycle: Option) { - *self.cycle_info.write().await = cycle; - } - - /// Get current cycle information - pub async fn get_cycle(&self) -> Option { - self.cycle_info.read().await.clone() - } - - /// Get current active paths - pub async fn get_current_paths(&self) -> Vec { - let mut result = Vec::new(); - let paths = self.current_paths.read().await; - - for (disk, tracker) in paths.iter() { - let path = tracker.get_path().await; - result.push(format!("{disk}/{path}")); - } - - result - } - - /// Get number of active drives - pub async fn active_drives(&self) -> usize { - self.current_paths.read().await.len() - } - - /// Generate metrics report - pub async fn report(&self) -> M_ScannerMetrics { - let mut metrics = M_ScannerMetrics::default(); - - // Set cycle information - if let Some(cycle) = self.get_cycle().await { - metrics.current_cycle = cycle.current; - metrics.cycles_completed_at = cycle.cycle_completed; - metrics.current_started = cycle.started; - } - - metrics.collected_at = Utc::now(); - metrics.active_paths = self.get_current_paths().await; - - // Lifetime operations - for i in 0..ScannerMetric::Last as usize { - let count = self.operations[i].load(Ordering::Relaxed); - if count > 0 { - if let Some(metric) = ScannerMetric::from_index(i) { - metrics.life_time_ops.insert(metric.as_str().to_string(), count); - } - } - } - - // Last minute statistics for realtime metrics - for i in 0..ScannerMetric::LastRealtime as usize { - let last_min = self.latency[i].total().await; - if last_min.n > 0 { - if let Some(_metric) = ScannerMetric::from_index(i) { - // Convert to madmin TimedAction format if needed - // This would require implementing the conversion - } - } - } - - metrics - } -} - -// Type aliases for compatibility with existing code -pub type UpdateCurrentPathFn = Arc Pin + Send>> + Send + Sync>; -pub type CloseDiskFn = Arc Pin + Send>> + Send + Sync>; - -/// Create a current path updater for tracking scan progress -pub fn current_path_updater(disk: &str, initial: &str) -> (UpdateCurrentPathFn, CloseDiskFn) { - let tracker = Arc::new(CurrentPathTracker::new(initial.to_string())); - let disk_name = disk.to_string(); - - // Store the tracker in global metrics - let tracker_clone = Arc::clone(&tracker); - let disk_clone = disk_name.clone(); - tokio::spawn(async move { - globalScannerMetrics - .current_paths - .write() - .await - .insert(disk_clone, tracker_clone); - }); - - let update_fn = { - let tracker = Arc::clone(&tracker); - Arc::new(move |path: &str| -> Pin + Send>> { - let tracker = Arc::clone(&tracker); - let path = path.to_string(); - Box::pin(async move { - tracker.update_path(path).await; - }) - }) - }; - - let done_fn = { - let disk_name = disk_name.clone(); - Arc::new(move || -> Pin + Send>> { - let disk_name = disk_name.clone(); - Box::pin(async move { - globalScannerMetrics.current_paths.write().await.remove(&disk_name); - }) - }) - }; - - (update_fn, done_fn) -} - -impl Default for ScannerMetrics { - fn default() -> Self { - Self::new() - } -} diff --git a/crates/ecstore/src/heal/data_usage.rs b/crates/ecstore/src/heal/data_usage.rs deleted file mode 100644 index a6d569971..000000000 --- a/crates/ecstore/src/heal/data_usage.rs +++ /dev/null @@ -1,221 +0,0 @@ -// 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::error::{Error, Result}; -use crate::{ - bucket::metadata_sys::get_replication_config, - config::com::{read_config, save_config}, - disk::{BUCKET_META_PREFIX, RUSTFS_META_BUCKET}, - error::to_object_err, - new_object_layer_fn, - store::ECStore, -}; -use lazy_static::lazy_static; -use rustfs_utils::path::SLASH_SEPARATOR; -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, sync::Arc, time::SystemTime}; -use tokio::sync::mpsc::Receiver; -use tracing::{error, warn}; - -pub const DATA_USAGE_ROOT: &str = SLASH_SEPARATOR; -const DATA_USAGE_OBJ_NAME: &str = ".usage.json"; -const DATA_USAGE_BLOOM_NAME: &str = ".bloomcycle.bin"; -pub const DATA_USAGE_CACHE_NAME: &str = ".usage-cache.bin"; -lazy_static! { - pub static ref DATA_USAGE_BUCKET: String = format!("{}{}{}", RUSTFS_META_BUCKET, SLASH_SEPARATOR, BUCKET_META_PREFIX); - pub static ref DATA_USAGE_OBJ_NAME_PATH: String = format!("{}{}{}", BUCKET_META_PREFIX, SLASH_SEPARATOR, DATA_USAGE_OBJ_NAME); - pub static ref DATA_USAGE_BLOOM_NAME_PATH: String = - format!("{}{}{}", BUCKET_META_PREFIX, SLASH_SEPARATOR, DATA_USAGE_BLOOM_NAME); - pub static ref BACKGROUND_HEAL_INFO_PATH: String = - format!("{}{}{}", BUCKET_META_PREFIX, SLASH_SEPARATOR, ".background-heal.json"); -} - -// BucketTargetUsageInfo - bucket target usage info provides -// - replicated size for all objects sent to this target -// - replica size for all objects received from this target -// - replication pending size for all objects pending replication to this target -// - replication failed size for all objects failed replication to this target -// - replica pending count -// - replica failed count -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct BucketTargetUsageInfo { - pub replication_pending_size: u64, - pub replication_failed_size: u64, - pub replicated_size: u64, - pub replica_size: u64, - pub replication_pending_count: u64, - pub replication_failed_count: u64, - pub replicated_count: u64, -} - -// BucketUsageInfo - bucket usage info provides -// - total size of the bucket -// - total objects in a bucket -// - object size histogram per bucket -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct BucketUsageInfo { - pub size: u64, - // Following five fields suffixed with V1 are here for backward compatibility - // Total Size for objects that have not yet been replicated - pub replication_pending_size_v1: u64, - // Total size for objects that have witness one or more failures and will be retried - pub replication_failed_size_v1: u64, - // Total size for objects that have been replicated to destination - pub replicated_size_v1: u64, - // Total number of objects pending replication - pub replication_pending_count_v1: u64, - // Total number of objects that failed replication - pub replication_failed_count_v1: u64, - - pub objects_count: u64, - pub object_size_histogram: HashMap, - pub object_versions_histogram: HashMap, - pub versions_count: u64, - pub delete_markers_count: u64, - pub replica_size: u64, - pub replica_count: u64, - pub replication_info: HashMap, -} - -// DataUsageInfo represents data usage stats of the underlying Object API -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct DataUsageInfo { - pub total_capacity: u64, - pub total_used_capacity: u64, - pub total_free_capacity: u64, - - // LastUpdate is the timestamp of when the data usage info was last updated. - // This does not indicate a full scan. - pub last_update: Option, - - // Objects total count across all buckets - pub objects_total_count: u64, - // Versions total count across all buckets - pub versions_total_count: u64, - // Delete markers total count across all buckets - pub delete_markers_total_count: u64, - // Objects total size across all buckets - pub objects_total_size: u64, - pub replication_info: HashMap, - - // Total number of buckets in this cluster - pub buckets_count: u64, - // Buckets usage info provides following information across all buckets - // - total size of the bucket - // - total objects in a bucket - // - object size histogram per bucket - pub buckets_usage: HashMap, - // Deprecated kept here for backward compatibility reasons. - pub bucket_sizes: HashMap, - // Todo: TierStats - // TierStats contains per-tier stats of all configured remote tiers -} - -pub async fn store_data_usage_in_backend(mut rx: Receiver) { - let Some(store) = new_object_layer_fn() else { - error!("errServerNotInitialized"); - return; - }; - - let mut attempts = 1; - loop { - match rx.recv().await { - Some(data_usage_info) => { - if let Ok(data) = serde_json::to_vec(&data_usage_info) { - if attempts > 10 { - let _ = - save_config(store.clone(), &format!("{}{}", *DATA_USAGE_OBJ_NAME_PATH, ".bkp"), data.clone()).await; - attempts += 1; - } - let _ = save_config(store.clone(), &DATA_USAGE_OBJ_NAME_PATH, data).await; - attempts += 1; - } else { - continue; - } - } - None => { - return; - } - } - } -} - -// TODO: cancel ctx -pub async fn load_data_usage_from_backend(store: Arc) -> Result { - let buf = match read_config(store, &DATA_USAGE_OBJ_NAME_PATH).await { - Ok(data) => data, - Err(e) => { - error!("Failed to read data usage info from backend: {}", e); - if e == Error::ConfigNotFound { - return Ok(DataUsageInfo::default()); - } - - return Err(to_object_err(e, vec![RUSTFS_META_BUCKET, &DATA_USAGE_OBJ_NAME_PATH])); - } - }; - - let mut data_usage_info: DataUsageInfo = serde_json::from_slice(&buf)?; - - warn!("Loaded data usage info from backend {:?}", &data_usage_info); - - if data_usage_info.buckets_usage.is_empty() { - data_usage_info.buckets_usage = data_usage_info - .bucket_sizes - .iter() - .map(|(bucket, &size)| { - ( - bucket.clone(), - BucketUsageInfo { - size, - ..Default::default() - }, - ) - }) - .collect(); - } - - if data_usage_info.bucket_sizes.is_empty() { - data_usage_info.bucket_sizes = data_usage_info - .buckets_usage - .iter() - .map(|(bucket, bui)| (bucket.clone(), bui.size)) - .collect(); - } - - for (bucket, bui) in &data_usage_info.buckets_usage { - if bui.replicated_size_v1 > 0 - || bui.replication_failed_count_v1 > 0 - || bui.replication_failed_size_v1 > 0 - || bui.replication_pending_count_v1 > 0 - { - if let Ok((cfg, _)) = get_replication_config(bucket).await { - if !cfg.role.is_empty() { - data_usage_info.replication_info.insert( - cfg.role.clone(), - BucketTargetUsageInfo { - replication_failed_size: bui.replication_failed_size_v1, - replication_failed_count: bui.replication_failed_count_v1, - replicated_size: bui.replicated_size_v1, - replication_pending_count: bui.replication_pending_count_v1, - replication_pending_size: bui.replication_pending_size_v1, - ..Default::default() - }, - ); - } - } - } - } - - Ok(data_usage_info) -} diff --git a/crates/ecstore/src/heal/data_usage_cache.rs b/crates/ecstore/src/heal/data_usage_cache.rs deleted file mode 100644 index 70b509cf6..000000000 --- a/crates/ecstore/src/heal/data_usage_cache.rs +++ /dev/null @@ -1,928 +0,0 @@ -// 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::config::com::save_config; -use crate::disk::{BUCKET_META_PREFIX, RUSTFS_META_BUCKET}; -use crate::error::{Error, Result}; -use crate::new_object_layer_fn; -use crate::set_disk::SetDisks; -use crate::store_api::{BucketInfo, ObjectIO, ObjectOptions}; -use bytesize::ByteSize; -use http::HeaderMap; -use path_clean::PathClean; -use rand::Rng; -use rmp_serde::Serializer; -use s3s::dto::{BucketLifecycleConfiguration, ReplicationConfiguration}; -use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; -use std::hash::{DefaultHasher, Hash, Hasher}; -use std::path::Path; -use std::time::{Duration, SystemTime}; -use tokio::sync::mpsc::Sender; -use tokio::time::sleep; - -use super::data_scanner::{DATA_SCANNER_FORCE_COMPACT_AT_FOLDERS, SizeSummary}; -use super::data_usage::{BucketTargetUsageInfo, BucketUsageInfo, DataUsageInfo}; - -// DATA_USAGE_BUCKET_LEN must be length of ObjectsHistogramIntervals -pub const DATA_USAGE_BUCKET_LEN: usize = 11; -pub const DATA_USAGE_VERSION_LEN: usize = 7; - -pub type DataUsageHashMap = HashSet; - -struct ObjectHistogramInterval { - name: &'static str, - start: u64, - end: u64, -} - -const OBJECTS_HISTOGRAM_INTERVALS: [ObjectHistogramInterval; DATA_USAGE_BUCKET_LEN] = [ - ObjectHistogramInterval { - name: "LESS_THAN_1024_B", - start: 0, - end: ByteSize::kib(1).as_u64() - 1, - }, - ObjectHistogramInterval { - name: "BETWEEN_1024_B_AND_64_KB", - start: ByteSize::kib(1).as_u64(), - end: ByteSize::kib(64).as_u64() - 1, - }, - ObjectHistogramInterval { - name: "BETWEEN_64_KB_AND_256_KB", - start: ByteSize::kib(64).as_u64(), - end: ByteSize::kib(256).as_u64() - 1, - }, - ObjectHistogramInterval { - name: "BETWEEN_256_KB_AND_512_KB", - start: ByteSize::kib(256).as_u64(), - end: ByteSize::kib(512).as_u64() - 1, - }, - ObjectHistogramInterval { - name: "BETWEEN_512_KB_AND_1_MB", - start: ByteSize::kib(512).as_u64(), - end: ByteSize::mib(1).as_u64() - 1, - }, - ObjectHistogramInterval { - name: "BETWEEN_1024B_AND_1_MB", - start: ByteSize::kib(1).as_u64(), - end: ByteSize::mib(1).as_u64() - 1, - }, - ObjectHistogramInterval { - name: "BETWEEN_1_MB_AND_10_MB", - start: ByteSize::mib(1).as_u64(), - end: ByteSize::mib(10).as_u64() - 1, - }, - ObjectHistogramInterval { - name: "BETWEEN_10_MB_AND_64_MB", - start: ByteSize::mib(10).as_u64(), - end: ByteSize::mib(64).as_u64() - 1, - }, - ObjectHistogramInterval { - name: "BETWEEN_64_MB_AND_128_MB", - start: ByteSize::mib(64).as_u64(), - end: ByteSize::mib(128).as_u64() - 1, - }, - ObjectHistogramInterval { - name: "BETWEEN_128_MB_AND_512_MB", - start: ByteSize::mib(128).as_u64(), - end: ByteSize::mib(512).as_u64() - 1, - }, - ObjectHistogramInterval { - name: "GREATER_THAN_512_MB", - start: ByteSize::mib(512).as_u64(), - end: u64::MAX, - }, -]; - -const OBJECTS_VERSION_COUNT_INTERVALS: [ObjectHistogramInterval; DATA_USAGE_VERSION_LEN] = [ - ObjectHistogramInterval { - name: "UNVERSIONED", - start: 0, - end: 0, - }, - ObjectHistogramInterval { - name: "SINGLE_VERSION", - start: 1, - end: 1, - }, - ObjectHistogramInterval { - name: "BETWEEN_2_AND_10", - start: 2, - end: 9, - }, - ObjectHistogramInterval { - name: "BETWEEN_10_AND_100", - start: 10, - end: 99, - }, - ObjectHistogramInterval { - name: "BETWEEN_100_AND_1000", - start: 100, - end: 999, - }, - ObjectHistogramInterval { - name: "BETWEEN_1000_AND_10000", - start: 1000, - end: 9999, - }, - ObjectHistogramInterval { - name: "GREATER_THAN_10000", - start: 10000, - end: u64::MAX, - }, -]; - -#[derive(Clone, Copy, Default)] -pub struct TierStats { - pub total_size: u64, - pub num_versions: i32, - pub num_objects: i32, -} - -impl TierStats { - pub fn add(&self, u: &TierStats) -> TierStats { - TierStats { - total_size: self.total_size + u.total_size, - num_versions: self.num_versions + u.num_versions, - num_objects: self.num_objects + u.num_objects, - } - } -} - -struct AllTierStats { - tiers: HashMap, -} - -impl AllTierStats { - pub fn new() -> Self { - Self { tiers: HashMap::new() } - } - - fn add_sizes(&mut self, tiers: HashMap) { - for (tier, st) in tiers { - self.tiers.insert(tier.clone(), self.tiers[&tier].add(&st)); - } - } - - fn merge(&mut self, other: AllTierStats) { - for (tier, st) in other.tiers { - self.tiers.insert(tier.clone(), self.tiers[&tier].add(&st)); - } - } - - fn populate_stats(&self, stats: &mut HashMap) { - for (tier, st) in &self.tiers { - stats.insert( - tier.clone(), - TierStats { - total_size: st.total_size, - num_versions: st.num_versions, - num_objects: st.num_objects, - }, - ); - } - } -} - -// sizeHistogram is a size histogram. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct SizeHistogram(Vec); - -impl Default for SizeHistogram { - fn default() -> Self { - Self(vec![0; DATA_USAGE_BUCKET_LEN]) - } -} - -impl SizeHistogram { - fn add(&mut self, size: u64) { - for (idx, interval) in OBJECTS_HISTOGRAM_INTERVALS.iter().enumerate() { - if size >= interval.start && size <= interval.end { - self.0[idx] += 1; - break; - } - } - } - - pub fn to_map(&self) -> HashMap { - let mut res = HashMap::new(); - let mut spl_count = 0; - for (count, oh) in self.0.iter().zip(OBJECTS_HISTOGRAM_INTERVALS.iter()) { - if ByteSize::kib(1).as_u64() == oh.start && oh.end == ByteSize::mib(1).as_u64() - 1 { - res.insert(oh.name.to_string(), spl_count); - } else if ByteSize::kib(1).as_u64() <= oh.start && oh.end < ByteSize::mib(1).as_u64() { - spl_count += count; - res.insert(oh.name.to_string(), *count); - } else { - res.insert(oh.name.to_string(), *count); - } - } - res - } -} - -// versionsHistogram is a histogram of number of versions in an object. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct VersionsHistogram(Vec); - -impl Default for VersionsHistogram { - fn default() -> Self { - Self(vec![0; DATA_USAGE_VERSION_LEN]) - } -} - -impl VersionsHistogram { - fn add(&mut self, size: u64) { - for (idx, interval) in OBJECTS_VERSION_COUNT_INTERVALS.iter().enumerate() { - if size >= interval.start && size <= interval.end { - self.0[idx] += 1; - break; - } - } - } - - pub fn to_map(&self) -> HashMap { - let mut res = HashMap::new(); - for (count, ov) in self.0.iter().zip(OBJECTS_VERSION_COUNT_INTERVALS.iter()) { - res.insert(ov.name.to_string(), *count); - } - res - } -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct ReplicationStats { - pub pending_size: u64, - pub replicated_size: u64, - pub failed_size: u64, - pub failed_count: u64, - pub pending_count: u64, - pub missed_threshold_size: u64, - pub after_threshold_size: u64, - pub missed_threshold_count: u64, - pub after_threshold_count: u64, - pub replicated_count: u64, -} - -impl ReplicationStats { - pub fn empty(&self) -> bool { - self.replicated_size == 0 && self.failed_size == 0 && self.failed_count == 0 - } -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct ReplicationAllStats { - pub targets: HashMap, - pub replica_size: u64, - pub replica_count: u64, -} - -impl ReplicationAllStats { - pub fn empty(&self) -> bool { - if self.replica_size != 0 && self.replica_count != 0 { - return false; - } - for (_, v) in self.targets.iter() { - if !v.empty() { - return false; - } - } - - true - } -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct DataUsageEntry { - pub children: DataUsageHashMap, - // These fields do not include any children. - pub size: usize, - pub objects: usize, - pub versions: usize, - pub delete_markers: usize, - pub obj_sizes: SizeHistogram, - pub obj_versions: VersionsHistogram, - pub replication_stats: Option, - // Todo: tier - // pub all_tier_stats: , - pub compacted: bool, -} - -impl DataUsageEntry { - pub fn add_child(&mut self, hash: &DataUsageHash) { - if self.children.contains(&hash.key()) { - return; - } - - self.children.insert(hash.key()); - } - - pub fn add_sizes(&mut self, summary: &SizeSummary) { - self.size += summary.total_size; - self.versions += summary.versions; - self.delete_markers += summary.delete_markers; - self.obj_sizes.add(summary.total_size as u64); - self.obj_versions.add(summary.versions as u64); - - let replication_stats = if self.replication_stats.is_none() { - self.replication_stats = Some(ReplicationAllStats::default()); - self.replication_stats.as_mut().unwrap() - } else { - self.replication_stats.as_mut().unwrap() - }; - replication_stats.replica_size += summary.replica_size as u64; - replication_stats.replica_count += summary.replica_count as u64; - - for (arn, st) in &summary.repl_target_stats { - let tgt_stat = replication_stats - .targets - .entry(arn.to_string()) - .or_insert(ReplicationStats::default()); - tgt_stat.pending_size += st.pending_size as u64; - tgt_stat.failed_size += st.failed_size as u64; - tgt_stat.replicated_size += st.replicated_size as u64; - tgt_stat.replicated_count += st.replicated_count as u64; - tgt_stat.failed_count += st.failed_count as u64; - tgt_stat.pending_count += st.pending_count as u64; - } - // Todo:: tiers - } - - pub fn merge(&mut self, other: &DataUsageEntry) { - self.objects += other.objects; - self.versions += other.versions; - self.delete_markers += other.delete_markers; - self.size += other.size; - if let Some(o_rep) = &other.replication_stats { - if self.replication_stats.is_none() { - self.replication_stats = Some(ReplicationAllStats::default()); - } - let s_rep = self.replication_stats.as_mut().unwrap(); - s_rep.targets.clear(); - s_rep.replica_size += o_rep.replica_size; - s_rep.replica_count += o_rep.replica_count; - for (arn, stat) in o_rep.targets.iter() { - let st = s_rep.targets.entry(arn.clone()).or_default(); - *st = ReplicationStats { - pending_size: stat.pending_size + st.pending_size, - failed_size: stat.failed_size + st.failed_size, - replicated_size: stat.replicated_size + st.replicated_size, - pending_count: stat.pending_count + st.pending_count, - failed_count: stat.failed_count + st.failed_count, - replicated_count: stat.replicated_count + st.replicated_count, - ..Default::default() - }; - } - } - - for (i, v) in other.obj_sizes.0.iter().enumerate() { - self.obj_sizes.0[i] += v; - } - - for (i, v) in other.obj_versions.0.iter().enumerate() { - self.obj_versions.0[i] += v; - } - - // todo: tiers - } -} - -#[derive(Clone)] -pub struct DataUsageEntryInfo { - pub name: String, - pub parent: String, - pub entry: DataUsageEntry, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct DataUsageCacheInfo { - pub name: String, - pub next_cycle: u32, - pub last_update: Option, - pub skip_healing: bool, - #[serde(skip)] - pub lifecycle: Option, - #[serde(skip)] - pub updates: Option>, - #[serde(skip)] - pub replication: Option, -} - -// impl Default for DataUsageCacheInfo { -// fn default() -> Self { -// Self { -// name: Default::default(), -// next_cycle: Default::default(), -// last_update: SystemTime::now(), -// skip_healing: Default::default(), -// updates: Default::default(), -// replication: Default::default(), -// } -// } -// } - -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct DataUsageCache { - pub info: DataUsageCacheInfo, - pub cache: HashMap, -} - -impl DataUsageCache { - pub async fn load(store: &SetDisks, name: &str) -> Result { - let mut d = DataUsageCache::default(); - let mut retries = 0; - while retries < 5 { - let path = Path::new(BUCKET_META_PREFIX).join(name); - // warn!("Loading data usage cache from backend: {}", path.display()); - match store - .get_object_reader( - RUSTFS_META_BUCKET, - path.to_str().unwrap(), - None, - HeaderMap::new(), - &ObjectOptions { - no_lock: true, - ..Default::default() - }, - ) - .await - { - Ok(mut reader) => { - if let Ok(info) = Self::unmarshal(&reader.read_all().await?) { - d = info - } - break; - } - Err(err) => { - // warn!("Failed to load data usage cache from backend: {}", &err); - match err { - Error::FileNotFound | Error::VolumeNotFound => { - match store - .get_object_reader( - RUSTFS_META_BUCKET, - name, - None, - HeaderMap::new(), - &ObjectOptions { - no_lock: true, - ..Default::default() - }, - ) - .await - { - Ok(mut reader) => { - if let Ok(info) = Self::unmarshal(&reader.read_all().await?) { - d = info - } - break; - } - Err(_) => match err { - Error::FileNotFound | Error::VolumeNotFound => { - break; - } - _ => {} - }, - } - } - _ => { - break; - } - } - } - } - retries += 1; - let dur = { - let mut rng = rand::rng(); - rng.random_range(0..1_000) - }; - sleep(Duration::from_millis(dur)).await; - } - Ok(d) - } - - pub async fn save(&self, name: &str) -> Result<()> { - let Some(store) = new_object_layer_fn() else { - return Err(Error::other("errServerNotInitialized")); - }; - let buf = self.marshal_msg()?; - let buf_clone = buf.clone(); - - let store_clone = store.clone(); - - let name = Path::new(BUCKET_META_PREFIX).join(name).to_string_lossy().to_string(); - - let name_clone = name.clone(); - tokio::spawn(async move { - let _ = save_config(store_clone, &format!("{}{}", &name_clone, ".bkp"), buf_clone).await; - }); - save_config(store, &name, buf).await?; - Ok(()) - } - - pub fn replace(&mut self, path: &str, parent: &str, e: DataUsageEntry) { - let hash = hash_path(path); - self.cache.insert(hash.key(), e); - if !parent.is_empty() { - let phash = hash_path(parent); - let p = { - let p = self.cache.entry(phash.key()).or_default(); - p.add_child(&hash); - p.clone() - }; - self.cache.insert(phash.key(), p); - } - } - - pub fn replace_hashed(&mut self, hash: &DataUsageHash, parent: &Option, e: &DataUsageEntry) { - self.cache.insert(hash.key(), e.clone()); - if let Some(parent) = parent { - self.cache.entry(parent.key()).or_default().add_child(hash); - } - } - - pub fn find(&self, path: &str) -> Option { - self.cache.get(&hash_path(path).key()).cloned() - } - - pub fn find_children_copy(&mut self, h: DataUsageHash) -> DataUsageHashMap { - self.cache.entry(h.string()).or_default().children.clone() - } - - pub fn flatten(&self, root: &DataUsageEntry) -> DataUsageEntry { - let mut root = root.clone(); - for id in root.children.clone().iter() { - if let Some(e) = self.cache.get(id) { - let mut e = e.clone(); - if !e.children.is_empty() { - e = self.flatten(&e); - } - root.merge(&e); - } - } - root.children.clear(); - root - } - - pub fn copy_with_children(&mut self, src: &DataUsageCache, hash: &DataUsageHash, parent: &Option) { - if let Some(e) = src.cache.get(&hash.string()) { - self.cache.insert(hash.key(), e.clone()); - for ch in e.children.iter() { - if *ch == hash.key() { - return; - } - self.copy_with_children(src, &DataUsageHash(ch.to_string()), &Some(hash.clone())); - } - if let Some(parent) = parent { - let p = self.cache.entry(parent.key()).or_default(); - p.add_child(hash); - } - } - } - - pub fn delete_recursive(&mut self, hash: &DataUsageHash) { - let mut need_remove = Vec::new(); - if let Some(v) = self.cache.get(&hash.string()) { - for child in v.children.iter() { - need_remove.push(child.clone()); - } - } - self.cache.remove(&hash.string()); - need_remove.iter().for_each(|child| { - self.delete_recursive(&DataUsageHash(child.to_string())); - }); - } - - pub fn size_recursive(&self, path: &str) -> Option { - match self.find(path) { - Some(root) => { - if root.children.is_empty() { - return Some(root); - } - let mut flat = self.flatten(&root); - if flat.replication_stats.is_some() && flat.replication_stats.as_ref().unwrap().empty() { - flat.replication_stats = None; - } - Some(flat) - } - None => None, - } - } - - pub fn search_parent(&self, hash: &DataUsageHash) -> Option { - let want = hash.key(); - if let Some(last_index) = want.rfind('/') { - if let Some(v) = self.find(&want[0..last_index]) { - if v.children.contains(&want) { - let found = hash_path(&want[0..last_index]); - return Some(found); - } - } - } - - for (k, v) in self.cache.iter() { - if v.children.contains(&want) { - let found = DataUsageHash(k.clone()); - return Some(found); - } - } - None - } - - pub fn is_compacted(&self, hash: &DataUsageHash) -> bool { - match self.cache.get(&hash.key()) { - Some(due) => due.compacted, - None => false, - } - } - - pub fn force_compact(&mut self, limit: usize) { - if self.cache.len() < limit { - return; - } - let top = hash_path(&self.info.name).key(); - let top_e = match self.find(&top) { - Some(e) => e, - None => return, - }; - if top_e.children.len() > >::try_into(DATA_SCANNER_FORCE_COMPACT_AT_FOLDERS).unwrap() { - self.reduce_children_of(&hash_path(&self.info.name), limit, true); - } - if self.cache.len() <= limit { - return; - } - - let mut found = HashSet::new(); - found.insert(top); - mark(self, &top_e, &mut found); - self.cache.retain(|k, _| { - if !found.contains(k) { - return false; - } - true - }); - } - - pub fn reduce_children_of(&mut self, path: &DataUsageHash, limit: usize, compact_self: bool) { - let e = match self.cache.get(&path.key()) { - Some(e) => e, - None => return, - }; - - if e.compacted { - return; - } - - if e.children.len() > limit && compact_self { - let mut flat = self.size_recursive(&path.key()).unwrap_or_default(); - flat.compacted = true; - self.delete_recursive(path); - self.replace_hashed(path, &None, &flat); - return; - } - let total = self.total_children_rec(&path.key()); - if total < limit { - return; - } - - let mut leaves = Vec::new(); - let mut remove = total - limit; - add(self, path, &mut leaves); - leaves.sort_by(|a, b| a.objects.cmp(&b.objects)); - - while remove > 0 && !leaves.is_empty() { - let e = leaves.first().unwrap(); - let candidate = e.path.clone(); - if candidate == *path && !compact_self { - break; - } - let removing = self.total_children_rec(&candidate.key()); - let mut flat = match self.size_recursive(&candidate.key()) { - Some(flat) => flat, - None => { - leaves.remove(0); - continue; - } - }; - - flat.compacted = true; - self.delete_recursive(&candidate); - self.replace_hashed(&candidate, &None, &flat); - - remove -= removing; - leaves.remove(0); - } - } - - pub fn total_children_rec(&self, path: &str) -> usize { - let root = self.find(path); - - if root.is_none() { - return 0; - } - let root = root.unwrap(); - if root.children.is_empty() { - return 0; - } - - let mut n = root.children.len(); - for ch in root.children.iter() { - n += self.total_children_rec(ch); - } - n - } - - pub fn merge(&mut self, o: &DataUsageCache) { - let mut existing_root = self.root(); - let other_root = o.root(); - if existing_root.is_none() && other_root.is_none() { - return; - } - if other_root.is_none() { - return; - } - if existing_root.is_none() { - *self = o.clone(); - return; - } - if o.info.last_update.gt(&self.info.last_update) { - self.info.last_update = o.info.last_update; - } - - existing_root.as_mut().unwrap().merge(other_root.as_ref().unwrap()); - self.cache.insert(hash_path(&self.info.name).key(), existing_root.unwrap()); - let e_hash = self.root_hash(); - for key in other_root.as_ref().unwrap().children.iter() { - let entry = &o.cache[key]; - let flat = o.flatten(entry); - let mut existing = self.cache[key].clone(); - existing.merge(&flat); - self.replace_hashed(&DataUsageHash(key.clone()), &Some(e_hash.clone()), &existing); - } - } - - pub fn root_hash(&self) -> DataUsageHash { - hash_path(&self.info.name) - } - - pub fn root(&self) -> Option { - self.find(&self.info.name) - } - - pub fn dui(&self, path: &str, buckets: &[BucketInfo]) -> DataUsageInfo { - let e = match self.find(path) { - Some(e) => e, - None => return DataUsageInfo::default(), - }; - let flat = self.flatten(&e); - DataUsageInfo { - last_update: self.info.last_update, - objects_total_count: flat.objects as u64, - versions_total_count: flat.versions as u64, - delete_markers_total_count: flat.delete_markers as u64, - objects_total_size: flat.size as u64, - buckets_count: e.children.len() as u64, - buckets_usage: self.buckets_usage_info(buckets), - ..Default::default() - } - } - - pub fn buckets_usage_info(&self, buckets: &[BucketInfo]) -> HashMap { - let mut dst = HashMap::new(); - for bucket in buckets.iter() { - let e = match self.find(&bucket.name) { - Some(e) => e, - None => continue, - }; - let flat = self.flatten(&e); - let mut bui = BucketUsageInfo { - size: flat.size as u64, - versions_count: flat.versions as u64, - objects_count: flat.objects as u64, - delete_markers_count: flat.delete_markers as u64, - object_size_histogram: flat.obj_sizes.to_map(), - object_versions_histogram: flat.obj_versions.to_map(), - ..Default::default() - }; - if let Some(rs) = &flat.replication_stats { - bui.replica_size = rs.replica_size; - bui.replica_count = rs.replica_count; - - for (arn, stat) in rs.targets.iter() { - bui.replication_info.insert( - arn.clone(), - BucketTargetUsageInfo { - replication_pending_size: stat.pending_size, - replicated_size: stat.replicated_size, - replication_failed_size: stat.failed_size, - replication_pending_count: stat.pending_count, - replication_failed_count: stat.failed_count, - replicated_count: stat.replicated_count, - ..Default::default() - }, - ); - } - } - dst.insert(bucket.name.clone(), bui); - } - dst - } - - pub fn marshal_msg(&self) -> Result> { - let mut buf = Vec::new(); - - self.serialize(&mut Serializer::new(&mut buf))?; - - Ok(buf) - } - - pub fn unmarshal(buf: &[u8]) -> Result { - let t: Self = rmp_serde::from_slice(buf)?; - Ok(t) - } -} - -#[derive(Default, Clone)] -struct Inner { - objects: usize, - path: DataUsageHash, -} - -fn add(data_usage_cache: &DataUsageCache, path: &DataUsageHash, leaves: &mut Vec) { - let e = match data_usage_cache.cache.get(&path.key()) { - Some(e) => e, - None => return, - }; - if !e.children.is_empty() { - return; - } - - let sz = data_usage_cache.size_recursive(&path.key()).unwrap_or_default(); - leaves.push(Inner { - objects: sz.objects, - path: path.clone(), - }); - for ch in e.children.iter() { - add(data_usage_cache, &DataUsageHash(ch.clone()), leaves); - } -} - -fn mark(duc: &DataUsageCache, entry: &DataUsageEntry, found: &mut HashSet) { - for k in entry.children.iter() { - found.insert(k.to_string()); - if let Some(ch) = duc.cache.get(k) { - mark(duc, ch, found); - } - } -} - -#[derive(Clone, Debug, Default, Eq, PartialEq)] -pub struct DataUsageHash(pub String); - -impl DataUsageHash { - pub fn string(&self) -> String { - self.0.clone() - } - - pub fn key(&self) -> String { - self.0.clone() - } - - pub fn mod_(&self, cycle: u32, cycles: u32) -> bool { - if cycles <= 1 { - return cycles == 1; - } - - let hash = self.calculate_hash(); - hash as u32 % cycles == cycle % cycles - } - - pub fn mod_alt(&self, cycle: u32, cycles: u32) -> bool { - if cycles <= 1 { - return cycles == 1; - } - - let hash = self.calculate_hash(); - (hash >> 32) as u32 % cycles == cycle % cycles - } - - fn calculate_hash(&self) -> u64 { - let mut hasher = DefaultHasher::new(); - self.0.hash(&mut hasher); - hasher.finish() - } -} - -pub fn hash_path(data: &str) -> DataUsageHash { - DataUsageHash(Path::new(&data).clean().to_string_lossy().to_string()) -} diff --git a/crates/ecstore/src/heal/error.rs b/crates/ecstore/src/heal/error.rs deleted file mode 100644 index dd2db424e..000000000 --- a/crates/ecstore/src/heal/error.rs +++ /dev/null @@ -1,19 +0,0 @@ -// 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. - -pub const ERR_IGNORE_FILE_CONTRIB: &str = "ignore this file's contribution toward data-usage"; -pub const ERR_SKIP_FILE: &str = "skip this file"; -pub const ERR_HEAL_STOP_SIGNALLED: &str = "heal stop signaled"; -pub const ERR_HEAL_IDLE_TIMEOUT: &str = "healing results were not consumed for too long"; -pub const ERR_RETRY_HEALING: &str = "some items failed to heal, we will retry healing this drive again"; diff --git a/crates/ecstore/src/heal/heal_commands.rs b/crates/ecstore/src/heal/heal_commands.rs deleted file mode 100644 index 3df0f3ba1..000000000 --- a/crates/ecstore/src/heal/heal_commands.rs +++ /dev/null @@ -1,544 +0,0 @@ -// 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::{ - collections::{HashMap, HashSet}, - path::Path, - time::SystemTime, -}; - -use crate::{ - config::storageclass::{RRS, STANDARD}, - disk::{BUCKET_META_PREFIX, DeleteOptions, DiskAPI, DiskStore, RUSTFS_META_BUCKET, error::DiskError, fs::read_file}, - global::GLOBAL_BackgroundHealState, - heal::heal_ops::HEALING_TRACKER_FILENAME, - new_object_layer_fn, - store_api::{BucketInfo, StorageAPI}, -}; -use crate::{disk, error::Result}; -use chrono::{DateTime, Utc}; -use lazy_static::lazy_static; -use serde::{Deserialize, Serialize}; -use time::OffsetDateTime; -use tokio::sync::RwLock; - -use super::{background_heal_ops::get_local_disks_to_heal, heal_ops::BG_HEALING_UUID}; - -pub type HealScanMode = usize; - -pub const HEAL_UNKNOWN_SCAN: HealScanMode = 0; -pub const HEAL_NORMAL_SCAN: HealScanMode = 1; -pub const HEAL_DEEP_SCAN: HealScanMode = 2; - -pub const HEAL_ITEM_METADATA: &str = "metadata"; -pub const HEAL_ITEM_BUCKET: &str = "bucket"; -pub const HEAL_ITEM_BUCKET_METADATA: &str = "bucket-metadata"; -pub const HEAL_ITEM_OBJECT: &str = "object"; - -pub const DRIVE_STATE_OK: &str = "ok"; -pub const DRIVE_STATE_OFFLINE: &str = "offline"; -pub const DRIVE_STATE_CORRUPT: &str = "corrupt"; -pub const DRIVE_STATE_MISSING: &str = "missing"; -pub const DRIVE_STATE_PERMISSION: &str = "permission-denied"; -pub const DRIVE_STATE_FAULTY: &str = "faulty"; -pub const DRIVE_STATE_ROOT_MOUNT: &str = "root-mount"; -pub const DRIVE_STATE_UNKNOWN: &str = "unknown"; -pub const DRIVE_STATE_UNFORMATTED: &str = "unformatted"; // only returned by disk - -lazy_static! { - pub static ref TIME_SENTINEL: OffsetDateTime = OffsetDateTime::from_unix_timestamp(0).unwrap(); -} - -#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)] -pub struct HealOpts { - pub recursive: bool, - #[serde(rename = "dryRun")] - pub dry_run: bool, - pub remove: bool, - pub recreate: bool, - #[serde(rename = "scanMode")] - pub scan_mode: HealScanMode, - #[serde(rename = "updateParity")] - pub update_parity: bool, - #[serde(rename = "nolock")] - pub no_lock: bool, - pub pool: Option, - pub set: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct HealStartSuccess { - #[serde(rename = "clientToken")] - pub client_token: String, - #[serde(rename = "clientAddress")] - pub client_address: String, - #[serde(rename = "startTime")] - pub start_time: DateTime, -} - -impl Default for HealStartSuccess { - fn default() -> Self { - Self { - client_token: Default::default(), - client_address: Default::default(), - start_time: Utc::now(), - } - } -} - -pub type HealStopSuccess = HealStartSuccess; - -#[derive(Debug, Default, Deserialize, Serialize)] -pub struct HealingTracker { - #[serde(skip_serializing, skip_deserializing)] - pub disk: Option, - pub id: String, - pub pool_index: Option, - pub set_index: Option, - pub disk_index: Option, - pub path: String, - pub endpoint: String, - pub started: Option, - pub last_update: Option, - pub objects_total_count: u64, - pub objects_total_size: u64, - pub items_healed: u64, - pub items_failed: u64, - pub item_skipped: u64, - pub bytes_done: u64, - pub bytes_failed: u64, - pub bytes_skipped: u64, - pub bucket: String, - pub object: String, - pub resume_items_healed: u64, - pub resume_items_failed: u64, - pub resume_items_skipped: u64, - pub resume_bytes_done: u64, - pub resume_bytes_failed: u64, - pub resume_bytes_skipped: u64, - pub queue_buckets: Vec, - pub healed_buckets: Vec, - pub heal_id: String, - pub retry_attempts: u64, - pub finished: bool, - #[serde(skip_serializing, skip_deserializing)] - pub mu: RwLock, -} - -impl HealingTracker { - pub fn marshal_msg(&self) -> disk::error::Result> { - Ok(serde_json::to_vec(self)?) - } - - pub fn unmarshal_msg(data: &[u8]) -> disk::error::Result { - Ok(serde_json::from_slice::(data)?) - } - - pub async fn reset_healing(&mut self) { - let _ = self.mu.write().await; - self.items_healed = 0; - self.items_failed = 0; - self.bytes_done = 0; - self.bytes_failed = 0; - self.resume_items_healed = 0; - self.resume_items_failed = 0; - self.resume_bytes_done = 0; - self.resume_bytes_failed = 0; - self.item_skipped = 0; - self.bytes_skipped = 0; - - self.healed_buckets = Vec::new(); - self.bucket = String::new(); - self.object = String::new(); - } - - pub async fn get_last_update(&self) -> Option { - let _ = self.mu.read().await; - - self.last_update - } - - pub async fn get_bucket(&self) -> String { - let _ = self.mu.read().await; - - self.bucket.clone() - } - - pub async fn set_bucket(&mut self, bucket: &str) { - let _ = self.mu.write().await; - - self.bucket = bucket.to_string(); - } - - pub async fn get_object(&self) -> String { - let _ = self.mu.read().await; - - self.object.clone() - } - - pub async fn set_object(&mut self, object: &str) { - let _ = self.mu.write().await; - - self.object = object.to_string(); - } - - pub async fn update_progress(&mut self, success: bool, skipped: bool, by: u64) { - let _ = self.mu.write().await; - - if success { - self.items_healed += 1; - self.bytes_done += by; - } else if skipped { - self.item_skipped += 1; - self.bytes_skipped += by; - } else { - self.items_failed += 1; - self.bytes_failed += by; - } - } - - pub async fn update(&mut self) -> disk::error::Result<()> { - if let Some(disk) = &self.disk { - if healing(disk.path().to_string_lossy().as_ref()).await?.is_none() { - return Err(DiskError::other(format!("healingTracker: drive {} is not marked as healing", self.id))); - } - let _ = self.mu.write().await; - if self.id.is_empty() || self.pool_index.is_none() || self.set_index.is_none() || self.disk_index.is_none() { - self.id = disk.get_disk_id().await?.map_or("".to_string(), |id| id.to_string()); - let disk_location = disk.get_disk_location(); - self.pool_index = disk_location.pool_idx; - self.set_index = disk_location.set_idx; - self.disk_index = disk_location.disk_idx; - } - } - - self.save().await - } - - pub async fn save(&mut self) -> disk::error::Result<()> { - let _ = self.mu.write().await; - if self.pool_index.is_none() || self.set_index.is_none() || self.disk_index.is_none() { - let Some(store) = new_object_layer_fn() else { - return Err(DiskError::other("errServerNotInitialized")); - }; - - // TODO: check error type - (self.pool_index, self.set_index, self.disk_index) = - store.get_pool_and_set(&self.id).await.map_err(|_| DiskError::DiskNotFound)?; - } - - self.last_update = Some(SystemTime::now()); - - let htracker_bytes = self.marshal_msg()?; - - GLOBAL_BackgroundHealState.update_heal_status(self).await; - - if let Some(disk) = &self.disk { - let file_path = Path::new(BUCKET_META_PREFIX).join(HEALING_TRACKER_FILENAME); - disk.write_all(RUSTFS_META_BUCKET, file_path.to_str().unwrap(), htracker_bytes.into()) - .await?; - } - Ok(()) - } - - pub async fn delete(&self) -> Result<()> { - if let Some(disk) = &self.disk { - let file_path = Path::new(BUCKET_META_PREFIX).join(HEALING_TRACKER_FILENAME); - disk.delete( - RUSTFS_META_BUCKET, - file_path.to_str().unwrap(), - DeleteOptions { - recursive: false, - immediate: false, - ..Default::default() - }, - ) - .await?; - } - - Ok(()) - } - - pub async fn is_healed(&self, bucket: &str) -> bool { - let _ = self.mu.read().await; - for v in self.healed_buckets.iter() { - if v == bucket { - return true; - } - } - - false - } - - pub async fn resume(&mut self) { - let _ = self.mu.write().await; - - self.items_healed = self.resume_items_healed; - self.items_failed = self.resume_items_failed; - self.item_skipped = self.resume_items_skipped; - self.bytes_done = self.resume_bytes_done; - self.bytes_failed = self.resume_bytes_failed; - self.bytes_skipped = self.resume_bytes_skipped; - } - - pub async fn bucket_done(&mut self, bucket: &str) { - let _ = self.mu.write().await; - - self.resume_items_healed = self.items_healed; - self.resume_items_failed = self.items_failed; - self.resume_items_skipped = self.item_skipped; - self.resume_bytes_done = self.bytes_done; - self.resume_bytes_failed = self.bytes_failed; - self.resume_bytes_skipped = self.bytes_skipped; - self.healed_buckets.push(bucket.to_string()); - - self.queue_buckets.retain(|x| x != bucket); - } - - pub async fn set_queue_buckets(&mut self, buckets: &[BucketInfo]) { - let _ = self.mu.write().await; - - buckets.iter().for_each(|bucket| { - if !self.healed_buckets.contains(&bucket.name) { - self.queue_buckets.push(bucket.name.clone()); - } - }); - } - - pub async fn to_healing_disk(&self) -> rustfs_madmin::HealingDisk { - let _ = self.mu.read().await; - - rustfs_madmin::HealingDisk { - id: self.id.clone(), - heal_id: self.heal_id.clone(), - pool_index: self.pool_index, - set_index: self.set_index, - disk_index: self.disk_index, - endpoint: self.endpoint.clone(), - path: self.path.clone(), - started: self.started, - last_update: self.last_update, - retry_attempts: self.retry_attempts, - objects_total_count: self.objects_total_count, - objects_total_size: self.objects_total_size, - items_healed: self.items_healed, - items_failed: self.items_failed, - item_skipped: self.item_skipped, - bytes_done: self.bytes_done, - bytes_failed: self.bytes_failed, - bytes_skipped: self.bytes_skipped, - objects_healed: self.items_healed, - objects_failed: self.items_failed, - bucket: self.bucket.clone(), - object: self.object.clone(), - queue_buckets: self.queue_buckets.clone(), - healed_buckets: self.healed_buckets.clone(), - finished: self.finished, - } - } -} - -impl Clone for HealingTracker { - fn clone(&self) -> Self { - Self { - disk: self.disk.clone(), - id: self.id.clone(), - pool_index: self.pool_index, - set_index: self.set_index, - disk_index: self.disk_index, - path: self.path.clone(), - endpoint: self.endpoint.clone(), - started: self.started, - last_update: self.last_update, - objects_total_count: self.objects_total_count, - objects_total_size: self.objects_total_size, - items_healed: self.items_healed, - items_failed: self.items_failed, - item_skipped: self.item_skipped, - bytes_done: self.bytes_done, - bytes_failed: self.bytes_failed, - bytes_skipped: self.bytes_skipped, - bucket: self.bucket.clone(), - object: self.object.clone(), - resume_items_healed: self.resume_items_healed, - resume_items_failed: self.resume_items_failed, - resume_items_skipped: self.resume_items_skipped, - resume_bytes_done: self.resume_bytes_done, - resume_bytes_failed: self.resume_bytes_failed, - resume_bytes_skipped: self.resume_bytes_skipped, - queue_buckets: self.queue_buckets.clone(), - healed_buckets: self.healed_buckets.clone(), - heal_id: self.heal_id.clone(), - retry_attempts: self.retry_attempts, - finished: self.finished, - mu: RwLock::new(false), - } - } -} - -pub async fn load_healing_tracker(disk: &Option) -> disk::error::Result { - if let Some(disk) = disk { - let disk_id = disk.get_disk_id().await?; - if let Some(disk_id) = disk_id { - let disk_id = disk_id.to_string(); - let file_path = Path::new(BUCKET_META_PREFIX).join(HEALING_TRACKER_FILENAME); - let data = disk.read_all(RUSTFS_META_BUCKET, file_path.to_str().unwrap()).await?; - let mut healing_tracker = HealingTracker::unmarshal_msg(&data)?; - if healing_tracker.id != disk_id && !healing_tracker.id.is_empty() { - return Err(DiskError::other(format!( - "loadHealingTracker: drive id mismatch expected {}, got {}", - healing_tracker.id, disk_id - ))); - } - healing_tracker.id = disk_id; - healing_tracker.disk = Some(disk.clone()); - Ok(healing_tracker) - } else { - Err(DiskError::other("loadHealingTracker: disk not have id")) - } - } else { - Err(DiskError::other("loadHealingTracker: nil drive given")) - } -} - -pub async fn init_healing_tracker(disk: DiskStore, heal_id: &str) -> disk::error::Result { - let disk_location = disk.get_disk_location(); - Ok(HealingTracker { - id: disk - .get_disk_id() - .await - .map_or("".to_string(), |id| id.map_or("".to_string(), |id| id.to_string())), - heal_id: heal_id.to_string(), - path: disk.to_string(), - endpoint: disk.endpoint().to_string(), - started: Some(OffsetDateTime::now_utc()), - pool_index: disk_location.pool_idx, - set_index: disk_location.set_idx, - disk_index: disk_location.disk_idx, - disk: Some(disk), - ..Default::default() - }) -} - -pub async fn healing(derive_path: &str) -> disk::error::Result> { - let healing_file = Path::new(derive_path) - .join(RUSTFS_META_BUCKET) - .join(BUCKET_META_PREFIX) - .join(HEALING_TRACKER_FILENAME); - - let b = read_file(healing_file).await?; - if b.is_empty() { - return Ok(None); - } - - let healing_tracker = HealingTracker::unmarshal_msg(&b)?; - - Ok(Some(healing_tracker)) -} - -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct MRFStatus { - bytes_healed: u64, - items_healed: u64, -} - -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct SetStatus { - pub id: String, - pub pool_index: i32, - pub set_index: i32, - pub heal_status: String, - pub heal_priority: String, - pub total_objects: usize, - pub disks: Vec, -} - -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct BgHealState { - offline_endpoints: Vec, - scanned_items_count: u64, - heal_disks: Vec, - sets: Vec, - mrf: HashMap, - scparity: HashMap, -} - -pub async fn get_local_background_heal_status() -> (BgHealState, bool) { - let (bg_seq, ok) = GLOBAL_BackgroundHealState.get_heal_sequence_by_token(BG_HEALING_UUID).await; - if !ok { - return (BgHealState::default(), false); - } - let bg_seq = bg_seq.unwrap(); - let mut status = BgHealState { - scanned_items_count: bg_seq.get_scanned_items_count().await as u64, - ..Default::default() - }; - let mut heal_disks_map = HashSet::new(); - for ep in get_local_disks_to_heal().await.iter() { - heal_disks_map.insert(ep.to_string()); - } - - let Some(store) = new_object_layer_fn() else { - let healing = GLOBAL_BackgroundHealState.get_local_healing_disks().await; - for disk in healing.values() { - status.heal_disks.push(disk.endpoint.clone()); - } - return (status, true); - }; - - let si = store.local_storage_info().await; - let mut indexed = HashMap::new(); - for disk in si.disks.iter() { - let set_idx = format!("{}-{}", disk.pool_index, disk.set_index); - // indexed.insert(set_idx, disk); - indexed.entry(set_idx).or_insert(Vec::new()).push(disk); - } - - for (id, disks) in indexed { - let mut ss = SetStatus { - id, - set_index: disks[0].set_index, - pool_index: disks[0].pool_index, - ..Default::default() - }; - for disk in disks { - ss.disks.push(disk.clone()); - if disk.healing { - ss.heal_status = "healing".to_string(); - ss.heal_priority = "high".to_string(); - status.heal_disks.push(disk.endpoint.clone()); - } - } - ss.disks.sort_by(|a, b| { - if a.pool_index != b.pool_index { - return a.pool_index.cmp(&b.pool_index); - } - if a.set_index != b.set_index { - return a.set_index.cmp(&b.set_index); - } - a.disk_index.cmp(&b.disk_index) - }); - status.sets.push(ss); - } - status.sets.sort_by(|a, b| a.id.cmp(&b.id)); - let backend_info = store.backend_info().await; - status - .scparity - .insert(STANDARD.to_string(), backend_info.standard_sc_parity.unwrap_or_default()); - status - .scparity - .insert(RRS.to_string(), backend_info.rr_sc_parity.unwrap_or_default()); - - (status, true) -} diff --git a/crates/ecstore/src/heal/heal_ops.rs b/crates/ecstore/src/heal/heal_ops.rs deleted file mode 100644 index 2ee4aee10..000000000 --- a/crates/ecstore/src/heal/heal_ops.rs +++ /dev/null @@ -1,842 +0,0 @@ -// 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 super::{ - background_heal_ops::HealTask, - data_scanner::HEAL_DELETE_DANGLING, - error::ERR_SKIP_FILE, - heal_commands::{HEAL_ITEM_BUCKET_METADATA, HealOpts, HealScanMode, HealStopSuccess, HealingTracker}, -}; -use crate::error::{Error, Result}; -use crate::heal::heal_commands::{HEAL_ITEM_BUCKET, HEAL_ITEM_OBJECT}; -use crate::store_api::StorageAPI; -use crate::{ - config::com::CONFIG_PREFIX, - disk::RUSTFS_META_BUCKET, - global::GLOBAL_BackgroundHealRoutine, - heal::{error::ERR_HEAL_STOP_SIGNALLED, heal_commands::DRIVE_STATE_OK}, -}; -use crate::{ - disk::endpoint::Endpoint, - endpoints::Endpoints, - global::GLOBAL_IsDistErasure, - heal::heal_commands::{HEAL_UNKNOWN_SCAN, HealStartSuccess}, - new_object_layer_fn, -}; -use chrono::Utc; -use futures::join; -use lazy_static::lazy_static; -use rustfs_filemeta::MetaCacheEntry; -use rustfs_madmin::heal_commands::{HealDriveInfo, HealItemType, HealResultItem}; -use rustfs_utils::path::has_prefix; -use rustfs_utils::path::path_join; -use serde::{Deserialize, Serialize}; -use std::{ - collections::HashMap, - future::Future, - path::PathBuf, - pin::Pin, - sync::Arc, - time::{Duration, SystemTime, UNIX_EPOCH}, -}; -use tokio::{ - select, spawn, - sync::{ - RwLock, broadcast, - mpsc::{self, Receiver as M_Receiver, Sender as M_Sender}, - watch::{self, Receiver as W_Receiver, Sender as W_Sender}, - }, - time::{interval, sleep}, -}; -use tracing::{error, info}; -use uuid::Uuid; - -type HealStatusSummary = String; -type ItemsMap = HashMap; -pub type HealEntryFn = - Arc Pin> + Send>> + Send + Sync + 'static>; - -pub const BG_HEALING_UUID: &str = "0000-0000-0000-0000"; -pub const HEALING_TRACKER_FILENAME: &str = ".healing.bin"; -const KEEP_HEAL_SEQ_STATE_DURATION: Duration = Duration::from_secs(10 * 60); -const HEAL_NOT_STARTED_STATUS: &str = "not started"; -const HEAL_RUNNING_STATUS: &str = "running"; -const HEAL_STOPPED_STATUS: &str = "stopped"; -const HEAL_FINISHED_STATUS: &str = "finished"; - -pub const RUSTFS_RESERVED_BUCKET: &str = "rustfs"; -pub const RUSTFS_RESERVED_BUCKET_PATH: &str = "/rustfs"; -pub const LOGIN_PATH_PREFIX: &str = "/login"; - -const MAX_UNCONSUMED_HEAL_RESULT_ITEMS: usize = 1000; -const HEAL_UNCONSUMED_TIMEOUT: Duration = Duration::from_secs(24 * 60 * 60); -pub const NOP_HEAL: &str = ""; - -lazy_static! {} - -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct HealSequenceStatus { - pub summary: HealStatusSummary, - pub failure_detail: String, - pub start_time: u64, - pub heal_setting: HealOpts, - pub items: Vec, -} - -#[derive(Debug, Default)] -pub struct HealSource { - pub bucket: String, - pub object: String, - pub version_id: String, - pub no_wait: bool, - pub opts: Option, -} - -#[derive(Debug)] -pub struct HealSequence { - pub bucket: String, - pub object: String, - pub report_progress: bool, - pub start_time: SystemTime, - pub end_time: Arc>, - pub client_token: String, - pub client_address: String, - pub force_started: bool, - pub setting: HealOpts, - pub current_status: Arc>, - pub last_sent_result_index: RwLock, - pub scanned_items_map: RwLock, - pub healed_items_map: RwLock, - pub heal_failed_items_map: RwLock, - pub last_heal_activity: RwLock, - - traverse_and_heal_done_tx: Arc>>>, - traverse_and_heal_done_rx: Arc>>>, - - tx: W_Sender, - rx: W_Receiver, -} - -pub fn new_bg_heal_sequence() -> HealSequence { - let hs = HealOpts { - remove: HEAL_DELETE_DANGLING, - ..Default::default() - }; - - HealSequence { - start_time: SystemTime::now(), - client_token: BG_HEALING_UUID.to_string(), - bucket: RUSTFS_RESERVED_BUCKET.to_string(), - setting: hs, - current_status: Arc::new(RwLock::new(HealSequenceStatus { - summary: HEAL_NOT_STARTED_STATUS.to_string(), - heal_setting: hs, - ..Default::default() - })), - report_progress: false, - scanned_items_map: HashMap::new().into(), - healed_items_map: HashMap::new().into(), - heal_failed_items_map: HashMap::new().into(), - ..Default::default() - } -} - -pub fn new_heal_sequence(bucket: &str, obj_prefix: &str, client_addr: &str, hs: HealOpts, force_start: bool) -> HealSequence { - let client_token = Uuid::new_v4().to_string(); - let (tx, rx) = mpsc::channel(10); - HealSequence { - bucket: bucket.to_string(), - object: obj_prefix.to_string(), - report_progress: true, - start_time: SystemTime::now(), - client_token, - client_address: client_addr.to_string(), - force_started: force_start, - setting: hs, - current_status: Arc::new(RwLock::new(HealSequenceStatus { - summary: HEAL_NOT_STARTED_STATUS.to_string(), - heal_setting: hs, - ..Default::default() - })), - traverse_and_heal_done_tx: Arc::new(RwLock::new(tx)), - traverse_and_heal_done_rx: Arc::new(RwLock::new(rx)), - scanned_items_map: HashMap::new().into(), - healed_items_map: HashMap::new().into(), - heal_failed_items_map: HashMap::new().into(), - ..Default::default() - } -} - -impl Default for HealSequence { - fn default() -> Self { - let (h_tx, h_rx) = mpsc::channel(1); - let (tx, rx) = watch::channel(false); - Self { - bucket: Default::default(), - object: Default::default(), - report_progress: Default::default(), - start_time: SystemTime::now(), - end_time: Arc::new(RwLock::new(SystemTime::now())), - client_token: Default::default(), - client_address: Default::default(), - force_started: Default::default(), - setting: Default::default(), - current_status: Default::default(), - last_sent_result_index: Default::default(), - scanned_items_map: Default::default(), - healed_items_map: Default::default(), - heal_failed_items_map: Default::default(), - last_heal_activity: RwLock::new(SystemTime::now()), - traverse_and_heal_done_tx: Arc::new(RwLock::new(h_tx)), - traverse_and_heal_done_rx: Arc::new(RwLock::new(h_rx)), - tx, - rx, - } - } -} - -impl HealSequence { - pub fn new(bucket: &str, obj_prefix: &str, client_addr: &str, hs: HealOpts, force_start: bool) -> Self { - let client_token = Uuid::new_v4().to_string(); - - Self { - bucket: bucket.to_string(), - object: obj_prefix.to_string(), - report_progress: true, - client_token, - client_address: client_addr.to_string(), - force_started: force_start, - setting: hs, - current_status: Arc::new(RwLock::new(HealSequenceStatus { - summary: HEAL_NOT_STARTED_STATUS.to_string(), - heal_setting: hs, - ..Default::default() - })), - ..Default::default() - } - } -} - -impl HealSequence { - pub async fn get_scanned_items_count(&self) -> usize { - self.scanned_items_map.read().await.values().sum() - } - - async fn _get_scanned_items_map(&self) -> ItemsMap { - self.scanned_items_map.read().await.clone() - } - - async fn _get_healed_items_map(&self) -> ItemsMap { - self.healed_items_map.read().await.clone() - } - - async fn _get_heal_failed_items_map(&self) -> ItemsMap { - self.heal_failed_items_map.read().await.clone() - } - - pub async fn count_failed(&self, heal_type: HealItemType) { - *self.heal_failed_items_map.write().await.entry(heal_type).or_insert(0) += 1; - *self.last_heal_activity.write().await = SystemTime::now(); - } - - pub async fn count_scanned(&self, heal_type: HealItemType) { - *self.scanned_items_map.write().await.entry(heal_type).or_insert(0) += 1; - *self.last_heal_activity.write().await = SystemTime::now(); - } - - pub async fn count_healed(&self, heal_type: HealItemType) { - *self.healed_items_map.write().await.entry(heal_type).or_insert(0) += 1; - *self.last_heal_activity.write().await = SystemTime::now(); - } - - async fn is_quitting(&self) -> bool { - if let Ok(true) = self.rx.has_changed() { - info!("quited"); - return true; - } - false - } - - async fn has_ended(&self) -> bool { - if self.client_token == *BG_HEALING_UUID { - return false; - } - - *(self.end_time.read().await) != self.start_time - } - - async fn stop(&self) { - let _ = self.tx.send(true); - } - - async fn push_heal_result_item(&self, r: &HealResultItem) -> Result<()> { - let mut r = r.clone(); - let mut interval_timer = interval(HEAL_UNCONSUMED_TIMEOUT); - #[allow(unused_assignments)] - let mut items_len = 0; - loop { - { - let current_status_r = self.current_status.read().await; - items_len = current_status_r.items.len(); - } - - if items_len == MAX_UNCONSUMED_HEAL_RESULT_ITEMS { - select! { - _ = sleep(Duration::from_secs(1)) => { - - } - _ = self.is_done() => { - return Err(Error::other("stopped")); - } - _ = interval_timer.tick() => { - return Err(Error::other("timeout")); - } - } - } else { - break; - } - } - - let mut current_status_w = self.current_status.write().await; - if items_len > 0 { - r.result_index = 1 + current_status_w.items[items_len - 1].result_index; - } else { - r.result_index = 1 + *self.last_sent_result_index.read().await; - } - - current_status_w.items.push(r); - - Ok(()) - } - - pub async fn queue_heal_task(&self, source: HealSource, heal_type: HealItemType) -> Result<()> { - let mut task = HealTask::new(&source.bucket, &source.object, &source.version_id, &self.setting); - info!("queue_heal_task, {:?}", task); - if let Some(opts) = source.opts { - task.opts = opts; - } else { - task.opts.scan_mode = HEAL_UNKNOWN_SCAN; - } - - self.count_scanned(heal_type.clone()).await; - - if source.no_wait { - let task_str = format!("{task:?}"); - if GLOBAL_BackgroundHealRoutine.tasks_tx.try_send(task).is_ok() { - info!("Task in the queue: {:?}", task_str); - } - return Ok(()); - } - - let (resp_tx, mut resp_rx) = mpsc::channel(1); - task.resp_tx = Some(resp_tx); - - let task_str = format!("{task:?}"); - if GLOBAL_BackgroundHealRoutine.tasks_tx.try_send(task).is_ok() { - info!("Task in the queue: {:?}", task_str); - } else { - error!("push task to queue failed"); - } - let count_ok_drives = |drivers: &[HealDriveInfo]| { - let mut count = 0; - for drive in drivers.iter() { - if drive.state == DRIVE_STATE_OK { - count += 1; - } - } - count - }; - - match resp_rx.recv().await { - Some(mut res) => { - if res.err.is_none() { - self.count_healed(heal_type.clone()).await; - } else { - self.count_failed(heal_type.clone()).await; - } - if !self.report_progress { - return if let Some(err) = res.err { - if err.to_string() == ERR_SKIP_FILE { - return Ok(()); - } - Err(err) - } else { - Ok(()) - }; - } - res.result.heal_item_type = heal_type.clone(); - if let Some(err) = res.err.as_ref() { - res.result.detail = err.to_string(); - } - if res.result.parity_blocks > 0 && res.result.data_blocks > 0 && res.result.data_blocks > res.result.parity_blocks - { - let got = count_ok_drives(&res.result.after.drives); - if got < res.result.parity_blocks { - res.result.detail = format!( - "quorum loss - expected {} minimum, got drive states in OK {}", - res.result.parity_blocks, got - ); - } - } - - info!("queue_heal_task, HealResult: {:?}", res); - self.push_heal_result_item(&res.result).await - } - None => Ok(()), - } - } - - async fn heal_disk_meta(h: Arc) -> Result<()> { - HealSequence::heal_rustfs_sys_meta(h, CONFIG_PREFIX).await - } - - async fn heal_items(h: Arc, buckets_only: bool) -> Result<()> { - if h.client_token == *BG_HEALING_UUID { - return Ok(()); - } - - let bucket = h.bucket.clone(); - let task1 = Self::heal_disk_meta(h.clone()); - let task2 = Self::heal_bucket(h.clone(), &bucket, buckets_only); - let results = join!(task1, task2); - results.0?; - results.1?; - - Ok(()) - } - - async fn traverse_and_heal(h: Arc) { - let buckets_only = false; - let result = Self::heal_items(h.clone(), buckets_only).await.err(); - let _ = h.traverse_and_heal_done_tx.read().await.send(result).await; - } - - async fn heal_rustfs_sys_meta(h: Arc, meta_prefix: &str) -> Result<()> { - info!("heal_rustfs_sys_meta, h: {:?}", h); - let Some(store) = new_object_layer_fn() else { - return Err(Error::other("errServerNotInitialized")); - }; - let setting = h.setting; - store - .heal_objects(RUSTFS_META_BUCKET, meta_prefix, &setting, h.clone(), true) - .await - } - - async fn is_done(&self) -> bool { - if let Ok(true) = self.rx.has_changed() { - return true; - } - false - } - - pub async fn heal_bucket(hs: Arc, bucket: &str, bucket_only: bool) -> Result<()> { - info!("heal_bucket, hs: {:?}", hs); - let (object, setting) = { - hs.queue_heal_task( - HealSource { - bucket: bucket.to_string(), - ..Default::default() - }, - HEAL_ITEM_BUCKET.to_string(), - ) - .await?; - - if bucket_only { - return Ok(()); - } - - if !hs.setting.recursive { - if !hs.object.is_empty() { - HealSequence::heal_object(hs.clone(), bucket, &hs.object, "", hs.setting.scan_mode).await?; - } - return Ok(()); - } - (hs.object.clone(), hs.setting) - }; - let Some(store) = new_object_layer_fn() else { - return Err(Error::other("errServerNotInitialized")); - }; - store.heal_objects(bucket, &object, &setting, hs.clone(), false).await - } - - pub async fn heal_object( - hs: Arc, - bucket: &str, - object: &str, - version_id: &str, - _scan_mode: HealScanMode, - ) -> Result<()> { - info!("heal_object"); - if hs.is_quitting().await { - info!("heal_object hs is quitting"); - return Err(Error::other(ERR_HEAL_STOP_SIGNALLED)); - } - - info!("will queue task"); - hs.queue_heal_task( - HealSource { - bucket: bucket.to_string(), - object: object.to_string(), - version_id: version_id.to_string(), - opts: Some(hs.setting), - ..Default::default() - }, - HEAL_ITEM_OBJECT.to_string(), - ) - .await?; - - Ok(()) - } - - pub async fn heal_meta_object( - hs: Arc, - bucket: &str, - object: &str, - version_id: &str, - _scan_mode: HealScanMode, - ) -> Result<()> { - if hs.is_quitting().await { - return Err(Error::other(ERR_HEAL_STOP_SIGNALLED)); - } - - hs.queue_heal_task( - HealSource { - bucket: bucket.to_string(), - object: object.to_string(), - version_id: version_id.to_string(), - ..Default::default() - }, - HEAL_ITEM_BUCKET_METADATA.to_string(), - ) - .await?; - - Ok(()) - } -} - -pub async fn heal_sequence_start(h: Arc) { - { - let mut current_status_w = h.current_status.write().await; - current_status_w.summary = HEAL_RUNNING_STATUS.to_string(); - current_status_w.start_time = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - } - - let h_clone = h.clone(); - spawn(async move { - HealSequence::traverse_and_heal(h_clone).await; - }); - - let h_clone_1 = h.clone(); - let mut x = h.traverse_and_heal_done_rx.write().await; - select! { - _ = h.is_done() => { - *(h.end_time.write().await) = SystemTime::now(); - let mut current_status_w = h.current_status.write().await; - current_status_w.summary = HEAL_FINISHED_STATUS.to_string(); - - spawn(async move { - let mut rx_w = h_clone_1.traverse_and_heal_done_rx.write().await; - rx_w.recv().await; - }); - } - result = x.recv() => { - if let Some(err) = result { - match err { - Some(err) => { - let mut current_status_w = h.current_status.write().await; - current_status_w.summary = HEAL_STOPPED_STATUS.to_string(); - current_status_w.failure_detail = err.to_string(); - }, - None => { - let mut current_status_w = h.current_status.write().await; - current_status_w.summary = HEAL_FINISHED_STATUS.to_string(); - } - } - } - - } - } -} - -#[derive(Debug, Default)] -pub struct AllHealState { - mu: RwLock, - - heal_seq_map: RwLock>>, - heal_local_disks: RwLock>, - heal_status: RwLock>, -} - -impl AllHealState { - pub fn new(cleanup: bool) -> Arc { - let state = Arc::new(AllHealState::default()); - let (_, mut rx) = broadcast::channel(1); - if cleanup { - let state_clone = state.clone(); - spawn(async move { - loop { - select! { - result = rx.recv() =>{ - if let Ok(true) = result { - return; - } - } - _ = sleep(Duration::from_secs(5 * 60)) => { - state_clone.periodic_heal_seqs_clean().await; - } - } - } - }); - } - - state - } - - pub async fn pop_heal_local_disks(&self, heal_local_disks: &[Endpoint]) { - let _ = self.mu.write().await; - - self.heal_local_disks.write().await.retain(|k, _| { - if heal_local_disks.contains(k) { - return false; - } - true - }); - - let heal_local_disks = heal_local_disks.iter().map(|s| s.to_string()).collect::>(); - self.heal_status.write().await.retain(|_, v| { - if heal_local_disks.contains(&v.endpoint) { - return false; - } - - true - }); - } - - pub async fn pop_heal_status_json(&self, heal_path: &str, client_token: &str) -> Result> { - match self.get_heal_sequence(heal_path).await { - Some(h) => { - if client_token != h.client_token { - info!("err heal invalid client token"); - return Err(Error::other("err heal invalid client token")); - } - let num_items = h.current_status.read().await.items.len(); - let mut last_result_index = *h.last_sent_result_index.read().await; - if num_items > 0 { - if let Some(item) = h.current_status.read().await.items.last() { - last_result_index = item.result_index; - } - } - *h.last_sent_result_index.write().await = last_result_index; - let data = h.current_status.read().await.clone(); - match serde_json::to_vec(&data) { - Ok(b) => { - h.current_status.write().await.items.clear(); - Ok(b) - } - Err(e) => { - h.current_status.write().await.items.clear(); - info!("json encode err, e: {}", e); - Err(Error::other(e.to_string())) - } - } - } - None => serde_json::to_vec(&HealSequenceStatus { - summary: HEAL_FINISHED_STATUS.to_string(), - ..Default::default() - }) - .map_err(|e| { - info!("json encode err, e: {}", e); - Error::other(e.to_string()) - }), - } - } - - pub async fn update_heal_status(&self, tracker: &HealingTracker) { - let _ = self.mu.write().await; - let _ = tracker.mu.read().await; - - self.heal_status.write().await.insert(tracker.id.clone(), tracker.clone()); - } - - pub async fn get_local_healing_disks(&self) -> HashMap { - let _ = self.mu.read().await; - - let mut dst = HashMap::new(); - for v in self.heal_status.read().await.values() { - dst.insert(v.endpoint.clone(), v.to_healing_disk().await); - } - - dst - } - - pub async fn get_heal_local_disk_endpoints(&self) -> Endpoints { - let _ = self.mu.read().await; - - let mut endpoints = Vec::new(); - self.heal_local_disks.read().await.iter().for_each(|(k, v)| { - if !v { - endpoints.push(k.clone()); - } - }); - - Endpoints::from(endpoints) - } - - pub async fn set_disk_healing_status(&self, ep: Endpoint, healing: bool) { - let _ = self.mu.write().await; - - self.heal_local_disks.write().await.insert(ep, healing); - } - - pub async fn push_heal_local_disks(&self, heal_local_disks: &[Endpoint]) { - let _ = self.mu.write().await; - - for heal_local_disk in heal_local_disks.iter() { - self.heal_local_disks.write().await.insert(heal_local_disk.clone(), false); - } - } - - pub async fn periodic_heal_seqs_clean(&self) { - let _ = self.mu.write().await; - let now = SystemTime::now(); - - let mut keys_to_remove = Vec::new(); - for (k, v) in self.heal_seq_map.read().await.iter() { - if v.has_ended().await && now.duration_since(*(v.end_time.read().await)).unwrap() > KEEP_HEAL_SEQ_STATE_DURATION { - keys_to_remove.push(k.clone()) - } - } - for key in keys_to_remove.iter() { - self.heal_seq_map.write().await.remove(key); - } - } - - pub async fn get_heal_sequence_by_token(&self, token: &str) -> (Option>, bool) { - let _ = self.mu.read().await; - - for v in self.heal_seq_map.read().await.values() { - if v.client_token == token { - return (Some(v.clone()), true); - } - } - - (None, false) - } - - pub async fn get_heal_sequence(&self, path: &str) -> Option> { - let _ = self.mu.read().await; - - self.heal_seq_map.read().await.get(path).cloned() - } - - pub async fn stop_heal_sequence(&self, path: &str) -> Result> { - let mut hsp = HealStopSuccess::default(); - if let Some(he) = self.get_heal_sequence(path).await { - let client_token = he.client_token.clone(); - if *GLOBAL_IsDistErasure.read().await { - // TODO: proxy - } - - hsp.client_token = client_token; - hsp.client_address = he.client_address.clone(); - hsp.start_time = Utc::now(); - - he.stop().await; - - loop { - if he.has_ended().await { - break; - } - - sleep(Duration::from_secs(1)).await; - } - - let _ = self.mu.write().await; - self.heal_seq_map.write().await.remove(path); - } else { - hsp.client_token = "unknown".to_string(); - } - - let b = serde_json::to_string(&hsp)?; - Ok(b.as_bytes().to_vec()) - } - - // LaunchNewHealSequence - launches a background routine that performs - // healing according to the healSequence argument. For each heal - // sequence, state is stored in the `globalAllHealState`, which is a - // map of the heal path to `healSequence` which holds state about the - // heal sequence. - // - // Heal results are persisted in server memory for - // `keepHealSeqStateDuration`. This function also launches a - // background routine to clean up heal results after the - // aforementioned duration. - pub async fn launch_new_heal_sequence(&self, heal_sequence: Arc) -> Result> { - let path = path_join(&[ - PathBuf::from(heal_sequence.bucket.clone()), - PathBuf::from(heal_sequence.object.clone()), - ]); - let path_s = path.to_str().unwrap(); - if heal_sequence.force_started { - self.stop_heal_sequence(path_s).await?; - } else if let Some(hs) = self.get_heal_sequence(path_s).await { - if !hs.has_ended().await { - return Err(Error::other(format!( - "Heal is already running on the given path (use force-start option to stop and start afresh). The heal was started by IP {} at {:?}, token is {}", - heal_sequence.client_address, heal_sequence.start_time, heal_sequence.client_token - ))); - } - } - - let _ = self.mu.write().await; - - for (k, v) in self.heal_seq_map.read().await.iter() { - if (has_prefix(k, path_s) || has_prefix(path_s, k)) && !v.has_ended().await { - return Err(Error::other(format!( - "The provided heal sequence path overlaps with an existing heal path: {k}" - ))); - } - } - - self.heal_seq_map - .write() - .await - .insert(path_s.to_string(), heal_sequence.clone()); - - let client_token = heal_sequence.client_token.clone(); - if *GLOBAL_IsDistErasure.read().await { - // TODO: proxy - } - - if heal_sequence.client_token == BG_HEALING_UUID { - // For background heal do nothing, do not spawn an unnecessary goroutine. - } else { - let heal_sequence_clone = heal_sequence.clone(); - spawn(async { - heal_sequence_start(heal_sequence_clone).await; - }); - } - - let b = serde_json::to_vec(&HealStartSuccess { - client_token, - client_address: heal_sequence.client_address.clone(), - // start_time: Utc::now(), - start_time: heal_sequence.start_time.into(), - })?; - Ok(b) - } -} diff --git a/crates/ecstore/src/heal/mod.rs b/crates/ecstore/src/heal/mod.rs deleted file mode 100644 index 6b34dc620..000000000 --- a/crates/ecstore/src/heal/mod.rs +++ /dev/null @@ -1,23 +0,0 @@ -// 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. - -// pub mod background_heal_ops; -// pub mod data_scanner; -// pub mod data_scanner_metric; -// pub mod data_usage; -// pub mod data_usage_cache; -// pub mod error; -// pub mod heal_commands; -// pub mod heal_ops; -// pub mod mrf; diff --git a/crates/ecstore/src/heal/mrf.rs b/crates/ecstore/src/heal/mrf.rs deleted file mode 100644 index 3f42fa202..000000000 --- a/crates/ecstore/src/heal/mrf.rs +++ /dev/null @@ -1,183 +0,0 @@ -// 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::disk::{BUCKET_META_PREFIX, RUSTFS_META_BUCKET}; -use crate::heal::background_heal_ops::{heal_bucket, heal_object}; -use crate::heal::heal_commands::{HEAL_DEEP_SCAN, HEAL_NORMAL_SCAN}; -use chrono::{DateTime, Utc}; -use lazy_static::lazy_static; -use regex::Regex; -use rustfs_utils::path::SLASH_SEPARATOR; -use std::ops::Sub; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::time::Duration; -use tokio::sync::RwLock; -use tokio::sync::mpsc::{Receiver, Sender}; -use tokio::time::sleep; -use tokio_util::sync::CancellationToken; -use tracing::{error, info}; -use uuid::Uuid; - -pub const MRF_OPS_QUEUE_SIZE: u64 = 100000; -pub const HEAL_DIR: &str = ".heal"; -pub const HEAL_MRFMETA_FORMAT: u64 = 1; -pub const HEAL_MRFMETA_VERSION_V1: u64 = 1; - -lazy_static! { - pub static ref HEAL_MRF_DIR: String = - format!("{}{}{}{}{}", BUCKET_META_PREFIX, SLASH_SEPARATOR, HEAL_DIR, SLASH_SEPARATOR, "mrf"); - static ref PATTERNS: Vec = vec![ - Regex::new(r"^buckets/.*/.metacache/.*").unwrap(), - Regex::new(r"^tmp/.*").unwrap(), - Regex::new(r"^multipart/.*").unwrap(), - Regex::new(r"^tmp-old/.*").unwrap(), - ]; -} - -#[derive(Default)] -pub struct PartialOperation { - pub bucket: String, - pub object: String, - pub version_id: Option, - pub versions: Vec, - pub set_index: usize, - pub pool_index: usize, - pub queued: DateTime, - pub bitrot_scan: bool, -} - -pub struct MRFState { - tx: Sender, - rx: RwLock>, - closed: AtomicBool, - closing: AtomicBool, -} - -impl Default for MRFState { - fn default() -> Self { - Self::new() - } -} - -impl MRFState { - pub fn new() -> MRFState { - let (tx, rx) = tokio::sync::mpsc::channel(MRF_OPS_QUEUE_SIZE as usize); - MRFState { - tx, - rx: RwLock::new(rx), - closed: Default::default(), - closing: Default::default(), - } - } - - pub async fn add_partial(&self, op: PartialOperation) { - if self.closed.load(Ordering::SeqCst) || self.closing.load(Ordering::SeqCst) { - return; - } - let _ = self.tx.send(op).await; - } - - /// Enhanced heal routine with cancellation support - /// - /// This method implements the same healing logic as the original heal_routine, - /// but adds proper cancellation support via CancellationToken. - /// The core logic remains identical to maintain compatibility. - pub async fn heal_routine_with_cancel(&self, cancel_token: CancellationToken) { - info!("MRF heal routine started with cancellation support"); - - loop { - tokio::select! { - _ = cancel_token.cancelled() => { - info!("MRF heal routine received shutdown signal, exiting gracefully"); - break; - } - op_result = async { - let mut rx_guard = self.rx.write().await; - rx_guard.recv().await - } => { - if let Some(op) = op_result { - // Special path filtering (original logic) - if op.bucket == RUSTFS_META_BUCKET { - for pattern in &*PATTERNS { - if pattern.is_match(&op.object) { - continue; // Skip this operation, continue with next - } - } - } - - // Network reconnection delay (original logic) - let now = Utc::now(); - if now.sub(op.queued).num_seconds() < 1 { - tokio::select! { - _ = cancel_token.cancelled() => { - info!("MRF heal routine cancelled during reconnection delay"); - break; - } - _ = sleep(Duration::from_secs(1)) => {} - } - } - - // Core healing logic (original logic preserved) - let scan_mode = if op.bitrot_scan { HEAL_DEEP_SCAN } else { HEAL_NORMAL_SCAN }; - - if op.object.is_empty() { - // Heal bucket (original logic) - if let Err(err) = heal_bucket(&op.bucket).await { - error!("heal bucket failed, bucket: {}, err: {:?}", op.bucket, err); - } - } else if op.versions.is_empty() { - // Heal single object (original logic) - if let Err(err) = heal_object( - &op.bucket, - &op.object, - &op.version_id.clone().unwrap_or_default(), - scan_mode - ).await { - error!("heal object failed, bucket: {}, object: {}, err: {:?}", op.bucket, op.object, err); - } - } else { - // Heal multiple versions (original logic) - let vers = op.versions.len() / 16; - if vers > 0 { - for i in 0..vers { - // Check for cancellation before each version - if cancel_token.is_cancelled() { - info!("MRF heal routine cancelled during version processing"); - return; - } - - let start = i * 16; - let end = start + 16; - if let Err(err) = heal_object( - &op.bucket, - &op.object, - &Uuid::from_slice(&op.versions[start..end]).expect("").to_string(), - scan_mode, - ).await { - error!("heal object failed, bucket: {}, object: {}, err: {:?}", op.bucket, op.object, err); - } - } - } - } - } else { - info!("MRF heal routine channel closed, exiting"); - break; - } - } - } - } - - info!("MRF heal routine stopped gracefully"); - } -} diff --git a/crates/ecstore/src/lib.rs b/crates/ecstore/src/lib.rs index 177618892..770e04027 100644 --- a/crates/ecstore/src/lib.rs +++ b/crates/ecstore/src/lib.rs @@ -30,7 +30,6 @@ pub mod endpoints; pub mod erasure_coding; pub mod error; pub mod global; -pub mod heal; pub mod lock_utils; pub mod metrics_realtime; pub mod notification_sys; From 29795fac51d18c20b3b9d53693453281f6236dfb Mon Sep 17 00:00:00 2001 From: junxiang Mu <1948535941@qq.com> Date: Thu, 24 Jul 2025 11:56:36 +0800 Subject: [PATCH 26/29] fix Cargo.toml Signed-off-by: junxiang Mu <1948535941@qq.com> --- crates/ahm/Cargo.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/ahm/Cargo.toml b/crates/ahm/Cargo.toml index 817e3019e..bd0fc3545 100644 --- a/crates/ahm/Cargo.toml +++ b/crates/ahm/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "rustfs-ahm" -version = "0.0.5" -edition = "2021" +version.workspace = true +edition.workspace = true authors = ["RustFS Team"] -license = "Apache-2.0" +license.workspace = true description = "RustFS AHM (Automatic Health Management) Scanner" [dependencies] @@ -32,10 +32,10 @@ chrono = { workspace = true } [dev-dependencies] rmp-serde = { workspace = true } -tokio-test = "0.4" +tokio-test = { workspace = true } serde_json = { workspace = true } serial_test = "3.2.0" once_cell = { workspace = true } tracing-subscriber = { workspace = true } walkdir = "2.5.0" -tempfile = "3.10" \ No newline at end of file +tempfile = { workspace = true } \ No newline at end of file From b667927216aa8c9bbf499c24ab9d86253d4ea09f Mon Sep 17 00:00:00 2001 From: junxiang Mu <1948535941@qq.com> Date: Thu, 24 Jul 2025 12:10:03 +0800 Subject: [PATCH 27/29] fix fmt Signed-off-by: junxiang Mu <1948535941@qq.com> --- crates/ahm/src/heal/manager.rs | 2 +- crates/ahm/src/heal/resume.rs | 4 ++-- crates/ahm/src/heal/storage.rs | 2 +- crates/ahm/src/lib.rs | 2 +- crates/ahm/src/scanner/data_scanner.rs | 9 +++++---- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/crates/ahm/src/heal/manager.rs b/crates/ahm/src/heal/manager.rs index cb2418838..044289e16 100644 --- a/crates/ahm/src/heal/manager.rs +++ b/crates/ahm/src/heal/manager.rs @@ -18,8 +18,8 @@ use crate::heal::{ storage::HealStorageAPI, task::{HealOptions, HealPriority, HealRequest, HealTask, HealTaskStatus, HealType}, }; -use rustfs_ecstore::disk::error::DiskError; use rustfs_ecstore::disk::DiskAPI; +use rustfs_ecstore::disk::error::DiskError; use rustfs_ecstore::global::GLOBAL_LOCAL_DISK_MAP; use std::{ collections::{HashMap, VecDeque}, diff --git a/crates/ahm/src/heal/resume.rs b/crates/ahm/src/heal/resume.rs index 260a22433..cf9d5cae5 100644 --- a/crates/ahm/src/heal/resume.rs +++ b/crates/ahm/src/heal/resume.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::error::{Error, Result}; -use rustfs_ecstore::disk::{DiskAPI, DiskStore, BUCKET_META_PREFIX, RUSTFS_META_BUCKET}; +use rustfs_ecstore::disk::{BUCKET_META_PREFIX, DiskAPI, DiskStore, RUSTFS_META_BUCKET}; use serde::{Deserialize, Serialize}; use std::path::Path; use std::sync::Arc; @@ -618,7 +618,7 @@ mod tests { #[tokio::test] async fn test_get_resumable_tasks_integration() { - use rustfs_ecstore::disk::{endpoint::Endpoint, new_disk, DiskOption}; + use rustfs_ecstore::disk::{DiskOption, endpoint::Endpoint, new_disk}; use tempfile::TempDir; // Create a temporary directory for testing diff --git a/crates/ahm/src/heal/storage.rs b/crates/ahm/src/heal/storage.rs index dcdf9781a..43ca41638 100644 --- a/crates/ahm/src/heal/storage.rs +++ b/crates/ahm/src/heal/storage.rs @@ -16,7 +16,7 @@ use crate::error::{Error, Result}; use async_trait::async_trait; use rustfs_common::heal_channel::{HealOpts, HealScanMode}; use rustfs_ecstore::{ - disk::{endpoint::Endpoint, DiskStore}, + disk::{DiskStore, endpoint::Endpoint}, store::ECStore, store_api::{BucketInfo, ObjectIO, StorageAPI}, }; diff --git a/crates/ahm/src/lib.rs b/crates/ahm/src/lib.rs index 7bc1b689c..17a70ff4e 100644 --- a/crates/ahm/src/lib.rs +++ b/crates/ahm/src/lib.rs @@ -21,7 +21,7 @@ pub mod heal; pub mod scanner; pub use error::{Error, Result}; -pub use heal::{channel::HealChannelProcessor, HealManager, HealOptions, HealPriority, HealRequest, HealType}; +pub use heal::{HealManager, HealOptions, HealPriority, HealRequest, HealType, channel::HealChannelProcessor}; pub use scanner::Scanner; // Global cancellation token for AHM services (scanner and other background tasks) diff --git a/crates/ahm/src/scanner/data_scanner.rs b/crates/ahm/src/scanner/data_scanner.rs index 5a28186ae..8083ecf73 100644 --- a/crates/ahm/src/scanner/data_scanner.rs +++ b/crates/ahm/src/scanner/data_scanner.rs @@ -22,7 +22,7 @@ use ecstore::{ disk::{DiskAPI, DiskStore, WalkDirOptions}, set_disk::SetDisks, }; -use rustfs_ecstore::{self as ecstore, data_usage::store_data_usage_in_backend, StorageAPI}; +use rustfs_ecstore::{self as ecstore, StorageAPI, data_usage::store_data_usage_in_backend}; use rustfs_filemeta::MetacacheReader; use tokio::sync::{Mutex, RwLock}; use tokio_util::sync::CancellationToken; @@ -31,12 +31,13 @@ use tracing::{debug, error, info, warn}; use super::metrics::{BucketMetrics, DiskMetrics, MetricsCollector, ScannerMetrics}; use crate::heal::HealManager; use crate::{ + HealRequest, error::{Error, Result}, - get_ahm_services_cancel_token, HealRequest, + get_ahm_services_cancel_token, }; use rustfs_common::{ data_usage::DataUsageInfo, - metrics::{globalMetrics, Metric, Metrics}, + metrics::{Metric, Metrics, globalMetrics}, }; use rustfs_ecstore::disk::RUSTFS_META_BUCKET; @@ -1393,8 +1394,8 @@ mod tests { use rustfs_ecstore::endpoints::{EndpointServerPools, Endpoints, PoolEndpoints}; use rustfs_ecstore::store::ECStore; use rustfs_ecstore::{ - store_api::{MakeBucketOptions, ObjectIO, PutObjReader}, StorageAPI, + store_api::{MakeBucketOptions, ObjectIO, PutObjReader}, }; use serial_test::serial; use std::fs; From 91b1c844302cb9fd144424460ac88652c6eec548 Mon Sep 17 00:00:00 2001 From: junxiang Mu <1948535941@qq.com> Date: Thu, 24 Jul 2025 12:18:05 +0800 Subject: [PATCH 28/29] rebase Signed-off-by: junxiang Mu <1948535941@qq.com> --- crates/ecstore/src/rpc/tonic_service.rs | 31 ++++++++++++++++--------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/crates/ecstore/src/rpc/tonic_service.rs b/crates/ecstore/src/rpc/tonic_service.rs index ad6d58d91..cbf30dcc2 100644 --- a/crates/ecstore/src/rpc/tonic_service.rs +++ b/crates/ecstore/src/rpc/tonic_service.rs @@ -36,6 +36,7 @@ use rustfs_common::{globals::GLOBAL_Local_Node_Name, heal_channel::HealOpts}; use bytes::Bytes; use rmp_serde::{Deserializer, Serializer}; use rustfs_filemeta::{FileInfo, MetacacheReader}; +use rustfs_lock::{LockClient, LockRequest}; use rustfs_madmin::health::{ get_cpus, get_mem_info, get_os_info, get_partitions, get_proc_info, get_sys_config, get_sys_errors, get_sys_services, }; @@ -1436,20 +1437,28 @@ impl Node for NodeService { async fn lock(&self, request: Request) -> Result, Status> { let request = request.into_inner(); - match &serde_json::from_str::(&request.args) { - Ok(args) => match GLOBAL_LOCAL_SERVER.write().await.lock(args).await { - Ok(result) => Ok(tonic::Response::new(GenerallyLockResponse { - success: result, - error_info: None, - })), - Err(err) => Ok(tonic::Response::new(GenerallyLockResponse { + // Parse the request to extract resource and owner + let args: LockRequest = match serde_json::from_str(&request.args) { + Ok(args) => args, + Err(err) => { + return Ok(tonic::Response::new(GenerallyLockResponse { success: false, - error_info: Some(format!("can not lock, args: {args}, err: {err}")), - })), - }, + error_info: Some(format!("can not decode args, err: {err}")), + })); + } + }; + + match self.lock_manager.acquire_exclusive(&args).await { + Ok(result) => Ok(tonic::Response::new(GenerallyLockResponse { + success: result.success, + error_info: None, + })), Err(err) => Ok(tonic::Response::new(GenerallyLockResponse { success: false, - error_info: Some(format!("can not decode args, err: {err}")), + error_info: Some(format!( + "can not lock, resource: {0}, owner: {1}, err: {2}", + args.resource, args.owner, err + )), })), } } From 77bc9af109f70c2c9905b8bb5786347138fe610c Mon Sep 17 00:00:00 2001 From: houseme Date: Thu, 24 Jul 2025 14:14:12 +0800 Subject: [PATCH 29/29] Update Cargo.toml --- crates/ahm/Cargo.toml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/ahm/Cargo.toml b/crates/ahm/Cargo.toml index bd0fc3545..8446f5802 100644 --- a/crates/ahm/Cargo.toml +++ b/crates/ahm/Cargo.toml @@ -5,6 +5,12 @@ edition.workspace = true authors = ["RustFS Team"] license.workspace = true description = "RustFS AHM (Automatic Health Management) Scanner" +repository.workspace = true +rust-version.workspace = true +homepage.workspace = true +documentation = "https://docs.rs/rustfs-ahm/latest/rustfs_ahm/" +keywords = ["RustFS", "AHM", "health-management", "scanner", "Minio"] +categories = ["web-programming", "development-tools", "filesystem"] [dependencies] rustfs-ecstore = { workspace = true } @@ -38,4 +44,4 @@ serial_test = "3.2.0" once_cell = { workspace = true } tracing-subscriber = { workspace = true } walkdir = "2.5.0" -tempfile = { workspace = true } \ No newline at end of file +tempfile = { workspace = true }