integration test. ec worker task

This commit is contained in:
chrislu committed 2025-07-24 07:50:03 -07:00
1 parent 0d8e1f8a17
commit 30ad65bb5f
21 files changed
+6363 -486

No files matched your search

File diff suppressed because it is too large. Load diff
+58
View File
@@ -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")
}
+442
View File
@@ -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
+324
View File
@@ -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
}
+434
View File
@@ -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")
}
+233
View File
@@ -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")
}
+545
View File
@@ -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
}