191 lines
4.4 KiB
Go
191 lines
4.4 KiB
Go
package task
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/edgeai/gateway/internal/observability"
|
|
)
|
|
|
|
// TaskState represents the lifecycle state of a task.
|
|
type TaskState string
|
|
|
|
const (
|
|
StateQueued TaskState = "QUEUED"
|
|
StateRunning TaskState = "RUNNING"
|
|
StateStreaming TaskState = "STREAMING"
|
|
StateCompleted TaskState = "COMPLETED"
|
|
StateFailed TaskState = "FAILED"
|
|
StateCancelled TaskState = "CANCELLED"
|
|
)
|
|
|
|
// TaskPriority levels (P0 highest, P4 lowest).
|
|
type TaskPriority int
|
|
|
|
const (
|
|
PriorityRealtime TaskPriority = 0 // P0
|
|
PriorityHigh TaskPriority = 1 // P1
|
|
PriorityNormal TaskPriority = 2 // P2 (default)
|
|
PriorityLow TaskPriority = 3 // P3
|
|
PriorityBackground TaskPriority = 4 // P4
|
|
)
|
|
|
|
// Task represents an inference task in the system.
|
|
type Task struct {
|
|
ID string
|
|
RequestID string
|
|
SessionID string
|
|
AppID string
|
|
TenantID string
|
|
LogicalModel string
|
|
ActualModel string
|
|
Priority TaskPriority
|
|
State TaskState
|
|
Stream bool
|
|
CreatedAt time.Time
|
|
StartedAt *time.Time
|
|
CompletedAt *time.Time
|
|
CancelReason string
|
|
ErrorMessage string
|
|
InputTokens int
|
|
OutputTokens int
|
|
NodeID string
|
|
Degraded bool
|
|
cancelCh chan struct{}
|
|
cancelOnce sync.Once
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// NewTask creates a new task in QUEUED state.
|
|
func NewTask(id, requestID, appID, tenantID, logicalModel string, priority TaskPriority, stream bool) *Task {
|
|
return &Task{
|
|
ID: id,
|
|
RequestID: requestID,
|
|
AppID: appID,
|
|
TenantID: tenantID,
|
|
LogicalModel: logicalModel,
|
|
Priority: priority,
|
|
State: StateQueued,
|
|
Stream: stream,
|
|
CreatedAt: time.Now(),
|
|
cancelCh: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// AllowedTransitions defines valid state transitions.
|
|
var allowedTransitions = map[TaskState][]TaskState{
|
|
StateQueued: {StateRunning, StateFailed, StateCancelled},
|
|
StateRunning: {StateStreaming, StateCompleted, StateFailed, StateCancelled},
|
|
StateStreaming: {StateCompleted, StateFailed, StateCancelled},
|
|
StateCompleted: {},
|
|
StateFailed: {},
|
|
StateCancelled: {},
|
|
}
|
|
|
|
// Transition changes the task state if the transition is valid.
|
|
func (t *Task) Transition(to TaskState) error {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
allowed, ok := allowedTransitions[t.State]
|
|
if !ok {
|
|
return fmt.Errorf("unknown current state: %s", t.State)
|
|
}
|
|
|
|
valid := false
|
|
for _, s := range allowed {
|
|
if s == to {
|
|
valid = true
|
|
break
|
|
}
|
|
}
|
|
if !valid {
|
|
return fmt.Errorf("invalid transition: %s -> %s", t.State, to)
|
|
}
|
|
|
|
from := t.State
|
|
t.State = to
|
|
now := time.Now()
|
|
|
|
switch to {
|
|
case StateRunning:
|
|
t.StartedAt = &now
|
|
case StateCompleted, StateFailed, StateCancelled:
|
|
t.CompletedAt = &now
|
|
}
|
|
|
|
_ = from
|
|
return nil
|
|
}
|
|
|
|
// Cancel signals task cancellation and transitions to CANCELLED if possible.
|
|
func (t *Task) Cancel(reason string) error {
|
|
t.cancelOnce.Do(func() {
|
|
close(t.cancelCh)
|
|
})
|
|
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
if t.State == StateCompleted || t.State == StateFailed || t.State == StateCancelled {
|
|
return errors.New("task already in terminal state")
|
|
}
|
|
|
|
t.CancelReason = reason
|
|
t.State = StateCancelled
|
|
now := time.Now()
|
|
t.CompletedAt = &now
|
|
return nil
|
|
}
|
|
|
|
// Cancelled returns a channel that's closed when the task is cancelled.
|
|
func (t *Task) Cancelled() <-chan struct{} {
|
|
return t.cancelCh
|
|
}
|
|
|
|
// IsCancelled returns true if the task has been cancelled.
|
|
func (t *Task) IsCancelled() bool {
|
|
select {
|
|
case <-t.cancelCh:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// GetState returns the current state (thread-safe).
|
|
func (t *Task) GetState() TaskState {
|
|
t.mu.RLock()
|
|
defer t.mu.RUnlock()
|
|
return t.State
|
|
}
|
|
|
|
// IsTerminal returns true if the task is in a terminal state.
|
|
func (t *Task) IsTerminal() bool {
|
|
s := t.GetState()
|
|
return s == StateCompleted || s == StateFailed || s == StateCancelled
|
|
}
|
|
|
|
// StateMachineLogger logs state transitions.
|
|
type StateMachineLogger struct {
|
|
logger *observability.Logger
|
|
}
|
|
|
|
func NewStateMachineLogger(logger *observability.Logger) *StateMachineLogger {
|
|
return &StateMachineLogger{logger: logger}
|
|
}
|
|
|
|
// LogTransition logs a state transition.
|
|
func (sml *StateMachineLogger) LogTransition(task *Task, from, to TaskState, reason string) {
|
|
sml.logger.Info("task state transition",
|
|
observability.F().
|
|
Event("state_transition").
|
|
TaskID(task.ID).
|
|
Set("from_state", string(from)).
|
|
Set("to_state", string(to)).
|
|
Reason(reason))
|
|
_ = from // used in log field above
|
|
}
|