mirror of
https://github.com/j3ssie/osmedeus.git
synced 2026-08-26 09:32:28 +02:00
267 lines
6.6 KiB
Go
267 lines
6.6 KiB
Go
package distributed
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/j3ssie/osmedeus/v5/internal/config"
|
|
"github.com/j3ssie/osmedeus/v5/internal/core"
|
|
"github.com/j3ssie/osmedeus/v5/internal/executor"
|
|
"github.com/j3ssie/osmedeus/v5/internal/parser"
|
|
"github.com/j3ssie/osmedeus/v5/internal/terminal"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// Worker represents a worker node that processes tasks
|
|
type Worker struct {
|
|
ID string
|
|
Hostname string
|
|
client *Client
|
|
config *config.Config
|
|
executor *executor.Executor
|
|
loader *parser.Loader
|
|
logger *zap.Logger
|
|
printer *terminal.Printer
|
|
|
|
// Stats
|
|
tasksComplete int
|
|
tasksFailed int
|
|
}
|
|
|
|
// NewWorker creates a new worker node
|
|
func NewWorker(cfg *config.Config) (*Worker, error) {
|
|
client, err := NewClientFromConfig(cfg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create redis client: %w", err)
|
|
}
|
|
|
|
hostname, _ := os.Hostname()
|
|
workerID := fmt.Sprintf("%s-%s", hostname, uuid.NewString()[:8])
|
|
|
|
logger, _ := zap.NewProduction()
|
|
exec := executor.NewExecutor()
|
|
|
|
return &Worker{
|
|
ID: workerID,
|
|
Hostname: hostname,
|
|
client: client,
|
|
config: cfg,
|
|
executor: exec,
|
|
loader: parser.NewLoader(cfg.WorkflowsPath),
|
|
logger: logger,
|
|
printer: terminal.NewPrinter(),
|
|
}, nil
|
|
}
|
|
|
|
// Run starts the worker loop
|
|
func (w *Worker) Run(ctx context.Context) error {
|
|
// Test connection
|
|
if err := w.client.Ping(ctx); err != nil {
|
|
return fmt.Errorf("failed to connect to redis: %w", err)
|
|
}
|
|
|
|
// Register worker
|
|
if err := w.register(ctx); err != nil {
|
|
return fmt.Errorf("failed to register worker: %w", err)
|
|
}
|
|
|
|
w.printer.Success("Worker %s joined successfully", w.ID)
|
|
w.printer.Info("Waiting for tasks...")
|
|
|
|
// Start heartbeat goroutine
|
|
heartbeatCtx, cancelHeartbeat := context.WithCancel(ctx)
|
|
defer cancelHeartbeat()
|
|
go w.heartbeatLoop(heartbeatCtx)
|
|
|
|
// Main task loop
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
w.logger.Info("worker shutting down", zap.String("worker_id", w.ID))
|
|
w.cleanup(context.Background())
|
|
return nil
|
|
default:
|
|
if err := w.processNextTask(ctx); err != nil {
|
|
w.logger.Error("error processing task", zap.Error(err))
|
|
time.Sleep(time.Second) // Brief pause before retrying
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// register registers the worker with the master
|
|
func (w *Worker) register(ctx context.Context) error {
|
|
info := &WorkerInfo{
|
|
ID: w.ID,
|
|
Hostname: w.Hostname,
|
|
Status: "idle",
|
|
JoinedAt: time.Now(),
|
|
LastHeartbeat: time.Now(),
|
|
}
|
|
|
|
if err := w.client.RegisterWorker(ctx, info); err != nil {
|
|
return err
|
|
}
|
|
|
|
return w.client.UpdateWorkerHeartbeat(ctx, w.ID)
|
|
}
|
|
|
|
// heartbeatLoop sends periodic heartbeats
|
|
func (w *Worker) heartbeatLoop(ctx context.Context) {
|
|
ticker := time.NewTicker(HeartbeatInterval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
if err := w.client.UpdateWorkerHeartbeat(ctx, w.ID); err != nil {
|
|
w.logger.Warn("failed to send heartbeat", zap.Error(err))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// processNextTask waits for and processes the next task
|
|
func (w *Worker) processNextTask(ctx context.Context) error {
|
|
// Block waiting for a task
|
|
task, err := w.client.PopTask(ctx, TaskPollTimeout)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if task == nil {
|
|
return nil // Timeout, no task available
|
|
}
|
|
|
|
w.logger.Info("received task",
|
|
zap.String("task_id", task.ID),
|
|
zap.String("workflow", task.WorkflowName),
|
|
zap.String("target", task.Target),
|
|
)
|
|
w.printer.Info("Received task %s: %s -> %s", task.ID, task.WorkflowName, task.Target)
|
|
|
|
// Mark task as running
|
|
task.MarkRunning(w.ID)
|
|
if err := w.client.SetTaskRunning(ctx, task); err != nil {
|
|
w.logger.Error("failed to mark task running", zap.Error(err))
|
|
}
|
|
|
|
// Update worker status
|
|
w.updateStatus(ctx, "busy", task.ID)
|
|
|
|
// Execute the task
|
|
result := w.executeTask(ctx, task)
|
|
|
|
// Report result
|
|
if err := w.client.SetTaskResult(ctx, result); err != nil {
|
|
w.logger.Error("failed to report task result", zap.Error(err))
|
|
}
|
|
|
|
// Remove from running
|
|
if err := w.client.RemoveTaskRunning(ctx, task.ID); err != nil {
|
|
w.logger.Error("failed to remove task from running", zap.Error(err))
|
|
}
|
|
|
|
// Update stats and status
|
|
if result.Status == TaskStatusCompleted {
|
|
w.tasksComplete++
|
|
w.printer.Success("Task %s completed", task.ID)
|
|
} else {
|
|
w.tasksFailed++
|
|
w.printer.Error("Task %s failed: %s", task.ID, result.Error)
|
|
}
|
|
w.updateStatus(ctx, "idle", "")
|
|
|
|
return nil
|
|
}
|
|
|
|
// executeTask executes a workflow task
|
|
func (w *Worker) executeTask(ctx context.Context, task *Task) *TaskResult {
|
|
result := &TaskResult{
|
|
TaskID: task.ID,
|
|
CompletedAt: time.Now(),
|
|
}
|
|
|
|
// Load workflow
|
|
workflow, err := w.loader.LoadWorkflow(task.WorkflowName)
|
|
if err != nil {
|
|
result.Status = TaskStatusFailed
|
|
result.Error = fmt.Sprintf("failed to load workflow: %v", err)
|
|
return result
|
|
}
|
|
|
|
// Convert params to string map
|
|
params := make(map[string]string)
|
|
params["target"] = task.Target
|
|
for k, v := range task.Params {
|
|
if s, ok := v.(string); ok {
|
|
params[k] = s
|
|
}
|
|
}
|
|
|
|
// Execute based on workflow kind
|
|
var wfResult *core.WorkflowResult
|
|
if workflow.IsFlow() {
|
|
wfResult, err = w.executor.ExecuteFlow(ctx, workflow, params, w.config)
|
|
} else {
|
|
wfResult, err = w.executor.ExecuteModule(ctx, workflow, params, w.config)
|
|
}
|
|
|
|
if err != nil {
|
|
result.Status = TaskStatusFailed
|
|
result.Error = err.Error()
|
|
return result
|
|
}
|
|
|
|
// Check result status
|
|
if wfResult.Status == core.RunStatusFailed {
|
|
result.Status = TaskStatusFailed
|
|
if wfResult.Error != nil {
|
|
result.Error = wfResult.Error.Error()
|
|
} else {
|
|
result.Error = "workflow execution failed"
|
|
}
|
|
} else {
|
|
result.Status = TaskStatusCompleted
|
|
result.Exports = wfResult.Exports
|
|
}
|
|
|
|
result.CompletedAt = time.Now()
|
|
return result
|
|
}
|
|
|
|
// updateStatus updates the worker's status in Redis
|
|
func (w *Worker) updateStatus(ctx context.Context, status string, taskID string) {
|
|
info := &WorkerInfo{
|
|
ID: w.ID,
|
|
Hostname: w.Hostname,
|
|
Status: status,
|
|
CurrentTaskID: taskID,
|
|
JoinedAt: time.Now(), // This will be overwritten, but we need a value
|
|
LastHeartbeat: time.Now(),
|
|
TasksComplete: w.tasksComplete,
|
|
TasksFailed: w.tasksFailed,
|
|
}
|
|
if err := w.client.RegisterWorker(ctx, info); err != nil {
|
|
w.logger.Warn("failed to update worker status", zap.Error(err))
|
|
}
|
|
}
|
|
|
|
// cleanup removes the worker from the registry
|
|
func (w *Worker) cleanup(ctx context.Context) {
|
|
w.printer.Info("Cleaning up worker %s...", w.ID)
|
|
if err := w.client.RemoveWorker(ctx, w.ID); err != nil {
|
|
w.logger.Warn("failed to remove worker", zap.Error(err))
|
|
}
|
|
w.client.Close()
|
|
}
|
|
|
|
// GetID returns the worker ID
|
|
func (w *Worker) GetID() string {
|
|
return w.ID
|
|
}
|