| package scheduler |
|
|
| import ( |
| "crypto/rand" |
| "encoding/hex" |
| "fmt" |
| "sync" |
| "time" |
| ) |
|
|
| |
| type TaskState string |
|
|
| const ( |
| StateQueued TaskState = "queued" |
| StateAssigned TaskState = "assigned" |
| StateRunning TaskState = "running" |
| StateDone TaskState = "done" |
| StateFailed TaskState = "failed" |
| StateCancelled TaskState = "cancelled" |
| StateRejected TaskState = "rejected" |
| ) |
|
|
| |
| func (s TaskState) IsTerminal() bool { |
| switch s { |
| case StateDone, StateFailed, StateCancelled, StateRejected: |
| return true |
| } |
| return false |
| } |
|
|
| |
| type Task struct { |
| mu sync.RWMutex |
|
|
| ID string `json:"taskId"` |
| AgentID string `json:"agentId"` |
| Action string `json:"action"` |
| TabID string `json:"tabId,omitempty"` |
| Selector string `json:"selector,omitempty"` |
| Ref string `json:"ref,omitempty"` |
| Params map[string]any `json:"params,omitempty"` |
| Priority int `json:"priority"` |
| State TaskState `json:"state"` |
| Deadline time.Time `json:"deadline,omitempty"` |
|
|
| CreatedAt time.Time `json:"createdAt"` |
| StartedAt time.Time `json:"startedAt,omitempty"` |
| CompletedAt time.Time `json:"completedAt,omitempty"` |
| LatencyMs int64 `json:"latencyMs,omitempty"` |
|
|
| Result any `json:"result,omitempty"` |
| Error string `json:"error,omitempty"` |
|
|
| |
| CallbackURL string `json:"callbackUrl,omitempty"` |
|
|
| |
| Position int `json:"position,omitempty"` |
| } |
|
|
| |
| |
| func (t *Task) SetState(next TaskState) error { |
| t.mu.Lock() |
| defer t.mu.Unlock() |
|
|
| if t.State.IsTerminal() { |
| return fmt.Errorf("cannot transition from terminal state %q to %q", t.State, next) |
| } |
|
|
| switch { |
| case t.State == StateQueued && (next == StateAssigned || next == StateCancelled || next == StateFailed || next == StateRejected): |
| case t.State == StateAssigned && (next == StateRunning || next == StateCancelled): |
| case t.State == StateRunning && (next == StateDone || next == StateFailed || next == StateCancelled): |
| default: |
| return fmt.Errorf("invalid state transition: %q → %q", t.State, next) |
| } |
|
|
| now := time.Now() |
| t.State = next |
|
|
| switch next { |
| case StateAssigned: |
| t.StartedAt = now |
| case StateRunning: |
| if t.StartedAt.IsZero() { |
| t.StartedAt = now |
| } |
| case StateDone, StateFailed, StateCancelled: |
| t.CompletedAt = now |
| if !t.StartedAt.IsZero() { |
| t.LatencyMs = now.Sub(t.StartedAt).Milliseconds() |
| } |
| } |
| return nil |
| } |
|
|
| |
| func (t *Task) GetState() TaskState { |
| t.mu.RLock() |
| defer t.mu.RUnlock() |
| return t.State |
| } |
|
|
| |
| func (t *Task) Snapshot() *Task { |
| t.mu.RLock() |
| defer t.mu.RUnlock() |
| return &Task{ |
| ID: t.ID, |
| AgentID: t.AgentID, |
| Action: t.Action, |
| TabID: t.TabID, |
| Selector: t.Selector, |
| Ref: t.Ref, |
| Params: t.Params, |
| Priority: t.Priority, |
| State: t.State, |
| Deadline: t.Deadline, |
| CreatedAt: t.CreatedAt, |
| StartedAt: t.StartedAt, |
| CompletedAt: t.CompletedAt, |
| LatencyMs: t.LatencyMs, |
| Result: t.Result, |
| Error: t.Error, |
| CallbackURL: t.CallbackURL, |
| Position: t.Position, |
| } |
| } |
|
|
| |
| type SubmitRequest struct { |
| AgentID string `json:"agentId"` |
| Action string `json:"action"` |
| TabID string `json:"tabId,omitempty"` |
| Selector string `json:"selector,omitempty"` |
| Ref string `json:"ref,omitempty"` |
| Params map[string]any `json:"params,omitempty"` |
| Priority int `json:"priority,omitempty"` |
| Deadline string `json:"deadline,omitempty"` |
| CallbackURL string `json:"callbackUrl,omitempty"` |
| } |
|
|
| |
| func (r *SubmitRequest) Validate() error { |
| if r.AgentID == "" { |
| return fmt.Errorf("missing required field 'agentId'") |
| } |
| if r.Action == "" { |
| return fmt.Errorf("missing required field 'action'") |
| } |
| return nil |
| } |
|
|
| |
| func generateTaskID() string { |
| b := make([]byte, 4) |
| if _, err := rand.Read(b); err != nil { |
| |
| return fmt.Sprintf("tsk_%08x", time.Now().UnixNano()&0xFFFFFFFF) |
| } |
| return "tsk_" + hex.EncodeToString(b) |
| } |
|
|