yao/agent/robot/trigger/control.go
Max 12386ddb8b Refactor Cache Auto-Refresh Tests for Stability and Clarity
- Updated the auto-refresh tests in cache_test.go to utilize fresh cache instances for each test, ensuring isolation and reliability.
- Improved assertions to verify cache stability after stopping auto-refresh and confirmed that multiple start calls replace previous ones without leaking goroutines.
- Added tests to check for safe behavior during rapid start/stop cycles and ensured that stopping without starting does not cause panics.
- Enhanced overall test coverage and clarity, contributing to better maintainability and understanding of the cache's auto-refresh functionality.
2026-01-15 11:00:28 +08:00

248 lines
5.5 KiB
Go

package trigger
import (
"context"
"fmt"
"sync"
"time"
"github.com/yaoapp/yao/agent/robot/types"
)
// ExecutionController manages execution lifecycle (pause/resume/stop)
type ExecutionController struct {
executions map[string]*ControlledExecution
mu sync.RWMutex
}
// ControlledExecution represents an execution that can be controlled
type ControlledExecution struct {
ID string
MemberID string
TeamID string
Status types.ExecStatus
Phase types.Phase
StartTime time.Time
PausedAt *time.Time
// Control channels
ctx context.Context
cancel context.CancelFunc
paused bool
pauseMu sync.Mutex
resumeCh chan struct{} // signaled (closed) when resumed
}
// NewExecutionController creates a new execution controller
func NewExecutionController() *ExecutionController {
return &ExecutionController{
executions: make(map[string]*ControlledExecution),
}
}
// Track starts tracking an execution
func (c *ExecutionController) Track(execID, memberID, teamID string) *ControlledExecution {
c.mu.Lock()
defer c.mu.Unlock()
ctx, cancel := context.WithCancel(context.Background())
exec := &ControlledExecution{
ID: execID,
MemberID: memberID,
TeamID: teamID,
Status: types.ExecRunning,
Phase: types.PhaseInspiration,
StartTime: time.Now(),
ctx: ctx,
cancel: cancel,
paused: false,
resumeCh: nil, // nil when not paused, created on pause
}
c.executions[execID] = exec
return exec
}
// Untrack stops tracking an execution
func (c *ExecutionController) Untrack(execID string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.executions, execID)
}
// Get returns a tracked execution
func (c *ExecutionController) Get(execID string) *ControlledExecution {
c.mu.RLock()
defer c.mu.RUnlock()
return c.executions[execID]
}
// List returns all tracked executions
func (c *ExecutionController) List() []*ControlledExecution {
c.mu.RLock()
defer c.mu.RUnlock()
result := make([]*ControlledExecution, 0, len(c.executions))
for _, exec := range c.executions {
result = append(result, exec)
}
return result
}
// ListByMember returns all executions for a specific member
func (c *ExecutionController) ListByMember(memberID string) []*ControlledExecution {
c.mu.RLock()
defer c.mu.RUnlock()
var result []*ControlledExecution
for _, exec := range c.executions {
if exec.MemberID == memberID {
result = append(result, exec)
}
}
return result
}
// Pause pauses an execution
func (c *ExecutionController) Pause(execID string) error {
exec := c.Get(execID)
if exec == nil {
return fmt.Errorf("execution not found: %s", execID)
}
exec.pauseMu.Lock()
defer exec.pauseMu.Unlock()
if exec.paused {
return fmt.Errorf("execution already paused: %s", execID)
}
exec.paused = true
now := time.Now()
exec.PausedAt = &now
// Create a new resume channel that will be closed on resume
exec.resumeCh = make(chan struct{})
return nil
}
// Resume resumes a paused execution
func (c *ExecutionController) Resume(execID string) error {
exec := c.Get(execID)
if exec == nil {
return fmt.Errorf("execution not found: %s", execID)
}
exec.pauseMu.Lock()
defer exec.pauseMu.Unlock()
if !exec.paused {
return fmt.Errorf("execution not paused: %s", execID)
}
exec.paused = false
exec.PausedAt = nil
// Close the resume channel to signal resume to waiting goroutines
if exec.resumeCh != nil {
close(exec.resumeCh)
exec.resumeCh = nil
}
return nil
}
// Stop stops an execution
func (c *ExecutionController) Stop(execID string) error {
c.mu.Lock()
defer c.mu.Unlock()
exec, ok := c.executions[execID]
if !ok {
return fmt.Errorf("execution not found: %s", execID)
}
// Cancel the context to signal stop
if exec.cancel != nil {
exec.cancel()
}
exec.Status = types.ExecCancelled
// Remove from tracking
delete(c.executions, execID)
return nil
}
// ==================== ControlledExecution methods ====================
// IsPaused returns true if the execution is paused
func (e *ControlledExecution) IsPaused() bool {
e.pauseMu.Lock()
defer e.pauseMu.Unlock()
return e.paused
}
// IsCancelled returns true if the execution is cancelled
func (e *ControlledExecution) IsCancelled() bool {
select {
case <-e.ctx.Done():
return true
default:
return false
}
}
// Context returns the execution's context
func (e *ControlledExecution) Context() context.Context {
return e.ctx
}
// WaitIfPaused blocks until the execution is resumed or cancelled
// Returns error if cancelled
func (e *ControlledExecution) WaitIfPaused() error {
e.pauseMu.Lock()
paused := e.paused
resumeCh := e.resumeCh
e.pauseMu.Unlock()
if !paused {
return nil
}
// Safety check: if paused but resumeCh is nil (shouldn't happen in normal flow),
// treat as not paused to avoid blocking forever on nil channel
if resumeCh == nil {
return nil
}
// resumeCh is created when paused and closed when resumed
// Wait for resume signal or cancellation
select {
case <-e.ctx.Done():
return types.ErrExecutionCancelled
case <-resumeCh:
// Resume signal received, execution can continue
return nil
}
}
// CheckCancelled checks if the execution is cancelled and returns error if so
func (e *ControlledExecution) CheckCancelled() error {
if e.IsCancelled() {
return types.ErrExecutionCancelled
}
return nil
}
// UpdatePhase updates the current phase
func (e *ControlledExecution) UpdatePhase(phase types.Phase) {
e.Phase = phase
}
// UpdateStatus updates the execution status
func (e *ControlledExecution) UpdateStatus(status types.ExecStatus) {
e.Status = status
}