Files
silo-server/internal/playback/command_tracker.go
T

168 lines
3.1 KiB
Go

package playback
import (
"sync"
"time"
)
// CommandState describes the lifecycle state of a tracked realtime command.
type CommandState string
const (
CommandStatePending CommandState = "pending"
CommandStateAccepted CommandState = "accepted"
CommandStateCompleted CommandState = "completed"
)
type commandTrackerEntry struct {
state CommandState
timer *time.Timer
fallback func()
}
// CommandTracker tracks in-flight realtime commands by command_id.
type CommandTracker struct {
mu sync.Mutex
closed bool
commands map[string]*commandTrackerEntry
}
// NewCommandTracker creates an empty command tracker.
func NewCommandTracker() *CommandTracker {
return &CommandTracker{
commands: make(map[string]*commandTrackerEntry),
}
}
// Track records a new command as pending and arms its fallback deadline.
func (t *CommandTracker) Track(commandID string, deadline time.Duration, fallback func()) {
if t == nil || commandID == "" {
return
}
t.mu.Lock()
defer t.mu.Unlock()
if t.closed {
return
}
if _, exists := t.commands[commandID]; exists {
return
}
entry := &commandTrackerEntry{
state: CommandStatePending,
fallback: fallback,
}
t.commands[commandID] = entry
entry.timer = time.AfterFunc(deadline, func() {
t.fireDeadline(commandID)
})
}
// Ack marks a tracked command as accepted.
func (t *CommandTracker) Ack(commandID string) {
t.setState(commandID, CommandStateAccepted)
}
// Result marks a tracked command as completed.
func (t *CommandTracker) Result(commandID string) {
t.mu.Lock()
if t.closed {
t.mu.Unlock()
return
}
entry, ok := t.commands[commandID]
if !ok {
t.mu.Unlock()
return
}
entry.state = CommandStateCompleted
timer := entry.timer
delete(t.commands, commandID)
t.mu.Unlock()
if timer != nil {
timer.Stop()
}
}
// Status reports the current tracked state for a command.
func (t *CommandTracker) Status(commandID string) (CommandState, bool) {
if t == nil || commandID == "" {
return "", false
}
t.mu.Lock()
defer t.mu.Unlock()
entry, ok := t.commands[commandID]
if !ok {
return "", false
}
return entry.state, true
}
// Close stops all deadline timers and prevents future tracking.
func (t *CommandTracker) Close() {
if t == nil {
return
}
t.mu.Lock()
if t.closed {
t.mu.Unlock()
return
}
t.closed = true
commands := t.commands
t.commands = make(map[string]*commandTrackerEntry)
t.mu.Unlock()
for _, entry := range commands {
if entry.timer != nil {
entry.timer.Stop()
}
}
}
func (t *CommandTracker) setState(commandID string, state CommandState) {
t.mu.Lock()
if t.closed {
t.mu.Unlock()
return
}
entry, ok := t.commands[commandID]
if !ok {
t.mu.Unlock()
return
}
if entry.state != CommandStateCompleted {
entry.state = state
}
t.mu.Unlock()
}
func (t *CommandTracker) fireDeadline(commandID string) {
t.mu.Lock()
if t.closed {
t.mu.Unlock()
return
}
entry, ok := t.commands[commandID]
if !ok {
t.mu.Unlock()
return
}
if entry.state == CommandStateCompleted {
delete(t.commands, commandID)
t.mu.Unlock()
return
}
fallback := entry.fallback
delete(t.commands, commandID)
t.mu.Unlock()
if fallback != nil {
fallback()
}
}