Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
976 lines
31 KiB
Go
976 lines
31 KiB
Go
package worker
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/worker_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/worker/tasks"
|
|
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
|
|
|
// Import task packages to trigger their auto-registration
|
|
_ "github.com/seaweedfs/seaweedfs/weed/worker/tasks/balance"
|
|
_ "github.com/seaweedfs/seaweedfs/weed/worker/tasks/erasure_coding"
|
|
_ "github.com/seaweedfs/seaweedfs/weed/worker/tasks/vacuum"
|
|
)
|
|
|
|
// Worker represents a maintenance worker instance
|
|
type Worker struct {
|
|
id string
|
|
config *types.WorkerConfig
|
|
registry *tasks.TaskRegistry
|
|
cmds chan workerCommand
|
|
done chan struct{} // closed when managerLoop exits, so senders stop blocking
|
|
state *workerState
|
|
taskLogHandler *tasks.TaskLogHandler
|
|
}
|
|
type workerState struct {
|
|
running bool
|
|
adminClient AdminClient
|
|
startTime time.Time
|
|
stopChan chan struct{}
|
|
heartbeatTicker *time.Ticker
|
|
requestTicker *time.Ticker
|
|
currentTasks map[string]*types.TaskInput
|
|
tasksCompleted int
|
|
tasksFailed int
|
|
}
|
|
|
|
type workerAction string
|
|
|
|
const (
|
|
ActionStart workerAction = "start"
|
|
ActionStop workerAction = "stop"
|
|
ActionGetStatus workerAction = "getstatus"
|
|
ActionGetTaskLoad workerAction = "getload"
|
|
ActionSetTask workerAction = "settask"
|
|
ActionSetAdmin workerAction = "setadmin"
|
|
ActionRemoveTask workerAction = "removetask"
|
|
ActionGetAdmin workerAction = "getadmin"
|
|
ActionIncTaskFail workerAction = "inctaskfail"
|
|
ActionIncTaskComplete workerAction = "inctaskcomplete"
|
|
ActionGetHbTick workerAction = "gethbtick"
|
|
ActionGetReqTick workerAction = "getreqtick"
|
|
ActionGetStopChan workerAction = "getstopchan"
|
|
ActionSetHbTick workerAction = "sethbtick"
|
|
ActionSetReqTick workerAction = "setreqtick"
|
|
ActionGetStartTime workerAction = "getstarttime"
|
|
ActionGetCompletedTasks workerAction = "getcompletedtasks"
|
|
ActionGetFailedTasks workerAction = "getfailedtasks"
|
|
ActionCancelTask workerAction = "canceltask"
|
|
// ... other worker actions like Stop, Status, etc.
|
|
)
|
|
|
|
type statusResponse chan types.WorkerStatus
|
|
type workerCommand struct {
|
|
action workerAction
|
|
data any
|
|
resp chan error // for reporting success/failure
|
|
}
|
|
|
|
// AdminClient defines the interface for communicating with the admin server
|
|
type AdminClient interface {
|
|
Connect() error
|
|
Disconnect() error
|
|
RegisterWorker(worker *types.WorkerData) error
|
|
SendHeartbeat(workerID string, status *types.WorkerStatus) error
|
|
RequestTask(workerID string, capabilities []types.TaskType) (*types.TaskInput, error)
|
|
CompleteTask(taskID string, success bool, errorMsg string) error
|
|
CompleteTaskWithMetadata(taskID string, success bool, errorMsg string, metadata map[string]string) error
|
|
UpdateTaskProgress(taskID string, progress float64) error
|
|
IsConnected() bool
|
|
}
|
|
|
|
// GenerateOrLoadWorkerID generates a unique worker ID or loads existing one from working directory
|
|
func GenerateOrLoadWorkerID(workingDir string) (string, error) {
|
|
const workerIDFile = "worker.id"
|
|
|
|
var idFilePath string
|
|
if workingDir != "" {
|
|
idFilePath = filepath.Join(workingDir, workerIDFile)
|
|
} else {
|
|
// Use current working directory if none specified
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get working directory: %w", err)
|
|
}
|
|
idFilePath = filepath.Join(wd, workerIDFile)
|
|
}
|
|
|
|
// Try to read existing worker ID
|
|
if data, err := os.ReadFile(idFilePath); err == nil {
|
|
workerID := strings.TrimSpace(string(data))
|
|
if workerID != "" {
|
|
glog.Infof("Loaded existing worker ID from %s: %s", idFilePath, workerID)
|
|
return workerID, nil
|
|
}
|
|
}
|
|
|
|
// Generate simplified worker ID
|
|
hostname, _ := os.Hostname()
|
|
if hostname == "" {
|
|
hostname = "unknown"
|
|
}
|
|
|
|
// Use short hostname - take first 6 chars or last part after dots
|
|
shortHostname := hostname
|
|
if len(hostname) > 6 {
|
|
if parts := strings.Split(hostname, "."); len(parts) > 1 {
|
|
// Use last part before domain (e.g., "worker1" from "worker1.example.com")
|
|
shortHostname = parts[0]
|
|
if len(shortHostname) > 6 {
|
|
shortHostname = shortHostname[:6]
|
|
}
|
|
} else {
|
|
// Use first 6 characters
|
|
shortHostname = hostname[:6]
|
|
}
|
|
}
|
|
|
|
// Generate random component for uniqueness (2 bytes = 4 hex chars)
|
|
randomBytes := make([]byte, 2)
|
|
var workerID string
|
|
if _, err := rand.Read(randomBytes); err != nil {
|
|
// Fallback to short timestamp if crypto/rand fails
|
|
timestamp := time.Now().Unix() % 10000 // last 4 digits
|
|
workerID = fmt.Sprintf("w-%s-%04d", shortHostname, timestamp)
|
|
glog.Infof("Generated fallback worker ID: %s", workerID)
|
|
} else {
|
|
// Use random hex for uniqueness
|
|
randomHex := fmt.Sprintf("%x", randomBytes)
|
|
workerID = fmt.Sprintf("w-%s-%s", shortHostname, randomHex)
|
|
glog.Infof("Generated new worker ID: %s", workerID)
|
|
}
|
|
|
|
// Save worker ID to file
|
|
if err := os.WriteFile(idFilePath, []byte(workerID), 0644); err != nil {
|
|
glog.Warningf("Failed to save worker ID to %s: %v", idFilePath, err)
|
|
} else {
|
|
glog.Infof("Saved worker ID to %s", idFilePath)
|
|
}
|
|
|
|
return workerID, nil
|
|
}
|
|
|
|
// NewWorker creates a new worker instance
|
|
func NewWorker(config *types.WorkerConfig) (*Worker, error) {
|
|
if config == nil {
|
|
config = types.DefaultWorkerConfig()
|
|
}
|
|
|
|
// Generate or load persistent worker ID
|
|
workerID, err := GenerateOrLoadWorkerID(config.BaseWorkingDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate or load worker ID: %w", err)
|
|
}
|
|
|
|
// Use the global unified registry that already has all tasks registered
|
|
registry := tasks.GetGlobalTaskRegistry()
|
|
|
|
// Ensure the base working directory exists
|
|
if config.BaseWorkingDir != "" {
|
|
if err := os.MkdirAll(config.BaseWorkingDir, 0755); err != nil {
|
|
return nil, fmt.Errorf("failed to create base working directory %s: %v", config.BaseWorkingDir, err)
|
|
}
|
|
}
|
|
|
|
// Initialize task log handler
|
|
logDir := filepath.Join(config.BaseWorkingDir, "task_logs")
|
|
// Ensure the base task log directory exists to avoid errors when admin requests logs
|
|
if err := os.MkdirAll(logDir, 0755); err != nil {
|
|
glog.Warningf("Failed to create task log base directory %s: %v", logDir, err)
|
|
}
|
|
taskLogHandler := tasks.NewTaskLogHandler(logDir)
|
|
|
|
worker := &Worker{
|
|
id: workerID,
|
|
config: config,
|
|
registry: registry,
|
|
taskLogHandler: taskLogHandler,
|
|
cmds: make(chan workerCommand),
|
|
done: make(chan struct{}),
|
|
}
|
|
|
|
glog.V(1).Infof("Worker created with %d registered task types", len(registry.GetAll()))
|
|
go worker.managerLoop()
|
|
return worker, nil
|
|
}
|
|
|
|
func (w *Worker) managerLoop() {
|
|
w.state = &workerState{
|
|
running: false,
|
|
stopChan: make(chan struct{}),
|
|
currentTasks: make(map[string]*types.TaskInput),
|
|
}
|
|
out:
|
|
for cmd := range w.cmds {
|
|
switch cmd.action {
|
|
case ActionStart:
|
|
w.handleStart(cmd)
|
|
case ActionStop:
|
|
w.handleStop(cmd)
|
|
break out
|
|
case ActionGetStatus:
|
|
respCh := cmd.data.(statusResponse)
|
|
var currentTasks []types.TaskInput
|
|
for _, task := range w.state.currentTasks {
|
|
currentTasks = append(currentTasks, *task)
|
|
}
|
|
|
|
statusStr := "active"
|
|
if len(w.state.currentTasks) >= w.config.MaxConcurrent {
|
|
statusStr = "busy"
|
|
}
|
|
|
|
status := types.WorkerStatus{
|
|
WorkerID: w.id,
|
|
Status: statusStr,
|
|
Capabilities: w.config.Capabilities,
|
|
MaxConcurrent: w.config.MaxConcurrent,
|
|
CurrentLoad: len(w.state.currentTasks),
|
|
LastHeartbeat: time.Now(),
|
|
CurrentTasks: currentTasks,
|
|
Uptime: time.Since(w.state.startTime),
|
|
TasksCompleted: w.state.tasksCompleted,
|
|
TasksFailed: w.state.tasksFailed,
|
|
}
|
|
respCh <- status
|
|
case ActionGetTaskLoad:
|
|
respCh := cmd.data.(chan int)
|
|
respCh <- len(w.state.currentTasks)
|
|
case ActionSetTask:
|
|
currentLoad := len(w.state.currentTasks)
|
|
if currentLoad >= w.config.MaxConcurrent {
|
|
cmd.resp <- fmt.Errorf("worker is at capacity")
|
|
}
|
|
task := cmd.data.(*types.TaskInput)
|
|
w.state.currentTasks[task.ID] = task
|
|
cmd.resp <- nil
|
|
case ActionSetAdmin:
|
|
admin := cmd.data.(AdminClient)
|
|
w.state.adminClient = admin
|
|
case ActionRemoveTask:
|
|
taskID := cmd.data.(string)
|
|
delete(w.state.currentTasks, taskID)
|
|
case ActionGetAdmin:
|
|
respCh := cmd.data.(chan AdminClient)
|
|
respCh <- w.state.adminClient
|
|
case ActionIncTaskFail:
|
|
w.state.tasksFailed++
|
|
case ActionIncTaskComplete:
|
|
w.state.tasksCompleted++
|
|
case ActionGetHbTick:
|
|
respCh := cmd.data.(chan *time.Ticker)
|
|
respCh <- w.state.heartbeatTicker
|
|
case ActionGetReqTick:
|
|
respCh := cmd.data.(chan *time.Ticker)
|
|
respCh <- w.state.requestTicker
|
|
case ActionSetHbTick:
|
|
w.state.heartbeatTicker = cmd.data.(*time.Ticker)
|
|
case ActionSetReqTick:
|
|
w.state.requestTicker = cmd.data.(*time.Ticker)
|
|
case ActionGetStopChan:
|
|
cmd.data.(chan chan struct{}) <- w.state.stopChan
|
|
case ActionGetStartTime:
|
|
cmd.data.(chan time.Time) <- w.state.startTime
|
|
case ActionGetCompletedTasks:
|
|
cmd.data.(chan int) <- w.state.tasksCompleted
|
|
case ActionGetFailedTasks:
|
|
cmd.data.(chan int) <- w.state.tasksFailed
|
|
case ActionCancelTask:
|
|
taskID := cmd.data.(string)
|
|
if task, exists := w.state.currentTasks[taskID]; exists {
|
|
glog.Infof("Cancelling task %s", task.ID)
|
|
// TODO: Implement actual task cancellation logic
|
|
} else {
|
|
glog.Warningf("Cannot cancel task %s: task not found", taskID)
|
|
}
|
|
|
|
}
|
|
}
|
|
// The loop no longer receives. Wake any task goroutine still trying to
|
|
// report into w.cmds so it returns instead of blocking forever.
|
|
close(w.done)
|
|
}
|
|
|
|
// dispatch delivers a fire-and-forget command to the manager loop, aborting
|
|
// if the worker has stopped. Returns false when the loop is gone, so task
|
|
// goroutines that outlive a forced shutdown don't block on it forever.
|
|
func (w *Worker) dispatch(cmd workerCommand) bool {
|
|
select {
|
|
case w.cmds <- cmd:
|
|
return true
|
|
case <-w.done:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (w *Worker) getTaskLoad() int {
|
|
respCh := make(chan int, 1)
|
|
if !w.dispatch(workerCommand{action: ActionGetTaskLoad, data: respCh}) {
|
|
return 0
|
|
}
|
|
return <-respCh
|
|
}
|
|
|
|
func (w *Worker) setTask(task *types.TaskInput) error {
|
|
resp := make(chan error)
|
|
if !w.dispatch(workerCommand{action: ActionSetTask, data: task, resp: resp}) {
|
|
return fmt.Errorf("worker is stopping")
|
|
}
|
|
if err := <-resp; err != nil {
|
|
glog.Errorf("TASK REJECTED: Worker %s at capacity (%d/%d) - rejecting task %s",
|
|
w.id, w.getTaskLoad(), w.config.MaxConcurrent, task.ID)
|
|
return err
|
|
}
|
|
newLoad := w.getTaskLoad()
|
|
|
|
glog.Infof("TASK ACCEPTED: Worker %s accepted task %s - current load: %d/%d",
|
|
w.id, task.ID, newLoad, w.config.MaxConcurrent)
|
|
return nil
|
|
}
|
|
|
|
func (w *Worker) removeTask(task *types.TaskInput) int {
|
|
if !w.dispatch(workerCommand{action: ActionRemoveTask, data: task.ID}) {
|
|
return 0
|
|
}
|
|
return w.getTaskLoad()
|
|
}
|
|
|
|
func (w *Worker) getAdmin() AdminClient {
|
|
respCh := make(chan AdminClient, 1)
|
|
if !w.dispatch(workerCommand{action: ActionGetAdmin, data: respCh}) {
|
|
return nil
|
|
}
|
|
return <-respCh
|
|
}
|
|
|
|
func (w *Worker) getStopChan() chan struct{} {
|
|
respCh := make(chan chan struct{}, 1)
|
|
w.cmds <- workerCommand{
|
|
action: ActionGetStopChan,
|
|
data: respCh,
|
|
}
|
|
return <-respCh
|
|
}
|
|
|
|
func (w *Worker) getHbTick() *time.Ticker {
|
|
respCh := make(chan *time.Ticker, 1)
|
|
w.cmds <- workerCommand{
|
|
action: ActionGetHbTick,
|
|
data: respCh,
|
|
}
|
|
return <-respCh
|
|
}
|
|
|
|
func (w *Worker) getReqTick() *time.Ticker {
|
|
respCh := make(chan *time.Ticker, 1)
|
|
w.cmds <- workerCommand{
|
|
action: ActionGetReqTick,
|
|
data: respCh,
|
|
}
|
|
return <-respCh
|
|
}
|
|
|
|
func (w *Worker) setHbTick(tick *time.Ticker) *time.Ticker {
|
|
w.cmds <- workerCommand{
|
|
action: ActionSetHbTick,
|
|
data: tick,
|
|
}
|
|
return w.getHbTick()
|
|
}
|
|
|
|
func (w *Worker) setReqTick(tick *time.Ticker) *time.Ticker {
|
|
w.cmds <- workerCommand{
|
|
action: ActionSetReqTick,
|
|
data: tick,
|
|
}
|
|
return w.getReqTick()
|
|
}
|
|
|
|
// getTaskLoggerConfig returns the task logger configuration with worker's log directory
|
|
func (w *Worker) getTaskLoggerConfig() tasks.TaskLoggerConfig {
|
|
config := tasks.DefaultTaskLoggerConfig()
|
|
|
|
// Use worker's configured log directory (BaseWorkingDir is guaranteed to be non-empty)
|
|
logDir := filepath.Join(w.config.BaseWorkingDir, "task_logs")
|
|
config.BaseLogDir = logDir
|
|
|
|
return config
|
|
}
|
|
|
|
// ID returns the worker ID
|
|
func (w *Worker) ID() string {
|
|
return w.id
|
|
}
|
|
|
|
func (w *Worker) Start() error {
|
|
resp := make(chan error)
|
|
w.cmds <- workerCommand{
|
|
action: ActionStart,
|
|
resp: resp,
|
|
}
|
|
return <-resp
|
|
}
|
|
|
|
// Start starts the worker
|
|
func (w *Worker) handleStart(cmd workerCommand) {
|
|
glog.Infof("Worker %s handleStart called", w.id)
|
|
if w.state.running {
|
|
cmd.resp <- fmt.Errorf("worker is already running")
|
|
return
|
|
}
|
|
|
|
if w.state.adminClient == nil {
|
|
cmd.resp <- fmt.Errorf("admin client is not set")
|
|
return
|
|
}
|
|
|
|
w.state.running = true
|
|
w.state.startTime = time.Now()
|
|
|
|
// Prepare worker info for registration
|
|
workerInfo := &types.WorkerData{
|
|
ID: w.id,
|
|
Capabilities: w.config.Capabilities,
|
|
MaxConcurrent: w.config.MaxConcurrent,
|
|
Status: "active",
|
|
CurrentLoad: 0,
|
|
LastHeartbeat: time.Now(),
|
|
}
|
|
|
|
// Register worker info with client first (this stores it for use during connection)
|
|
if err := w.state.adminClient.RegisterWorker(workerInfo); err != nil {
|
|
glog.V(1).Infof("Worker info stored for registration: %v", err)
|
|
// This is expected if not connected yet
|
|
}
|
|
|
|
// Start connection attempt (will register immediately if successful)
|
|
glog.Infof("WORKER STARTING: Worker %s starting with capabilities %v, max concurrent: %d",
|
|
w.id, w.config.Capabilities, w.config.MaxConcurrent)
|
|
|
|
// Try initial connection, but don't fail if it doesn't work immediately
|
|
if err := w.state.adminClient.Connect(); err != nil {
|
|
glog.Warningf("INITIAL CONNECTION FAILED: Worker %s initial connection to admin server failed, will keep retrying: %v", w.id, err)
|
|
// Don't return error - let the reconnection loop handle it
|
|
} else {
|
|
glog.Infof("INITIAL CONNECTION SUCCESS: Worker %s successfully connected to admin server", w.id)
|
|
}
|
|
|
|
// Start worker loops regardless of initial connection status
|
|
// They will handle connection failures gracefully
|
|
glog.V(1).Infof("STARTING LOOPS: Worker %s starting background loops", w.id)
|
|
go w.heartbeatLoop()
|
|
go w.taskRequestLoop()
|
|
go w.connectionMonitorLoop()
|
|
go w.messageProcessingLoop()
|
|
|
|
glog.Infof("WORKER STARTED: Worker %s started successfully (connection attempts will continue in background)", w.id)
|
|
cmd.resp <- nil
|
|
}
|
|
|
|
func (w *Worker) Stop() error {
|
|
// Snapshot the admin client BEFORE ActionStop terminates the manager loop.
|
|
// Once the loop exits, getAdmin/getTaskLoad return zero values (they abort
|
|
// on w.done) rather than the real client, so grab it while it's still live.
|
|
adminClient := w.getAdmin()
|
|
|
|
// Best-effort task drain: wait up to 30s for in-flight tasks to finish.
|
|
// Still done BEFORE ActionStop so getTaskLoad can round-trip through the
|
|
// manager loop.
|
|
timeout := time.NewTimer(30 * time.Second)
|
|
defer timeout.Stop()
|
|
out:
|
|
for w.getTaskLoad() > 0 {
|
|
select {
|
|
case <-timeout.C:
|
|
glog.Warningf("Worker %s stopping with %d tasks still running", w.id, w.getTaskLoad())
|
|
break out
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
}
|
|
|
|
// Terminate the manager loop. A second Stop (e.g. an admin-shutdown timer
|
|
// racing an explicit Stop) finds the loop already gone, so abort on w.done
|
|
// instead of blocking on a send no one will receive.
|
|
resp := make(chan error)
|
|
select {
|
|
case w.cmds <- workerCommand{action: ActionStop, resp: resp}:
|
|
if err := <-resp; err != nil {
|
|
return err
|
|
}
|
|
case <-w.done:
|
|
}
|
|
|
|
// Disconnect from admin server.
|
|
if adminClient != nil {
|
|
if err := adminClient.Disconnect(); err != nil {
|
|
glog.Errorf("Error disconnecting from admin server: %v", err)
|
|
}
|
|
}
|
|
glog.Infof("Worker %s stopped", w.id)
|
|
return nil
|
|
}
|
|
|
|
// Stop stops the worker
|
|
func (w *Worker) handleStop(cmd workerCommand) {
|
|
if !w.state.running {
|
|
cmd.resp <- nil
|
|
return
|
|
}
|
|
|
|
w.state.running = false
|
|
close(w.state.stopChan)
|
|
|
|
// Stop tickers
|
|
if w.state.heartbeatTicker != nil {
|
|
w.state.heartbeatTicker.Stop()
|
|
}
|
|
if w.state.requestTicker != nil {
|
|
w.state.requestTicker.Stop()
|
|
}
|
|
|
|
cmd.resp <- nil
|
|
}
|
|
|
|
// HandleTask handles a task execution
|
|
func (w *Worker) HandleTask(task *types.TaskInput) error {
|
|
glog.V(1).Infof("Worker %s received task %s (type: %s, volume: %d)",
|
|
w.id, task.ID, task.Type, task.VolumeID)
|
|
|
|
if err := w.setTask(task); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Execute task in goroutine
|
|
go w.executeTask(task)
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetAdminClient sets the admin client
|
|
func (w *Worker) SetAdminClient(client AdminClient) {
|
|
w.cmds <- workerCommand{
|
|
action: ActionSetAdmin,
|
|
data: client,
|
|
}
|
|
}
|
|
|
|
// executeTask executes a task
|
|
func (w *Worker) executeTask(task *types.TaskInput) {
|
|
startTime := time.Now()
|
|
|
|
defer func() {
|
|
currentLoad := w.removeTask(task)
|
|
|
|
duration := time.Since(startTime)
|
|
glog.Infof("TASK EXECUTION FINISHED: Worker %s finished executing task %s after %v - current load: %d/%d",
|
|
w.id, task.ID, duration, currentLoad, w.config.MaxConcurrent)
|
|
}()
|
|
|
|
glog.Infof("TASK EXECUTION STARTED: Worker %s starting execution of task %s (type: %s, volume: %d, server: %s, collection: %s) at %v",
|
|
w.id, task.ID, task.Type, task.VolumeID, task.Server, task.Collection, startTime.Format(time.RFC3339))
|
|
|
|
// Report task start to admin server
|
|
if admin := w.getAdmin(); admin != nil {
|
|
if err := admin.UpdateTaskProgress(task.ID, 0.0); err != nil {
|
|
glog.V(1).Infof("Failed to report task start to admin: %v", err)
|
|
}
|
|
}
|
|
|
|
// Determine task-specific working directory (BaseWorkingDir is guaranteed to be non-empty)
|
|
taskWorkingDir := filepath.Join(w.config.BaseWorkingDir, string(task.Type))
|
|
glog.V(2).Infof("WORKING DIRECTORY: Task %s using working directory: %s", task.ID, taskWorkingDir)
|
|
|
|
// Check if we have typed protobuf parameters
|
|
if task.TypedParams == nil {
|
|
w.completeTask(task.ID, false, "task has no typed parameters - task was not properly planned")
|
|
glog.Errorf("Worker %s rejecting task %s: no typed parameters", w.id, task.ID)
|
|
return
|
|
}
|
|
|
|
// Use new task execution system with unified Task interface
|
|
glog.V(1).Infof("Executing task %s with typed protobuf parameters", task.ID)
|
|
|
|
// Initialize a file-based task logger so admin can retrieve logs
|
|
// Build minimal params for logger metadata
|
|
loggerParams := types.TaskParams{
|
|
VolumeID: task.VolumeID,
|
|
Collection: task.Collection,
|
|
TypedParams: task.TypedParams,
|
|
}
|
|
loggerConfig := w.getTaskLoggerConfig()
|
|
fileLogger, logErr := tasks.NewTaskLogger(task.ID, task.Type, w.id, loggerParams, loggerConfig)
|
|
if logErr != nil {
|
|
glog.Warningf("Failed to initialize file logger for task %s: %v", task.ID, logErr)
|
|
} else {
|
|
defer func() {
|
|
if err := fileLogger.Close(); err != nil {
|
|
glog.V(1).Infof("Failed to close task logger for %s: %v", task.ID, err)
|
|
}
|
|
}()
|
|
fileLogger.Info("Task %s started (type=%s, server=%s, collection=%s)", task.ID, task.Type, task.Server, task.Collection)
|
|
}
|
|
|
|
taskFactory := w.registry.Get(task.Type)
|
|
if taskFactory == nil {
|
|
w.completeTask(task.ID, false, fmt.Sprintf("task factory not available for %s: task type not found", task.Type))
|
|
glog.Errorf("Worker %s failed to get task factory for %s type %v", w.id, task.ID, task.Type)
|
|
|
|
// Log supported task types for debugging
|
|
allFactories := w.registry.GetAll()
|
|
glog.Errorf("Available task types: %d", len(allFactories))
|
|
for taskType := range allFactories {
|
|
glog.Errorf("Supported task type: %v", taskType)
|
|
}
|
|
return
|
|
}
|
|
|
|
taskInstance, err := taskFactory.Create(task.TypedParams)
|
|
if err != nil {
|
|
w.completeTask(task.ID, false, fmt.Sprintf("failed to create task for %s: %v", task.Type, err))
|
|
glog.Errorf("Worker %s failed to create task %s type %v: %v", w.id, task.ID, task.Type, err)
|
|
return
|
|
}
|
|
|
|
// Task execution uses the new unified Task interface
|
|
glog.V(2).Infof("Executing task %s in working directory: %s", task.ID, taskWorkingDir)
|
|
taskInstance.SetWorkingDir(taskWorkingDir)
|
|
|
|
// If we have a file logger, adapt it so task WithFields logs are captured into file
|
|
if fileLogger != nil {
|
|
if withLogger, ok := taskInstance.(interface{ SetLogger(types.Logger) }); ok {
|
|
withLogger.SetLogger(newTaskLoggerAdapter(fileLogger))
|
|
}
|
|
}
|
|
|
|
// Set progress callback that reports to admin server
|
|
taskInstance.SetProgressCallback(func(progress float64, stage string) {
|
|
// Report progress updates to admin server
|
|
glog.V(2).Infof("Task %s progress: %.1f%% - %s", task.ID, progress, stage)
|
|
if admin := w.getAdmin(); admin != nil {
|
|
if err := admin.UpdateTaskProgress(task.ID, progress); err != nil {
|
|
glog.V(1).Infof("Failed to report task progress to admin: %v", err)
|
|
}
|
|
}
|
|
if fileLogger != nil {
|
|
// Use meaningful stage description or fallback to generic message
|
|
message := stage
|
|
if message == "" {
|
|
message = fmt.Sprintf("Progress: %.1f%%", progress)
|
|
}
|
|
fileLogger.LogProgress(progress, message)
|
|
}
|
|
})
|
|
|
|
// Execute task with context
|
|
ctx := context.Background()
|
|
err = taskInstance.Execute(ctx, task.TypedParams)
|
|
|
|
// Report completion
|
|
if fileLogger != nil {
|
|
fileLogger.Sync()
|
|
}
|
|
if err != nil {
|
|
w.completeTask(task.ID, false, err.Error())
|
|
w.dispatch(workerCommand{action: ActionIncTaskFail})
|
|
glog.Errorf("Worker %s failed to execute task %s: %v", w.id, task.ID, err)
|
|
if fileLogger != nil {
|
|
fileLogger.LogStatus("failed", err.Error())
|
|
fileLogger.Error("Task %s failed: %v", task.ID, err)
|
|
}
|
|
} else {
|
|
if fileLogger != nil {
|
|
fileLogger.Info("Task %s completed successfully", task.ID)
|
|
fileLogger.Sync()
|
|
}
|
|
w.completeTask(task.ID, true, "")
|
|
w.dispatch(workerCommand{action: ActionIncTaskComplete})
|
|
glog.Infof("Worker %s completed task %s successfully", w.id, task.ID)
|
|
}
|
|
}
|
|
|
|
// completeTask reports task completion to admin server
|
|
func (w *Worker) completeTask(taskID string, success bool, errorMsg string) {
|
|
admin := w.getAdmin()
|
|
if admin == nil {
|
|
return
|
|
}
|
|
if err := admin.CompleteTask(taskID, success, errorMsg); err != nil {
|
|
glog.Errorf("Failed to report task completion: %v", err)
|
|
}
|
|
}
|
|
|
|
// heartbeatLoop sends periodic heartbeats to the admin server
|
|
func (w *Worker) heartbeatLoop() {
|
|
defer w.setHbTick(time.NewTicker(w.config.HeartbeatInterval)).Stop()
|
|
ticker := w.getHbTick()
|
|
stopChan := w.getStopChan()
|
|
for {
|
|
select {
|
|
case <-stopChan:
|
|
return
|
|
case <-ticker.C:
|
|
w.sendHeartbeat()
|
|
}
|
|
}
|
|
}
|
|
|
|
// taskRequestLoop periodically requests new tasks from the admin server
|
|
func (w *Worker) taskRequestLoop() {
|
|
defer w.setReqTick(time.NewTicker(w.config.TaskRequestInterval)).Stop()
|
|
ticker := w.getReqTick()
|
|
stopChan := w.getStopChan()
|
|
for {
|
|
select {
|
|
case <-stopChan:
|
|
return
|
|
case <-ticker.C:
|
|
w.requestTasks()
|
|
}
|
|
}
|
|
}
|
|
|
|
// sendHeartbeat sends heartbeat to admin server
|
|
func (w *Worker) sendHeartbeat() {
|
|
admin := w.getAdmin()
|
|
if admin == nil {
|
|
return
|
|
}
|
|
if err := admin.SendHeartbeat(w.id, &types.WorkerStatus{
|
|
WorkerID: w.id,
|
|
Status: "active",
|
|
Capabilities: w.config.Capabilities,
|
|
MaxConcurrent: w.config.MaxConcurrent,
|
|
CurrentLoad: w.getTaskLoad(),
|
|
LastHeartbeat: time.Now(),
|
|
}); err != nil {
|
|
glog.Warningf("Failed to send heartbeat: %v", err)
|
|
}
|
|
}
|
|
|
|
// requestTasks requests new tasks from the admin server
|
|
func (w *Worker) requestTasks() {
|
|
currentLoad := w.getTaskLoad()
|
|
|
|
if currentLoad >= w.config.MaxConcurrent {
|
|
glog.V(3).Infof("TASK REQUEST SKIPPED: Worker %s at capacity (%d/%d)",
|
|
w.id, currentLoad, w.config.MaxConcurrent)
|
|
return // Already at capacity
|
|
}
|
|
|
|
admin := w.getAdmin()
|
|
if admin != nil {
|
|
glog.V(4).Infof("REQUESTING TASK: Worker %s requesting task from admin server (current load: %d/%d, capabilities: %v)",
|
|
w.id, currentLoad, w.config.MaxConcurrent, w.config.Capabilities)
|
|
|
|
task, err := admin.RequestTask(w.id, w.config.Capabilities)
|
|
if err != nil {
|
|
glog.V(2).Infof("TASK REQUEST FAILED: Worker %s failed to request task: %v", w.id, err)
|
|
return
|
|
}
|
|
|
|
if task != nil {
|
|
glog.Infof("TASK RESPONSE RECEIVED: Worker %s received task from admin server - ID: %s, Type: %s",
|
|
w.id, task.ID, task.Type)
|
|
if err := w.HandleTask(task); err != nil {
|
|
glog.Errorf("TASK HANDLING FAILED: Worker %s failed to handle task %s: %v", w.id, task.ID, err)
|
|
}
|
|
} else {
|
|
glog.V(4).Infof("NO TASK AVAILABLE: Worker %s - admin server has no tasks available", w.id)
|
|
}
|
|
}
|
|
}
|
|
|
|
// connectionMonitorLoop monitors connection status
|
|
func (w *Worker) connectionMonitorLoop() {
|
|
ticker := time.NewTicker(30 * time.Second) // Check every 30 seconds
|
|
defer ticker.Stop()
|
|
|
|
lastConnectionStatus := false
|
|
stopChan := w.getStopChan()
|
|
for {
|
|
select {
|
|
case <-stopChan:
|
|
glog.V(1).Infof("CONNECTION MONITOR STOPPING: Worker %s connection monitor loop stopping", w.id)
|
|
return
|
|
case <-ticker.C:
|
|
// Monitor connection status and log changes
|
|
admin := w.getAdmin()
|
|
currentConnectionStatus := admin != nil && admin.IsConnected()
|
|
|
|
if currentConnectionStatus != lastConnectionStatus {
|
|
if currentConnectionStatus {
|
|
glog.Infof("CONNECTION RESTORED: Worker %s connection status changed: connected", w.id)
|
|
} else {
|
|
glog.Warningf("CONNECTION LOST: Worker %s connection status changed: disconnected", w.id)
|
|
}
|
|
lastConnectionStatus = currentConnectionStatus
|
|
} else {
|
|
if currentConnectionStatus {
|
|
glog.V(4).Infof("CONNECTION OK: Worker %s connection status: connected", w.id)
|
|
} else {
|
|
glog.V(1).Infof("CONNECTION DOWN: Worker %s connection status: disconnected, reconnection in progress", w.id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// messageProcessingLoop processes incoming admin messages
|
|
func (w *Worker) messageProcessingLoop() {
|
|
glog.Infof("MESSAGE LOOP STARTED: Worker %s message processing loop started", w.id)
|
|
|
|
// Get access to the incoming message channel from gRPC client
|
|
grpcClient, ok := w.getAdmin().(*GrpcAdminClient)
|
|
if !ok {
|
|
glog.Warningf("MESSAGE LOOP UNAVAILABLE: Worker %s admin client is not gRPC client, message processing not available", w.id)
|
|
return
|
|
}
|
|
|
|
incomingChan := grpcClient.GetIncomingChannel()
|
|
glog.V(1).Infof("MESSAGE CHANNEL READY: Worker %s connected to incoming message channel", w.id)
|
|
stopChan := w.getStopChan()
|
|
for {
|
|
select {
|
|
case <-stopChan:
|
|
glog.Infof("MESSAGE LOOP STOPPING: Worker %s message processing loop stopping", w.id)
|
|
return
|
|
case message := <-incomingChan:
|
|
if message != nil {
|
|
glog.V(4).Infof("MESSAGE PROCESSING: Worker %s processing incoming message", w.id)
|
|
w.processAdminMessage(message)
|
|
} else {
|
|
glog.V(4).Infof("NULL MESSAGE: Worker %s received nil message", w.id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// processAdminMessage processes different types of admin messages
|
|
func (w *Worker) processAdminMessage(message *worker_pb.AdminMessage) {
|
|
glog.V(4).Infof("ADMIN MESSAGE RECEIVED: Worker %s received admin message: %T", w.id, message.Message)
|
|
|
|
switch msg := message.Message.(type) {
|
|
case *worker_pb.AdminMessage_RegistrationResponse:
|
|
glog.V(2).Infof("REGISTRATION RESPONSE: Worker %s received registration response", w.id)
|
|
w.handleRegistrationResponse(msg.RegistrationResponse)
|
|
case *worker_pb.AdminMessage_HeartbeatResponse:
|
|
glog.V(3).Infof("HEARTBEAT RESPONSE: Worker %s received heartbeat response", w.id)
|
|
w.handleHeartbeatResponse(msg.HeartbeatResponse)
|
|
case *worker_pb.AdminMessage_TaskLogRequest:
|
|
glog.V(1).Infof("TASK LOG REQUEST: Worker %s received task log request for task %s", w.id, msg.TaskLogRequest.TaskId)
|
|
w.handleTaskLogRequest(msg.TaskLogRequest)
|
|
case *worker_pb.AdminMessage_TaskAssignment:
|
|
taskAssign := msg.TaskAssignment
|
|
if taskAssign.TaskId == "" {
|
|
glog.V(4).Infof("Worker %s received empty task assignment, going to sleep", w.id)
|
|
return
|
|
}
|
|
glog.V(1).Infof("Worker %s received direct task assignment %s (type: %s, volume: %d)",
|
|
w.id, taskAssign.TaskId, taskAssign.TaskType, taskAssign.Params.VolumeId)
|
|
|
|
// Convert to task and handle it
|
|
task := &types.TaskInput{
|
|
ID: taskAssign.TaskId,
|
|
Type: types.TaskType(taskAssign.TaskType),
|
|
Status: types.TaskStatusAssigned,
|
|
VolumeID: taskAssign.Params.VolumeId,
|
|
Server: getServerFromParams(taskAssign.Params),
|
|
Collection: taskAssign.Params.Collection,
|
|
Priority: types.TaskPriority(taskAssign.Priority),
|
|
CreatedAt: time.Unix(taskAssign.CreatedTime, 0),
|
|
TypedParams: taskAssign.Params,
|
|
}
|
|
|
|
if err := w.HandleTask(task); err != nil {
|
|
glog.Errorf("DIRECT TASK ASSIGNMENT FAILED: Worker %s failed to handle direct task assignment %s: %v", w.id, task.ID, err)
|
|
}
|
|
case *worker_pb.AdminMessage_TaskCancellation:
|
|
glog.Infof("TASK CANCELLATION: Worker %s received task cancellation for task %s", w.id, msg.TaskCancellation.TaskId)
|
|
w.handleTaskCancellation(msg.TaskCancellation)
|
|
case *worker_pb.AdminMessage_AdminShutdown:
|
|
glog.Infof("ADMIN SHUTDOWN: Worker %s received admin shutdown message", w.id)
|
|
w.handleAdminShutdown(msg.AdminShutdown)
|
|
default:
|
|
glog.V(1).Infof("UNKNOWN MESSAGE: Worker %s received unknown admin message type: %T", w.id, message.Message)
|
|
}
|
|
}
|
|
|
|
// handleTaskLogRequest processes task log requests from admin server
|
|
func (w *Worker) handleTaskLogRequest(request *worker_pb.TaskLogRequest) {
|
|
glog.V(1).Infof("Worker %s handling task log request for task %s", w.id, request.TaskId)
|
|
|
|
// Use the task log handler to process the request
|
|
response := w.taskLogHandler.HandleLogRequest(request)
|
|
|
|
// Send response back to admin server
|
|
responseMsg := &worker_pb.WorkerMessage{
|
|
WorkerId: w.id,
|
|
Timestamp: time.Now().Unix(),
|
|
Message: &worker_pb.WorkerMessage_TaskLogResponse{
|
|
TaskLogResponse: response,
|
|
},
|
|
}
|
|
|
|
grpcClient, ok := w.getAdmin().(*GrpcAdminClient)
|
|
if !ok {
|
|
glog.Errorf("Cannot send task log response: admin client is not gRPC client")
|
|
return
|
|
}
|
|
|
|
select {
|
|
case grpcClient.outgoing <- responseMsg:
|
|
glog.V(1).Infof("Task log response sent for task %s", request.TaskId)
|
|
case <-time.After(5 * time.Second):
|
|
glog.Errorf("Failed to send task log response for task %s: timeout", request.TaskId)
|
|
}
|
|
}
|
|
|
|
// handleTaskCancellation processes task cancellation requests
|
|
func (w *Worker) handleTaskCancellation(cancellation *worker_pb.TaskCancellation) {
|
|
glog.Infof("Worker %s received task cancellation for task %s", w.id, cancellation.TaskId)
|
|
|
|
w.dispatch(workerCommand{action: ActionCancelTask, data: cancellation.TaskId})
|
|
}
|
|
|
|
// handleAdminShutdown processes admin shutdown notifications
|
|
func (w *Worker) handleAdminShutdown(shutdown *worker_pb.AdminShutdown) {
|
|
glog.Infof("Worker %s received admin shutdown notification: %s", w.id, shutdown.Reason)
|
|
|
|
gracefulSeconds := shutdown.GracefulShutdownSeconds
|
|
if gracefulSeconds > 0 {
|
|
glog.Infof("Graceful shutdown in %d seconds", gracefulSeconds)
|
|
time.AfterFunc(time.Duration(gracefulSeconds)*time.Second, func() {
|
|
w.Stop()
|
|
})
|
|
} else {
|
|
// Immediate shutdown
|
|
go w.Stop()
|
|
}
|
|
}
|
|
|
|
// handleRegistrationResponse processes registration response from admin server
|
|
func (w *Worker) handleRegistrationResponse(response *worker_pb.RegistrationResponse) {
|
|
glog.V(2).Infof("Worker %s processed registration response: success=%v", w.id, response.Success)
|
|
if !response.Success {
|
|
glog.Warningf("Worker %s registration failed: %s", w.id, response.Message)
|
|
}
|
|
// Registration responses are typically handled by the gRPC client during connection setup
|
|
// No additional action needed here
|
|
}
|
|
|
|
// handleHeartbeatResponse processes heartbeat response from admin server
|
|
func (w *Worker) handleHeartbeatResponse(response *worker_pb.HeartbeatResponse) {
|
|
glog.V(4).Infof("Worker %s processed heartbeat response", w.id)
|
|
// Heartbeat responses are mainly for keeping the connection alive
|
|
// The admin may include configuration updates or status information in the future
|
|
// For now, just acknowledge receipt
|
|
}
|