- Refactor internal/worker and internal/queue packages - Update cmd/tui for monitoring interface - Update test configurations
130 lines
3.9 KiB
Go
130 lines
3.9 KiB
Go
// Package lifecycle provides task lifecycle management with explicit state transitions.
|
|
package lifecycle
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"time"
|
|
|
|
"github.com/jfraeys/fetch_ml/internal/audit"
|
|
"github.com/jfraeys/fetch_ml/internal/domain"
|
|
)
|
|
|
|
// TaskState represents the current state of a task in its lifecycle.
|
|
// These states form a finite state machine with valid transitions defined below.
|
|
type TaskState string
|
|
|
|
const (
|
|
// StateQueued indicates the task is waiting to be picked up by a worker.
|
|
StateQueued TaskState = "queued"
|
|
// StatePreparing indicates the task is being prepared (workspace setup, data staging).
|
|
StatePreparing TaskState = "preparing"
|
|
// StateRunning indicates the task is currently executing in a container.
|
|
StateRunning TaskState = "running"
|
|
// StateCollecting indicates the task has finished execution and results are being collected.
|
|
StateCollecting TaskState = "collecting"
|
|
// StateCompleted indicates the task finished successfully.
|
|
StateCompleted TaskState = "completed"
|
|
// StateFailed indicates the task failed during execution.
|
|
StateFailed TaskState = "failed"
|
|
)
|
|
|
|
// ValidTransitions defines the allowed state transitions.
|
|
// The key is the "from" state, the value is a list of valid "to" states.
|
|
// This enforces that state transitions follow the expected lifecycle.
|
|
var ValidTransitions = map[TaskState][]TaskState{
|
|
StateQueued: {StatePreparing, StateFailed},
|
|
StatePreparing: {StateRunning, StateFailed},
|
|
StateRunning: {StateCollecting, StateFailed},
|
|
StateCollecting: {StateCompleted, StateFailed},
|
|
StateCompleted: {},
|
|
StateFailed: {},
|
|
}
|
|
|
|
// StateTransitionError is returned when an invalid state transition is attempted.
|
|
type StateTransitionError struct {
|
|
From TaskState
|
|
To TaskState
|
|
}
|
|
|
|
func (e StateTransitionError) Error() string {
|
|
return fmt.Sprintf("invalid state transition: %s -> %s", e.From, e.To)
|
|
}
|
|
|
|
// StateManager manages task state transitions with audit logging.
|
|
type StateManager struct {
|
|
enabled bool
|
|
auditor *audit.Logger
|
|
}
|
|
|
|
// NewStateManager creates a new state manager with the given audit logger.
|
|
func NewStateManager(auditor *audit.Logger) *StateManager {
|
|
return &StateManager{
|
|
enabled: auditor != nil,
|
|
auditor: auditor,
|
|
}
|
|
}
|
|
|
|
// Transition attempts to transition a task from its current state to a new state.
|
|
// It validates the transition, updates the task status, and logs the event.
|
|
// Returns StateTransitionError if the transition is not valid.
|
|
func (sm *StateManager) Transition(task *domain.Task, to TaskState) error {
|
|
from := TaskState(task.Status)
|
|
|
|
// Validate the transition
|
|
if err := sm.validateTransition(from, to); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Audit the transition before updating
|
|
if sm.enabled && sm.auditor != nil {
|
|
sm.auditor.Log(audit.Event{
|
|
EventType: audit.EventJobStarted,
|
|
Timestamp: time.Now(),
|
|
Resource: task.ID,
|
|
Action: "task_state_change",
|
|
Success: true,
|
|
Metadata: map[string]interface{}{
|
|
"job_name": task.JobName,
|
|
"old_state": string(from),
|
|
"new_state": string(to),
|
|
},
|
|
})
|
|
}
|
|
|
|
// Update task state
|
|
task.Status = string(to)
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateTransition checks if a transition from one state to another is valid.
|
|
func (sm *StateManager) validateTransition(from, to TaskState) error {
|
|
// Check if "from" state is valid
|
|
allowed, ok := ValidTransitions[from]
|
|
if !ok {
|
|
return StateTransitionError{From: from, To: to}
|
|
}
|
|
|
|
// Check if "to" state is in the allowed list
|
|
if slices.Contains(allowed, to) {
|
|
return nil
|
|
}
|
|
|
|
return StateTransitionError{From: from, To: to}
|
|
}
|
|
|
|
// IsTerminalState returns true if the state is terminal (no further transitions allowed).
|
|
func IsTerminalState(state TaskState) bool {
|
|
return state == StateCompleted || state == StateFailed
|
|
}
|
|
|
|
// CanTransition returns true if a transition from -> to is valid.
|
|
func CanTransition(from, to TaskState) bool {
|
|
allowed, ok := ValidTransitions[from]
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
return slices.Contains(allowed, to)
|
|
}
|