// 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) }