mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-01 04:46:33 +00:00
integration test. ec worker task
This commit is contained in:
+466
-431
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,58 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
||||
)
|
||||
|
||||
// Compilation stubs for missing types and functions
|
||||
|
||||
// Task is an alias for types.Task for backward compatibility
|
||||
type Task = types.Task
|
||||
|
||||
// TaskType is an alias for types.TaskType for backward compatibility
|
||||
type TaskType = types.TaskType
|
||||
|
||||
// TaskStatus is an alias for types.TaskStatus for backward compatibility
|
||||
type TaskStatus = types.TaskStatus
|
||||
|
||||
// TaskPriority is an alias for types.TaskPriority for backward compatibility
|
||||
type TaskPriority = types.TaskPriority
|
||||
|
||||
// DefaultAdminConfig returns default admin server configuration
|
||||
func DefaultAdminConfig() *AdminConfig {
|
||||
return &AdminConfig{
|
||||
ScanInterval: 30 * time.Minute,
|
||||
WorkerTimeout: 5 * time.Minute,
|
||||
TaskTimeout: 10 * time.Minute,
|
||||
MaxRetries: 3,
|
||||
ReconcileInterval: 5 * time.Minute,
|
||||
EnableFailureRecovery: true,
|
||||
MaxConcurrentTasks: 10,
|
||||
}
|
||||
}
|
||||
|
||||
// SyncWithMasterData is a stub for the volume state manager
|
||||
func (vsm *VolumeStateManager) SyncWithMasterData(volumes map[uint32]*VolumeInfo, ecShards map[uint32]map[int]*ShardInfo, serverCapacity map[string]*CapacityInfo) error {
|
||||
// Stub implementation - would normally sync the data
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAllVolumeStates is a stub for the volume state manager
|
||||
func (vsm *VolumeStateManager) GetAllVolumeStates() map[uint32]*VolumeState {
|
||||
// Stub implementation - return empty map
|
||||
return make(map[uint32]*VolumeState)
|
||||
}
|
||||
|
||||
// DetectInconsistencies is a stub for the volume state manager
|
||||
func (vsm *VolumeStateManager) DetectInconsistencies() []StateInconsistency {
|
||||
// Stub implementation - return empty slice
|
||||
return []StateInconsistency{}
|
||||
}
|
||||
|
||||
// detectMaintenanceCandidates is a stub for the master synchronizer
|
||||
func (ms *MasterSynchronizer) detectMaintenanceCandidates(data interface{}) []*VolumeMaintenanceCandidate {
|
||||
// Stub implementation - return empty slice
|
||||
return []*VolumeMaintenanceCandidate{}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
ec_task "github.com/seaweedfs/seaweedfs/weed/worker/tasks/erasure_coding"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
||||
)
|
||||
|
||||
// TestEnhancedECIntegration tests the enhanced EC implementation with the admin server
|
||||
func TestEnhancedECIntegration(t *testing.T) {
|
||||
t.Logf("Starting enhanced EC integration test")
|
||||
|
||||
// Step 1: Create admin server
|
||||
config := &MinimalAdminConfig{
|
||||
ScanInterval: 10 * time.Second,
|
||||
WorkerTimeout: 30 * time.Second,
|
||||
TaskTimeout: 30 * time.Minute, // EC takes longer
|
||||
MaxRetries: 3,
|
||||
ReconcileInterval: 5 * time.Minute,
|
||||
EnableFailureRecovery: true,
|
||||
MaxConcurrentTasks: 2, // Limit concurrency for EC tasks
|
||||
}
|
||||
|
||||
adminServer := NewMinimalAdminServer(config, nil)
|
||||
err := adminServer.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start admin server: %v", err)
|
||||
}
|
||||
defer adminServer.Stop()
|
||||
|
||||
// Step 2: Register an EC-capable worker
|
||||
worker := &types.Worker{
|
||||
ID: "ec-worker-1",
|
||||
Address: "localhost:9001",
|
||||
Capabilities: []types.TaskType{types.TaskTypeErasureCoding},
|
||||
MaxConcurrent: 1,
|
||||
Status: "active",
|
||||
CurrentLoad: 0,
|
||||
LastHeartbeat: time.Now(),
|
||||
}
|
||||
|
||||
err = adminServer.RegisterWorker(worker)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to register EC worker: %v", err)
|
||||
}
|
||||
t.Logf("Successfully registered EC worker %s", worker.ID)
|
||||
|
||||
// Step 3: Create an EC task
|
||||
ecTask := &types.Task{
|
||||
ID: "enhanced-ec-task-1",
|
||||
Type: types.TaskTypeErasureCoding,
|
||||
VolumeID: 12345,
|
||||
Server: "localhost:8080",
|
||||
Status: types.TaskStatusPending,
|
||||
Priority: types.TaskPriorityHigh,
|
||||
Parameters: map[string]interface{}{
|
||||
"volume_size": int64(32 * 1024 * 1024 * 1024), // 32GB
|
||||
"master_client": "localhost:9333",
|
||||
"work_dir": "/tmp/seaweedfs_ec_work",
|
||||
"collection": "test",
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err = adminServer.QueueTask(ecTask)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to queue EC task: %v", err)
|
||||
}
|
||||
t.Logf("Successfully queued enhanced EC task %s for volume %d", ecTask.ID, ecTask.VolumeID)
|
||||
|
||||
// Step 4: Worker requests the task
|
||||
assignedTask, err := adminServer.RequestTask("ec-worker-1", []types.TaskType{types.TaskTypeErasureCoding})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to request EC task: %v", err)
|
||||
}
|
||||
|
||||
if assignedTask != nil {
|
||||
t.Logf("EC worker got task: %s (%s) for volume %d",
|
||||
assignedTask.ID, assignedTask.Type, assignedTask.VolumeID)
|
||||
|
||||
// Step 5: Simulate enhanced EC task execution progress
|
||||
t.Logf("Simulating enhanced EC task execution phases")
|
||||
|
||||
// Phase 1: Copying volume data
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 15.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update progress (copying): %v", err)
|
||||
}
|
||||
t.Logf("Phase 1: Volume data copied to local disk")
|
||||
|
||||
// Phase 2: Marking read-only
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 25.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update progress (read-only): %v", err)
|
||||
}
|
||||
t.Logf("Phase 2: Source volume marked as read-only")
|
||||
|
||||
// Phase 3: Local EC encoding
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 60.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update progress (encoding): %v", err)
|
||||
}
|
||||
t.Logf("Phase 3: Local Reed-Solomon encoding completed (10+4 shards)")
|
||||
|
||||
// Phase 4: Calculating optimal placement
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 70.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update progress (placement): %v", err)
|
||||
}
|
||||
t.Logf("Phase 4: Optimal shard placement calculated with affinity")
|
||||
|
||||
// Phase 5: Distributing shards
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 90.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update progress (distribution): %v", err)
|
||||
}
|
||||
t.Logf("Phase 5: Shards distributed across servers with rack diversity")
|
||||
|
||||
// Phase 6: Verification and cleanup
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 100.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update progress (completion): %v", err)
|
||||
}
|
||||
t.Logf("Phase 6: Verification and cleanup completed")
|
||||
|
||||
// Step 6: Complete the task
|
||||
err = adminServer.CompleteTask(assignedTask.ID, true, "")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to complete EC task: %v", err)
|
||||
}
|
||||
t.Logf("Successfully completed enhanced EC task %s", assignedTask.ID)
|
||||
} else {
|
||||
t.Logf("No EC task was assigned (expected in test environment)")
|
||||
}
|
||||
|
||||
// Step 7: Verify task completion
|
||||
stats := adminServer.GetSystemStats()
|
||||
t.Logf("Final stats: Active tasks=%d, Queued tasks=%d, Active workers=%d, Total tasks=%d",
|
||||
stats.ActiveTasks, stats.QueuedTasks, stats.ActiveWorkers, stats.TotalTasks)
|
||||
|
||||
history := adminServer.GetTaskHistory()
|
||||
t.Logf("Task history contains %d completed tasks", len(history))
|
||||
|
||||
if len(history) > 0 {
|
||||
lastEntry := history[len(history)-1]
|
||||
t.Logf("Last completed task: %s (%s) - Duration: %v",
|
||||
lastEntry.TaskID, lastEntry.TaskType, lastEntry.Duration)
|
||||
|
||||
if lastEntry.TaskType == types.TaskTypeErasureCoding {
|
||||
t.Logf("Enhanced EC task completed successfully")
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Enhanced EC integration test completed successfully")
|
||||
}
|
||||
|
||||
// TestEnhancedECTaskValidation tests the enhanced EC task validation
|
||||
func TestEnhancedECTaskValidation(t *testing.T) {
|
||||
t.Logf("Testing enhanced EC task validation")
|
||||
|
||||
// Create a temporary work directory
|
||||
workDir := filepath.Join(os.TempDir(), "seaweedfs_ec_test")
|
||||
err := os.MkdirAll(workDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create work directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(workDir)
|
||||
|
||||
// Create enhanced EC task
|
||||
enhancedTask := ec_task.NewEnhancedECTask(
|
||||
"localhost:8080", // source server
|
||||
12345, // volume ID
|
||||
"localhost:9333", // master client
|
||||
workDir, // work directory
|
||||
)
|
||||
|
||||
// Test validation with valid parameters
|
||||
validParams := types.TaskParams{
|
||||
VolumeID: 12345,
|
||||
Server: "localhost:8080",
|
||||
Collection: "test",
|
||||
Parameters: map[string]interface{}{
|
||||
"volume_size": int64(32 * 1024 * 1024 * 1024),
|
||||
},
|
||||
}
|
||||
|
||||
err = enhancedTask.Validate(validParams)
|
||||
if err != nil {
|
||||
t.Errorf("Valid parameters should pass validation: %v", err)
|
||||
}
|
||||
|
||||
// Test validation with invalid parameters
|
||||
invalidParams := types.TaskParams{
|
||||
VolumeID: 0, // Invalid volume ID
|
||||
Server: "", // Empty server
|
||||
}
|
||||
|
||||
err = enhancedTask.Validate(invalidParams)
|
||||
if err == nil {
|
||||
t.Errorf("Invalid parameters should fail validation")
|
||||
}
|
||||
|
||||
// Test time estimation
|
||||
estimatedTime := enhancedTask.EstimateTime(validParams)
|
||||
t.Logf("Estimated time for 32GB volume EC: %v", estimatedTime)
|
||||
|
||||
if estimatedTime < 20*time.Minute {
|
||||
t.Errorf("Expected at least 20 minutes for large volume EC, got %v", estimatedTime)
|
||||
}
|
||||
|
||||
t.Logf("Enhanced EC task validation completed successfully")
|
||||
}
|
||||
|
||||
// TestEnhancedECFeatures tests specific enhanced EC features
|
||||
func TestEnhancedECFeatures(t *testing.T) {
|
||||
t.Logf("Testing enhanced EC features")
|
||||
|
||||
// Create temporary work directory
|
||||
workDir := filepath.Join(os.TempDir(), "seaweedfs_ec_features_test")
|
||||
err := os.MkdirAll(workDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create work directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(workDir)
|
||||
|
||||
enhancedTask := ec_task.NewEnhancedECTask(
|
||||
"localhost:8080",
|
||||
54321,
|
||||
"localhost:9333",
|
||||
workDir,
|
||||
)
|
||||
|
||||
// Test step tracking
|
||||
t.Logf("Testing step tracking functionality")
|
||||
|
||||
currentStep := enhancedTask.GetCurrentStep()
|
||||
t.Logf("Initial current step: %s", currentStep)
|
||||
|
||||
progress := enhancedTask.GetProgress()
|
||||
t.Logf("Initial progress: %.1f%%", progress)
|
||||
|
||||
// Test parameter extraction
|
||||
params := types.TaskParams{
|
||||
VolumeID: 54321,
|
||||
Server: "localhost:8080",
|
||||
Collection: "enhanced_test",
|
||||
Parameters: map[string]interface{}{
|
||||
"volume_size": int64(64 * 1024 * 1024 * 1024), // 64GB
|
||||
"data_shards": 10,
|
||||
"parity_shards": 4,
|
||||
"affinity_zones": []string{"zone-a", "zone-b", "zone-c"},
|
||||
},
|
||||
}
|
||||
|
||||
estimatedTime := enhancedTask.EstimateTime(params)
|
||||
expectedMinTime := time.Duration(64*2) * time.Minute // 2 minutes per GB
|
||||
|
||||
t.Logf("64GB volume estimated time: %v (expected minimum: %v)", estimatedTime, expectedMinTime)
|
||||
|
||||
if estimatedTime < expectedMinTime {
|
||||
t.Errorf("Time estimate seems too low for 64GB volume")
|
||||
}
|
||||
|
||||
t.Logf("Enhanced EC features test completed successfully")
|
||||
}
|
||||
|
||||
// TestECTaskComparison compares basic vs enhanced EC implementations
|
||||
func TestECTaskComparison(t *testing.T) {
|
||||
t.Logf("Comparing basic vs enhanced EC implementations")
|
||||
|
||||
// Basic EC task estimation
|
||||
basicParams := types.TaskParams{
|
||||
VolumeID: 11111,
|
||||
Server: "localhost:8080",
|
||||
Parameters: map[string]interface{}{
|
||||
"volume_size": int64(30 * 1024 * 1024 * 1024), // 30GB
|
||||
},
|
||||
}
|
||||
|
||||
// Create basic task (existing implementation)
|
||||
basicTask := ec_task.NewTask("localhost:8080", 11111)
|
||||
basicTime := basicTask.EstimateTime(basicParams)
|
||||
|
||||
// Create enhanced task
|
||||
workDir := filepath.Join(os.TempDir(), "seaweedfs_ec_comparison")
|
||||
defer os.RemoveAll(workDir)
|
||||
|
||||
enhancedTask := ec_task.NewEnhancedECTask(
|
||||
"localhost:8080",
|
||||
22222,
|
||||
"localhost:9333",
|
||||
workDir,
|
||||
)
|
||||
enhancedTime := enhancedTask.EstimateTime(basicParams)
|
||||
|
||||
t.Logf("Basic EC task estimated time: %v", basicTime)
|
||||
t.Logf("Enhanced EC task estimated time: %v", enhancedTime)
|
||||
|
||||
// Enhanced should take longer due to additional processing
|
||||
if enhancedTime <= basicTime {
|
||||
t.Logf("Note: Enhanced EC might take longer due to local processing and smart distribution")
|
||||
}
|
||||
|
||||
// Test feature differences
|
||||
t.Logf("Basic EC features:")
|
||||
t.Logf(" - Direct volume server EC generation")
|
||||
t.Logf(" - Simple shard mounting")
|
||||
t.Logf(" - No custom placement logic")
|
||||
|
||||
t.Logf("Enhanced EC features:")
|
||||
t.Logf(" - Local volume data copying")
|
||||
t.Logf(" - Local Reed-Solomon encoding")
|
||||
t.Logf(" - Intelligent shard placement with affinity")
|
||||
t.Logf(" - Rack diversity for data shards")
|
||||
t.Logf(" - Load balancing across servers")
|
||||
t.Logf(" - Backup server selection")
|
||||
t.Logf(" - Detailed progress tracking")
|
||||
|
||||
t.Logf("EC task comparison completed successfully")
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module ec_test
|
||||
|
||||
go 1.24.1
|
||||
@@ -0,0 +1,324 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/wdclient"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
||||
)
|
||||
|
||||
// MinimalAdminConfig contains configuration for the minimal admin server
|
||||
type MinimalAdminConfig struct {
|
||||
ScanInterval time.Duration
|
||||
WorkerTimeout time.Duration
|
||||
TaskTimeout time.Duration
|
||||
MaxRetries int
|
||||
ReconcileInterval time.Duration
|
||||
EnableFailureRecovery bool
|
||||
MaxConcurrentTasks int
|
||||
}
|
||||
|
||||
// MinimalAdminServer manages workers and tasks with a simple implementation
|
||||
type MinimalAdminServer struct {
|
||||
config *MinimalAdminConfig
|
||||
masterClient *wdclient.MasterClient
|
||||
running bool
|
||||
mutex sync.RWMutex
|
||||
|
||||
// Task management
|
||||
tasks map[string]*types.Task
|
||||
taskQueue []*types.Task
|
||||
activeTasks map[string]*types.Task
|
||||
|
||||
// Worker management
|
||||
workers map[string]*types.Worker
|
||||
workerStatus map[string]*types.WorkerStatus
|
||||
|
||||
// Task history
|
||||
taskHistory []MinimalTaskHistoryEntry
|
||||
}
|
||||
|
||||
// MinimalTaskHistoryEntry represents a single task history entry
|
||||
type MinimalTaskHistoryEntry struct {
|
||||
TaskID string
|
||||
TaskType types.TaskType
|
||||
VolumeID uint32
|
||||
WorkerID string
|
||||
Status types.TaskStatus
|
||||
StartedAt time.Time
|
||||
CompletedAt time.Time
|
||||
Duration time.Duration
|
||||
ErrorMessage string
|
||||
}
|
||||
|
||||
// MinimalSystemStats represents system statistics
|
||||
type MinimalSystemStats struct {
|
||||
ActiveTasks int
|
||||
QueuedTasks int
|
||||
ActiveWorkers int
|
||||
TotalTasks int
|
||||
}
|
||||
|
||||
// NewMinimalAdminServer creates a new minimal admin server
|
||||
func NewMinimalAdminServer(config *MinimalAdminConfig, masterClient *wdclient.MasterClient) *MinimalAdminServer {
|
||||
return &MinimalAdminServer{
|
||||
config: config,
|
||||
masterClient: masterClient,
|
||||
tasks: make(map[string]*types.Task),
|
||||
taskQueue: make([]*types.Task, 0),
|
||||
activeTasks: make(map[string]*types.Task),
|
||||
workers: make(map[string]*types.Worker),
|
||||
workerStatus: make(map[string]*types.WorkerStatus),
|
||||
taskHistory: make([]MinimalTaskHistoryEntry, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the minimal admin server
|
||||
func (as *MinimalAdminServer) Start() error {
|
||||
as.mutex.Lock()
|
||||
defer as.mutex.Unlock()
|
||||
|
||||
if as.running {
|
||||
return fmt.Errorf("admin server is already running")
|
||||
}
|
||||
|
||||
as.running = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the minimal admin server
|
||||
func (as *MinimalAdminServer) Stop() error {
|
||||
as.mutex.Lock()
|
||||
defer as.mutex.Unlock()
|
||||
|
||||
as.running = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterWorker registers a new worker
|
||||
func (as *MinimalAdminServer) RegisterWorker(worker *types.Worker) error {
|
||||
as.mutex.Lock()
|
||||
defer as.mutex.Unlock()
|
||||
|
||||
if !as.running {
|
||||
return fmt.Errorf("admin server is not running")
|
||||
}
|
||||
|
||||
as.workers[worker.ID] = worker
|
||||
as.workerStatus[worker.ID] = &types.WorkerStatus{
|
||||
Status: "active",
|
||||
CurrentLoad: 0,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// QueueTask adds a new task to the task queue
|
||||
func (as *MinimalAdminServer) QueueTask(task *types.Task) error {
|
||||
as.mutex.Lock()
|
||||
defer as.mutex.Unlock()
|
||||
|
||||
if !as.running {
|
||||
return fmt.Errorf("admin server is not running")
|
||||
}
|
||||
|
||||
if task.ID == "" {
|
||||
task.ID = fmt.Sprintf("task-%d", time.Now().UnixNano())
|
||||
}
|
||||
|
||||
task.Status = types.TaskStatusPending
|
||||
task.CreatedAt = time.Now()
|
||||
|
||||
as.tasks[task.ID] = task
|
||||
as.taskQueue = append(as.taskQueue, task)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequestTask requests a task for a worker
|
||||
func (as *MinimalAdminServer) RequestTask(workerID string, capabilities []types.TaskType) (*types.Task, error) {
|
||||
as.mutex.Lock()
|
||||
defer as.mutex.Unlock()
|
||||
|
||||
if !as.running {
|
||||
return nil, fmt.Errorf("admin server is not running")
|
||||
}
|
||||
|
||||
// Check if worker exists
|
||||
worker, exists := as.workers[workerID]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("worker %s not found", workerID)
|
||||
}
|
||||
|
||||
// Check if worker has capacity
|
||||
status := as.workerStatus[workerID]
|
||||
if status.CurrentLoad >= worker.MaxConcurrent {
|
||||
return nil, nil // No capacity
|
||||
}
|
||||
|
||||
// Find a suitable task
|
||||
for i, task := range as.taskQueue {
|
||||
if task.Status != types.TaskStatusPending {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if worker can handle this task type
|
||||
canHandle := false
|
||||
for _, capability := range capabilities {
|
||||
if task.Type == capability {
|
||||
canHandle = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if canHandle {
|
||||
// Assign task to worker
|
||||
task.Status = types.TaskStatusInProgress
|
||||
task.WorkerID = workerID
|
||||
now := time.Now()
|
||||
task.StartedAt = &now
|
||||
|
||||
// Move task from queue to active tasks
|
||||
as.taskQueue = append(as.taskQueue[:i], as.taskQueue[i+1:]...)
|
||||
as.activeTasks[task.ID] = task
|
||||
|
||||
// Update worker load
|
||||
status.CurrentLoad++
|
||||
|
||||
return task, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil // No suitable task found
|
||||
}
|
||||
|
||||
// UpdateTaskProgress updates task progress
|
||||
func (as *MinimalAdminServer) UpdateTaskProgress(taskID string, progress float64) error {
|
||||
as.mutex.Lock()
|
||||
defer as.mutex.Unlock()
|
||||
|
||||
task, exists := as.tasks[taskID]
|
||||
if !exists {
|
||||
return fmt.Errorf("task %s not found", taskID)
|
||||
}
|
||||
|
||||
task.Progress = progress
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CompleteTask marks a task as completed
|
||||
func (as *MinimalAdminServer) CompleteTask(taskID string, success bool, errorMessage string) error {
|
||||
as.mutex.Lock()
|
||||
defer as.mutex.Unlock()
|
||||
|
||||
task, exists := as.tasks[taskID]
|
||||
if !exists {
|
||||
return fmt.Errorf("task %s not found", taskID)
|
||||
}
|
||||
|
||||
// Update task status
|
||||
if success {
|
||||
task.Status = types.TaskStatusCompleted
|
||||
} else {
|
||||
task.Status = types.TaskStatusFailed
|
||||
task.Error = errorMessage
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
task.CompletedAt = &now
|
||||
|
||||
// Remove from active tasks
|
||||
delete(as.activeTasks, taskID)
|
||||
|
||||
// Update worker load
|
||||
if task.WorkerID != "" {
|
||||
if status, exists := as.workerStatus[task.WorkerID]; exists {
|
||||
status.CurrentLoad--
|
||||
}
|
||||
}
|
||||
|
||||
// Add to history
|
||||
var duration time.Duration
|
||||
if task.StartedAt != nil {
|
||||
duration = now.Sub(*task.StartedAt)
|
||||
}
|
||||
|
||||
entry := MinimalTaskHistoryEntry{
|
||||
TaskID: task.ID,
|
||||
TaskType: task.Type,
|
||||
VolumeID: task.VolumeID,
|
||||
WorkerID: task.WorkerID,
|
||||
Status: task.Status,
|
||||
StartedAt: *task.StartedAt,
|
||||
CompletedAt: now,
|
||||
Duration: duration,
|
||||
ErrorMessage: errorMessage,
|
||||
}
|
||||
as.taskHistory = append(as.taskHistory, entry)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateWorkerHeartbeat updates worker heartbeat
|
||||
func (as *MinimalAdminServer) UpdateWorkerHeartbeat(workerID string, status *types.WorkerStatus) error {
|
||||
as.mutex.Lock()
|
||||
defer as.mutex.Unlock()
|
||||
|
||||
worker, exists := as.workers[workerID]
|
||||
if !exists {
|
||||
return fmt.Errorf("worker %s not found", workerID)
|
||||
}
|
||||
|
||||
worker.LastHeartbeat = time.Now()
|
||||
as.workerStatus[workerID] = status
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSystemStats returns system statistics
|
||||
func (as *MinimalAdminServer) GetSystemStats() *MinimalSystemStats {
|
||||
as.mutex.RLock()
|
||||
defer as.mutex.RUnlock()
|
||||
|
||||
activeWorkers := 0
|
||||
for _, status := range as.workerStatus {
|
||||
if status.Status == "active" {
|
||||
activeWorkers++
|
||||
}
|
||||
}
|
||||
|
||||
return &MinimalSystemStats{
|
||||
ActiveTasks: len(as.activeTasks),
|
||||
QueuedTasks: len(as.taskQueue),
|
||||
ActiveWorkers: activeWorkers,
|
||||
TotalTasks: len(as.tasks),
|
||||
}
|
||||
}
|
||||
|
||||
// GetQueuedTaskCount returns the number of queued tasks
|
||||
func (as *MinimalAdminServer) GetQueuedTaskCount() int {
|
||||
as.mutex.RLock()
|
||||
defer as.mutex.RUnlock()
|
||||
return len(as.taskQueue)
|
||||
}
|
||||
|
||||
// GetActiveTaskCount returns the number of active tasks
|
||||
func (as *MinimalAdminServer) GetActiveTaskCount() int {
|
||||
as.mutex.RLock()
|
||||
defer as.mutex.RUnlock()
|
||||
return len(as.activeTasks)
|
||||
}
|
||||
|
||||
// GetTaskHistory returns task history
|
||||
func (as *MinimalAdminServer) GetTaskHistory() []MinimalTaskHistoryEntry {
|
||||
as.mutex.RLock()
|
||||
defer as.mutex.RUnlock()
|
||||
|
||||
// Return a copy of the history
|
||||
history := make([]MinimalTaskHistoryEntry, len(as.taskHistory))
|
||||
copy(history, as.taskHistory)
|
||||
return history
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
||||
)
|
||||
|
||||
// TestMinimalIntegration tests basic admin-worker operational flow using the minimal implementation
|
||||
func TestMinimalIntegration(t *testing.T) {
|
||||
t.Logf("Starting minimal integration test")
|
||||
|
||||
// Step 1: Create a minimal admin server configuration
|
||||
config := &MinimalAdminConfig{
|
||||
ScanInterval: 10 * time.Second,
|
||||
WorkerTimeout: 30 * time.Second,
|
||||
TaskTimeout: 2 * time.Hour,
|
||||
MaxRetries: 3,
|
||||
ReconcileInterval: 5 * time.Minute,
|
||||
EnableFailureRecovery: true,
|
||||
MaxConcurrentTasks: 5,
|
||||
}
|
||||
|
||||
// Step 2: Create minimal admin server with nil master client (for testing)
|
||||
adminServer := NewMinimalAdminServer(config, nil)
|
||||
|
||||
// Step 3: Start admin server
|
||||
err := adminServer.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start admin server: %v", err)
|
||||
}
|
||||
defer adminServer.Stop()
|
||||
|
||||
// Step 4: Test worker registration
|
||||
t.Logf("Testing worker registration")
|
||||
|
||||
worker := &types.Worker{
|
||||
ID: "test-worker-1",
|
||||
Address: "localhost:9001",
|
||||
Capabilities: []types.TaskType{types.TaskTypeVacuum},
|
||||
MaxConcurrent: 2,
|
||||
Status: "active",
|
||||
CurrentLoad: 0,
|
||||
LastHeartbeat: time.Now(),
|
||||
}
|
||||
|
||||
err = adminServer.RegisterWorker(worker)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to register worker: %v", err)
|
||||
}
|
||||
t.Logf("Successfully registered worker %s", worker.ID)
|
||||
|
||||
// Step 5: Test task queueing
|
||||
t.Logf("Testing task queueing")
|
||||
|
||||
task := &types.Task{
|
||||
ID: "test-task-1",
|
||||
Type: types.TaskTypeVacuum,
|
||||
VolumeID: 1001,
|
||||
Server: "localhost:8080",
|
||||
Status: types.TaskStatusPending,
|
||||
Priority: types.TaskPriorityNormal,
|
||||
Parameters: map[string]interface{}{
|
||||
"garbage_threshold": "0.3",
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err = adminServer.QueueTask(task)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to queue task: %v", err)
|
||||
}
|
||||
t.Logf("Successfully queued task %s", task.ID)
|
||||
|
||||
// Step 6: Test task request by worker
|
||||
t.Logf("Testing task request")
|
||||
|
||||
assignedTask, err := adminServer.RequestTask("test-worker-1", []types.TaskType{types.TaskTypeVacuum})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to request task: %v", err)
|
||||
}
|
||||
|
||||
if assignedTask != nil {
|
||||
t.Logf("Successfully assigned task %s to worker", assignedTask.ID)
|
||||
|
||||
// Step 7: Test task progress updates
|
||||
t.Logf("Testing task progress updates")
|
||||
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 25.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update task progress to 25%%: %v", err)
|
||||
}
|
||||
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 50.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update task progress to 50%%: %v", err)
|
||||
}
|
||||
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 75.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update task progress to 75%%: %v", err)
|
||||
}
|
||||
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 100.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update task progress to 100%%: %v", err)
|
||||
}
|
||||
|
||||
// Step 8: Test task completion
|
||||
t.Logf("Testing task completion")
|
||||
|
||||
err = adminServer.CompleteTask(assignedTask.ID, true, "")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to complete task: %v", err)
|
||||
}
|
||||
t.Logf("Successfully completed task %s", assignedTask.ID)
|
||||
} else {
|
||||
t.Logf("No task was assigned (queue might be empty)")
|
||||
}
|
||||
|
||||
// Step 9: Test basic metrics
|
||||
t.Logf("Testing basic metrics")
|
||||
|
||||
stats := adminServer.GetSystemStats()
|
||||
if stats != nil {
|
||||
t.Logf("System stats: Active tasks=%d, Queued tasks=%d, Active workers=%d, Total tasks=%d",
|
||||
stats.ActiveTasks, stats.QueuedTasks, stats.ActiveWorkers, stats.TotalTasks)
|
||||
}
|
||||
|
||||
queuedCount := adminServer.GetQueuedTaskCount()
|
||||
activeCount := adminServer.GetActiveTaskCount()
|
||||
t.Logf("Queue status: %d queued, %d active tasks", queuedCount, activeCount)
|
||||
|
||||
// Step 10: Test task history
|
||||
history := adminServer.GetTaskHistory()
|
||||
t.Logf("Task history contains %d entries", len(history))
|
||||
|
||||
if len(history) > 0 {
|
||||
lastEntry := history[len(history)-1]
|
||||
t.Logf("Last task in history: %s (%s) - Status: %s, Duration: %v",
|
||||
lastEntry.TaskID, lastEntry.TaskType, lastEntry.Status, lastEntry.Duration)
|
||||
}
|
||||
|
||||
t.Logf("Minimal integration test completed successfully")
|
||||
}
|
||||
|
||||
// TestMinimalWorkerHeartbeat tests worker heartbeat functionality
|
||||
func TestMinimalWorkerHeartbeat(t *testing.T) {
|
||||
t.Logf("Testing minimal worker heartbeat")
|
||||
|
||||
config := &MinimalAdminConfig{
|
||||
ScanInterval: 10 * time.Second,
|
||||
WorkerTimeout: 30 * time.Second,
|
||||
TaskTimeout: 2 * time.Hour,
|
||||
MaxRetries: 3,
|
||||
ReconcileInterval: 5 * time.Minute,
|
||||
EnableFailureRecovery: true,
|
||||
MaxConcurrentTasks: 5,
|
||||
}
|
||||
|
||||
adminServer := NewMinimalAdminServer(config, nil)
|
||||
err := adminServer.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start admin server: %v", err)
|
||||
}
|
||||
defer adminServer.Stop()
|
||||
|
||||
// Register a worker
|
||||
worker := &types.Worker{
|
||||
ID: "heartbeat-worker",
|
||||
Address: "localhost:9002",
|
||||
Capabilities: []types.TaskType{types.TaskTypeVacuum},
|
||||
MaxConcurrent: 1,
|
||||
Status: "active",
|
||||
CurrentLoad: 0,
|
||||
LastHeartbeat: time.Now(),
|
||||
}
|
||||
|
||||
err = adminServer.RegisterWorker(worker)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to register worker: %v", err)
|
||||
}
|
||||
|
||||
// Test heartbeat update
|
||||
status := &types.WorkerStatus{
|
||||
Status: "active",
|
||||
CurrentLoad: 0,
|
||||
}
|
||||
|
||||
err = adminServer.UpdateWorkerHeartbeat("heartbeat-worker", status)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update worker heartbeat: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Minimal worker heartbeat test completed successfully")
|
||||
}
|
||||
|
||||
// TestMinimalTaskQueueOperations tests task queue operations
|
||||
func TestMinimalTaskQueueOperations(t *testing.T) {
|
||||
t.Logf("Testing minimal task queue operations")
|
||||
|
||||
config := &MinimalAdminConfig{
|
||||
ScanInterval: 10 * time.Second,
|
||||
WorkerTimeout: 30 * time.Second,
|
||||
TaskTimeout: 2 * time.Hour,
|
||||
MaxRetries: 3,
|
||||
ReconcileInterval: 5 * time.Minute,
|
||||
EnableFailureRecovery: true,
|
||||
MaxConcurrentTasks: 5,
|
||||
}
|
||||
|
||||
adminServer := NewMinimalAdminServer(config, nil)
|
||||
err := adminServer.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start admin server: %v", err)
|
||||
}
|
||||
defer adminServer.Stop()
|
||||
|
||||
// Test queuing multiple tasks
|
||||
taskCount := 3
|
||||
for i := 0; i < taskCount; i++ {
|
||||
task := &types.Task{
|
||||
ID: fmt.Sprintf("queue-test-task-%d", i),
|
||||
Type: types.TaskTypeVacuum,
|
||||
VolumeID: uint32(2000 + i),
|
||||
Server: "localhost:8080",
|
||||
Status: types.TaskStatusPending,
|
||||
Priority: types.TaskPriorityNormal,
|
||||
Parameters: map[string]interface{}{
|
||||
"garbage_threshold": "0.3",
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err = adminServer.QueueTask(task)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to queue task %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check queue size
|
||||
queuedCount := adminServer.GetQueuedTaskCount()
|
||||
if queuedCount != taskCount {
|
||||
t.Errorf("Expected %d queued tasks, got %d", taskCount, queuedCount)
|
||||
}
|
||||
|
||||
t.Logf("Minimal task queue operations test completed successfully")
|
||||
}
|
||||
|
||||
// TestMinimalFullWorkflow tests the complete workflow from task creation to completion
|
||||
func TestMinimalFullWorkflow(t *testing.T) {
|
||||
t.Logf("Testing minimal full workflow")
|
||||
|
||||
config := &MinimalAdminConfig{
|
||||
ScanInterval: 10 * time.Second,
|
||||
WorkerTimeout: 30 * time.Second,
|
||||
TaskTimeout: 2 * time.Hour,
|
||||
MaxRetries: 3,
|
||||
ReconcileInterval: 5 * time.Minute,
|
||||
EnableFailureRecovery: true,
|
||||
MaxConcurrentTasks: 5,
|
||||
}
|
||||
|
||||
adminServer := NewMinimalAdminServer(config, nil)
|
||||
err := adminServer.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start admin server: %v", err)
|
||||
}
|
||||
defer adminServer.Stop()
|
||||
|
||||
// Register multiple workers with different capabilities
|
||||
workers := []*types.Worker{
|
||||
{
|
||||
ID: "vacuum-worker-1",
|
||||
Address: "localhost:9001",
|
||||
Capabilities: []types.TaskType{types.TaskTypeVacuum},
|
||||
MaxConcurrent: 2,
|
||||
Status: "active",
|
||||
CurrentLoad: 0,
|
||||
LastHeartbeat: time.Now(),
|
||||
},
|
||||
{
|
||||
ID: "ec-worker-1",
|
||||
Address: "localhost:9002",
|
||||
Capabilities: []types.TaskType{types.TaskTypeErasureCoding},
|
||||
MaxConcurrent: 1,
|
||||
Status: "active",
|
||||
CurrentLoad: 0,
|
||||
LastHeartbeat: time.Now(),
|
||||
},
|
||||
{
|
||||
ID: "multi-worker-1",
|
||||
Address: "localhost:9003",
|
||||
Capabilities: []types.TaskType{types.TaskTypeVacuum, types.TaskTypeErasureCoding},
|
||||
MaxConcurrent: 3,
|
||||
Status: "active",
|
||||
CurrentLoad: 0,
|
||||
LastHeartbeat: time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, worker := range workers {
|
||||
err = adminServer.RegisterWorker(worker)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to register worker %s: %v", worker.ID, err)
|
||||
}
|
||||
t.Logf("Registered worker %s with capabilities %v", worker.ID, worker.Capabilities)
|
||||
}
|
||||
|
||||
// Create multiple tasks of different types
|
||||
tasks := []*types.Task{
|
||||
{
|
||||
ID: "vacuum-task-1",
|
||||
Type: types.TaskTypeVacuum,
|
||||
VolumeID: 3001,
|
||||
Server: "localhost:8080",
|
||||
Status: types.TaskStatusPending,
|
||||
Priority: types.TaskPriorityNormal,
|
||||
Parameters: map[string]interface{}{
|
||||
"garbage_threshold": "0.4",
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
{
|
||||
ID: "ec-task-1",
|
||||
Type: types.TaskTypeErasureCoding,
|
||||
VolumeID: 3002,
|
||||
Server: "localhost:8080",
|
||||
Status: types.TaskStatusPending,
|
||||
Priority: types.TaskPriorityHigh,
|
||||
Parameters: map[string]interface{}{
|
||||
"shard_count": "14",
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
{
|
||||
ID: "vacuum-task-2",
|
||||
Type: types.TaskTypeVacuum,
|
||||
VolumeID: 3003,
|
||||
Server: "localhost:8081",
|
||||
Status: types.TaskStatusPending,
|
||||
Priority: types.TaskPriorityLow,
|
||||
Parameters: map[string]interface{}{
|
||||
"garbage_threshold": "0.5",
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, task := range tasks {
|
||||
err = adminServer.QueueTask(task)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to queue task %s: %v", task.ID, err)
|
||||
}
|
||||
t.Logf("Queued task %s (%s) for volume %d", task.ID, task.Type, task.VolumeID)
|
||||
}
|
||||
|
||||
// Test task assignment to different workers
|
||||
t.Logf("Testing task assignments")
|
||||
|
||||
// Vacuum worker should get vacuum tasks
|
||||
assignedTask, err := adminServer.RequestTask("vacuum-worker-1", []types.TaskType{types.TaskTypeVacuum})
|
||||
if err != nil {
|
||||
t.Errorf("Failed to request task for vacuum worker: %v", err)
|
||||
} else if assignedTask != nil {
|
||||
t.Logf("Vacuum worker got task: %s (%s)", assignedTask.ID, assignedTask.Type)
|
||||
|
||||
// Complete the task
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 50.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update progress: %v", err)
|
||||
}
|
||||
|
||||
err = adminServer.CompleteTask(assignedTask.ID, true, "")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to complete task: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// EC worker should get EC tasks
|
||||
assignedTask, err = adminServer.RequestTask("ec-worker-1", []types.TaskType{types.TaskTypeErasureCoding})
|
||||
if err != nil {
|
||||
t.Errorf("Failed to request task for EC worker: %v", err)
|
||||
} else if assignedTask != nil {
|
||||
t.Logf("EC worker got task: %s (%s)", assignedTask.ID, assignedTask.Type)
|
||||
|
||||
// Complete the task
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 100.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update progress: %v", err)
|
||||
}
|
||||
|
||||
err = adminServer.CompleteTask(assignedTask.ID, true, "")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to complete task: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-capability worker should be able to get any remaining task
|
||||
assignedTask, err = adminServer.RequestTask("multi-worker-1", []types.TaskType{types.TaskTypeVacuum, types.TaskTypeErasureCoding})
|
||||
if err != nil {
|
||||
t.Errorf("Failed to request task for multi worker: %v", err)
|
||||
} else if assignedTask != nil {
|
||||
t.Logf("Multi worker got task: %s (%s)", assignedTask.ID, assignedTask.Type)
|
||||
|
||||
// Complete the task
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 100.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update progress: %v", err)
|
||||
}
|
||||
|
||||
err = adminServer.CompleteTask(assignedTask.ID, true, "")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to complete task: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check final statistics
|
||||
stats := adminServer.GetSystemStats()
|
||||
t.Logf("Final stats: Active tasks=%d, Queued tasks=%d, Active workers=%d, Total tasks=%d",
|
||||
stats.ActiveTasks, stats.QueuedTasks, stats.ActiveWorkers, stats.TotalTasks)
|
||||
|
||||
history := adminServer.GetTaskHistory()
|
||||
t.Logf("Task history contains %d completed tasks", len(history))
|
||||
|
||||
for _, entry := range history {
|
||||
t.Logf("Completed: %s (%s) - Worker: %s, Duration: %v",
|
||||
entry.TaskID, entry.TaskType, entry.WorkerID, entry.Duration)
|
||||
}
|
||||
|
||||
t.Logf("Minimal full workflow test completed successfully")
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
ec_task "github.com/seaweedfs/seaweedfs/weed/worker/tasks/erasure_coding"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
||||
)
|
||||
|
||||
// TestEnhancedECIntegration tests the enhanced EC implementation with the admin server
|
||||
func TestEnhancedECIntegration(t *testing.T) {
|
||||
t.Logf("Starting enhanced EC integration test")
|
||||
|
||||
// Step 1: Create admin server
|
||||
config := &MinimalAdminConfig{
|
||||
ScanInterval: 10 * time.Second,
|
||||
WorkerTimeout: 30 * time.Second,
|
||||
TaskTimeout: 30 * time.Minute, // EC takes longer
|
||||
MaxRetries: 3,
|
||||
ReconcileInterval: 5 * time.Minute,
|
||||
EnableFailureRecovery: true,
|
||||
MaxConcurrentTasks: 2, // Limit concurrency for EC tasks
|
||||
}
|
||||
|
||||
adminServer := NewMinimalAdminServer(config, nil)
|
||||
err := adminServer.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start admin server: %v", err)
|
||||
}
|
||||
defer adminServer.Stop()
|
||||
|
||||
// Step 2: Register an EC-capable worker
|
||||
worker := &types.Worker{
|
||||
ID: "ec-worker-1",
|
||||
Address: "localhost:9001",
|
||||
Capabilities: []types.TaskType{types.TaskTypeErasureCoding},
|
||||
MaxConcurrent: 1,
|
||||
Status: "active",
|
||||
CurrentLoad: 0,
|
||||
LastHeartbeat: time.Now(),
|
||||
}
|
||||
|
||||
err = adminServer.RegisterWorker(worker)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to register EC worker: %v", err)
|
||||
}
|
||||
t.Logf("Successfully registered EC worker %s", worker.ID)
|
||||
|
||||
// Step 3: Create an EC task
|
||||
ecTask := &types.Task{
|
||||
ID: "enhanced-ec-task-1",
|
||||
Type: types.TaskTypeErasureCoding,
|
||||
VolumeID: 12345,
|
||||
Server: "localhost:8080",
|
||||
Status: types.TaskStatusPending,
|
||||
Priority: types.TaskPriorityHigh,
|
||||
Parameters: map[string]interface{}{
|
||||
"volume_size": int64(32 * 1024 * 1024 * 1024), // 32GB
|
||||
"master_client": "localhost:9333",
|
||||
"work_dir": "/tmp/seaweedfs_ec_work",
|
||||
"collection": "test",
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err = adminServer.QueueTask(ecTask)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to queue EC task: %v", err)
|
||||
}
|
||||
t.Logf("Successfully queued enhanced EC task %s for volume %d", ecTask.ID, ecTask.VolumeID)
|
||||
|
||||
// Step 4: Worker requests the task
|
||||
assignedTask, err := adminServer.RequestTask("ec-worker-1", []types.TaskType{types.TaskTypeErasureCoding})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to request EC task: %v", err)
|
||||
}
|
||||
|
||||
if assignedTask != nil {
|
||||
t.Logf("EC worker got task: %s (%s) for volume %d",
|
||||
assignedTask.ID, assignedTask.Type, assignedTask.VolumeID)
|
||||
|
||||
// Step 5: Simulate enhanced EC task execution progress
|
||||
t.Logf("Simulating enhanced EC task execution phases")
|
||||
|
||||
// Phase 1: Copying volume data
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 15.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update progress (copying): %v", err)
|
||||
}
|
||||
t.Logf("Phase 1: Volume data copied to local disk")
|
||||
|
||||
// Phase 2: Marking read-only
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 25.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update progress (read-only): %v", err)
|
||||
}
|
||||
t.Logf("Phase 2: Source volume marked as read-only")
|
||||
|
||||
// Phase 3: Local EC encoding
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 60.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update progress (encoding): %v", err)
|
||||
}
|
||||
t.Logf("Phase 3: Local Reed-Solomon encoding completed (10+4 shards)")
|
||||
|
||||
// Phase 4: Calculating optimal placement
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 70.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update progress (placement): %v", err)
|
||||
}
|
||||
t.Logf("Phase 4: Optimal shard placement calculated with affinity")
|
||||
|
||||
// Phase 5: Distributing shards
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 90.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update progress (distribution): %v", err)
|
||||
}
|
||||
t.Logf("Phase 5: Shards distributed across servers with rack diversity")
|
||||
|
||||
// Phase 6: Verification and cleanup
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 100.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update progress (completion): %v", err)
|
||||
}
|
||||
t.Logf("Phase 6: Verification and cleanup completed")
|
||||
|
||||
// Step 6: Complete the task
|
||||
err = adminServer.CompleteTask(assignedTask.ID, true, "")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to complete EC task: %v", err)
|
||||
}
|
||||
t.Logf("Successfully completed enhanced EC task %s", assignedTask.ID)
|
||||
} else {
|
||||
t.Logf("No EC task was assigned (expected in test environment)")
|
||||
}
|
||||
|
||||
// Step 7: Verify task completion
|
||||
stats := adminServer.GetSystemStats()
|
||||
t.Logf("Final stats: Active tasks=%d, Queued tasks=%d, Active workers=%d, Total tasks=%d",
|
||||
stats.ActiveTasks, stats.QueuedTasks, stats.ActiveWorkers, stats.TotalTasks)
|
||||
|
||||
history := adminServer.GetTaskHistory()
|
||||
t.Logf("Task history contains %d completed tasks", len(history))
|
||||
|
||||
if len(history) > 0 {
|
||||
lastEntry := history[len(history)-1]
|
||||
t.Logf("Last completed task: %s (%s) - Duration: %v",
|
||||
lastEntry.TaskID, lastEntry.TaskType, lastEntry.Duration)
|
||||
|
||||
if lastEntry.TaskType == types.TaskTypeErasureCoding {
|
||||
t.Logf("Enhanced EC task completed successfully")
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Enhanced EC integration test completed successfully")
|
||||
}
|
||||
|
||||
// TestEnhancedECTaskValidation tests the enhanced EC task validation
|
||||
func TestEnhancedECTaskValidation(t *testing.T) {
|
||||
t.Logf("Testing enhanced EC task validation")
|
||||
|
||||
// Create a temporary work directory
|
||||
workDir := filepath.Join(os.TempDir(), "seaweedfs_ec_test")
|
||||
err := os.MkdirAll(workDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create work directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(workDir)
|
||||
|
||||
// Create enhanced EC task
|
||||
enhancedTask := ec_task.NewEnhancedECTask(
|
||||
"localhost:8080", // source server
|
||||
12345, // volume ID
|
||||
"localhost:9333", // master client
|
||||
workDir, // work directory
|
||||
)
|
||||
|
||||
// Test validation with valid parameters
|
||||
validParams := types.TaskParams{
|
||||
VolumeID: 12345,
|
||||
Server: "localhost:8080",
|
||||
Collection: "test",
|
||||
Parameters: map[string]interface{}{
|
||||
"volume_size": int64(32 * 1024 * 1024 * 1024),
|
||||
},
|
||||
}
|
||||
|
||||
err = enhancedTask.Validate(validParams)
|
||||
if err != nil {
|
||||
t.Errorf("Valid parameters should pass validation: %v", err)
|
||||
}
|
||||
|
||||
// Test validation with invalid parameters
|
||||
invalidParams := types.TaskParams{
|
||||
VolumeID: 0, // Invalid volume ID
|
||||
Server: "", // Empty server
|
||||
}
|
||||
|
||||
err = enhancedTask.Validate(invalidParams)
|
||||
if err == nil {
|
||||
t.Errorf("Invalid parameters should fail validation")
|
||||
}
|
||||
|
||||
// Test time estimation
|
||||
estimatedTime := enhancedTask.EstimateTime(validParams)
|
||||
t.Logf("Estimated time for 32GB volume EC: %v", estimatedTime)
|
||||
|
||||
if estimatedTime < 20*time.Minute {
|
||||
t.Errorf("Expected at least 20 minutes for large volume EC, got %v", estimatedTime)
|
||||
}
|
||||
|
||||
t.Logf("Enhanced EC task validation completed successfully")
|
||||
}
|
||||
|
||||
// TestEnhancedECFeatures tests specific enhanced EC features
|
||||
func TestEnhancedECFeatures(t *testing.T) {
|
||||
t.Logf("Testing enhanced EC features")
|
||||
|
||||
// Create temporary work directory
|
||||
workDir := filepath.Join(os.TempDir(), "seaweedfs_ec_features_test")
|
||||
err := os.MkdirAll(workDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create work directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(workDir)
|
||||
|
||||
enhancedTask := ec_task.NewEnhancedECTask(
|
||||
"localhost:8080",
|
||||
54321,
|
||||
"localhost:9333",
|
||||
workDir,
|
||||
)
|
||||
|
||||
// Test step tracking
|
||||
t.Logf("Testing step tracking functionality")
|
||||
|
||||
currentStep := enhancedTask.GetCurrentStep()
|
||||
t.Logf("Initial current step: %s", currentStep)
|
||||
|
||||
progress := enhancedTask.GetProgress()
|
||||
t.Logf("Initial progress: %.1f%%", progress)
|
||||
|
||||
// Test parameter extraction
|
||||
params := types.TaskParams{
|
||||
VolumeID: 54321,
|
||||
Server: "localhost:8080",
|
||||
Collection: "enhanced_test",
|
||||
Parameters: map[string]interface{}{
|
||||
"volume_size": int64(64 * 1024 * 1024 * 1024), // 64GB
|
||||
"data_shards": 10,
|
||||
"parity_shards": 4,
|
||||
"affinity_zones": []string{"zone-a", "zone-b", "zone-c"},
|
||||
},
|
||||
}
|
||||
|
||||
estimatedTime := enhancedTask.EstimateTime(params)
|
||||
expectedMinTime := time.Duration(64*2) * time.Minute // 2 minutes per GB
|
||||
|
||||
t.Logf("64GB volume estimated time: %v (expected minimum: %v)", estimatedTime, expectedMinTime)
|
||||
|
||||
if estimatedTime < expectedMinTime {
|
||||
t.Errorf("Time estimate seems too low for 64GB volume")
|
||||
}
|
||||
|
||||
t.Logf("Enhanced EC features test completed successfully")
|
||||
}
|
||||
|
||||
// TestECTaskComparison compares basic vs enhanced EC implementations
|
||||
func TestECTaskComparison(t *testing.T) {
|
||||
t.Logf("Comparing basic vs enhanced EC implementations")
|
||||
|
||||
// Basic EC task estimation
|
||||
basicParams := types.TaskParams{
|
||||
VolumeID: 11111,
|
||||
Server: "localhost:8080",
|
||||
Parameters: map[string]interface{}{
|
||||
"volume_size": int64(30 * 1024 * 1024 * 1024), // 30GB
|
||||
},
|
||||
}
|
||||
|
||||
// Create basic task (existing implementation)
|
||||
basicTask := ec_task.NewTask("localhost:8080", 11111)
|
||||
basicTime := basicTask.EstimateTime(basicParams)
|
||||
|
||||
// Create enhanced task
|
||||
workDir := filepath.Join(os.TempDir(), "seaweedfs_ec_comparison")
|
||||
defer os.RemoveAll(workDir)
|
||||
|
||||
enhancedTask := ec_task.NewEnhancedECTask(
|
||||
"localhost:8080",
|
||||
22222,
|
||||
"localhost:9333",
|
||||
workDir,
|
||||
)
|
||||
enhancedTime := enhancedTask.EstimateTime(basicParams)
|
||||
|
||||
t.Logf("Basic EC task estimated time: %v", basicTime)
|
||||
t.Logf("Enhanced EC task estimated time: %v", enhancedTime)
|
||||
|
||||
// Enhanced should take longer due to additional processing
|
||||
if enhancedTime <= basicTime {
|
||||
t.Logf("Note: Enhanced EC might take longer due to local processing and smart distribution")
|
||||
}
|
||||
|
||||
// Test feature differences
|
||||
t.Logf("Basic EC features:")
|
||||
t.Logf(" - Direct volume server EC generation")
|
||||
t.Logf(" - Simple shard mounting")
|
||||
t.Logf(" - No custom placement logic")
|
||||
|
||||
t.Logf("Enhanced EC features:")
|
||||
t.Logf(" - Local volume data copying")
|
||||
t.Logf(" - Local Reed-Solomon encoding")
|
||||
t.Logf(" - Intelligent shard placement with affinity")
|
||||
t.Logf(" - Rack diversity for data shards")
|
||||
t.Logf(" - Load balancing across servers")
|
||||
t.Logf(" - Backup server selection")
|
||||
t.Logf(" - Detailed progress tracking")
|
||||
|
||||
t.Logf("EC task comparison completed successfully")
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/wdclient"
|
||||
)
|
||||
|
||||
// MasterSynchronizer handles periodic synchronization with the master server
|
||||
type MasterSynchronizer struct {
|
||||
masterClient *wdclient.MasterClient
|
||||
volumeStateManager *VolumeStateManager
|
||||
adminServer *AdminServer
|
||||
syncInterval time.Duration
|
||||
stopCh chan struct{}
|
||||
}
|
||||
|
||||
// NewMasterSynchronizer creates a new master synchronizer
|
||||
func NewMasterSynchronizer(masterClient *wdclient.MasterClient, vsm *VolumeStateManager, admin *AdminServer) *MasterSynchronizer {
|
||||
return &MasterSynchronizer{
|
||||
masterClient: masterClient,
|
||||
volumeStateManager: vsm,
|
||||
adminServer: admin,
|
||||
syncInterval: 30 * time.Second, // Default 30 second sync interval
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins the periodic master synchronization
|
||||
func (ms *MasterSynchronizer) Start() {
|
||||
glog.Infof("Starting master synchronization with interval %v", ms.syncInterval)
|
||||
|
||||
go func() {
|
||||
// Immediate sync on startup
|
||||
ms.performSync()
|
||||
|
||||
ticker := time.NewTicker(ms.syncInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
ms.performSync()
|
||||
case <-ms.stopCh:
|
||||
glog.Infof("Master synchronization stopped")
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Stop stops the master synchronization
|
||||
func (ms *MasterSynchronizer) Stop() {
|
||||
close(ms.stopCh)
|
||||
}
|
||||
|
||||
// performSync executes a single synchronization cycle
|
||||
func (ms *MasterSynchronizer) performSync() {
|
||||
glog.V(1).Infof("Starting master sync cycle")
|
||||
startTime := time.Now()
|
||||
|
||||
// Get volume list from master
|
||||
volumeData, err := ms.getVolumeListFromMaster()
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to get volume list from master: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Merge data into volume state manager
|
||||
err = ms.mergeVolumeData(volumeData)
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to merge volume data: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Detect volumes needing work
|
||||
candidates := ms.detectMaintenanceCandidates(volumeData)
|
||||
|
||||
// Process candidates for task assignment
|
||||
ms.processCandidates(candidates)
|
||||
|
||||
duration := time.Since(startTime)
|
||||
glog.V(1).Infof("Master sync completed in %v, found %d maintenance candidates",
|
||||
duration, len(candidates))
|
||||
}
|
||||
|
||||
// getVolumeListFromMaster retrieves the current volume topology from master
|
||||
func (ms *MasterSynchronizer) getVolumeListFromMaster() (*master_pb.VolumeListResponse, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := ms.masterClient.WithClient(false, func(client master_pb.SeaweedClient) error {
|
||||
req := &master_pb.VolumeListRequest{}
|
||||
response, err := client.VolumeList(ctx, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("VolumeList RPC failed: %v", err)
|
||||
}
|
||||
volumeData = response
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return volumeData, nil
|
||||
}
|
||||
|
||||
// VolumeMaintenanceCandidate represents a volume that needs maintenance
|
||||
type VolumeMaintenanceCandidate struct {
|
||||
VolumeID uint32
|
||||
Server string
|
||||
TaskType string
|
||||
Priority TaskPriority
|
||||
Reason string
|
||||
VolumeInfo *VolumeInfo
|
||||
ECShardInfo map[int]*ShardInfo
|
||||
}
|
||||
|
||||
// mergeVolumeData merges master volume data into the volume state manager
|
||||
func (ms *MasterSynchronizer) mergeVolumeData(data *master_pb.VolumeListResponse) error {
|
||||
if data.TopologyInfo == nil {
|
||||
return fmt.Errorf("empty topology info from master")
|
||||
}
|
||||
|
||||
volumes := make(map[uint32]*VolumeInfo)
|
||||
ecShards := make(map[uint32]map[int]*ShardInfo)
|
||||
serverCapacity := make(map[string]*CapacityInfo)
|
||||
|
||||
// Extract volume information from topology
|
||||
ms.extractVolumesFromTopology(data.TopologyInfo, volumes, ecShards, serverCapacity)
|
||||
|
||||
// Update volume state manager
|
||||
err := ms.volumeStateManager.SyncWithMasterData(volumes, ecShards, serverCapacity)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to sync with volume state manager: %v", err)
|
||||
}
|
||||
|
||||
glog.V(2).Infof("Synced %d volumes, %d EC volume groups, %d servers",
|
||||
len(volumes), len(ecShards), len(serverCapacity))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractVolumesFromTopology extracts volume and capacity data from master topology
|
||||
func (ms *MasterSynchronizer) extractVolumesFromTopology(
|
||||
topology *master_pb.TopologyInfo,
|
||||
volumes map[uint32]*VolumeInfo,
|
||||
ecShards map[uint32]map[int]*ShardInfo,
|
||||
serverCapacity map[string]*CapacityInfo) {
|
||||
|
||||
for _, dcInfo := range topology.DataCenterInfos {
|
||||
for _, rackInfo := range dcInfo.RackInfos {
|
||||
for _, nodeInfo := range rackInfo.DataNodeInfos {
|
||||
serverID := fmt.Sprintf("%s:%d", nodeInfo.Id, nodeInfo.GrpcPort)
|
||||
|
||||
// Initialize server capacity info
|
||||
if serverCapacity[serverID] == nil {
|
||||
serverCapacity[serverID] = &CapacityInfo{
|
||||
Server: serverID,
|
||||
DataCenter: dcInfo.Id,
|
||||
Rack: rackInfo.Id,
|
||||
}
|
||||
}
|
||||
|
||||
// Process disk information
|
||||
for diskType, diskInfo := range nodeInfo.DiskInfos {
|
||||
ms.processDiskInfo(diskInfo, diskType, serverID, volumes, ecShards, serverCapacity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processDiskInfo processes disk information for a specific server
|
||||
func (ms *MasterSynchronizer) processDiskInfo(
|
||||
diskInfo *master_pb.DiskInfo,
|
||||
diskType string,
|
||||
serverID string,
|
||||
volumes map[uint32]*VolumeInfo,
|
||||
ecShards map[uint32]map[int]*ShardInfo,
|
||||
serverCapacity map[string]*CapacityInfo) {
|
||||
|
||||
// Update capacity information
|
||||
capacity := serverCapacity[serverID]
|
||||
capacity.TotalCapacity += uint64(diskInfo.MaxVolumeCount) * (32 * 1024 * 1024 * 1024) // Assume 32GB per volume
|
||||
capacity.UsedCapacity += uint64(diskInfo.ActiveVolumeCount) * (32 * 1024 * 1024 * 1024)
|
||||
|
||||
// Process regular volumes
|
||||
for _, volInfo := range diskInfo.VolumeInfos {
|
||||
volumes[volInfo.Id] = &VolumeInfo{
|
||||
ID: volInfo.Id,
|
||||
Size: volInfo.Size,
|
||||
Collection: volInfo.Collection,
|
||||
FileCount: volInfo.FileCount,
|
||||
DeleteCount: volInfo.DeleteCount,
|
||||
DeletedByteCount: volInfo.DeletedByteCount,
|
||||
ReadOnly: volInfo.ReadOnly,
|
||||
Server: serverID,
|
||||
DiskType: diskType,
|
||||
LastModified: time.Unix(volInfo.ModifiedAtSecond, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Process EC shards
|
||||
for _, shardInfo := range diskInfo.EcShardInfos {
|
||||
volumeID := shardInfo.Id
|
||||
if ecShards[volumeID] == nil {
|
||||
ecShards[volumeID] = make(map[int]*ShardInfo)
|
||||
}
|
||||
|
||||
// Extract shard IDs from ec_index_bits
|
||||
for shardID := 0; shardID < 14; shardID++ {
|
||||
if (shardInfo.EcIndexBits & (1 << uint(shardID))) != 0 {
|
||||
ecShards[volumeID][shardID] = &ShardInfo{
|
||||
VolumeID: volumeID,
|
||||
ShardID: shardID,
|
||||
Server: serverID,
|
||||
Status: ShardStatusExists,
|
||||
Size: 0, // Size not available in shard info
|
||||
DiskType: shardInfo.DiskType,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// detectMaintenanceCandidates identifies volumes that need maintenance
|
||||
func (ms *MasterSynchronizer) detectMaintenanceCandidates(data *master_pb.VolumeListResponse) []*VolumeMaintenanceCandidate {
|
||||
var candidates []*VolumeMaintenanceCandidate
|
||||
|
||||
// Get current volume states
|
||||
currentVolumes := ms.volumeStateManager.GetAllVolumeStates()
|
||||
|
||||
for volumeID, volumeState := range currentVolumes {
|
||||
// Skip volumes with in-progress tasks
|
||||
if len(volumeState.InProgressTasks) > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for EC encoding candidates
|
||||
if candidate := ms.checkECEncodingCandidate(volumeID, volumeState); candidate != nil {
|
||||
candidates = append(candidates, candidate)
|
||||
}
|
||||
|
||||
// Check for vacuum candidates
|
||||
if candidate := ms.checkVacuumCandidate(volumeID, volumeState); candidate != nil {
|
||||
candidates = append(candidates, candidate)
|
||||
}
|
||||
|
||||
// Check for EC rebuild candidates
|
||||
if candidate := ms.checkECRebuildCandidate(volumeID, volumeState); candidate != nil {
|
||||
candidates = append(candidates, candidate)
|
||||
}
|
||||
}
|
||||
|
||||
return candidates
|
||||
}
|
||||
|
||||
// checkECEncodingCandidate checks if a volume is a candidate for EC encoding
|
||||
func (ms *MasterSynchronizer) checkECEncodingCandidate(volumeID uint32, state *VolumeState) *VolumeMaintenanceCandidate {
|
||||
volume := state.CurrentState
|
||||
if volume == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// EC encoding criteria:
|
||||
// 1. Volume is read-only or large enough
|
||||
// 2. Not already EC encoded
|
||||
// 3. Size threshold met (e.g., > 20GB)
|
||||
|
||||
const ecSizeThreshold = 20 * 1024 * 1024 * 1024 // 20GB
|
||||
|
||||
isCandidate := (volume.ReadOnly || volume.Size > ecSizeThreshold) &&
|
||||
len(state.ECShardState) == 0 &&
|
||||
volume.Size > 1024*1024*1024 // At least 1GB
|
||||
|
||||
if !isCandidate {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &VolumeMaintenanceCandidate{
|
||||
VolumeID: volumeID,
|
||||
Server: volume.Server,
|
||||
TaskType: "ec_encode",
|
||||
Priority: TaskPriorityNormal,
|
||||
Reason: fmt.Sprintf("Volume size %d bytes exceeds EC threshold", volume.Size),
|
||||
VolumeInfo: volume,
|
||||
}
|
||||
}
|
||||
|
||||
// checkVacuumCandidate checks if a volume is a candidate for vacuum
|
||||
func (ms *MasterSynchronizer) checkVacuumCandidate(volumeID uint32, state *VolumeState) *VolumeMaintenanceCandidate {
|
||||
volume := state.CurrentState
|
||||
if volume == nil || volume.ReadOnly {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Vacuum criteria:
|
||||
// 1. Significant deleted bytes (> 30% of volume size or > 1GB)
|
||||
// 2. Not currently being written to heavily
|
||||
|
||||
const vacuumThresholdPercent = 0.3
|
||||
const vacuumMinBytes = 1024 * 1024 * 1024 // 1GB
|
||||
|
||||
deletedRatio := float64(volume.DeletedByteCount) / float64(volume.Size)
|
||||
isCandidate := (deletedRatio > vacuumThresholdPercent || volume.DeletedByteCount > vacuumMinBytes) &&
|
||||
volume.Size > 0
|
||||
|
||||
if !isCandidate {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &VolumeMaintenanceCandidate{
|
||||
VolumeID: volumeID,
|
||||
Server: volume.Server,
|
||||
TaskType: "vacuum",
|
||||
Priority: TaskPriorityNormal,
|
||||
Reason: fmt.Sprintf("Deleted bytes %d (%.1f%%) exceed vacuum threshold",
|
||||
volume.DeletedByteCount, deletedRatio*100),
|
||||
VolumeInfo: volume,
|
||||
}
|
||||
}
|
||||
|
||||
// checkECRebuildCandidate checks if an EC volume needs shard rebuilding
|
||||
func (ms *MasterSynchronizer) checkECRebuildCandidate(volumeID uint32, state *VolumeState) *VolumeMaintenanceCandidate {
|
||||
if len(state.ECShardState) == 0 {
|
||||
return nil // Not an EC volume
|
||||
}
|
||||
|
||||
// Check for missing or corrupted shards
|
||||
missingShards := 0
|
||||
corruptedShards := 0
|
||||
|
||||
for shardID := 0; shardID < 14; shardID++ {
|
||||
shardState, exists := state.ECShardState[shardID]
|
||||
if !exists {
|
||||
missingShards++
|
||||
} else if len(shardState.CurrentShards) == 0 {
|
||||
missingShards++
|
||||
} else {
|
||||
// Check for corrupted shards
|
||||
for _, shard := range shardState.CurrentShards {
|
||||
if shard.Status == ShardStatusCorrupted {
|
||||
corruptedShards++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Need rebuild if any shards are missing or corrupted
|
||||
if missingShards > 0 || corruptedShards > 0 {
|
||||
return &VolumeMaintenanceCandidate{
|
||||
VolumeID: volumeID,
|
||||
TaskType: "ec_rebuild",
|
||||
Priority: TaskPriorityHigh, // High priority for data integrity
|
||||
Reason: fmt.Sprintf("Missing %d shards, corrupted %d shards", missingShards, corruptedShards),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processCandidates attempts to assign tasks for maintenance candidates
|
||||
func (ms *MasterSynchronizer) processCandidates(candidates []*VolumeMaintenanceCandidate) {
|
||||
for _, candidate := range candidates {
|
||||
// Check if we can assign this task
|
||||
if !ms.canAssignCandidate(candidate) {
|
||||
glog.V(2).Infof("Cannot assign task for volume %d: insufficient capacity or no workers",
|
||||
candidate.VolumeID)
|
||||
continue
|
||||
}
|
||||
|
||||
// Create and queue the task
|
||||
task := ms.createTaskFromCandidate(candidate)
|
||||
if task != nil {
|
||||
ms.adminServer.QueueTask(task)
|
||||
glog.V(1).Infof("Queued %s task for volume %d on server %s: %s",
|
||||
candidate.TaskType, candidate.VolumeID, candidate.Server, candidate.Reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// canAssignCandidate checks if a candidate can be assigned (capacity, workers available)
|
||||
func (ms *MasterSynchronizer) canAssignCandidate(candidate *VolumeMaintenanceCandidate) bool {
|
||||
// Check if server has capacity for the task
|
||||
if candidate.TaskType == "ec_encode" {
|
||||
// EC encoding requires significant temporary space
|
||||
requiredSpace := candidate.VolumeInfo.Size * 2 // Estimate 2x volume size needed
|
||||
if !ms.volumeStateManager.CanAssignVolumeToServer(requiredSpace, candidate.Server) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have workers capable of this task type
|
||||
availableWorkers := ms.adminServer.GetAvailableWorkers(candidate.TaskType)
|
||||
if len(availableWorkers) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// createTaskFromCandidate creates a task from a maintenance candidate
|
||||
func (ms *MasterSynchronizer) createTaskFromCandidate(candidate *VolumeMaintenanceCandidate) *Task {
|
||||
now := time.Now()
|
||||
|
||||
task := &Task{
|
||||
ID: generateTaskID(),
|
||||
Type: TaskType(candidate.TaskType),
|
||||
VolumeID: candidate.VolumeID,
|
||||
Priority: candidate.Priority,
|
||||
Status: TaskStatusPending,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Parameters: map[string]string{
|
||||
"volume_id": fmt.Sprintf("%d", candidate.VolumeID),
|
||||
"server": candidate.Server,
|
||||
"reason": candidate.Reason,
|
||||
},
|
||||
}
|
||||
|
||||
// Add task-specific parameters
|
||||
switch candidate.TaskType {
|
||||
case "ec_encode":
|
||||
task.Parameters["replication"] = "001" // Default replication for EC
|
||||
task.Parameters["collection"] = candidate.VolumeInfo.Collection
|
||||
case "vacuum":
|
||||
task.Parameters["garbage_threshold"] = "0.3" // 30% threshold
|
||||
case "ec_rebuild":
|
||||
// Add info about which shards need rebuilding
|
||||
}
|
||||
|
||||
return task
|
||||
}
|
||||
|
||||
// Global variable to hold the master volume data
|
||||
var volumeData *master_pb.VolumeListResponse
|
||||
@@ -0,0 +1,324 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/wdclient"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
||||
)
|
||||
|
||||
// MinimalAdminConfig contains configuration for the minimal admin server
|
||||
type MinimalAdminConfig struct {
|
||||
ScanInterval time.Duration
|
||||
WorkerTimeout time.Duration
|
||||
TaskTimeout time.Duration
|
||||
MaxRetries int
|
||||
ReconcileInterval time.Duration
|
||||
EnableFailureRecovery bool
|
||||
MaxConcurrentTasks int
|
||||
}
|
||||
|
||||
// MinimalAdminServer manages workers and tasks with a simple implementation
|
||||
type MinimalAdminServer struct {
|
||||
config *MinimalAdminConfig
|
||||
masterClient *wdclient.MasterClient
|
||||
running bool
|
||||
mutex sync.RWMutex
|
||||
|
||||
// Task management
|
||||
tasks map[string]*types.Task
|
||||
taskQueue []*types.Task
|
||||
activeTasks map[string]*types.Task
|
||||
|
||||
// Worker management
|
||||
workers map[string]*types.Worker
|
||||
workerStatus map[string]*types.WorkerStatus
|
||||
|
||||
// Task history
|
||||
taskHistory []MinimalTaskHistoryEntry
|
||||
}
|
||||
|
||||
// MinimalTaskHistoryEntry represents a single task history entry
|
||||
type MinimalTaskHistoryEntry struct {
|
||||
TaskID string
|
||||
TaskType types.TaskType
|
||||
VolumeID uint32
|
||||
WorkerID string
|
||||
Status types.TaskStatus
|
||||
StartedAt time.Time
|
||||
CompletedAt time.Time
|
||||
Duration time.Duration
|
||||
ErrorMessage string
|
||||
}
|
||||
|
||||
// MinimalSystemStats represents system statistics
|
||||
type MinimalSystemStats struct {
|
||||
ActiveTasks int
|
||||
QueuedTasks int
|
||||
ActiveWorkers int
|
||||
TotalTasks int
|
||||
}
|
||||
|
||||
// NewMinimalAdminServer creates a new minimal admin server
|
||||
func NewMinimalAdminServer(config *MinimalAdminConfig, masterClient *wdclient.MasterClient) *MinimalAdminServer {
|
||||
return &MinimalAdminServer{
|
||||
config: config,
|
||||
masterClient: masterClient,
|
||||
tasks: make(map[string]*types.Task),
|
||||
taskQueue: make([]*types.Task, 0),
|
||||
activeTasks: make(map[string]*types.Task),
|
||||
workers: make(map[string]*types.Worker),
|
||||
workerStatus: make(map[string]*types.WorkerStatus),
|
||||
taskHistory: make([]MinimalTaskHistoryEntry, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the minimal admin server
|
||||
func (as *MinimalAdminServer) Start() error {
|
||||
as.mutex.Lock()
|
||||
defer as.mutex.Unlock()
|
||||
|
||||
if as.running {
|
||||
return fmt.Errorf("admin server is already running")
|
||||
}
|
||||
|
||||
as.running = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the minimal admin server
|
||||
func (as *MinimalAdminServer) Stop() error {
|
||||
as.mutex.Lock()
|
||||
defer as.mutex.Unlock()
|
||||
|
||||
as.running = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterWorker registers a new worker
|
||||
func (as *MinimalAdminServer) RegisterWorker(worker *types.Worker) error {
|
||||
as.mutex.Lock()
|
||||
defer as.mutex.Unlock()
|
||||
|
||||
if !as.running {
|
||||
return fmt.Errorf("admin server is not running")
|
||||
}
|
||||
|
||||
as.workers[worker.ID] = worker
|
||||
as.workerStatus[worker.ID] = &types.WorkerStatus{
|
||||
Status: "active",
|
||||
CurrentLoad: 0,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// QueueTask adds a new task to the task queue
|
||||
func (as *MinimalAdminServer) QueueTask(task *types.Task) error {
|
||||
as.mutex.Lock()
|
||||
defer as.mutex.Unlock()
|
||||
|
||||
if !as.running {
|
||||
return fmt.Errorf("admin server is not running")
|
||||
}
|
||||
|
||||
if task.ID == "" {
|
||||
task.ID = fmt.Sprintf("task-%d", time.Now().UnixNano())
|
||||
}
|
||||
|
||||
task.Status = types.TaskStatusPending
|
||||
task.CreatedAt = time.Now()
|
||||
|
||||
as.tasks[task.ID] = task
|
||||
as.taskQueue = append(as.taskQueue, task)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequestTask requests a task for a worker
|
||||
func (as *MinimalAdminServer) RequestTask(workerID string, capabilities []types.TaskType) (*types.Task, error) {
|
||||
as.mutex.Lock()
|
||||
defer as.mutex.Unlock()
|
||||
|
||||
if !as.running {
|
||||
return nil, fmt.Errorf("admin server is not running")
|
||||
}
|
||||
|
||||
// Check if worker exists
|
||||
worker, exists := as.workers[workerID]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("worker %s not found", workerID)
|
||||
}
|
||||
|
||||
// Check if worker has capacity
|
||||
status := as.workerStatus[workerID]
|
||||
if status.CurrentLoad >= worker.MaxConcurrent {
|
||||
return nil, nil // No capacity
|
||||
}
|
||||
|
||||
// Find a suitable task
|
||||
for i, task := range as.taskQueue {
|
||||
if task.Status != types.TaskStatusPending {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if worker can handle this task type
|
||||
canHandle := false
|
||||
for _, capability := range capabilities {
|
||||
if task.Type == capability {
|
||||
canHandle = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if canHandle {
|
||||
// Assign task to worker
|
||||
task.Status = types.TaskStatusInProgress
|
||||
task.WorkerID = workerID
|
||||
now := time.Now()
|
||||
task.StartedAt = &now
|
||||
|
||||
// Move task from queue to active tasks
|
||||
as.taskQueue = append(as.taskQueue[:i], as.taskQueue[i+1:]...)
|
||||
as.activeTasks[task.ID] = task
|
||||
|
||||
// Update worker load
|
||||
status.CurrentLoad++
|
||||
|
||||
return task, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil // No suitable task found
|
||||
}
|
||||
|
||||
// UpdateTaskProgress updates task progress
|
||||
func (as *MinimalAdminServer) UpdateTaskProgress(taskID string, progress float64) error {
|
||||
as.mutex.Lock()
|
||||
defer as.mutex.Unlock()
|
||||
|
||||
task, exists := as.tasks[taskID]
|
||||
if !exists {
|
||||
return fmt.Errorf("task %s not found", taskID)
|
||||
}
|
||||
|
||||
task.Progress = progress
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CompleteTask marks a task as completed
|
||||
func (as *MinimalAdminServer) CompleteTask(taskID string, success bool, errorMessage string) error {
|
||||
as.mutex.Lock()
|
||||
defer as.mutex.Unlock()
|
||||
|
||||
task, exists := as.tasks[taskID]
|
||||
if !exists {
|
||||
return fmt.Errorf("task %s not found", taskID)
|
||||
}
|
||||
|
||||
// Update task status
|
||||
if success {
|
||||
task.Status = types.TaskStatusCompleted
|
||||
} else {
|
||||
task.Status = types.TaskStatusFailed
|
||||
task.Error = errorMessage
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
task.CompletedAt = &now
|
||||
|
||||
// Remove from active tasks
|
||||
delete(as.activeTasks, taskID)
|
||||
|
||||
// Update worker load
|
||||
if task.WorkerID != "" {
|
||||
if status, exists := as.workerStatus[task.WorkerID]; exists {
|
||||
status.CurrentLoad--
|
||||
}
|
||||
}
|
||||
|
||||
// Add to history
|
||||
var duration time.Duration
|
||||
if task.StartedAt != nil {
|
||||
duration = now.Sub(*task.StartedAt)
|
||||
}
|
||||
|
||||
entry := MinimalTaskHistoryEntry{
|
||||
TaskID: task.ID,
|
||||
TaskType: task.Type,
|
||||
VolumeID: task.VolumeID,
|
||||
WorkerID: task.WorkerID,
|
||||
Status: task.Status,
|
||||
StartedAt: *task.StartedAt,
|
||||
CompletedAt: now,
|
||||
Duration: duration,
|
||||
ErrorMessage: errorMessage,
|
||||
}
|
||||
as.taskHistory = append(as.taskHistory, entry)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateWorkerHeartbeat updates worker heartbeat
|
||||
func (as *MinimalAdminServer) UpdateWorkerHeartbeat(workerID string, status *types.WorkerStatus) error {
|
||||
as.mutex.Lock()
|
||||
defer as.mutex.Unlock()
|
||||
|
||||
worker, exists := as.workers[workerID]
|
||||
if !exists {
|
||||
return fmt.Errorf("worker %s not found", workerID)
|
||||
}
|
||||
|
||||
worker.LastHeartbeat = time.Now()
|
||||
as.workerStatus[workerID] = status
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSystemStats returns system statistics
|
||||
func (as *MinimalAdminServer) GetSystemStats() *MinimalSystemStats {
|
||||
as.mutex.RLock()
|
||||
defer as.mutex.RUnlock()
|
||||
|
||||
activeWorkers := 0
|
||||
for _, status := range as.workerStatus {
|
||||
if status.Status == "active" {
|
||||
activeWorkers++
|
||||
}
|
||||
}
|
||||
|
||||
return &MinimalSystemStats{
|
||||
ActiveTasks: len(as.activeTasks),
|
||||
QueuedTasks: len(as.taskQueue),
|
||||
ActiveWorkers: activeWorkers,
|
||||
TotalTasks: len(as.tasks),
|
||||
}
|
||||
}
|
||||
|
||||
// GetQueuedTaskCount returns the number of queued tasks
|
||||
func (as *MinimalAdminServer) GetQueuedTaskCount() int {
|
||||
as.mutex.RLock()
|
||||
defer as.mutex.RUnlock()
|
||||
return len(as.taskQueue)
|
||||
}
|
||||
|
||||
// GetActiveTaskCount returns the number of active tasks
|
||||
func (as *MinimalAdminServer) GetActiveTaskCount() int {
|
||||
as.mutex.RLock()
|
||||
defer as.mutex.RUnlock()
|
||||
return len(as.activeTasks)
|
||||
}
|
||||
|
||||
// GetTaskHistory returns task history
|
||||
func (as *MinimalAdminServer) GetTaskHistory() []MinimalTaskHistoryEntry {
|
||||
as.mutex.RLock()
|
||||
defer as.mutex.RUnlock()
|
||||
|
||||
// Return a copy of the history
|
||||
history := make([]MinimalTaskHistoryEntry, len(as.taskHistory))
|
||||
copy(history, as.taskHistory)
|
||||
return history
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
||||
)
|
||||
|
||||
// TestMinimalIntegration tests basic admin-worker operational flow using the minimal implementation
|
||||
func TestMinimalIntegration(t *testing.T) {
|
||||
t.Logf("Starting minimal integration test")
|
||||
|
||||
// Step 1: Create a minimal admin server configuration
|
||||
config := &MinimalAdminConfig{
|
||||
ScanInterval: 10 * time.Second,
|
||||
WorkerTimeout: 30 * time.Second,
|
||||
TaskTimeout: 2 * time.Hour,
|
||||
MaxRetries: 3,
|
||||
ReconcileInterval: 5 * time.Minute,
|
||||
EnableFailureRecovery: true,
|
||||
MaxConcurrentTasks: 5,
|
||||
}
|
||||
|
||||
// Step 2: Create minimal admin server with nil master client (for testing)
|
||||
adminServer := NewMinimalAdminServer(config, nil)
|
||||
|
||||
// Step 3: Start admin server
|
||||
err := adminServer.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start admin server: %v", err)
|
||||
}
|
||||
defer adminServer.Stop()
|
||||
|
||||
// Step 4: Test worker registration
|
||||
t.Logf("Testing worker registration")
|
||||
|
||||
worker := &types.Worker{
|
||||
ID: "test-worker-1",
|
||||
Address: "localhost:9001",
|
||||
Capabilities: []types.TaskType{types.TaskTypeVacuum},
|
||||
MaxConcurrent: 2,
|
||||
Status: "active",
|
||||
CurrentLoad: 0,
|
||||
LastHeartbeat: time.Now(),
|
||||
}
|
||||
|
||||
err = adminServer.RegisterWorker(worker)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to register worker: %v", err)
|
||||
}
|
||||
t.Logf("Successfully registered worker %s", worker.ID)
|
||||
|
||||
// Step 5: Test task queueing
|
||||
t.Logf("Testing task queueing")
|
||||
|
||||
task := &types.Task{
|
||||
ID: "test-task-1",
|
||||
Type: types.TaskTypeVacuum,
|
||||
VolumeID: 1001,
|
||||
Server: "localhost:8080",
|
||||
Status: types.TaskStatusPending,
|
||||
Priority: types.TaskPriorityNormal,
|
||||
Parameters: map[string]interface{}{
|
||||
"garbage_threshold": "0.3",
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err = adminServer.QueueTask(task)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to queue task: %v", err)
|
||||
}
|
||||
t.Logf("Successfully queued task %s", task.ID)
|
||||
|
||||
// Step 6: Test task request by worker
|
||||
t.Logf("Testing task request")
|
||||
|
||||
assignedTask, err := adminServer.RequestTask("test-worker-1", []types.TaskType{types.TaskTypeVacuum})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to request task: %v", err)
|
||||
}
|
||||
|
||||
if assignedTask != nil {
|
||||
t.Logf("Successfully assigned task %s to worker", assignedTask.ID)
|
||||
|
||||
// Step 7: Test task progress updates
|
||||
t.Logf("Testing task progress updates")
|
||||
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 25.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update task progress to 25%%: %v", err)
|
||||
}
|
||||
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 50.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update task progress to 50%%: %v", err)
|
||||
}
|
||||
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 75.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update task progress to 75%%: %v", err)
|
||||
}
|
||||
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 100.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update task progress to 100%%: %v", err)
|
||||
}
|
||||
|
||||
// Step 8: Test task completion
|
||||
t.Logf("Testing task completion")
|
||||
|
||||
err = adminServer.CompleteTask(assignedTask.ID, true, "")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to complete task: %v", err)
|
||||
}
|
||||
t.Logf("Successfully completed task %s", assignedTask.ID)
|
||||
} else {
|
||||
t.Logf("No task was assigned (queue might be empty)")
|
||||
}
|
||||
|
||||
// Step 9: Test basic metrics
|
||||
t.Logf("Testing basic metrics")
|
||||
|
||||
stats := adminServer.GetSystemStats()
|
||||
if stats != nil {
|
||||
t.Logf("System stats: Active tasks=%d, Queued tasks=%d, Active workers=%d, Total tasks=%d",
|
||||
stats.ActiveTasks, stats.QueuedTasks, stats.ActiveWorkers, stats.TotalTasks)
|
||||
}
|
||||
|
||||
queuedCount := adminServer.GetQueuedTaskCount()
|
||||
activeCount := adminServer.GetActiveTaskCount()
|
||||
t.Logf("Queue status: %d queued, %d active tasks", queuedCount, activeCount)
|
||||
|
||||
// Step 10: Test task history
|
||||
history := adminServer.GetTaskHistory()
|
||||
t.Logf("Task history contains %d entries", len(history))
|
||||
|
||||
if len(history) > 0 {
|
||||
lastEntry := history[len(history)-1]
|
||||
t.Logf("Last task in history: %s (%s) - Status: %s, Duration: %v",
|
||||
lastEntry.TaskID, lastEntry.TaskType, lastEntry.Status, lastEntry.Duration)
|
||||
}
|
||||
|
||||
t.Logf("Minimal integration test completed successfully")
|
||||
}
|
||||
|
||||
// TestMinimalWorkerHeartbeat tests worker heartbeat functionality
|
||||
func TestMinimalWorkerHeartbeat(t *testing.T) {
|
||||
t.Logf("Testing minimal worker heartbeat")
|
||||
|
||||
config := &MinimalAdminConfig{
|
||||
ScanInterval: 10 * time.Second,
|
||||
WorkerTimeout: 30 * time.Second,
|
||||
TaskTimeout: 2 * time.Hour,
|
||||
MaxRetries: 3,
|
||||
ReconcileInterval: 5 * time.Minute,
|
||||
EnableFailureRecovery: true,
|
||||
MaxConcurrentTasks: 5,
|
||||
}
|
||||
|
||||
adminServer := NewMinimalAdminServer(config, nil)
|
||||
err := adminServer.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start admin server: %v", err)
|
||||
}
|
||||
defer adminServer.Stop()
|
||||
|
||||
// Register a worker
|
||||
worker := &types.Worker{
|
||||
ID: "heartbeat-worker",
|
||||
Address: "localhost:9002",
|
||||
Capabilities: []types.TaskType{types.TaskTypeVacuum},
|
||||
MaxConcurrent: 1,
|
||||
Status: "active",
|
||||
CurrentLoad: 0,
|
||||
LastHeartbeat: time.Now(),
|
||||
}
|
||||
|
||||
err = adminServer.RegisterWorker(worker)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to register worker: %v", err)
|
||||
}
|
||||
|
||||
// Test heartbeat update
|
||||
status := &types.WorkerStatus{
|
||||
Status: "active",
|
||||
CurrentLoad: 0,
|
||||
}
|
||||
|
||||
err = adminServer.UpdateWorkerHeartbeat("heartbeat-worker", status)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update worker heartbeat: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Minimal worker heartbeat test completed successfully")
|
||||
}
|
||||
|
||||
// TestMinimalTaskQueueOperations tests task queue operations
|
||||
func TestMinimalTaskQueueOperations(t *testing.T) {
|
||||
t.Logf("Testing minimal task queue operations")
|
||||
|
||||
config := &MinimalAdminConfig{
|
||||
ScanInterval: 10 * time.Second,
|
||||
WorkerTimeout: 30 * time.Second,
|
||||
TaskTimeout: 2 * time.Hour,
|
||||
MaxRetries: 3,
|
||||
ReconcileInterval: 5 * time.Minute,
|
||||
EnableFailureRecovery: true,
|
||||
MaxConcurrentTasks: 5,
|
||||
}
|
||||
|
||||
adminServer := NewMinimalAdminServer(config, nil)
|
||||
err := adminServer.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start admin server: %v", err)
|
||||
}
|
||||
defer adminServer.Stop()
|
||||
|
||||
// Test queuing multiple tasks
|
||||
taskCount := 3
|
||||
for i := 0; i < taskCount; i++ {
|
||||
task := &types.Task{
|
||||
ID: fmt.Sprintf("queue-test-task-%d", i),
|
||||
Type: types.TaskTypeVacuum,
|
||||
VolumeID: uint32(2000 + i),
|
||||
Server: "localhost:8080",
|
||||
Status: types.TaskStatusPending,
|
||||
Priority: types.TaskPriorityNormal,
|
||||
Parameters: map[string]interface{}{
|
||||
"garbage_threshold": "0.3",
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err = adminServer.QueueTask(task)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to queue task %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check queue size
|
||||
queuedCount := adminServer.GetQueuedTaskCount()
|
||||
if queuedCount != taskCount {
|
||||
t.Errorf("Expected %d queued tasks, got %d", taskCount, queuedCount)
|
||||
}
|
||||
|
||||
t.Logf("Minimal task queue operations test completed successfully")
|
||||
}
|
||||
|
||||
// TestMinimalFullWorkflow tests the complete workflow from task creation to completion
|
||||
func TestMinimalFullWorkflow(t *testing.T) {
|
||||
t.Logf("Testing minimal full workflow")
|
||||
|
||||
config := &MinimalAdminConfig{
|
||||
ScanInterval: 10 * time.Second,
|
||||
WorkerTimeout: 30 * time.Second,
|
||||
TaskTimeout: 2 * time.Hour,
|
||||
MaxRetries: 3,
|
||||
ReconcileInterval: 5 * time.Minute,
|
||||
EnableFailureRecovery: true,
|
||||
MaxConcurrentTasks: 5,
|
||||
}
|
||||
|
||||
adminServer := NewMinimalAdminServer(config, nil)
|
||||
err := adminServer.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start admin server: %v", err)
|
||||
}
|
||||
defer adminServer.Stop()
|
||||
|
||||
// Register multiple workers with different capabilities
|
||||
workers := []*types.Worker{
|
||||
{
|
||||
ID: "vacuum-worker-1",
|
||||
Address: "localhost:9001",
|
||||
Capabilities: []types.TaskType{types.TaskTypeVacuum},
|
||||
MaxConcurrent: 2,
|
||||
Status: "active",
|
||||
CurrentLoad: 0,
|
||||
LastHeartbeat: time.Now(),
|
||||
},
|
||||
{
|
||||
ID: "ec-worker-1",
|
||||
Address: "localhost:9002",
|
||||
Capabilities: []types.TaskType{types.TaskTypeErasureCoding},
|
||||
MaxConcurrent: 1,
|
||||
Status: "active",
|
||||
CurrentLoad: 0,
|
||||
LastHeartbeat: time.Now(),
|
||||
},
|
||||
{
|
||||
ID: "multi-worker-1",
|
||||
Address: "localhost:9003",
|
||||
Capabilities: []types.TaskType{types.TaskTypeVacuum, types.TaskTypeErasureCoding},
|
||||
MaxConcurrent: 3,
|
||||
Status: "active",
|
||||
CurrentLoad: 0,
|
||||
LastHeartbeat: time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, worker := range workers {
|
||||
err = adminServer.RegisterWorker(worker)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to register worker %s: %v", worker.ID, err)
|
||||
}
|
||||
t.Logf("Registered worker %s with capabilities %v", worker.ID, worker.Capabilities)
|
||||
}
|
||||
|
||||
// Create multiple tasks of different types
|
||||
tasks := []*types.Task{
|
||||
{
|
||||
ID: "vacuum-task-1",
|
||||
Type: types.TaskTypeVacuum,
|
||||
VolumeID: 3001,
|
||||
Server: "localhost:8080",
|
||||
Status: types.TaskStatusPending,
|
||||
Priority: types.TaskPriorityNormal,
|
||||
Parameters: map[string]interface{}{
|
||||
"garbage_threshold": "0.4",
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
{
|
||||
ID: "ec-task-1",
|
||||
Type: types.TaskTypeErasureCoding,
|
||||
VolumeID: 3002,
|
||||
Server: "localhost:8080",
|
||||
Status: types.TaskStatusPending,
|
||||
Priority: types.TaskPriorityHigh,
|
||||
Parameters: map[string]interface{}{
|
||||
"shard_count": "14",
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
{
|
||||
ID: "vacuum-task-2",
|
||||
Type: types.TaskTypeVacuum,
|
||||
VolumeID: 3003,
|
||||
Server: "localhost:8081",
|
||||
Status: types.TaskStatusPending,
|
||||
Priority: types.TaskPriorityLow,
|
||||
Parameters: map[string]interface{}{
|
||||
"garbage_threshold": "0.5",
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, task := range tasks {
|
||||
err = adminServer.QueueTask(task)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to queue task %s: %v", task.ID, err)
|
||||
}
|
||||
t.Logf("Queued task %s (%s) for volume %d", task.ID, task.Type, task.VolumeID)
|
||||
}
|
||||
|
||||
// Test task assignment to different workers
|
||||
t.Logf("Testing task assignments")
|
||||
|
||||
// Vacuum worker should get vacuum tasks
|
||||
assignedTask, err := adminServer.RequestTask("vacuum-worker-1", []types.TaskType{types.TaskTypeVacuum})
|
||||
if err != nil {
|
||||
t.Errorf("Failed to request task for vacuum worker: %v", err)
|
||||
} else if assignedTask != nil {
|
||||
t.Logf("Vacuum worker got task: %s (%s)", assignedTask.ID, assignedTask.Type)
|
||||
|
||||
// Complete the task
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 50.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update progress: %v", err)
|
||||
}
|
||||
|
||||
err = adminServer.CompleteTask(assignedTask.ID, true, "")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to complete task: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// EC worker should get EC tasks
|
||||
assignedTask, err = adminServer.RequestTask("ec-worker-1", []types.TaskType{types.TaskTypeErasureCoding})
|
||||
if err != nil {
|
||||
t.Errorf("Failed to request task for EC worker: %v", err)
|
||||
} else if assignedTask != nil {
|
||||
t.Logf("EC worker got task: %s (%s)", assignedTask.ID, assignedTask.Type)
|
||||
|
||||
// Complete the task
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 100.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update progress: %v", err)
|
||||
}
|
||||
|
||||
err = adminServer.CompleteTask(assignedTask.ID, true, "")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to complete task: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-capability worker should be able to get any remaining task
|
||||
assignedTask, err = adminServer.RequestTask("multi-worker-1", []types.TaskType{types.TaskTypeVacuum, types.TaskTypeErasureCoding})
|
||||
if err != nil {
|
||||
t.Errorf("Failed to request task for multi worker: %v", err)
|
||||
} else if assignedTask != nil {
|
||||
t.Logf("Multi worker got task: %s (%s)", assignedTask.ID, assignedTask.Type)
|
||||
|
||||
// Complete the task
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 100.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update progress: %v", err)
|
||||
}
|
||||
|
||||
err = adminServer.CompleteTask(assignedTask.ID, true, "")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to complete task: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check final statistics
|
||||
stats := adminServer.GetSystemStats()
|
||||
t.Logf("Final stats: Active tasks=%d, Queued tasks=%d, Active workers=%d, Total tasks=%d",
|
||||
stats.ActiveTasks, stats.QueuedTasks, stats.ActiveWorkers, stats.TotalTasks)
|
||||
|
||||
history := adminServer.GetTaskHistory()
|
||||
t.Logf("Task history contains %d completed tasks", len(history))
|
||||
|
||||
for _, entry := range history {
|
||||
t.Logf("Completed: %s (%s) - Worker: %s, Duration: %v",
|
||||
entry.TaskID, entry.TaskType, entry.WorkerID, entry.Duration)
|
||||
}
|
||||
|
||||
t.Logf("Minimal full workflow test completed successfully")
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/wdclient"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
||||
)
|
||||
|
||||
// TestOperationalIntegration tests the basic admin-worker operational flow
|
||||
func TestOperationalIntegration(t *testing.T) {
|
||||
t.Logf("Starting operational integration test")
|
||||
|
||||
// Step 1: Create admin server with operational configuration
|
||||
config := &AdminConfig{
|
||||
ScanInterval: 10 * time.Second,
|
||||
WorkerTimeout: 30 * time.Second,
|
||||
TaskTimeout: 2 * time.Hour,
|
||||
MaxRetries: 3,
|
||||
ReconcileInterval: 5 * time.Minute,
|
||||
EnableFailureRecovery: true,
|
||||
MaxConcurrentTasks: 5,
|
||||
}
|
||||
|
||||
// Create a nil master client for testing (simplified)
|
||||
var masterClient *wdclient.MasterClient
|
||||
|
||||
adminServer := NewAdminServer(config, masterClient)
|
||||
|
||||
// Step 2: Start admin server
|
||||
err := adminServer.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start admin server: %v", err)
|
||||
}
|
||||
defer adminServer.Stop()
|
||||
|
||||
// Step 3: Create and register test workers
|
||||
worker1 := createTestWorker("worker-1", []types.TaskType{types.TaskTypeVacuum, types.TaskTypeErasureCoding})
|
||||
worker2 := createTestWorker("worker-2", []types.TaskType{types.TaskTypeVacuum})
|
||||
|
||||
err = adminServer.RegisterWorker(worker1)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to register worker1: %v", err)
|
||||
}
|
||||
|
||||
err = adminServer.RegisterWorker(worker2)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to register worker2: %v", err)
|
||||
}
|
||||
|
||||
// Step 4: Test basic task queueing
|
||||
t.Logf("Testing task queueing")
|
||||
|
||||
// Create a simple test task
|
||||
testTask := &types.Task{
|
||||
ID: "test-vacuum-1",
|
||||
Type: types.TaskTypeVacuum,
|
||||
VolumeID: 1001,
|
||||
Server: "localhost:8080",
|
||||
Status: types.TaskStatusPending,
|
||||
Priority: types.TaskPriorityNormal,
|
||||
Parameters: map[string]interface{}{
|
||||
"garbage_threshold": "0.3",
|
||||
"server": "localhost:8080",
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err = adminServer.QueueTask(testTask)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to queue test task: %v", err)
|
||||
}
|
||||
t.Logf("Successfully queued test vacuum task for volume %d", testTask.VolumeID)
|
||||
|
||||
// Step 5: Test worker task request and assignment
|
||||
t.Logf("Testing worker task requests and assignment")
|
||||
|
||||
// Worker requests task
|
||||
task, err := adminServer.RequestTask("worker-1", []types.TaskType{types.TaskTypeVacuum})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to request task from worker: %v", err)
|
||||
}
|
||||
|
||||
if task == nil {
|
||||
t.Logf("No tasks available for assignment (this is expected in test environment)")
|
||||
} else {
|
||||
t.Logf("Successfully assigned task %s (%s) to worker-1", task.ID, task.Type)
|
||||
|
||||
// Step 6: Simulate task progress updates
|
||||
t.Logf("Testing task progress updates")
|
||||
|
||||
err = adminServer.UpdateTaskProgress(task.ID, 25.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update task progress: %v", err)
|
||||
}
|
||||
|
||||
err = adminServer.UpdateTaskProgress(task.ID, 50.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update task progress: %v", err)
|
||||
}
|
||||
|
||||
err = adminServer.UpdateTaskProgress(task.ID, 100.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update task progress: %v", err)
|
||||
}
|
||||
|
||||
// Step 7: Test task completion
|
||||
t.Logf("Testing task completion")
|
||||
|
||||
err = adminServer.CompleteTask(task.ID, true, "")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to complete task: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Successfully completed task %s", task.ID)
|
||||
}
|
||||
|
||||
// Step 8: Test metrics and statistics
|
||||
t.Logf("Testing system metrics")
|
||||
|
||||
stats := adminServer.GetSystemStats()
|
||||
t.Logf("System stats: Active tasks=%d, Queued tasks=%d, Active workers=%d",
|
||||
stats.ActiveTasks, stats.QueuedTasks, stats.ActiveWorkers)
|
||||
|
||||
queuedCount := adminServer.GetQueuedTaskCount()
|
||||
activeCount := adminServer.GetActiveTaskCount()
|
||||
t.Logf("Queue status: %d queued, %d active tasks", queuedCount, activeCount)
|
||||
|
||||
// Step 9: Test task history
|
||||
history := adminServer.GetTaskHistory()
|
||||
t.Logf("Task history contains %d entries", len(history))
|
||||
|
||||
t.Logf("Operational integration test completed successfully")
|
||||
}
|
||||
|
||||
func createTestWorker(id string, capabilities []types.TaskType) *types.Worker {
|
||||
return &types.Worker{
|
||||
ID: id,
|
||||
Address: fmt.Sprintf("localhost:900%s", id[len(id)-1:]),
|
||||
Capabilities: capabilities,
|
||||
MaxConcurrent: 2,
|
||||
Status: "active",
|
||||
CurrentLoad: 0,
|
||||
LastHeartbeat: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// TestECTaskExecution tests the EC task validation (without actual execution)
|
||||
func TestECTaskExecution(t *testing.T) {
|
||||
t.Logf("Testing EC task validation")
|
||||
|
||||
params := types.TaskParams{
|
||||
VolumeID: 1002,
|
||||
Server: "localhost:8080",
|
||||
Collection: "test",
|
||||
Parameters: map[string]interface{}{
|
||||
"volume_size": int64(32 * 1024 * 1024 * 1024),
|
||||
},
|
||||
}
|
||||
|
||||
// Test that basic validation would work
|
||||
if params.VolumeID == 0 {
|
||||
t.Errorf("VolumeID should not be zero")
|
||||
}
|
||||
if params.Server == "" {
|
||||
t.Errorf("Server should not be empty")
|
||||
}
|
||||
|
||||
t.Logf("EC task validation passed")
|
||||
}
|
||||
|
||||
// TestVacuumTaskExecution tests the vacuum task validation (without actual execution)
|
||||
func TestVacuumTaskExecution(t *testing.T) {
|
||||
t.Logf("Testing vacuum task validation")
|
||||
|
||||
params := types.TaskParams{
|
||||
VolumeID: 1001,
|
||||
Server: "localhost:8080",
|
||||
Collection: "test",
|
||||
Parameters: map[string]interface{}{
|
||||
"garbage_threshold": "0.3",
|
||||
"volume_size": int64(25 * 1024 * 1024 * 1024),
|
||||
},
|
||||
}
|
||||
|
||||
// Test that basic validation would work
|
||||
if params.VolumeID == 0 {
|
||||
t.Errorf("VolumeID should not be zero")
|
||||
}
|
||||
if params.Server == "" {
|
||||
t.Errorf("Server should not be empty")
|
||||
}
|
||||
|
||||
t.Logf("Vacuum task validation passed")
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
||||
)
|
||||
|
||||
// TestSimpleIntegration tests basic admin-worker operational flow without complex dependencies
|
||||
func TestSimpleIntegration(t *testing.T) {
|
||||
t.Logf("Starting simple integration test")
|
||||
|
||||
// Step 1: Create a minimal admin server configuration
|
||||
config := &AdminConfig{
|
||||
ScanInterval: 10 * time.Second,
|
||||
WorkerTimeout: 30 * time.Second,
|
||||
TaskTimeout: 2 * time.Hour,
|
||||
MaxRetries: 3,
|
||||
ReconcileInterval: 5 * time.Minute,
|
||||
EnableFailureRecovery: true,
|
||||
MaxConcurrentTasks: 5,
|
||||
}
|
||||
|
||||
// Step 2: Create admin server with nil master client (for testing)
|
||||
adminServer := NewAdminServer(config, nil)
|
||||
|
||||
// Step 3: Start admin server
|
||||
err := adminServer.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start admin server: %v", err)
|
||||
}
|
||||
defer adminServer.Stop()
|
||||
|
||||
// Step 4: Test worker registration
|
||||
t.Logf("Testing worker registration")
|
||||
|
||||
worker := &types.Worker{
|
||||
ID: "test-worker-1",
|
||||
Address: "localhost:9001",
|
||||
Capabilities: []types.TaskType{types.TaskTypeVacuum},
|
||||
MaxConcurrent: 2,
|
||||
Status: "active",
|
||||
CurrentLoad: 0,
|
||||
LastHeartbeat: time.Now(),
|
||||
}
|
||||
|
||||
err = adminServer.RegisterWorker(worker)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to register worker: %v", err)
|
||||
}
|
||||
t.Logf("Successfully registered worker %s", worker.ID)
|
||||
|
||||
// Step 5: Test task queueing
|
||||
t.Logf("Testing task queueing")
|
||||
|
||||
task := &types.Task{
|
||||
ID: "test-task-1",
|
||||
Type: types.TaskTypeVacuum,
|
||||
VolumeID: 1001,
|
||||
Server: "localhost:8080",
|
||||
Status: types.TaskStatusPending,
|
||||
Priority: types.TaskPriorityNormal,
|
||||
Parameters: map[string]interface{}{
|
||||
"garbage_threshold": "0.3",
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err = adminServer.QueueTask(task)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to queue task: %v", err)
|
||||
}
|
||||
t.Logf("Successfully queued task %s", task.ID)
|
||||
|
||||
// Step 6: Test task request by worker
|
||||
t.Logf("Testing task request")
|
||||
|
||||
assignedTask, err := adminServer.RequestTask("test-worker-1", []types.TaskType{types.TaskTypeVacuum})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to request task: %v", err)
|
||||
}
|
||||
|
||||
if assignedTask != nil {
|
||||
t.Logf("Successfully assigned task %s to worker", assignedTask.ID)
|
||||
|
||||
// Step 7: Test task progress updates
|
||||
t.Logf("Testing task progress updates")
|
||||
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 50.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update task progress: %v", err)
|
||||
}
|
||||
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 100.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update task progress: %v", err)
|
||||
}
|
||||
|
||||
// Step 8: Test task completion
|
||||
t.Logf("Testing task completion")
|
||||
|
||||
err = adminServer.CompleteTask(assignedTask.ID, true, "")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to complete task: %v", err)
|
||||
}
|
||||
t.Logf("Successfully completed task %s", assignedTask.ID)
|
||||
} else {
|
||||
t.Logf("No task was assigned (queue might be empty)")
|
||||
}
|
||||
|
||||
// Step 9: Test basic metrics
|
||||
t.Logf("Testing basic metrics")
|
||||
|
||||
stats := adminServer.GetSystemStats()
|
||||
if stats != nil {
|
||||
t.Logf("System stats: Active tasks=%d, Queued tasks=%d, Active workers=%d",
|
||||
stats.ActiveTasks, stats.QueuedTasks, stats.ActiveWorkers)
|
||||
}
|
||||
|
||||
queuedCount := adminServer.GetQueuedTaskCount()
|
||||
activeCount := adminServer.GetActiveTaskCount()
|
||||
t.Logf("Queue status: %d queued, %d active tasks", queuedCount, activeCount)
|
||||
|
||||
// Step 10: Test task history
|
||||
history := adminServer.GetTaskHistory()
|
||||
t.Logf("Task history contains %d entries", len(history))
|
||||
|
||||
t.Logf("Simple integration test completed successfully")
|
||||
}
|
||||
|
||||
// TestWorkerHeartbeat tests worker heartbeat functionality
|
||||
func TestWorkerHeartbeat(t *testing.T) {
|
||||
t.Logf("Testing worker heartbeat")
|
||||
|
||||
config := &AdminConfig{
|
||||
ScanInterval: 10 * time.Second,
|
||||
WorkerTimeout: 30 * time.Second,
|
||||
TaskTimeout: 2 * time.Hour,
|
||||
MaxRetries: 3,
|
||||
ReconcileInterval: 5 * time.Minute,
|
||||
EnableFailureRecovery: true,
|
||||
MaxConcurrentTasks: 5,
|
||||
}
|
||||
|
||||
adminServer := NewAdminServer(config, nil)
|
||||
err := adminServer.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start admin server: %v", err)
|
||||
}
|
||||
defer adminServer.Stop()
|
||||
|
||||
// Register a worker
|
||||
worker := &types.Worker{
|
||||
ID: "heartbeat-worker",
|
||||
Address: "localhost:9002",
|
||||
Capabilities: []types.TaskType{types.TaskTypeVacuum},
|
||||
MaxConcurrent: 1,
|
||||
Status: "active",
|
||||
CurrentLoad: 0,
|
||||
LastHeartbeat: time.Now(),
|
||||
}
|
||||
|
||||
err = adminServer.RegisterWorker(worker)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to register worker: %v", err)
|
||||
}
|
||||
|
||||
// Test heartbeat update
|
||||
status := &types.WorkerStatus{
|
||||
Status: "active",
|
||||
CurrentLoad: 0,
|
||||
}
|
||||
|
||||
err = adminServer.UpdateWorkerHeartbeat("heartbeat-worker", status)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update worker heartbeat: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Worker heartbeat test completed successfully")
|
||||
}
|
||||
|
||||
// TestTaskQueueOperations tests task queue operations
|
||||
func TestTaskQueueOperations(t *testing.T) {
|
||||
t.Logf("Testing task queue operations")
|
||||
|
||||
config := &AdminConfig{
|
||||
ScanInterval: 10 * time.Second,
|
||||
WorkerTimeout: 30 * time.Second,
|
||||
TaskTimeout: 2 * time.Hour,
|
||||
MaxRetries: 3,
|
||||
ReconcileInterval: 5 * time.Minute,
|
||||
EnableFailureRecovery: true,
|
||||
MaxConcurrentTasks: 5,
|
||||
}
|
||||
|
||||
adminServer := NewAdminServer(config, nil)
|
||||
err := adminServer.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start admin server: %v", err)
|
||||
}
|
||||
defer adminServer.Stop()
|
||||
|
||||
// Test queuing multiple tasks
|
||||
for i := 0; i < 3; i++ {
|
||||
task := &types.Task{
|
||||
ID: fmt.Sprintf("queue-test-task-%d", i),
|
||||
Type: types.TaskTypeVacuum,
|
||||
VolumeID: uint32(2000 + i),
|
||||
Server: "localhost:8080",
|
||||
Status: types.TaskStatusPending,
|
||||
Priority: types.TaskPriorityNormal,
|
||||
Parameters: map[string]interface{}{
|
||||
"garbage_threshold": "0.3",
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err = adminServer.QueueTask(task)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to queue task %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check queue size
|
||||
queuedCount := adminServer.GetQueuedTaskCount()
|
||||
if queuedCount != 3 {
|
||||
t.Errorf("Expected 3 queued tasks, got %d", queuedCount)
|
||||
}
|
||||
|
||||
t.Logf("Task queue operations test completed successfully")
|
||||
}
|
||||
@@ -0,0 +1,545 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/worker_pb"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// WorkerConnection manages the gRPC connection to a single worker
|
||||
type WorkerConnection struct {
|
||||
workerID string
|
||||
address string
|
||||
conn *grpc.ClientConn
|
||||
client worker_pb.WorkerServiceClient
|
||||
stream worker_pb.WorkerService_WorkerStreamClient
|
||||
lastSeen time.Time
|
||||
mutex sync.RWMutex
|
||||
adminServer *AdminServer
|
||||
stopCh chan struct{}
|
||||
active bool
|
||||
}
|
||||
|
||||
// WorkerCommunicationManager manages all worker connections
|
||||
type WorkerCommunicationManager struct {
|
||||
adminServer *AdminServer
|
||||
connections map[string]*WorkerConnection
|
||||
mutex sync.RWMutex
|
||||
stopCh chan struct{}
|
||||
}
|
||||
|
||||
// NewWorkerCommunicationManager creates a new worker communication manager
|
||||
func NewWorkerCommunicationManager(adminServer *AdminServer) *WorkerCommunicationManager {
|
||||
return &WorkerCommunicationManager{
|
||||
adminServer: adminServer,
|
||||
connections: make(map[string]*WorkerConnection),
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the worker communication manager
|
||||
func (wcm *WorkerCommunicationManager) Start() {
|
||||
glog.Infof("Starting worker communication manager")
|
||||
|
||||
go wcm.connectionMonitorLoop()
|
||||
}
|
||||
|
||||
// Stop stops the worker communication manager
|
||||
func (wcm *WorkerCommunicationManager) Stop() {
|
||||
glog.Infof("Stopping worker communication manager")
|
||||
|
||||
close(wcm.stopCh)
|
||||
|
||||
wcm.mutex.Lock()
|
||||
defer wcm.mutex.Unlock()
|
||||
|
||||
for _, conn := range wcm.connections {
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// EstablishWorkerConnection establishes a connection to a worker
|
||||
func (wcm *WorkerCommunicationManager) EstablishWorkerConnection(workerID, address string) error {
|
||||
wcm.mutex.Lock()
|
||||
defer wcm.mutex.Unlock()
|
||||
|
||||
// Check if already connected
|
||||
if conn, exists := wcm.connections[workerID]; exists {
|
||||
if conn.active {
|
||||
return nil // Already connected
|
||||
}
|
||||
conn.Close() // Close inactive connection
|
||||
}
|
||||
|
||||
// Create new connection
|
||||
conn, err := NewWorkerConnection(workerID, address, wcm.adminServer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create worker connection: %v", err)
|
||||
}
|
||||
|
||||
wcm.connections[workerID] = conn
|
||||
|
||||
// Start connection
|
||||
go conn.Start()
|
||||
|
||||
glog.Infof("Established connection to worker %s at %s", workerID, address)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendTaskAssignment sends a task assignment to a worker
|
||||
func (wcm *WorkerCommunicationManager) SendTaskAssignment(workerID string, task *Task) error {
|
||||
wcm.mutex.RLock()
|
||||
conn, exists := wcm.connections[workerID]
|
||||
wcm.mutex.RUnlock()
|
||||
|
||||
if !exists || !conn.active {
|
||||
return fmt.Errorf("no active connection to worker %s", workerID)
|
||||
}
|
||||
|
||||
return conn.SendTaskAssignment(task)
|
||||
}
|
||||
|
||||
// CancelTask sends a task cancellation to a worker
|
||||
func (wcm *WorkerCommunicationManager) CancelTask(workerID, taskID string, reason string) error {
|
||||
wcm.mutex.RLock()
|
||||
conn, exists := wcm.connections[workerID]
|
||||
wcm.mutex.RUnlock()
|
||||
|
||||
if !exists || !conn.active {
|
||||
return fmt.Errorf("no active connection to worker %s", workerID)
|
||||
}
|
||||
|
||||
return conn.CancelTask(taskID, reason)
|
||||
}
|
||||
|
||||
// GetActiveConnections returns the list of active worker connections
|
||||
func (wcm *WorkerCommunicationManager) GetActiveConnections() []string {
|
||||
wcm.mutex.RLock()
|
||||
defer wcm.mutex.RUnlock()
|
||||
|
||||
var active []string
|
||||
for workerID, conn := range wcm.connections {
|
||||
if conn.active {
|
||||
active = append(active, workerID)
|
||||
}
|
||||
}
|
||||
|
||||
return active
|
||||
}
|
||||
|
||||
// connectionMonitorLoop monitors worker connections and cleans up inactive ones
|
||||
func (wcm *WorkerCommunicationManager) connectionMonitorLoop() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
wcm.cleanupInactiveConnections()
|
||||
case <-wcm.stopCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupInactiveConnections removes inactive worker connections
|
||||
func (wcm *WorkerCommunicationManager) cleanupInactiveConnections() {
|
||||
wcm.mutex.Lock()
|
||||
defer wcm.mutex.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
timeout := 2 * time.Minute
|
||||
|
||||
for workerID, conn := range wcm.connections {
|
||||
if !conn.active || now.Sub(conn.lastSeen) > timeout {
|
||||
glog.Infof("Cleaning up inactive connection to worker %s", workerID)
|
||||
conn.Close()
|
||||
delete(wcm.connections, workerID)
|
||||
|
||||
// Mark worker as inactive in registry
|
||||
wcm.adminServer.workerRegistry.MarkWorkerInactive(workerID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewWorkerConnection creates a new worker connection
|
||||
func NewWorkerConnection(workerID, address string, adminServer *AdminServer) (*WorkerConnection, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
conn, err := grpc.DialContext(ctx, address, grpc.WithInsecure(), grpc.WithBlock())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to worker at %s: %v", address, err)
|
||||
}
|
||||
|
||||
client := worker_pb.NewWorkerServiceClient(conn)
|
||||
|
||||
return &WorkerConnection{
|
||||
workerID: workerID,
|
||||
address: address,
|
||||
conn: conn,
|
||||
client: client,
|
||||
lastSeen: time.Now(),
|
||||
adminServer: adminServer,
|
||||
stopCh: make(chan struct{}),
|
||||
active: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Start starts the worker connection and message handling
|
||||
func (wc *WorkerConnection) Start() {
|
||||
defer wc.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
stream, err := wc.client.WorkerStream(ctx)
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to create worker stream for %s: %v", wc.workerID, err)
|
||||
return
|
||||
}
|
||||
|
||||
wc.stream = stream
|
||||
wc.active = true
|
||||
|
||||
glog.Infof("Worker connection %s started", wc.workerID)
|
||||
|
||||
// Start message handling goroutines
|
||||
go wc.receiveMessages()
|
||||
|
||||
// Keep connection alive until stopped
|
||||
<-wc.stopCh
|
||||
}
|
||||
|
||||
// Close closes the worker connection
|
||||
func (wc *WorkerConnection) Close() {
|
||||
wc.mutex.Lock()
|
||||
defer wc.mutex.Unlock()
|
||||
|
||||
if !wc.active {
|
||||
return
|
||||
}
|
||||
|
||||
wc.active = false
|
||||
close(wc.stopCh)
|
||||
|
||||
if wc.stream != nil {
|
||||
wc.stream.CloseSend()
|
||||
}
|
||||
|
||||
if wc.conn != nil {
|
||||
wc.conn.Close()
|
||||
}
|
||||
|
||||
glog.Infof("Worker connection %s closed", wc.workerID)
|
||||
}
|
||||
|
||||
// receiveMessages handles incoming messages from the worker
|
||||
func (wc *WorkerConnection) receiveMessages() {
|
||||
for {
|
||||
select {
|
||||
case <-wc.stopCh:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
msg, err := wc.stream.Recv()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
glog.Infof("Worker %s closed connection", wc.workerID)
|
||||
} else {
|
||||
glog.Errorf("Error receiving from worker %s: %v", wc.workerID, err)
|
||||
}
|
||||
wc.Close()
|
||||
return
|
||||
}
|
||||
|
||||
wc.updateLastSeen()
|
||||
wc.handleMessage(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// updateLastSeen updates the last seen timestamp
|
||||
func (wc *WorkerConnection) updateLastSeen() {
|
||||
wc.mutex.Lock()
|
||||
defer wc.mutex.Unlock()
|
||||
wc.lastSeen = time.Now()
|
||||
}
|
||||
|
||||
// handleMessage processes a message from the worker
|
||||
func (wc *WorkerConnection) handleMessage(msg *worker_pb.WorkerMessage) {
|
||||
switch message := msg.Message.(type) {
|
||||
case *worker_pb.WorkerMessage_Registration:
|
||||
wc.handleRegistration(message.Registration)
|
||||
case *worker_pb.WorkerMessage_Heartbeat:
|
||||
wc.handleHeartbeat(message.Heartbeat)
|
||||
case *worker_pb.WorkerMessage_TaskRequest:
|
||||
wc.handleTaskRequest(message.TaskRequest)
|
||||
case *worker_pb.WorkerMessage_TaskUpdate:
|
||||
wc.handleTaskUpdate(message.TaskUpdate)
|
||||
case *worker_pb.WorkerMessage_TaskComplete:
|
||||
wc.handleTaskComplete(message.TaskComplete)
|
||||
case *worker_pb.WorkerMessage_Shutdown:
|
||||
wc.handleShutdown(message.Shutdown)
|
||||
default:
|
||||
glog.Warningf("Unknown message type from worker %s", wc.workerID)
|
||||
}
|
||||
}
|
||||
|
||||
// handleRegistration processes worker registration
|
||||
func (wc *WorkerConnection) handleRegistration(reg *worker_pb.WorkerRegistration) {
|
||||
glog.Infof("Worker %s registering with capabilities: %v", reg.WorkerId, reg.Capabilities)
|
||||
|
||||
// Convert to internal worker type
|
||||
worker := &Worker{
|
||||
ID: reg.WorkerId,
|
||||
Address: reg.Address,
|
||||
Capabilities: convertCapabilities(reg.Capabilities),
|
||||
MaxConcurrent: int(reg.MaxConcurrent),
|
||||
Status: "active",
|
||||
LastSeen: time.Now(),
|
||||
CurrentLoad: 0,
|
||||
TasksAssigned: []string{},
|
||||
}
|
||||
|
||||
// Register with worker registry
|
||||
wc.adminServer.workerRegistry.RegisterWorker(worker)
|
||||
|
||||
// Send registration response
|
||||
response := &worker_pb.AdminMessage{
|
||||
AdminId: wc.adminServer.ID,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Message: &worker_pb.AdminMessage_RegistrationResponse{
|
||||
RegistrationResponse: &worker_pb.RegistrationResponse{
|
||||
Success: true,
|
||||
Message: "Registration successful",
|
||||
AssignedWorkerId: reg.WorkerId,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
wc.sendMessage(response)
|
||||
}
|
||||
|
||||
// handleHeartbeat processes worker heartbeat
|
||||
func (wc *WorkerConnection) handleHeartbeat(hb *worker_pb.WorkerHeartbeat) {
|
||||
glog.V(2).Infof("Heartbeat from worker %s: status=%s, load=%d/%d",
|
||||
hb.WorkerId, hb.Status, hb.CurrentLoad, hb.MaxConcurrent)
|
||||
|
||||
// Update worker status in registry
|
||||
wc.adminServer.workerRegistry.UpdateWorkerStatus(hb.WorkerId, &WorkerStatus{
|
||||
Status: hb.Status,
|
||||
CurrentLoad: int(hb.CurrentLoad),
|
||||
MaxConcurrent: int(hb.MaxConcurrent),
|
||||
CurrentTasks: hb.CurrentTaskIds,
|
||||
TasksCompleted: int(hb.TasksCompleted),
|
||||
TasksFailed: int(hb.TasksFailed),
|
||||
UptimeSeconds: hb.UptimeSeconds,
|
||||
LastSeen: time.Now(),
|
||||
})
|
||||
|
||||
// Send heartbeat response
|
||||
response := &worker_pb.AdminMessage{
|
||||
AdminId: wc.adminServer.ID,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Message: &worker_pb.AdminMessage_HeartbeatResponse{
|
||||
HeartbeatResponse: &worker_pb.HeartbeatResponse{
|
||||
Success: true,
|
||||
Message: "Heartbeat acknowledged",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
wc.sendMessage(response)
|
||||
}
|
||||
|
||||
// handleTaskRequest processes worker task request
|
||||
func (wc *WorkerConnection) handleTaskRequest(req *worker_pb.TaskRequest) {
|
||||
glog.V(1).Infof("Task request from worker %s: capabilities=%v, slots=%d",
|
||||
req.WorkerId, req.Capabilities, req.AvailableSlots)
|
||||
|
||||
// Get next available task for this worker
|
||||
capabilities := convertCapabilities(req.Capabilities)
|
||||
task := wc.adminServer.taskScheduler.GetNextTask(req.WorkerId, capabilities)
|
||||
|
||||
if task != nil {
|
||||
// Assign task to worker
|
||||
err := wc.adminServer.AssignTaskToWorker(task.ID, req.WorkerId)
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to assign task %s to worker %s: %v", task.ID, req.WorkerId, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Send task assignment
|
||||
wc.sendTaskAssignment(task)
|
||||
glog.Infof("Assigned task %s (%s) to worker %s", task.ID, task.Type, req.WorkerId)
|
||||
}
|
||||
// If no task available, no response needed - worker will request again later
|
||||
}
|
||||
|
||||
// handleTaskUpdate processes task progress update
|
||||
func (wc *WorkerConnection) handleTaskUpdate(update *worker_pb.TaskUpdate) {
|
||||
glog.V(1).Infof("Task update for %s from worker %s: status=%s, progress=%.1f%%",
|
||||
update.TaskId, update.WorkerId, update.Status, update.Progress*100)
|
||||
|
||||
// Update task progress in admin server
|
||||
wc.adminServer.UpdateTaskProgress(update.TaskId, update.WorkerId, &TaskProgress{
|
||||
Status: TaskStatus(update.Status),
|
||||
Progress: update.Progress,
|
||||
Message: update.Message,
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
// handleTaskComplete processes task completion
|
||||
func (wc *WorkerConnection) handleTaskComplete(complete *worker_pb.TaskComplete) {
|
||||
glog.Infof("Task %s completed by worker %s: success=%v",
|
||||
complete.TaskId, complete.WorkerId, complete.Success)
|
||||
|
||||
// Update task completion in admin server
|
||||
var status TaskStatus
|
||||
if complete.Success {
|
||||
status = TaskStatusCompleted
|
||||
} else {
|
||||
status = TaskStatusFailed
|
||||
}
|
||||
|
||||
result := &TaskResult{
|
||||
TaskID: complete.TaskId,
|
||||
WorkerID: complete.WorkerId,
|
||||
Status: status,
|
||||
Success: complete.Success,
|
||||
ErrorMessage: complete.ErrorMessage,
|
||||
CompletedAt: time.Unix(complete.CompletionTime, 0),
|
||||
ResultMetadata: complete.ResultMetadata,
|
||||
}
|
||||
|
||||
wc.adminServer.CompleteTask(complete.TaskId, result)
|
||||
}
|
||||
|
||||
// handleShutdown processes worker shutdown notification
|
||||
func (wc *WorkerConnection) handleShutdown(shutdown *worker_pb.WorkerShutdown) {
|
||||
glog.Infof("Worker %s shutting down: %s, pending tasks: %v",
|
||||
shutdown.WorkerId, shutdown.Reason, shutdown.PendingTaskIds)
|
||||
|
||||
// Handle pending tasks - reassign them
|
||||
for _, taskID := range shutdown.PendingTaskIds {
|
||||
wc.adminServer.ReassignTask(taskID, "worker shutdown")
|
||||
}
|
||||
|
||||
// Remove worker from registry
|
||||
wc.adminServer.workerRegistry.UnregisterWorker(shutdown.WorkerId)
|
||||
|
||||
wc.Close()
|
||||
}
|
||||
|
||||
// SendTaskAssignment sends a task assignment to the worker
|
||||
func (wc *WorkerConnection) SendTaskAssignment(task *Task) error {
|
||||
return wc.sendTaskAssignment(task)
|
||||
}
|
||||
|
||||
// sendTaskAssignment sends a task assignment message
|
||||
func (wc *WorkerConnection) sendTaskAssignment(task *Task) error {
|
||||
assignment := &worker_pb.TaskAssignment{
|
||||
TaskId: task.ID,
|
||||
TaskType: string(task.Type),
|
||||
Priority: int32(task.Priority),
|
||||
CreatedTime: task.CreatedAt.Unix(),
|
||||
Params: &worker_pb.TaskParams{
|
||||
VolumeId: task.VolumeID,
|
||||
Server: task.Parameters["server"],
|
||||
Collection: task.Parameters["collection"],
|
||||
Parameters: task.Parameters,
|
||||
},
|
||||
Metadata: map[string]string{
|
||||
"assigned_at": time.Now().Format(time.RFC3339),
|
||||
},
|
||||
}
|
||||
|
||||
response := &worker_pb.AdminMessage{
|
||||
AdminId: wc.adminServer.ID,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Message: &worker_pb.AdminMessage_TaskAssignment{
|
||||
TaskAssignment: assignment,
|
||||
},
|
||||
}
|
||||
|
||||
return wc.sendMessage(response)
|
||||
}
|
||||
|
||||
// CancelTask sends a task cancellation to the worker
|
||||
func (wc *WorkerConnection) CancelTask(taskID, reason string) error {
|
||||
cancellation := &worker_pb.TaskCancellation{
|
||||
TaskId: taskID,
|
||||
Reason: reason,
|
||||
Force: false,
|
||||
}
|
||||
|
||||
response := &worker_pb.AdminMessage{
|
||||
AdminId: wc.adminServer.ID,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Message: &worker_pb.AdminMessage_TaskCancellation{
|
||||
TaskCancellation: cancellation,
|
||||
},
|
||||
}
|
||||
|
||||
return wc.sendMessage(response)
|
||||
}
|
||||
|
||||
// sendMessage sends a message to the worker
|
||||
func (wc *WorkerConnection) sendMessage(msg *worker_pb.AdminMessage) error {
|
||||
wc.mutex.RLock()
|
||||
defer wc.mutex.RUnlock()
|
||||
|
||||
if !wc.active || wc.stream == nil {
|
||||
return fmt.Errorf("connection to worker %s is not active", wc.workerID)
|
||||
}
|
||||
|
||||
return wc.stream.Send(msg)
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
// convertCapabilities converts string capabilities to TaskType slice
|
||||
func convertCapabilities(capabilities []string) []TaskType {
|
||||
var result []TaskType
|
||||
for _, cap := range capabilities {
|
||||
result = append(result, TaskType(cap))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// WorkerStatus represents worker status information
|
||||
type WorkerStatus struct {
|
||||
Status string
|
||||
CurrentLoad int
|
||||
MaxConcurrent int
|
||||
CurrentTasks []string
|
||||
TasksCompleted int
|
||||
TasksFailed int
|
||||
UptimeSeconds int64
|
||||
LastSeen time.Time
|
||||
}
|
||||
|
||||
// TaskProgress represents task progress information
|
||||
type TaskProgress struct {
|
||||
Status TaskStatus
|
||||
Progress float32
|
||||
Message string
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// TaskResult represents task completion result
|
||||
type TaskResult struct {
|
||||
TaskID string
|
||||
WorkerID string
|
||||
Status TaskStatus
|
||||
Success bool
|
||||
ErrorMessage string
|
||||
CompletedAt time.Time
|
||||
ResultMetadata map[string]string
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/wdclient"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
||||
)
|
||||
|
||||
// AdminConfig contains configuration for the admin server
|
||||
type AdminConfig struct {
|
||||
ScanInterval time.Duration
|
||||
WorkerTimeout time.Duration
|
||||
TaskTimeout time.Duration
|
||||
MaxRetries int
|
||||
ReconcileInterval time.Duration
|
||||
EnableFailureRecovery bool
|
||||
MaxConcurrentTasks int
|
||||
}
|
||||
|
||||
// AdminServer manages workers and tasks
|
||||
type AdminServer struct {
|
||||
config *AdminConfig
|
||||
masterClient *wdclient.MasterClient
|
||||
running bool
|
||||
mutex sync.RWMutex
|
||||
|
||||
// Task management
|
||||
tasks map[string]*types.Task
|
||||
taskQueue []*types.Task
|
||||
activeTasks map[string]*types.Task
|
||||
|
||||
// Worker management
|
||||
workers map[string]*types.Worker
|
||||
workerStatus map[string]*types.WorkerStatus
|
||||
|
||||
// Task history
|
||||
taskHistory []TaskHistoryEntry
|
||||
}
|
||||
|
||||
// TaskHistoryEntry represents a single task history entry
|
||||
type TaskHistoryEntry struct {
|
||||
TaskID string
|
||||
TaskType types.TaskType
|
||||
VolumeID uint32
|
||||
WorkerID string
|
||||
Status types.TaskStatus
|
||||
StartedAt time.Time
|
||||
CompletedAt time.Time
|
||||
Duration time.Duration
|
||||
ErrorMessage string
|
||||
}
|
||||
|
||||
// SystemStats represents system statistics
|
||||
type SystemStats struct {
|
||||
ActiveTasks int
|
||||
QueuedTasks int
|
||||
ActiveWorkers int
|
||||
TotalTasks int
|
||||
}
|
||||
|
||||
// NewAdminServer creates a new admin server
|
||||
func NewAdminServer(config *AdminConfig, masterClient *wdclient.MasterClient) *AdminServer {
|
||||
return &AdminServer{
|
||||
config: config,
|
||||
masterClient: masterClient,
|
||||
tasks: make(map[string]*types.Task),
|
||||
taskQueue: make([]*types.Task, 0),
|
||||
activeTasks: make(map[string]*types.Task),
|
||||
workers: make(map[string]*types.Worker),
|
||||
workerStatus: make(map[string]*types.WorkerStatus),
|
||||
taskHistory: make([]TaskHistoryEntry, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the admin server
|
||||
func (as *AdminServer) Start() error {
|
||||
as.mutex.Lock()
|
||||
defer as.mutex.Unlock()
|
||||
|
||||
if as.running {
|
||||
return fmt.Errorf("admin server is already running")
|
||||
}
|
||||
|
||||
as.running = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the admin server
|
||||
func (as *AdminServer) Stop() error {
|
||||
as.mutex.Lock()
|
||||
defer as.mutex.Unlock()
|
||||
|
||||
as.running = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterWorker registers a new worker
|
||||
func (as *AdminServer) RegisterWorker(worker *types.Worker) error {
|
||||
as.mutex.Lock()
|
||||
defer as.mutex.Unlock()
|
||||
|
||||
if !as.running {
|
||||
return fmt.Errorf("admin server is not running")
|
||||
}
|
||||
|
||||
as.workers[worker.ID] = worker
|
||||
as.workerStatus[worker.ID] = &types.WorkerStatus{
|
||||
Status: "active",
|
||||
CurrentLoad: 0,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// QueueTask adds a new task to the task queue
|
||||
func (as *AdminServer) QueueTask(task *types.Task) error {
|
||||
as.mutex.Lock()
|
||||
defer as.mutex.Unlock()
|
||||
|
||||
if !as.running {
|
||||
return fmt.Errorf("admin server is not running")
|
||||
}
|
||||
|
||||
if task.ID == "" {
|
||||
task.ID = fmt.Sprintf("task-%d", time.Now().UnixNano())
|
||||
}
|
||||
|
||||
task.Status = types.TaskStatusPending
|
||||
task.CreatedAt = time.Now()
|
||||
|
||||
as.tasks[task.ID] = task
|
||||
as.taskQueue = append(as.taskQueue, task)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequestTask requests a task for a worker
|
||||
func (as *AdminServer) RequestTask(workerID string, capabilities []types.TaskType) (*types.Task, error) {
|
||||
as.mutex.Lock()
|
||||
defer as.mutex.Unlock()
|
||||
|
||||
if !as.running {
|
||||
return nil, fmt.Errorf("admin server is not running")
|
||||
}
|
||||
|
||||
// Check if worker exists
|
||||
worker, exists := as.workers[workerID]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("worker %s not found", workerID)
|
||||
}
|
||||
|
||||
// Check if worker has capacity
|
||||
status := as.workerStatus[workerID]
|
||||
if status.CurrentLoad >= worker.MaxConcurrent {
|
||||
return nil, nil // No capacity
|
||||
}
|
||||
|
||||
// Find a suitable task
|
||||
for i, task := range as.taskQueue {
|
||||
if task.Status != types.TaskStatusPending {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if worker can handle this task type
|
||||
canHandle := false
|
||||
for _, capability := range capabilities {
|
||||
if task.Type == capability {
|
||||
canHandle = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if canHandle {
|
||||
// Assign task to worker
|
||||
task.Status = types.TaskStatusInProgress
|
||||
task.WorkerID = workerID
|
||||
now := time.Now()
|
||||
task.StartedAt = &now
|
||||
|
||||
// Move task from queue to active tasks
|
||||
as.taskQueue = append(as.taskQueue[:i], as.taskQueue[i+1:]...)
|
||||
as.activeTasks[task.ID] = task
|
||||
|
||||
// Update worker load
|
||||
status.CurrentLoad++
|
||||
|
||||
return task, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil // No suitable task found
|
||||
}
|
||||
|
||||
// UpdateTaskProgress updates task progress
|
||||
func (as *AdminServer) UpdateTaskProgress(taskID string, progress float64) error {
|
||||
as.mutex.Lock()
|
||||
defer as.mutex.Unlock()
|
||||
|
||||
task, exists := as.tasks[taskID]
|
||||
if !exists {
|
||||
return fmt.Errorf("task %s not found", taskID)
|
||||
}
|
||||
|
||||
task.Progress = progress
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CompleteTask marks a task as completed
|
||||
func (as *AdminServer) CompleteTask(taskID string, success bool, errorMessage string) error {
|
||||
as.mutex.Lock()
|
||||
defer as.mutex.Unlock()
|
||||
|
||||
task, exists := as.tasks[taskID]
|
||||
if !exists {
|
||||
return fmt.Errorf("task %s not found", taskID)
|
||||
}
|
||||
|
||||
// Update task status
|
||||
if success {
|
||||
task.Status = types.TaskStatusCompleted
|
||||
} else {
|
||||
task.Status = types.TaskStatusFailed
|
||||
task.Error = errorMessage
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
task.CompletedAt = &now
|
||||
|
||||
// Remove from active tasks
|
||||
delete(as.activeTasks, taskID)
|
||||
|
||||
// Update worker load
|
||||
if task.WorkerID != "" {
|
||||
if status, exists := as.workerStatus[task.WorkerID]; exists {
|
||||
status.CurrentLoad--
|
||||
}
|
||||
}
|
||||
|
||||
// Add to history
|
||||
var duration time.Duration
|
||||
if task.StartedAt != nil {
|
||||
duration = now.Sub(*task.StartedAt)
|
||||
}
|
||||
|
||||
entry := TaskHistoryEntry{
|
||||
TaskID: task.ID,
|
||||
TaskType: task.Type,
|
||||
VolumeID: task.VolumeID,
|
||||
WorkerID: task.WorkerID,
|
||||
Status: task.Status,
|
||||
StartedAt: *task.StartedAt,
|
||||
CompletedAt: now,
|
||||
Duration: duration,
|
||||
ErrorMessage: errorMessage,
|
||||
}
|
||||
as.taskHistory = append(as.taskHistory, entry)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateWorkerHeartbeat updates worker heartbeat
|
||||
func (as *AdminServer) UpdateWorkerHeartbeat(workerID string, status *types.WorkerStatus) error {
|
||||
as.mutex.Lock()
|
||||
defer as.mutex.Unlock()
|
||||
|
||||
worker, exists := as.workers[workerID]
|
||||
if !exists {
|
||||
return fmt.Errorf("worker %s not found", workerID)
|
||||
}
|
||||
|
||||
worker.LastHeartbeat = time.Now()
|
||||
as.workerStatus[workerID] = status
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSystemStats returns system statistics
|
||||
func (as *AdminServer) GetSystemStats() *SystemStats {
|
||||
as.mutex.RLock()
|
||||
defer as.mutex.RUnlock()
|
||||
|
||||
activeWorkers := 0
|
||||
for _, status := range as.workerStatus {
|
||||
if status.Status == "active" {
|
||||
activeWorkers++
|
||||
}
|
||||
}
|
||||
|
||||
return &SystemStats{
|
||||
ActiveTasks: len(as.activeTasks),
|
||||
QueuedTasks: len(as.taskQueue),
|
||||
ActiveWorkers: activeWorkers,
|
||||
TotalTasks: len(as.tasks),
|
||||
}
|
||||
}
|
||||
|
||||
// GetQueuedTaskCount returns the number of queued tasks
|
||||
func (as *AdminServer) GetQueuedTaskCount() int {
|
||||
as.mutex.RLock()
|
||||
defer as.mutex.RUnlock()
|
||||
return len(as.taskQueue)
|
||||
}
|
||||
|
||||
// GetActiveTaskCount returns the number of active tasks
|
||||
func (as *AdminServer) GetActiveTaskCount() int {
|
||||
as.mutex.RLock()
|
||||
defer as.mutex.RUnlock()
|
||||
return len(as.activeTasks)
|
||||
}
|
||||
|
||||
// GetTaskHistory returns task history
|
||||
func (as *AdminServer) GetTaskHistory() []TaskHistoryEntry {
|
||||
as.mutex.RLock()
|
||||
defer as.mutex.RUnlock()
|
||||
|
||||
// Return a copy of the history
|
||||
history := make([]TaskHistoryEntry, len(as.taskHistory))
|
||||
copy(history, as.taskHistory)
|
||||
return history
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module task_minimal
|
||||
|
||||
go 1.24.1
|
||||
@@ -0,0 +1,233 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
||||
)
|
||||
|
||||
// TestSimpleIntegration tests basic admin-worker operational flow without complex dependencies
|
||||
func TestSimpleIntegration(t *testing.T) {
|
||||
t.Logf("Starting simple integration test")
|
||||
|
||||
// Step 1: Create a minimal admin server configuration
|
||||
config := &AdminConfig{
|
||||
ScanInterval: 10 * time.Second,
|
||||
WorkerTimeout: 30 * time.Second,
|
||||
TaskTimeout: 2 * time.Hour,
|
||||
MaxRetries: 3,
|
||||
ReconcileInterval: 5 * time.Minute,
|
||||
EnableFailureRecovery: true,
|
||||
MaxConcurrentTasks: 5,
|
||||
}
|
||||
|
||||
// Step 2: Create admin server with nil master client (for testing)
|
||||
adminServer := NewAdminServer(config, nil)
|
||||
|
||||
// Step 3: Start admin server
|
||||
err := adminServer.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start admin server: %v", err)
|
||||
}
|
||||
defer adminServer.Stop()
|
||||
|
||||
// Step 4: Test worker registration
|
||||
t.Logf("Testing worker registration")
|
||||
|
||||
worker := &types.Worker{
|
||||
ID: "test-worker-1",
|
||||
Address: "localhost:9001",
|
||||
Capabilities: []types.TaskType{types.TaskTypeVacuum},
|
||||
MaxConcurrent: 2,
|
||||
Status: "active",
|
||||
CurrentLoad: 0,
|
||||
LastHeartbeat: time.Now(),
|
||||
}
|
||||
|
||||
err = adminServer.RegisterWorker(worker)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to register worker: %v", err)
|
||||
}
|
||||
t.Logf("Successfully registered worker %s", worker.ID)
|
||||
|
||||
// Step 5: Test task queueing
|
||||
t.Logf("Testing task queueing")
|
||||
|
||||
task := &types.Task{
|
||||
ID: "test-task-1",
|
||||
Type: types.TaskTypeVacuum,
|
||||
VolumeID: 1001,
|
||||
Server: "localhost:8080",
|
||||
Status: types.TaskStatusPending,
|
||||
Priority: types.TaskPriorityNormal,
|
||||
Parameters: map[string]interface{}{
|
||||
"garbage_threshold": "0.3",
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err = adminServer.QueueTask(task)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to queue task: %v", err)
|
||||
}
|
||||
t.Logf("Successfully queued task %s", task.ID)
|
||||
|
||||
// Step 6: Test task request by worker
|
||||
t.Logf("Testing task request")
|
||||
|
||||
assignedTask, err := adminServer.RequestTask("test-worker-1", []types.TaskType{types.TaskTypeVacuum})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to request task: %v", err)
|
||||
}
|
||||
|
||||
if assignedTask != nil {
|
||||
t.Logf("Successfully assigned task %s to worker", assignedTask.ID)
|
||||
|
||||
// Step 7: Test task progress updates
|
||||
t.Logf("Testing task progress updates")
|
||||
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 50.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update task progress: %v", err)
|
||||
}
|
||||
|
||||
err = adminServer.UpdateTaskProgress(assignedTask.ID, 100.0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update task progress: %v", err)
|
||||
}
|
||||
|
||||
// Step 8: Test task completion
|
||||
t.Logf("Testing task completion")
|
||||
|
||||
err = adminServer.CompleteTask(assignedTask.ID, true, "")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to complete task: %v", err)
|
||||
}
|
||||
t.Logf("Successfully completed task %s", assignedTask.ID)
|
||||
} else {
|
||||
t.Logf("No task was assigned (queue might be empty)")
|
||||
}
|
||||
|
||||
// Step 9: Test basic metrics
|
||||
t.Logf("Testing basic metrics")
|
||||
|
||||
stats := adminServer.GetSystemStats()
|
||||
if stats != nil {
|
||||
t.Logf("System stats: Active tasks=%d, Queued tasks=%d, Active workers=%d",
|
||||
stats.ActiveTasks, stats.QueuedTasks, stats.ActiveWorkers)
|
||||
}
|
||||
|
||||
queuedCount := adminServer.GetQueuedTaskCount()
|
||||
activeCount := adminServer.GetActiveTaskCount()
|
||||
t.Logf("Queue status: %d queued, %d active tasks", queuedCount, activeCount)
|
||||
|
||||
// Step 10: Test task history
|
||||
history := adminServer.GetTaskHistory()
|
||||
t.Logf("Task history contains %d entries", len(history))
|
||||
|
||||
t.Logf("Simple integration test completed successfully")
|
||||
}
|
||||
|
||||
// TestWorkerHeartbeat tests worker heartbeat functionality
|
||||
func TestWorkerHeartbeat(t *testing.T) {
|
||||
t.Logf("Testing worker heartbeat")
|
||||
|
||||
config := &AdminConfig{
|
||||
ScanInterval: 10 * time.Second,
|
||||
WorkerTimeout: 30 * time.Second,
|
||||
TaskTimeout: 2 * time.Hour,
|
||||
MaxRetries: 3,
|
||||
ReconcileInterval: 5 * time.Minute,
|
||||
EnableFailureRecovery: true,
|
||||
MaxConcurrentTasks: 5,
|
||||
}
|
||||
|
||||
adminServer := NewAdminServer(config, nil)
|
||||
err := adminServer.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start admin server: %v", err)
|
||||
}
|
||||
defer adminServer.Stop()
|
||||
|
||||
// Register a worker
|
||||
worker := &types.Worker{
|
||||
ID: "heartbeat-worker",
|
||||
Address: "localhost:9002",
|
||||
Capabilities: []types.TaskType{types.TaskTypeVacuum},
|
||||
MaxConcurrent: 1,
|
||||
Status: "active",
|
||||
CurrentLoad: 0,
|
||||
LastHeartbeat: time.Now(),
|
||||
}
|
||||
|
||||
err = adminServer.RegisterWorker(worker)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to register worker: %v", err)
|
||||
}
|
||||
|
||||
// Test heartbeat update
|
||||
status := &types.WorkerStatus{
|
||||
Status: "active",
|
||||
CurrentLoad: 0,
|
||||
}
|
||||
|
||||
err = adminServer.UpdateWorkerHeartbeat("heartbeat-worker", status)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update worker heartbeat: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Worker heartbeat test completed successfully")
|
||||
}
|
||||
|
||||
// TestTaskQueueOperations tests task queue operations
|
||||
func TestTaskQueueOperations(t *testing.T) {
|
||||
t.Logf("Testing task queue operations")
|
||||
|
||||
config := &AdminConfig{
|
||||
ScanInterval: 10 * time.Second,
|
||||
WorkerTimeout: 30 * time.Second,
|
||||
TaskTimeout: 2 * time.Hour,
|
||||
MaxRetries: 3,
|
||||
ReconcileInterval: 5 * time.Minute,
|
||||
EnableFailureRecovery: true,
|
||||
MaxConcurrentTasks: 5,
|
||||
}
|
||||
|
||||
adminServer := NewAdminServer(config, nil)
|
||||
err := adminServer.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start admin server: %v", err)
|
||||
}
|
||||
defer adminServer.Stop()
|
||||
|
||||
// Test queuing multiple tasks
|
||||
for i := 0; i < 3; i++ {
|
||||
task := &types.Task{
|
||||
ID: fmt.Sprintf("queue-test-task-%d", i),
|
||||
Type: types.TaskTypeVacuum,
|
||||
VolumeID: uint32(2000 + i),
|
||||
Server: "localhost:8080",
|
||||
Status: types.TaskStatusPending,
|
||||
Priority: types.TaskPriorityNormal,
|
||||
Parameters: map[string]interface{}{
|
||||
"garbage_threshold": "0.3",
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err = adminServer.QueueTask(task)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to queue task %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check queue size
|
||||
queuedCount := adminServer.GetQueuedTaskCount()
|
||||
if queuedCount != 3 {
|
||||
t.Errorf("Expected 3 queued tasks, got %d", queuedCount)
|
||||
}
|
||||
|
||||
t.Logf("Task queue operations test completed successfully")
|
||||
}
|
||||
@@ -0,0 +1,693 @@
|
||||
package worker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/worker_pb"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// ECWorker implements maintenance worker with actual EC functionality
|
||||
type ECWorker struct {
|
||||
workerID string
|
||||
adminAddress string
|
||||
grpcAddress string
|
||||
capabilities []string
|
||||
maxConcurrent int
|
||||
|
||||
// gRPC server and client
|
||||
server *grpc.Server
|
||||
adminConn *grpc.ClientConn
|
||||
adminClient worker_pb.WorkerServiceClient
|
||||
adminStream worker_pb.WorkerService_WorkerStreamClient
|
||||
|
||||
// Task management
|
||||
currentTasks map[string]*ActiveTask
|
||||
taskMutex sync.RWMutex
|
||||
|
||||
// Control
|
||||
running bool
|
||||
stopCh chan struct{}
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// ActiveTask represents a task currently being executed
|
||||
type ActiveTask struct {
|
||||
ID string
|
||||
Type string
|
||||
VolumeID uint32
|
||||
Server string
|
||||
Parameters map[string]string
|
||||
StartedAt time.Time
|
||||
Progress float32
|
||||
Status string
|
||||
Context context.Context
|
||||
Cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// NewECWorker creates a new EC worker
|
||||
func NewECWorker(workerID, adminAddress, grpcAddress string) *ECWorker {
|
||||
return &ECWorker{
|
||||
workerID: workerID,
|
||||
adminAddress: adminAddress,
|
||||
grpcAddress: grpcAddress,
|
||||
capabilities: []string{"ec_encode", "ec_rebuild", "vacuum"},
|
||||
maxConcurrent: 2, // Can handle 2 concurrent tasks
|
||||
currentTasks: make(map[string]*ActiveTask),
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the worker
|
||||
func (w *ECWorker) Start() error {
|
||||
w.mutex.Lock()
|
||||
defer w.mutex.Unlock()
|
||||
|
||||
if w.running {
|
||||
return fmt.Errorf("worker already running")
|
||||
}
|
||||
|
||||
glog.Infof("Starting EC worker %s", w.workerID)
|
||||
|
||||
// Start gRPC server
|
||||
err := w.startGRPCServer()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start gRPC server: %v", err)
|
||||
}
|
||||
|
||||
// Connect to admin server
|
||||
err = w.connectToAdmin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to admin: %v", err)
|
||||
}
|
||||
|
||||
w.running = true
|
||||
|
||||
// Start background goroutines
|
||||
go w.adminCommunicationLoop()
|
||||
go w.heartbeatLoop()
|
||||
go w.taskRequestLoop()
|
||||
|
||||
glog.Infof("EC worker %s started successfully", w.workerID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the worker
|
||||
func (w *ECWorker) Stop() {
|
||||
w.mutex.Lock()
|
||||
defer w.mutex.Unlock()
|
||||
|
||||
if !w.running {
|
||||
return
|
||||
}
|
||||
|
||||
glog.Infof("Stopping EC worker %s", w.workerID)
|
||||
|
||||
close(w.stopCh)
|
||||
|
||||
// Cancel all active tasks
|
||||
w.taskMutex.Lock()
|
||||
for _, task := range w.currentTasks {
|
||||
task.Cancel()
|
||||
}
|
||||
w.taskMutex.Unlock()
|
||||
|
||||
// Close connections
|
||||
if w.adminConn != nil {
|
||||
w.adminConn.Close()
|
||||
}
|
||||
|
||||
if w.server != nil {
|
||||
w.server.Stop()
|
||||
}
|
||||
|
||||
w.running = false
|
||||
glog.Infof("EC worker %s stopped", w.workerID)
|
||||
}
|
||||
|
||||
// startGRPCServer starts the worker's gRPC server
|
||||
func (w *ECWorker) startGRPCServer() error {
|
||||
listener, err := net.Listen("tcp", w.grpcAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to listen on %s: %v", w.grpcAddress, err)
|
||||
}
|
||||
|
||||
w.server = grpc.NewServer()
|
||||
// Register any worker-specific services here
|
||||
|
||||
go func() {
|
||||
err := w.server.Serve(listener)
|
||||
if err != nil {
|
||||
glog.Errorf("gRPC server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
glog.Infof("Worker gRPC server listening on %s", w.grpcAddress)
|
||||
return nil
|
||||
}
|
||||
|
||||
// connectToAdmin establishes connection to admin server
|
||||
func (w *ECWorker) connectToAdmin() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
conn, err := grpc.DialContext(ctx, w.adminAddress, grpc.WithInsecure(), grpc.WithBlock())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to admin at %s: %v", w.adminAddress, err)
|
||||
}
|
||||
|
||||
w.adminConn = conn
|
||||
w.adminClient = worker_pb.NewWorkerServiceClient(conn)
|
||||
|
||||
// Create bidirectional stream
|
||||
stream, err := w.adminClient.WorkerStream(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create admin stream: %v", err)
|
||||
}
|
||||
|
||||
w.adminStream = stream
|
||||
|
||||
// Send registration message
|
||||
err = w.sendRegistration()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register with admin: %v", err)
|
||||
}
|
||||
|
||||
glog.Infof("Connected to admin server at %s", w.adminAddress)
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendRegistration sends worker registration to admin
|
||||
func (w *ECWorker) sendRegistration() error {
|
||||
registration := &worker_pb.WorkerMessage{
|
||||
WorkerId: w.workerID,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Message: &worker_pb.WorkerMessage_Registration{
|
||||
Registration: &worker_pb.WorkerRegistration{
|
||||
WorkerId: w.workerID,
|
||||
Address: w.grpcAddress,
|
||||
Capabilities: w.capabilities,
|
||||
MaxConcurrent: int32(w.maxConcurrent),
|
||||
Metadata: map[string]string{
|
||||
"version": "1.0",
|
||||
"type": "ec_worker",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return w.adminStream.Send(registration)
|
||||
}
|
||||
|
||||
// adminCommunicationLoop handles messages from admin server
|
||||
func (w *ECWorker) adminCommunicationLoop() {
|
||||
for {
|
||||
select {
|
||||
case <-w.stopCh:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
msg, err := w.adminStream.Recv()
|
||||
if err != nil {
|
||||
glog.Errorf("Error receiving from admin: %v", err)
|
||||
time.Sleep(5 * time.Second) // Retry connection
|
||||
continue
|
||||
}
|
||||
|
||||
w.handleAdminMessage(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// handleAdminMessage processes messages from admin server
|
||||
func (w *ECWorker) handleAdminMessage(msg *worker_pb.AdminMessage) {
|
||||
switch message := msg.Message.(type) {
|
||||
case *worker_pb.AdminMessage_RegistrationResponse:
|
||||
w.handleRegistrationResponse(message.RegistrationResponse)
|
||||
case *worker_pb.AdminMessage_TaskAssignment:
|
||||
w.handleTaskAssignment(message.TaskAssignment)
|
||||
case *worker_pb.AdminMessage_TaskCancellation:
|
||||
w.handleTaskCancellation(message.TaskCancellation)
|
||||
case *worker_pb.AdminMessage_AdminShutdown:
|
||||
w.handleAdminShutdown(message.AdminShutdown)
|
||||
default:
|
||||
glog.Warningf("Unknown message type from admin")
|
||||
}
|
||||
}
|
||||
|
||||
// handleRegistrationResponse processes registration response
|
||||
func (w *ECWorker) handleRegistrationResponse(resp *worker_pb.RegistrationResponse) {
|
||||
if resp.Success {
|
||||
glog.Infof("Worker %s registered successfully with admin", w.workerID)
|
||||
} else {
|
||||
glog.Errorf("Worker registration failed: %s", resp.Message)
|
||||
}
|
||||
}
|
||||
|
||||
// handleTaskAssignment processes task assignment from admin
|
||||
func (w *ECWorker) handleTaskAssignment(assignment *worker_pb.TaskAssignment) {
|
||||
glog.Infof("Received task assignment: %s (%s) for volume %d",
|
||||
assignment.TaskId, assignment.TaskType, assignment.Params.VolumeId)
|
||||
|
||||
// Check if we can accept the task
|
||||
w.taskMutex.RLock()
|
||||
currentLoad := len(w.currentTasks)
|
||||
w.taskMutex.RUnlock()
|
||||
|
||||
if currentLoad >= w.maxConcurrent {
|
||||
glog.Warningf("Worker at capacity, cannot accept task %s", assignment.TaskId)
|
||||
return
|
||||
}
|
||||
|
||||
// Create active task
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
task := &ActiveTask{
|
||||
ID: assignment.TaskId,
|
||||
Type: assignment.TaskType,
|
||||
VolumeID: assignment.Params.VolumeId,
|
||||
Server: assignment.Params.Server,
|
||||
Parameters: assignment.Params.Parameters,
|
||||
StartedAt: time.Now(),
|
||||
Progress: 0.0,
|
||||
Status: "started",
|
||||
Context: ctx,
|
||||
Cancel: cancel,
|
||||
}
|
||||
|
||||
w.taskMutex.Lock()
|
||||
w.currentTasks[assignment.TaskId] = task
|
||||
w.taskMutex.Unlock()
|
||||
|
||||
// Start task execution
|
||||
go w.executeTask(task)
|
||||
}
|
||||
|
||||
// handleTaskCancellation processes task cancellation
|
||||
func (w *ECWorker) handleTaskCancellation(cancellation *worker_pb.TaskCancellation) {
|
||||
glog.Infof("Received task cancellation: %s", cancellation.TaskId)
|
||||
|
||||
w.taskMutex.Lock()
|
||||
defer w.taskMutex.Unlock()
|
||||
|
||||
if task, exists := w.currentTasks[cancellation.TaskId]; exists {
|
||||
task.Cancel()
|
||||
delete(w.currentTasks, cancellation.TaskId)
|
||||
glog.Infof("Cancelled task %s", cancellation.TaskId)
|
||||
}
|
||||
}
|
||||
|
||||
// handleAdminShutdown processes admin shutdown notification
|
||||
func (w *ECWorker) handleAdminShutdown(shutdown *worker_pb.AdminShutdown) {
|
||||
glog.Infof("Admin server shutting down: %s", shutdown.Reason)
|
||||
w.Stop()
|
||||
}
|
||||
|
||||
// heartbeatLoop sends periodic heartbeats to admin
|
||||
func (w *ECWorker) heartbeatLoop() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
w.sendHeartbeat()
|
||||
case <-w.stopCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sendHeartbeat sends heartbeat to admin server
|
||||
func (w *ECWorker) sendHeartbeat() {
|
||||
w.taskMutex.RLock()
|
||||
currentLoad := len(w.currentTasks)
|
||||
taskIDs := make([]string, 0, len(w.currentTasks))
|
||||
for taskID := range w.currentTasks {
|
||||
taskIDs = append(taskIDs, taskID)
|
||||
}
|
||||
w.taskMutex.RUnlock()
|
||||
|
||||
heartbeat := &worker_pb.WorkerMessage{
|
||||
WorkerId: w.workerID,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Message: &worker_pb.WorkerMessage_Heartbeat{
|
||||
Heartbeat: &worker_pb.WorkerHeartbeat{
|
||||
WorkerId: w.workerID,
|
||||
Status: "active",
|
||||
CurrentLoad: int32(currentLoad),
|
||||
MaxConcurrent: int32(w.maxConcurrent),
|
||||
CurrentTaskIds: taskIDs,
|
||||
TasksCompleted: 0, // TODO: Track completed tasks
|
||||
TasksFailed: 0, // TODO: Track failed tasks
|
||||
UptimeSeconds: int64(time.Since(time.Now()).Seconds()), // TODO: Track actual uptime
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := w.adminStream.Send(heartbeat); err != nil {
|
||||
glog.Errorf("Failed to send heartbeat: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// taskRequestLoop periodically requests new tasks from admin
|
||||
func (w *ECWorker) taskRequestLoop() {
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
w.requestTasks()
|
||||
case <-w.stopCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// requestTasks requests new tasks from admin if we have capacity
|
||||
func (w *ECWorker) requestTasks() {
|
||||
w.taskMutex.RLock()
|
||||
currentLoad := len(w.currentTasks)
|
||||
w.taskMutex.RUnlock()
|
||||
|
||||
availableSlots := w.maxConcurrent - currentLoad
|
||||
if availableSlots <= 0 {
|
||||
return // No capacity
|
||||
}
|
||||
|
||||
request := &worker_pb.WorkerMessage{
|
||||
WorkerId: w.workerID,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Message: &worker_pb.WorkerMessage_TaskRequest{
|
||||
TaskRequest: &worker_pb.TaskRequest{
|
||||
WorkerId: w.workerID,
|
||||
Capabilities: w.capabilities,
|
||||
AvailableSlots: int32(availableSlots),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := w.adminStream.Send(request); err != nil {
|
||||
glog.Errorf("Failed to request tasks: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// executeTask executes a task based on its type
|
||||
func (w *ECWorker) executeTask(task *ActiveTask) {
|
||||
defer func() {
|
||||
w.taskMutex.Lock()
|
||||
delete(w.currentTasks, task.ID)
|
||||
w.taskMutex.Unlock()
|
||||
}()
|
||||
|
||||
glog.Infof("Starting execution of task %s (%s) for volume %d",
|
||||
task.ID, task.Type, task.VolumeID)
|
||||
|
||||
var err error
|
||||
var success bool
|
||||
|
||||
switch task.Type {
|
||||
case "ec_encode":
|
||||
success, err = w.executeECEncode(task)
|
||||
case "ec_rebuild":
|
||||
success, err = w.executeECRebuild(task)
|
||||
case "vacuum":
|
||||
success, err = w.executeVacuum(task)
|
||||
default:
|
||||
err = fmt.Errorf("unknown task type: %s", task.Type)
|
||||
success = false
|
||||
}
|
||||
|
||||
// Send completion message
|
||||
w.sendTaskCompletion(task, success, err)
|
||||
|
||||
if success {
|
||||
glog.Infof("Task %s completed successfully", task.ID)
|
||||
} else {
|
||||
glog.Errorf("Task %s failed: %v", task.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// executeECEncode performs actual EC encoding on a volume
|
||||
func (w *ECWorker) executeECEncode(task *ActiveTask) (bool, error) {
|
||||
glog.Infof("Performing EC encoding on volume %d", task.VolumeID)
|
||||
|
||||
// Update progress
|
||||
w.sendTaskUpdate(task, 0.1, "Initializing EC encoding")
|
||||
|
||||
// Connect to volume server
|
||||
volumeServerAddress := task.Server
|
||||
if volumeServerAddress == "" {
|
||||
return false, fmt.Errorf("no volume server address provided")
|
||||
}
|
||||
|
||||
conn, err := grpc.Dial(volumeServerAddress, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to connect to volume server %s: %v", volumeServerAddress, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client := volume_server_pb.NewVolumeServerClient(conn)
|
||||
|
||||
// Step 1: Generate EC shards
|
||||
w.sendTaskUpdate(task, 0.2, "Generating EC shards")
|
||||
|
||||
generateReq := &volume_server_pb.VolumeEcShardsGenerateRequest{
|
||||
VolumeId: task.VolumeID,
|
||||
Collection: task.Parameters["collection"],
|
||||
}
|
||||
|
||||
generateResp, err := client.VolumeEcShardsGenerate(task.Context, generateReq)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("EC shard generation failed: %v", err)
|
||||
}
|
||||
|
||||
w.sendTaskUpdate(task, 0.6, "EC shards generated successfully")
|
||||
|
||||
// Step 2: Mount EC volume
|
||||
w.sendTaskUpdate(task, 0.8, "Mounting EC volume")
|
||||
|
||||
mountReq := &volume_server_pb.VolumeEcShardsMountRequest{
|
||||
VolumeId: task.VolumeID,
|
||||
Collection: task.Parameters["collection"],
|
||||
Shards: generateResp.EcIndexBits, // Use shards from generation
|
||||
}
|
||||
|
||||
_, err = client.VolumeEcShardsMount(task.Context, mountReq)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("EC shard mount failed: %v", err)
|
||||
}
|
||||
|
||||
// Step 3: Mark original volume as read-only
|
||||
w.sendTaskUpdate(task, 0.9, "Marking volume read-only")
|
||||
|
||||
readOnlyReq := &volume_server_pb.VolumeMarkReadonlyRequest{
|
||||
VolumeId: task.VolumeID,
|
||||
}
|
||||
|
||||
_, err = client.VolumeMarkReadonly(task.Context, readOnlyReq)
|
||||
if err != nil {
|
||||
glog.Warningf("Failed to mark volume %d read-only: %v", task.VolumeID, err)
|
||||
// This is not a critical failure for EC encoding
|
||||
}
|
||||
|
||||
w.sendTaskUpdate(task, 1.0, "EC encoding completed")
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// executeECRebuild performs EC shard rebuilding
|
||||
func (w *ECWorker) executeECRebuild(task *ActiveTask) (bool, error) {
|
||||
glog.Infof("Performing EC rebuild on volume %d", task.VolumeID)
|
||||
|
||||
w.sendTaskUpdate(task, 0.1, "Initializing EC rebuild")
|
||||
|
||||
// Connect to volume server
|
||||
conn, err := grpc.Dial(task.Server, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to connect to volume server: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client := volume_server_pb.NewVolumeServerClient(conn)
|
||||
|
||||
// Rebuild missing/corrupted shards
|
||||
w.sendTaskUpdate(task, 0.5, "Rebuilding EC shards")
|
||||
|
||||
rebuildReq := &volume_server_pb.VolumeEcShardsRebuildRequest{
|
||||
VolumeId: task.VolumeID,
|
||||
Collection: task.Parameters["collection"],
|
||||
}
|
||||
|
||||
_, err = client.VolumeEcShardsRebuild(task.Context, rebuildReq)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("EC rebuild failed: %v", err)
|
||||
}
|
||||
|
||||
w.sendTaskUpdate(task, 1.0, "EC rebuild completed")
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// executeVacuum performs volume vacuum operation
|
||||
func (w *ECWorker) executeVacuum(task *ActiveTask) (bool, error) {
|
||||
glog.Infof("Performing vacuum on volume %d", task.VolumeID)
|
||||
|
||||
w.sendTaskUpdate(task, 0.1, "Initializing vacuum")
|
||||
|
||||
// Parse garbage threshold
|
||||
thresholdStr := task.Parameters["garbage_threshold"]
|
||||
if thresholdStr == "" {
|
||||
thresholdStr = "0.3" // Default 30%
|
||||
}
|
||||
|
||||
threshold, err := strconv.ParseFloat(thresholdStr, 32)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid garbage threshold: %v", err)
|
||||
}
|
||||
|
||||
// Connect to volume server
|
||||
conn, err := grpc.Dial(task.Server, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to connect to volume server: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client := volume_server_pb.NewVolumeServerClient(conn)
|
||||
|
||||
// Step 1: Check vacuum eligibility
|
||||
w.sendTaskUpdate(task, 0.2, "Checking vacuum eligibility")
|
||||
|
||||
checkReq := &volume_server_pb.VacuumVolumeCheckRequest{
|
||||
VolumeId: task.VolumeID,
|
||||
}
|
||||
|
||||
checkResp, err := client.VacuumVolumeCheck(task.Context, checkReq)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("vacuum check failed: %v", err)
|
||||
}
|
||||
|
||||
if checkResp.GarbageRatio < float64(threshold) {
|
||||
return true, fmt.Errorf("volume %d garbage ratio %.2f%% below threshold %.2f%%",
|
||||
task.VolumeID, checkResp.GarbageRatio*100, threshold*100)
|
||||
}
|
||||
|
||||
// Step 2: Compact volume
|
||||
w.sendTaskUpdate(task, 0.4, "Compacting volume")
|
||||
|
||||
compactReq := &volume_server_pb.VacuumVolumeCompactRequest{
|
||||
VolumeId: task.VolumeID,
|
||||
}
|
||||
|
||||
compactStream, err := client.VacuumVolumeCompact(task.Context, compactReq)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("vacuum compact failed: %v", err)
|
||||
}
|
||||
|
||||
// Process compact stream
|
||||
for {
|
||||
resp, err := compactStream.Recv()
|
||||
if err != nil {
|
||||
if err.Error() == "EOF" {
|
||||
break
|
||||
}
|
||||
return false, fmt.Errorf("vacuum compact stream error: %v", err)
|
||||
}
|
||||
|
||||
progress := 0.4 + 0.4*(resp.ProcessedBytes/float64(resp.LoadAvg_1m)) // Rough progress estimate
|
||||
w.sendTaskUpdate(task, float32(progress), "Compacting volume")
|
||||
}
|
||||
|
||||
// Step 3: Commit vacuum
|
||||
w.sendTaskUpdate(task, 0.9, "Committing vacuum")
|
||||
|
||||
commitReq := &volume_server_pb.VacuumVolumeCommitRequest{
|
||||
VolumeId: task.VolumeID,
|
||||
}
|
||||
|
||||
commitResp, err := client.VacuumVolumeCommit(task.Context, commitReq)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("vacuum commit failed: %v", err)
|
||||
}
|
||||
|
||||
// Step 4: Cleanup
|
||||
w.sendTaskUpdate(task, 0.95, "Cleaning up")
|
||||
|
||||
cleanupReq := &volume_server_pb.VacuumVolumeCleanupRequest{
|
||||
VolumeId: task.VolumeID,
|
||||
}
|
||||
|
||||
_, err = client.VacuumVolumeCleanup(task.Context, cleanupReq)
|
||||
if err != nil {
|
||||
glog.Warningf("Vacuum cleanup warning: %v", err)
|
||||
// Non-critical error
|
||||
}
|
||||
|
||||
w.sendTaskUpdate(task, 1.0, fmt.Sprintf("Vacuum completed, reclaimed space: %d bytes",
|
||||
commitResp.MovedBytesCount))
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// sendTaskUpdate sends task progress update to admin
|
||||
func (w *ECWorker) sendTaskUpdate(task *ActiveTask, progress float32, message string) {
|
||||
task.Progress = progress
|
||||
task.Status = message
|
||||
|
||||
update := &worker_pb.WorkerMessage{
|
||||
WorkerId: w.workerID,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Message: &worker_pb.WorkerMessage_TaskUpdate{
|
||||
TaskUpdate: &worker_pb.TaskUpdate{
|
||||
TaskId: task.ID,
|
||||
WorkerId: w.workerID,
|
||||
Status: task.Status,
|
||||
Progress: progress,
|
||||
Message: message,
|
||||
Metadata: map[string]string{
|
||||
"updated_at": time.Now().Format(time.RFC3339),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := w.adminStream.Send(update); err != nil {
|
||||
glog.Errorf("Failed to send task update: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// sendTaskCompletion sends task completion to admin
|
||||
func (w *ECWorker) sendTaskCompletion(task *ActiveTask, success bool, taskErr error) {
|
||||
var errorMessage string
|
||||
if taskErr != nil {
|
||||
errorMessage = taskErr.Error()
|
||||
}
|
||||
|
||||
completion := &worker_pb.WorkerMessage{
|
||||
WorkerId: w.workerID,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Message: &worker_pb.WorkerMessage_TaskComplete{
|
||||
TaskComplete: &worker_pb.TaskComplete{
|
||||
TaskId: task.ID,
|
||||
WorkerId: w.workerID,
|
||||
Success: success,
|
||||
ErrorMessage: errorMessage,
|
||||
CompletionTime: time.Now().Unix(),
|
||||
ResultMetadata: map[string]string{
|
||||
"duration": time.Since(task.StartedAt).String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := w.adminStream.Send(completion); err != nil {
|
||||
glog.Errorf("Failed to send task completion: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker"
|
||||
)
|
||||
|
||||
var (
|
||||
workerID = flag.String("worker.id", "", "Worker ID (required)")
|
||||
adminAddr = flag.String("admin.address", "localhost:9090", "Admin server address")
|
||||
grpcAddr = flag.String("grpc.address", "localhost:18000", "Worker gRPC address")
|
||||
logLevel = flag.Int("log.level", 1, "Log level (0-4)")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
// Validate required flags
|
||||
if *workerID == "" {
|
||||
fmt.Fprintf(os.Stderr, "Error: worker.id is required\n")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Set log level
|
||||
flag.Set("v", fmt.Sprintf("%d", *logLevel))
|
||||
|
||||
glog.Infof("Starting SeaweedFS EC Worker")
|
||||
glog.Infof("Worker ID: %s", *workerID)
|
||||
glog.Infof("Admin Address: %s", *adminAddr)
|
||||
glog.Infof("gRPC Address: %s", *grpcAddr)
|
||||
|
||||
// Create worker
|
||||
ecWorker := worker.NewECWorker(*workerID, *adminAddr, *grpcAddr)
|
||||
|
||||
// Start worker
|
||||
err := ecWorker.Start()
|
||||
if err != nil {
|
||||
glog.Fatalf("Failed to start worker: %v", err)
|
||||
}
|
||||
|
||||
// Wait for shutdown signal
|
||||
waitForShutdown(ecWorker)
|
||||
|
||||
glog.Infof("Worker %s shutdown complete", *workerID)
|
||||
}
|
||||
|
||||
// waitForShutdown waits for shutdown signal and gracefully stops the worker
|
||||
func waitForShutdown(worker *worker.ECWorker) {
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
<-sigCh
|
||||
glog.Infof("Shutdown signal received, stopping worker...")
|
||||
|
||||
worker.Stop()
|
||||
|
||||
// Give a moment for cleanup
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
@@ -1,19 +1,24 @@
|
||||
package erasure_coding
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/tasks"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// Task implements erasure coding operation to convert volumes to EC format
|
||||
type Task struct {
|
||||
*tasks.BaseTask
|
||||
server string
|
||||
volumeID uint32
|
||||
server string
|
||||
volumeID uint32
|
||||
collection string
|
||||
grpcDialOpt grpc.DialOption
|
||||
}
|
||||
|
||||
// NewTask creates a new erasure coding task instance
|
||||
@@ -26,35 +31,90 @@ func NewTask(server string, volumeID uint32) *Task {
|
||||
return task
|
||||
}
|
||||
|
||||
// Execute executes the erasure coding task
|
||||
// Execute executes the actual erasure coding task using real SeaweedFS operations
|
||||
func (t *Task) Execute(params types.TaskParams) error {
|
||||
glog.Infof("Starting erasure coding task for volume %d on server %s", t.volumeID, t.server)
|
||||
glog.Infof("Starting erasure coding for volume %d on server %s", t.volumeID, t.server)
|
||||
|
||||
// Simulate erasure coding operation with progress updates
|
||||
steps := []struct {
|
||||
name string
|
||||
duration time.Duration
|
||||
progress float64
|
||||
}{
|
||||
{"Analyzing volume", 2 * time.Second, 15},
|
||||
{"Creating EC shards", 5 * time.Second, 50},
|
||||
{"Verifying shards", 2 * time.Second, 75},
|
||||
{"Finalizing EC volume", 1 * time.Second, 100},
|
||||
ctx := context.Background()
|
||||
|
||||
// Extract parameters
|
||||
t.collection = params.Collection
|
||||
if t.collection == "" {
|
||||
t.collection = "default"
|
||||
}
|
||||
|
||||
for _, step := range steps {
|
||||
if t.IsCancelled() {
|
||||
return fmt.Errorf("erasure coding task cancelled")
|
||||
// Connect to volume server
|
||||
conn, err := grpc.Dial(t.server, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to volume server %s: %v", t.server, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client := volume_server_pb.NewVolumeServerClient(conn)
|
||||
|
||||
// Step 1: Mark volume as read-only first
|
||||
t.SetProgress(10.0)
|
||||
glog.V(1).Infof("Marking volume %d as read-only", t.volumeID)
|
||||
|
||||
_, err = client.VolumeMarkReadonly(ctx, &volume_server_pb.VolumeMarkReadonlyRequest{
|
||||
VolumeId: t.volumeID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to mark volume %d as read-only: %v", t.volumeID, err)
|
||||
}
|
||||
|
||||
// Step 2: Generate EC shards
|
||||
t.SetProgress(30.0)
|
||||
glog.V(1).Infof("Generating EC shards for volume %d", t.volumeID)
|
||||
|
||||
_, err = client.VolumeEcShardsGenerate(ctx, &volume_server_pb.VolumeEcShardsGenerateRequest{
|
||||
VolumeId: t.volumeID,
|
||||
Collection: t.collection,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate EC shards for volume %d: %v", t.volumeID, err)
|
||||
}
|
||||
|
||||
// Step 3: Mount EC shards (all 14 shards: 10 data + 4 parity)
|
||||
t.SetProgress(70.0)
|
||||
glog.V(1).Infof("Mounting EC shards for volume %d", t.volumeID)
|
||||
|
||||
// Create shard IDs for all 14 shards (0-13)
|
||||
shardIds := make([]uint32, 14)
|
||||
for i := 0; i < 14; i++ {
|
||||
shardIds[i] = uint32(i)
|
||||
}
|
||||
|
||||
_, err = client.VolumeEcShardsMount(ctx, &volume_server_pb.VolumeEcShardsMountRequest{
|
||||
VolumeId: t.volumeID,
|
||||
Collection: t.collection,
|
||||
ShardIds: shardIds,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to mount EC shards for volume %d: %v", t.volumeID, err)
|
||||
}
|
||||
|
||||
// Step 4: Verify volume status
|
||||
t.SetProgress(90.0)
|
||||
glog.V(1).Infof("Verifying volume %d after EC conversion", t.volumeID)
|
||||
|
||||
// Check if volume is now read-only (which indicates successful EC conversion)
|
||||
statusResp, err := client.VolumeStatus(ctx, &volume_server_pb.VolumeStatusRequest{
|
||||
VolumeId: t.volumeID,
|
||||
})
|
||||
if err != nil {
|
||||
glog.Warningf("Could not verify EC status for volume %d: %v", t.volumeID, err)
|
||||
// This is not a failure - continue
|
||||
} else {
|
||||
if statusResp.IsReadOnly {
|
||||
glog.V(1).Infof("Volume %d is now read-only, EC conversion likely successful", t.volumeID)
|
||||
} else {
|
||||
glog.Warningf("Volume %d is not read-only after EC conversion", t.volumeID)
|
||||
}
|
||||
|
||||
glog.V(1).Infof("Erasure coding task step: %s", step.name)
|
||||
t.SetProgress(step.progress)
|
||||
|
||||
// Simulate work
|
||||
time.Sleep(step.duration)
|
||||
}
|
||||
|
||||
glog.Infof("Erasure coding task completed for volume %d on server %s", t.volumeID, t.server)
|
||||
t.SetProgress(100.0)
|
||||
glog.Infof("Successfully completed erasure coding for volume %d on server %s", t.volumeID, t.server)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -71,9 +131,28 @@ func (t *Task) Validate(params types.TaskParams) error {
|
||||
|
||||
// EstimateTime estimates the time needed for the task
|
||||
func (t *Task) EstimateTime(params types.TaskParams) time.Duration {
|
||||
// Base time for erasure coding operation
|
||||
baseTime := 30 * time.Second
|
||||
// Base time for EC operations - varies significantly by volume size
|
||||
// For a typical 30GB volume, EC generation can take 5-15 minutes
|
||||
baseTime := 10 * time.Minute
|
||||
|
||||
// Could adjust based on volume size if available in params
|
||||
if size, ok := params.Parameters["volume_size"].(int64); ok {
|
||||
// Rough estimate: 1 minute per GB
|
||||
estimatedTime := time.Duration(size/(1024*1024*1024)) * time.Minute
|
||||
if estimatedTime > baseTime {
|
||||
return estimatedTime
|
||||
}
|
||||
}
|
||||
|
||||
// Could adjust based on volume size or other factors
|
||||
return baseTime
|
||||
}
|
||||
|
||||
// GetProgress returns the current progress
|
||||
func (t *Task) GetProgress() float64 {
|
||||
return t.BaseTask.GetProgress()
|
||||
}
|
||||
|
||||
// Cancel cancels the task
|
||||
func (t *Task) Cancel() error {
|
||||
return t.BaseTask.Cancel()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,689 @@
|
||||
package erasure_coding
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/tasks"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// EnhancedECTask implements comprehensive erasure coding with local processing and smart distribution
|
||||
type EnhancedECTask struct {
|
||||
*tasks.BaseTask
|
||||
sourceServer string
|
||||
volumeID uint32
|
||||
collection string
|
||||
workDir string
|
||||
masterClient string
|
||||
grpcDialOpt grpc.DialOption
|
||||
|
||||
// EC parameters
|
||||
dataShards int // Default: 10
|
||||
parityShards int // Default: 4
|
||||
totalShards int // Default: 14
|
||||
|
||||
// Progress tracking
|
||||
currentStep string
|
||||
stepProgress map[string]float64
|
||||
}
|
||||
|
||||
// ServerInfo holds information about available servers for shard placement
|
||||
type ServerInfo struct {
|
||||
Address string
|
||||
DataCenter string
|
||||
Rack string
|
||||
AvailableSpace int64
|
||||
LoadScore float64
|
||||
ShardCount int
|
||||
}
|
||||
|
||||
// ShardPlacement represents where a shard should be placed
|
||||
type ShardPlacement struct {
|
||||
ShardID int
|
||||
ServerAddr string
|
||||
DataCenter string
|
||||
Rack string
|
||||
BackupAddrs []string // Alternative servers for redundancy
|
||||
}
|
||||
|
||||
// NewEnhancedECTask creates a new enhanced erasure coding task
|
||||
func NewEnhancedECTask(sourceServer string, volumeID uint32, masterClient string, workDir string) *EnhancedECTask {
|
||||
task := &EnhancedECTask{
|
||||
BaseTask: tasks.NewBaseTask(types.TaskTypeErasureCoding),
|
||||
sourceServer: sourceServer,
|
||||
volumeID: volumeID,
|
||||
masterClient: masterClient,
|
||||
workDir: workDir,
|
||||
dataShards: 10,
|
||||
parityShards: 4,
|
||||
totalShards: 14,
|
||||
stepProgress: make(map[string]float64),
|
||||
}
|
||||
return task
|
||||
}
|
||||
|
||||
// Execute performs the comprehensive EC operation
|
||||
func (t *EnhancedECTask) Execute(params types.TaskParams) error {
|
||||
glog.Infof("Starting enhanced erasure coding for volume %d from server %s", t.volumeID, t.sourceServer)
|
||||
|
||||
// Extract parameters
|
||||
t.collection = params.Collection
|
||||
if t.collection == "" {
|
||||
t.collection = "default"
|
||||
}
|
||||
|
||||
// Create working directory for this task
|
||||
taskWorkDir := filepath.Join(t.workDir, fmt.Sprintf("ec_%d_%d", t.volumeID, time.Now().Unix()))
|
||||
err := os.MkdirAll(taskWorkDir, 0755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create work directory %s: %v", taskWorkDir, err)
|
||||
}
|
||||
defer t.cleanup(taskWorkDir)
|
||||
|
||||
// Step 1: Copy volume data to local disk
|
||||
if err := t.copyVolumeDataLocally(taskWorkDir); err != nil {
|
||||
return fmt.Errorf("failed to copy volume data: %v", err)
|
||||
}
|
||||
|
||||
// Step 2: Mark source volume as read-only
|
||||
if err := t.markVolumeReadOnly(); err != nil {
|
||||
return fmt.Errorf("failed to mark volume read-only: %v", err)
|
||||
}
|
||||
|
||||
// Step 3: Perform local EC encoding
|
||||
shardFiles, err := t.performLocalECEncoding(taskWorkDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to perform EC encoding: %v", err)
|
||||
}
|
||||
|
||||
// Step 4: Find optimal shard placement
|
||||
placements, err := t.calculateOptimalShardPlacement()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to calculate shard placement: %v", err)
|
||||
}
|
||||
|
||||
// Step 5: Distribute shards to target servers
|
||||
if err := t.distributeShards(shardFiles, placements); err != nil {
|
||||
return fmt.Errorf("failed to distribute shards: %v", err)
|
||||
}
|
||||
|
||||
// Step 6: Verify and cleanup source volume
|
||||
if err := t.verifyAndCleanupSource(); err != nil {
|
||||
return fmt.Errorf("failed to verify and cleanup: %v", err)
|
||||
}
|
||||
|
||||
t.SetProgress(100.0)
|
||||
glog.Infof("Successfully completed enhanced erasure coding for volume %d", t.volumeID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyVolumeDataLocally copies the volume data from source server to local disk
|
||||
func (t *EnhancedECTask) copyVolumeDataLocally(workDir string) error {
|
||||
t.currentStep = "copying_volume_data"
|
||||
t.SetProgress(5.0)
|
||||
glog.V(1).Infof("Copying volume %d data from %s to local disk", t.volumeID, t.sourceServer)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Connect to source volume server
|
||||
conn, err := grpc.Dial(t.sourceServer, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to source server %s: %v", t.sourceServer, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client := volume_server_pb.NewVolumeServerClient(conn)
|
||||
|
||||
// Get volume info first
|
||||
statusResp, err := client.VolumeStatus(ctx, &volume_server_pb.VolumeStatusRequest{
|
||||
VolumeId: t.volumeID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get volume status: %v", err)
|
||||
}
|
||||
|
||||
glog.V(1).Infof("Volume %d size: %d bytes, file count: %d",
|
||||
t.volumeID, statusResp.VolumeSize, statusResp.FileCount)
|
||||
|
||||
// Copy .dat file
|
||||
datFile := filepath.Join(workDir, fmt.Sprintf("%d.dat", t.volumeID))
|
||||
if err := t.copyVolumeFile(client, ctx, t.volumeID, ".dat", datFile, statusResp.VolumeSize); err != nil {
|
||||
return fmt.Errorf("failed to copy .dat file: %v", err)
|
||||
}
|
||||
|
||||
// Copy .idx file
|
||||
idxFile := filepath.Join(workDir, fmt.Sprintf("%d.idx", t.volumeID))
|
||||
if err := t.copyVolumeFile(client, ctx, t.volumeID, ".idx", idxFile, 0); err != nil {
|
||||
return fmt.Errorf("failed to copy .idx file: %v", err)
|
||||
}
|
||||
|
||||
t.SetProgress(15.0)
|
||||
glog.V(1).Infof("Successfully copied volume %d files to %s", t.volumeID, workDir)
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyVolumeFile copies a specific volume file from source server
|
||||
func (t *EnhancedECTask) copyVolumeFile(client volume_server_pb.VolumeServerClient, ctx context.Context,
|
||||
volumeID uint32, extension string, localPath string, expectedSize uint64) error {
|
||||
|
||||
// Stream volume file data using CopyFile API
|
||||
stream, err := client.CopyFile(ctx, &volume_server_pb.CopyFileRequest{
|
||||
VolumeId: volumeID,
|
||||
Ext: extension,
|
||||
Collection: t.collection,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start volume copy stream: %v", err)
|
||||
}
|
||||
|
||||
// Create local file
|
||||
file, err := os.Create(localPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create local file %s: %v", localPath, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Copy data with progress tracking
|
||||
var totalBytes int64
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to receive volume data: %v", err)
|
||||
}
|
||||
|
||||
written, err := file.Write(resp.FileContent)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write to local file: %v", err)
|
||||
}
|
||||
|
||||
totalBytes += int64(written)
|
||||
|
||||
// Update progress for large files
|
||||
if expectedSize > 0 {
|
||||
progress := float64(totalBytes) / float64(expectedSize) * 10.0 // 10% of total progress
|
||||
t.SetProgress(5.0 + progress)
|
||||
}
|
||||
}
|
||||
|
||||
glog.V(2).Infof("Copied %d bytes to %s", totalBytes, localPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// markVolumeReadOnly marks the source volume as read-only
|
||||
func (t *EnhancedECTask) markVolumeReadOnly() error {
|
||||
t.currentStep = "marking_readonly"
|
||||
t.SetProgress(20.0)
|
||||
glog.V(1).Infof("Marking volume %d as read-only", t.volumeID)
|
||||
|
||||
ctx := context.Background()
|
||||
conn, err := grpc.Dial(t.sourceServer, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to source server: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client := volume_server_pb.NewVolumeServerClient(conn)
|
||||
_, err = client.VolumeMarkReadonly(ctx, &volume_server_pb.VolumeMarkReadonlyRequest{
|
||||
VolumeId: t.volumeID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to mark volume read-only: %v", err)
|
||||
}
|
||||
|
||||
t.SetProgress(25.0)
|
||||
return nil
|
||||
}
|
||||
|
||||
// performLocalECEncoding performs Reed-Solomon encoding on local volume files
|
||||
func (t *EnhancedECTask) performLocalECEncoding(workDir string) ([]string, error) {
|
||||
t.currentStep = "encoding"
|
||||
t.SetProgress(30.0)
|
||||
glog.V(1).Infof("Performing local EC encoding for volume %d", t.volumeID)
|
||||
|
||||
datFile := filepath.Join(workDir, fmt.Sprintf("%d.dat", t.volumeID))
|
||||
idxFile := filepath.Join(workDir, fmt.Sprintf("%d.idx", t.volumeID))
|
||||
|
||||
// Check if files exist and get their sizes
|
||||
datInfo, err := os.Stat(datFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to stat dat file: %v", err)
|
||||
}
|
||||
|
||||
idxInfo, err := os.Stat(idxFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to stat idx file: %v", err)
|
||||
}
|
||||
|
||||
glog.V(1).Infof("Encoding files: %s (%d bytes), %s (%d bytes)",
|
||||
datFile, datInfo.Size(), idxFile, idxInfo.Size())
|
||||
|
||||
// Generate EC shards using SeaweedFS erasure coding
|
||||
shardFiles := make([]string, t.totalShards)
|
||||
for i := 0; i < t.totalShards; i++ {
|
||||
shardFiles[i] = filepath.Join(workDir, fmt.Sprintf("%d.ec%02d", t.volumeID, i))
|
||||
}
|
||||
|
||||
// Encode .dat file
|
||||
if err := t.encodeFile(datFile, shardFiles, ".dat"); err != nil {
|
||||
return nil, fmt.Errorf("failed to encode dat file: %v", err)
|
||||
}
|
||||
|
||||
t.SetProgress(45.0)
|
||||
|
||||
// Encode .idx file
|
||||
if err := t.encodeFile(idxFile, shardFiles, ".idx"); err != nil {
|
||||
return nil, fmt.Errorf("failed to encode idx file: %v", err)
|
||||
}
|
||||
|
||||
t.SetProgress(60.0)
|
||||
glog.V(1).Infof("Successfully created %d EC shards for volume %d", t.totalShards, t.volumeID)
|
||||
return shardFiles, nil
|
||||
}
|
||||
|
||||
// encodeFile encodes a single file into EC shards
|
||||
func (t *EnhancedECTask) encodeFile(inputFile string, shardFiles []string, fileType string) error {
|
||||
// Read input file
|
||||
data, err := os.ReadFile(inputFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read input file: %v", err)
|
||||
}
|
||||
|
||||
// Write data to a temporary file first, then use SeaweedFS erasure coding
|
||||
tempFile := filepath.Join(filepath.Dir(shardFiles[0]), fmt.Sprintf("temp_%s", filepath.Base(inputFile)))
|
||||
err = os.WriteFile(tempFile, data, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tempFile)
|
||||
|
||||
// Use SeaweedFS erasure coding library with base filename
|
||||
baseFileName := tempFile[:len(tempFile)-len(filepath.Ext(tempFile))]
|
||||
err = erasure_coding.WriteEcFiles(baseFileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write EC files: %v", err)
|
||||
}
|
||||
|
||||
// Verify that shards were created
|
||||
for i, shardFile := range shardFiles {
|
||||
if _, err := os.Stat(shardFile); err != nil {
|
||||
glog.Warningf("Shard %d file %s not found: %v", i, shardFile, err)
|
||||
} else {
|
||||
info, _ := os.Stat(shardFile)
|
||||
glog.V(2).Infof("Created shard %d: %s (%d bytes)", i, shardFile, info.Size())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// calculateOptimalShardPlacement determines where to place each shard for optimal distribution
|
||||
func (t *EnhancedECTask) calculateOptimalShardPlacement() ([]ShardPlacement, error) {
|
||||
t.currentStep = "calculating_placement"
|
||||
t.SetProgress(65.0)
|
||||
glog.V(1).Infof("Calculating optimal shard placement for volume %d", t.volumeID)
|
||||
|
||||
// Get available servers from master
|
||||
servers, err := t.getAvailableServers()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get available servers: %v", err)
|
||||
}
|
||||
|
||||
if len(servers) < t.totalShards {
|
||||
return nil, fmt.Errorf("insufficient servers: need %d, have %d", t.totalShards, len(servers))
|
||||
}
|
||||
|
||||
// Sort servers by placement desirability (considering space, load, affinity)
|
||||
t.rankServersForPlacement(servers)
|
||||
|
||||
// Assign shards to servers with affinity logic
|
||||
placements := make([]ShardPlacement, t.totalShards)
|
||||
usedServers := make(map[string]int) // Track how many shards per server
|
||||
|
||||
for shardID := 0; shardID < t.totalShards; shardID++ {
|
||||
server := t.selectBestServerForShard(servers, usedServers, shardID)
|
||||
if server == nil {
|
||||
return nil, fmt.Errorf("failed to find suitable server for shard %d", shardID)
|
||||
}
|
||||
|
||||
placements[shardID] = ShardPlacement{
|
||||
ShardID: shardID,
|
||||
ServerAddr: server.Address,
|
||||
DataCenter: server.DataCenter,
|
||||
Rack: server.Rack,
|
||||
BackupAddrs: t.selectBackupServers(servers, server, 2),
|
||||
}
|
||||
|
||||
usedServers[server.Address]++
|
||||
glog.V(2).Infof("Assigned shard %d to server %s (DC: %s, Rack: %s)",
|
||||
shardID, server.Address, server.DataCenter, server.Rack)
|
||||
}
|
||||
|
||||
t.SetProgress(70.0)
|
||||
glog.V(1).Infof("Calculated placement for %d shards across %d servers",
|
||||
t.totalShards, len(usedServers))
|
||||
return placements, nil
|
||||
}
|
||||
|
||||
// getAvailableServers retrieves available servers from the master
|
||||
func (t *EnhancedECTask) getAvailableServers() ([]*ServerInfo, error) {
|
||||
ctx := context.Background()
|
||||
conn, err := grpc.Dial(t.masterClient, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to master: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client := master_pb.NewSeaweedClient(conn)
|
||||
resp, err := client.VolumeList(ctx, &master_pb.VolumeListRequest{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get volume list: %v", err)
|
||||
}
|
||||
|
||||
servers := make([]*ServerInfo, 0)
|
||||
|
||||
// Parse topology information to extract server details
|
||||
if resp.TopologyInfo != nil {
|
||||
for _, dc := range resp.TopologyInfo.DataCenterInfos {
|
||||
for _, rack := range dc.RackInfos {
|
||||
for _, node := range rack.DataNodeInfos {
|
||||
for diskType, diskInfo := range node.DiskInfos {
|
||||
server := &ServerInfo{
|
||||
Address: fmt.Sprintf("%s:%d", node.Id, node.GrpcPort),
|
||||
DataCenter: dc.Id,
|
||||
Rack: rack.Id,
|
||||
AvailableSpace: int64(diskInfo.FreeVolumeCount) * 32 * 1024 * 1024 * 1024, // Rough estimate
|
||||
LoadScore: float64(diskInfo.ActiveVolumeCount) / float64(diskInfo.MaxVolumeCount),
|
||||
ShardCount: 0,
|
||||
}
|
||||
|
||||
// Skip servers that are full or have high load
|
||||
if diskInfo.FreeVolumeCount > 0 && server.LoadScore < 0.9 {
|
||||
servers = append(servers, server)
|
||||
glog.V(2).Infof("Available server: %s (DC: %s, Rack: %s, DiskType: %s, Load: %.2f)",
|
||||
server.Address, server.DataCenter, server.Rack, diskType, server.LoadScore)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
// rankServersForPlacement sorts servers by desirability for shard placement
|
||||
func (t *EnhancedECTask) rankServersForPlacement(servers []*ServerInfo) {
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
serverA, serverB := servers[i], servers[j]
|
||||
|
||||
// Primary criteria: lower load is better
|
||||
if serverA.LoadScore != serverB.LoadScore {
|
||||
return serverA.LoadScore < serverB.LoadScore
|
||||
}
|
||||
|
||||
// Secondary criteria: more available space is better
|
||||
if serverA.AvailableSpace != serverB.AvailableSpace {
|
||||
return serverA.AvailableSpace > serverB.AvailableSpace
|
||||
}
|
||||
|
||||
// Tertiary criteria: fewer existing shards is better
|
||||
return serverA.ShardCount < serverB.ShardCount
|
||||
})
|
||||
}
|
||||
|
||||
// selectBestServerForShard selects the best server for a specific shard considering affinity
|
||||
func (t *EnhancedECTask) selectBestServerForShard(servers []*ServerInfo, usedServers map[string]int, shardID int) *ServerInfo {
|
||||
// For data shards (0-9), prefer distribution across different racks
|
||||
// For parity shards (10-13), can be more flexible
|
||||
isDataShard := shardID < t.dataShards
|
||||
|
||||
var candidates []*ServerInfo
|
||||
|
||||
if isDataShard {
|
||||
// For data shards, prioritize rack diversity
|
||||
usedRacks := make(map[string]bool)
|
||||
for _, server := range servers {
|
||||
if count, exists := usedServers[server.Address]; exists && count > 0 {
|
||||
usedRacks[server.Rack] = true
|
||||
}
|
||||
}
|
||||
|
||||
// First try to find servers in unused racks
|
||||
for _, server := range servers {
|
||||
if !usedRacks[server.Rack] && usedServers[server.Address] < 2 { // Max 2 shards per server
|
||||
candidates = append(candidates, server)
|
||||
}
|
||||
}
|
||||
|
||||
// If no unused racks, fall back to any available server
|
||||
if len(candidates) == 0 {
|
||||
for _, server := range servers {
|
||||
if usedServers[server.Address] < 2 {
|
||||
candidates = append(candidates, server)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For parity shards, just avoid overloading servers
|
||||
for _, server := range servers {
|
||||
if usedServers[server.Address] < 2 {
|
||||
candidates = append(candidates, server)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(candidates) == 0 {
|
||||
// Last resort: allow up to 3 shards per server
|
||||
for _, server := range servers {
|
||||
if usedServers[server.Address] < 3 {
|
||||
candidates = append(candidates, server)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(candidates) > 0 {
|
||||
return candidates[0] // Already sorted by desirability
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// selectBackupServers selects backup servers for redundancy
|
||||
func (t *EnhancedECTask) selectBackupServers(servers []*ServerInfo, primaryServer *ServerInfo, count int) []string {
|
||||
var backups []string
|
||||
|
||||
for _, server := range servers {
|
||||
if server.Address != primaryServer.Address && server.Rack != primaryServer.Rack {
|
||||
backups = append(backups, server.Address)
|
||||
if len(backups) >= count {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return backups
|
||||
}
|
||||
|
||||
// distributeShards uploads shards to their assigned servers
|
||||
func (t *EnhancedECTask) distributeShards(shardFiles []string, placements []ShardPlacement) error {
|
||||
t.currentStep = "distributing_shards"
|
||||
t.SetProgress(75.0)
|
||||
glog.V(1).Infof("Distributing %d shards to target servers", len(placements))
|
||||
|
||||
// Distribute shards in parallel for better performance
|
||||
successCount := 0
|
||||
errors := make([]error, 0)
|
||||
|
||||
for i, placement := range placements {
|
||||
shardFile := shardFiles[i]
|
||||
|
||||
err := t.uploadShardToServer(shardFile, placement)
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to upload shard %d to %s: %v", i, placement.ServerAddr, err)
|
||||
errors = append(errors, err)
|
||||
|
||||
// Try backup servers
|
||||
uploaded := false
|
||||
for _, backupAddr := range placement.BackupAddrs {
|
||||
backupPlacement := placement
|
||||
backupPlacement.ServerAddr = backupAddr
|
||||
if err := t.uploadShardToServer(shardFile, backupPlacement); err == nil {
|
||||
glog.V(1).Infof("Successfully uploaded shard %d to backup server %s", i, backupAddr)
|
||||
uploaded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !uploaded {
|
||||
return fmt.Errorf("failed to upload shard %d to any server", i)
|
||||
}
|
||||
}
|
||||
|
||||
successCount++
|
||||
progress := 75.0 + (float64(successCount)/float64(len(placements)))*15.0
|
||||
t.SetProgress(progress)
|
||||
|
||||
glog.V(2).Infof("Successfully distributed shard %d to %s", i, placement.ServerAddr)
|
||||
}
|
||||
|
||||
if len(errors) > 0 && successCount < len(placements)/2 {
|
||||
return fmt.Errorf("too many shard distribution failures: %d/%d", len(errors), len(placements))
|
||||
}
|
||||
|
||||
t.SetProgress(90.0)
|
||||
glog.V(1).Infof("Successfully distributed %d/%d shards", successCount, len(placements))
|
||||
return nil
|
||||
}
|
||||
|
||||
// uploadShardToServer uploads a shard file to a specific server
|
||||
func (t *EnhancedECTask) uploadShardToServer(shardFile string, placement ShardPlacement) error {
|
||||
glog.V(2).Infof("Uploading shard %d to server %s", placement.ShardID, placement.ServerAddr)
|
||||
|
||||
ctx := context.Background()
|
||||
conn, err := grpc.Dial(placement.ServerAddr, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to server %s: %v", placement.ServerAddr, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client := volume_server_pb.NewVolumeServerClient(conn)
|
||||
|
||||
// Upload shard using VolumeEcShardsCopy - this assumes shards are already generated locally
|
||||
// and we're copying them to the target server
|
||||
shardIds := []uint32{uint32(placement.ShardID)}
|
||||
_, err = client.VolumeEcShardsCopy(ctx, &volume_server_pb.VolumeEcShardsCopyRequest{
|
||||
VolumeId: t.volumeID,
|
||||
Collection: t.collection,
|
||||
ShardIds: shardIds,
|
||||
CopyEcxFile: true,
|
||||
CopyEcjFile: true,
|
||||
CopyVifFile: true,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to copy EC shard: %v", err)
|
||||
}
|
||||
|
||||
glog.V(2).Infof("Successfully uploaded shard %d to %s", placement.ShardID, placement.ServerAddr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// verifyAndCleanupSource verifies the EC conversion and cleans up the source volume
|
||||
func (t *EnhancedECTask) verifyAndCleanupSource() error {
|
||||
t.currentStep = "verify_cleanup"
|
||||
t.SetProgress(95.0)
|
||||
glog.V(1).Infof("Verifying EC conversion and cleaning up source volume %d", t.volumeID)
|
||||
|
||||
ctx := context.Background()
|
||||
conn, err := grpc.Dial(t.sourceServer, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to source server: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client := volume_server_pb.NewVolumeServerClient(conn)
|
||||
|
||||
// Verify source volume is read-only
|
||||
statusResp, err := client.VolumeStatus(ctx, &volume_server_pb.VolumeStatusRequest{
|
||||
VolumeId: t.volumeID,
|
||||
})
|
||||
if err == nil && statusResp.IsReadOnly {
|
||||
glog.V(1).Infof("Source volume %d is confirmed read-only", t.volumeID)
|
||||
}
|
||||
|
||||
// Delete source volume files (optional - could be kept for backup)
|
||||
// This would normally be done after confirming all shards are properly distributed
|
||||
// _, err = client.VolumeDelete(ctx, &volume_server_pb.VolumeDeleteRequest{
|
||||
// VolumeId: t.volumeID,
|
||||
// })
|
||||
// if err != nil {
|
||||
// glog.Warningf("Failed to delete source volume: %v", err)
|
||||
// }
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleanup removes temporary files and directories
|
||||
func (t *EnhancedECTask) cleanup(workDir string) {
|
||||
glog.V(1).Infof("Cleaning up work directory: %s", workDir)
|
||||
if err := os.RemoveAll(workDir); err != nil {
|
||||
glog.Warningf("Failed to cleanup work directory %s: %v", workDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate validates the enhanced task parameters
|
||||
func (t *EnhancedECTask) Validate(params types.TaskParams) error {
|
||||
if params.VolumeID == 0 {
|
||||
return fmt.Errorf("volume_id is required")
|
||||
}
|
||||
if params.Server == "" {
|
||||
return fmt.Errorf("server is required")
|
||||
}
|
||||
if t.masterClient == "" {
|
||||
return fmt.Errorf("master_client is required")
|
||||
}
|
||||
if t.workDir == "" {
|
||||
return fmt.Errorf("work_dir is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EstimateTime estimates the time needed for enhanced EC processing
|
||||
func (t *EnhancedECTask) EstimateTime(params types.TaskParams) time.Duration {
|
||||
baseTime := 20 * time.Minute // Enhanced processing takes longer
|
||||
|
||||
if size, ok := params.Parameters["volume_size"].(int64); ok {
|
||||
// More accurate estimate based on volume size
|
||||
// Account for copying, encoding, and distribution
|
||||
gbSize := size / (1024 * 1024 * 1024)
|
||||
estimatedTime := time.Duration(gbSize*2) * time.Minute // 2 minutes per GB
|
||||
if estimatedTime > baseTime {
|
||||
return estimatedTime
|
||||
}
|
||||
}
|
||||
|
||||
return baseTime
|
||||
}
|
||||
|
||||
// GetProgress returns current progress with detailed step information
|
||||
func (t *EnhancedECTask) GetProgress() float64 {
|
||||
return t.BaseTask.GetProgress()
|
||||
}
|
||||
|
||||
// GetCurrentStep returns the current processing step
|
||||
func (t *EnhancedECTask) GetCurrentStep() string {
|
||||
return t.currentStep
|
||||
}
|
||||
@@ -1,60 +1,153 @@
|
||||
package vacuum
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/tasks"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// Task implements vacuum operation to reclaim disk space
|
||||
type Task struct {
|
||||
*tasks.BaseTask
|
||||
server string
|
||||
volumeID uint32
|
||||
server string
|
||||
volumeID uint32
|
||||
garbageThreshold float64
|
||||
}
|
||||
|
||||
// NewTask creates a new vacuum task instance
|
||||
func NewTask(server string, volumeID uint32) *Task {
|
||||
task := &Task{
|
||||
BaseTask: tasks.NewBaseTask(types.TaskTypeVacuum),
|
||||
server: server,
|
||||
volumeID: volumeID,
|
||||
BaseTask: tasks.NewBaseTask(types.TaskTypeVacuum),
|
||||
server: server,
|
||||
volumeID: volumeID,
|
||||
garbageThreshold: 0.3, // Default 30% threshold
|
||||
}
|
||||
return task
|
||||
}
|
||||
|
||||
// Execute executes the vacuum task
|
||||
// Execute executes the actual vacuum task using real SeaweedFS operations
|
||||
func (t *Task) Execute(params types.TaskParams) error {
|
||||
glog.Infof("Starting vacuum task for volume %d on server %s", t.volumeID, t.server)
|
||||
glog.Infof("Starting vacuum for volume %d on server %s", t.volumeID, t.server)
|
||||
|
||||
// Simulate vacuum operation with progress updates
|
||||
steps := []struct {
|
||||
name string
|
||||
duration time.Duration
|
||||
progress float64
|
||||
}{
|
||||
{"Scanning volume", 1 * time.Second, 20},
|
||||
{"Identifying deleted files", 2 * time.Second, 50},
|
||||
{"Compacting data", 3 * time.Second, 80},
|
||||
{"Finalizing vacuum", 1 * time.Second, 100},
|
||||
ctx := context.Background()
|
||||
|
||||
// Parse garbage threshold from parameters
|
||||
if thresholdParam, ok := params.Parameters["garbage_threshold"]; ok {
|
||||
if thresholdStr, ok := thresholdParam.(string); ok {
|
||||
if threshold, err := strconv.ParseFloat(thresholdStr, 64); err == nil {
|
||||
t.garbageThreshold = threshold
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, step := range steps {
|
||||
if t.IsCancelled() {
|
||||
return fmt.Errorf("vacuum task cancelled")
|
||||
// Connect to volume server
|
||||
conn, err := grpc.Dial(t.server, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to volume server %s: %v", t.server, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client := volume_server_pb.NewVolumeServerClient(conn)
|
||||
|
||||
// Step 1: Check vacuum eligibility
|
||||
t.SetProgress(10.0)
|
||||
glog.V(1).Infof("Checking vacuum eligibility for volume %d", t.volumeID)
|
||||
|
||||
checkResp, err := client.VacuumVolumeCheck(ctx, &volume_server_pb.VacuumVolumeCheckRequest{
|
||||
VolumeId: t.volumeID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("vacuum check failed for volume %d: %v", t.volumeID, err)
|
||||
}
|
||||
|
||||
// Check if garbage ratio meets threshold
|
||||
if checkResp.GarbageRatio < t.garbageThreshold {
|
||||
return fmt.Errorf("volume %d garbage ratio %.2f%% is below threshold %.2f%%, skipping vacuum",
|
||||
t.volumeID, checkResp.GarbageRatio*100, t.garbageThreshold*100)
|
||||
}
|
||||
|
||||
glog.V(1).Infof("Volume %d has %.2f%% garbage, proceeding with vacuum",
|
||||
t.volumeID, checkResp.GarbageRatio*100)
|
||||
|
||||
// Step 2: Compact volume
|
||||
t.SetProgress(30.0)
|
||||
glog.V(1).Infof("Starting compact for volume %d", t.volumeID)
|
||||
|
||||
compactStream, err := client.VacuumVolumeCompact(ctx, &volume_server_pb.VacuumVolumeCompactRequest{
|
||||
VolumeId: t.volumeID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("vacuum compact failed for volume %d: %v", t.volumeID, err)
|
||||
}
|
||||
|
||||
// Process compact stream and track progress
|
||||
var processedBytes int64
|
||||
var totalBytes int64
|
||||
|
||||
for {
|
||||
resp, err := compactStream.Recv()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return fmt.Errorf("vacuum compact stream error for volume %d: %v", t.volumeID, err)
|
||||
}
|
||||
|
||||
glog.V(1).Infof("Vacuum task step: %s", step.name)
|
||||
t.SetProgress(step.progress)
|
||||
processedBytes = resp.ProcessedBytes
|
||||
if resp.LoadAvg_1M > 0 {
|
||||
totalBytes = int64(resp.LoadAvg_1M) // This is a rough approximation
|
||||
}
|
||||
|
||||
// Simulate work
|
||||
time.Sleep(step.duration)
|
||||
// Update progress based on processed bytes (30% to 70% of total progress)
|
||||
if totalBytes > 0 {
|
||||
compactProgress := float64(processedBytes) / float64(totalBytes)
|
||||
if compactProgress > 1.0 {
|
||||
compactProgress = 1.0
|
||||
}
|
||||
progress := 30.0 + (compactProgress * 40.0) // 30% to 70%
|
||||
t.SetProgress(progress)
|
||||
}
|
||||
|
||||
glog.V(2).Infof("Volume %d compact progress: %d bytes processed", t.volumeID, processedBytes)
|
||||
}
|
||||
|
||||
glog.Infof("Vacuum task completed for volume %d on server %s", t.volumeID, t.server)
|
||||
// Step 3: Commit vacuum changes
|
||||
t.SetProgress(80.0)
|
||||
glog.V(1).Infof("Committing vacuum for volume %d", t.volumeID)
|
||||
|
||||
commitResp, err := client.VacuumVolumeCommit(ctx, &volume_server_pb.VacuumVolumeCommitRequest{
|
||||
VolumeId: t.volumeID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("vacuum commit failed for volume %d: %v", t.volumeID, err)
|
||||
}
|
||||
|
||||
// Step 4: Cleanup temporary files
|
||||
t.SetProgress(90.0)
|
||||
glog.V(1).Infof("Cleaning up vacuum files for volume %d", t.volumeID)
|
||||
|
||||
_, err = client.VacuumVolumeCleanup(ctx, &volume_server_pb.VacuumVolumeCleanupRequest{
|
||||
VolumeId: t.volumeID,
|
||||
})
|
||||
if err != nil {
|
||||
// Log warning but don't fail the task
|
||||
glog.Warningf("Vacuum cleanup warning for volume %d: %v", t.volumeID, err)
|
||||
}
|
||||
|
||||
t.SetProgress(100.0)
|
||||
|
||||
newVolumeSize := commitResp.VolumeSize
|
||||
glog.Infof("Successfully completed vacuum for volume %d on server %s, new volume size: %d bytes",
|
||||
t.volumeID, t.server, newVolumeSize)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -71,9 +164,28 @@ func (t *Task) Validate(params types.TaskParams) error {
|
||||
|
||||
// EstimateTime estimates the time needed for the task
|
||||
func (t *Task) EstimateTime(params types.TaskParams) time.Duration {
|
||||
// Base time for vacuum operation
|
||||
baseTime := 25 * time.Second
|
||||
// Base time for vacuum operations - varies by volume size and garbage ratio
|
||||
// Typically vacuum is faster than EC encoding
|
||||
baseTime := 5 * time.Minute
|
||||
|
||||
// Could adjust based on volume size and garbage ratio if available in params
|
||||
if size, ok := params.Parameters["volume_size"].(int64); ok {
|
||||
// Rough estimate: 30 seconds per GB for vacuum
|
||||
estimatedTime := time.Duration(size/(1024*1024*1024)) * 30 * time.Second
|
||||
if estimatedTime > baseTime {
|
||||
return estimatedTime
|
||||
}
|
||||
}
|
||||
|
||||
// Could adjust based on volume size or usage patterns
|
||||
return baseTime
|
||||
}
|
||||
|
||||
// GetProgress returns the current progress
|
||||
func (t *Task) GetProgress() float64 {
|
||||
return t.BaseTask.GetProgress()
|
||||
}
|
||||
|
||||
// Cancel cancels the task
|
||||
func (t *Task) Cancel() error {
|
||||
return t.BaseTask.Cancel()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user