yao/agent/robot/pool/pool.go
Max 387e254f44 Add Execution Completion Callback to Worker Pool
- Introduced an `OnCompleteCallback` type and updated the `Pool` struct to include an optional callback for execution completion.
- Implemented `SetOnComplete` method to allow setting the completion callback.
- Enhanced worker execution logic to notify the completion callback with the execution status (success, failure, or cancellation) after each execution, improving tracking and management of execution states.
2026-01-28 19:35:38 +08:00

252 lines
7.2 KiB
Go

package pool
import (
"fmt"
"sync"
"sync/atomic"
"github.com/yaoapp/yao/agent/robot/types"
"github.com/yaoapp/yao/agent/robot/utils"
)
// Default configuration values
const (
DefaultWorkerSize = 10 // default number of workers
DefaultQueueSize = 100 // default global queue size
)
// Config holds pool configuration
type Config struct {
WorkerSize int // number of workers (default: 10)
QueueSize int // global queue size (default: 100)
}
// DefaultConfig returns default pool configuration
func DefaultConfig() *Config {
return &Config{
WorkerSize: DefaultWorkerSize,
QueueSize: DefaultQueueSize,
}
}
// ExecutorFactory creates an executor based on the mode
type ExecutorFactory func(mode types.ExecutorMode) types.Executor
// OnCompleteCallback is called when an execution completes (success or failure)
// Parameters: execID, memberID, status
type OnCompleteCallback func(execID, memberID string, status types.ExecStatus)
// Pool implements types.Pool interface
// Manages a pool of workers that execute robot jobs from a priority queue
type Pool struct {
size int // number of workers
queue *PriorityQueue // priority queue for pending jobs
executor types.Executor // default executor for running jobs
executorFactory ExecutorFactory // optional: factory for mode-specific executors
onComplete OnCompleteCallback // optional: callback when execution completes
workers []*Worker // worker goroutines
running atomic.Int32 // number of currently running jobs
wg sync.WaitGroup // wait group for graceful shutdown
started bool // whether pool has been started
mu sync.RWMutex // protects started flag
}
// New creates a new pool instance with default configuration
func New() *Pool {
return NewWithConfig(nil)
}
// NewWithConfig creates a new pool instance with custom configuration
func NewWithConfig(config *Config) *Pool {
if config == nil {
config = DefaultConfig()
}
// Apply defaults for zero values
workerSize := config.WorkerSize
if workerSize <= 0 {
workerSize = DefaultWorkerSize
}
queueSize := config.QueueSize
if queueSize <= 0 {
queueSize = DefaultQueueSize
}
return &Pool{
size: workerSize,
queue: NewPriorityQueue(queueSize),
}
}
// SetExecutor sets the default executor for the pool
// Must be called before Start()
func (p *Pool) SetExecutor(executor types.Executor) {
p.executor = executor
}
// SetExecutorFactory sets the executor factory for mode-specific executors
// If set, the factory is used to create executors based on ExecutorMode
func (p *Pool) SetExecutorFactory(factory ExecutorFactory) {
p.executorFactory = factory
}
// SetOnComplete sets the callback for execution completion
// Called when an execution finishes (completed, failed, or cancelled)
func (p *Pool) SetOnComplete(callback OnCompleteCallback) {
p.onComplete = callback
}
// GetExecutor returns the appropriate executor for the given mode
// If factory is set and mode is specified, uses factory; otherwise uses default
func (p *Pool) GetExecutor(mode types.ExecutorMode) types.Executor {
// If factory is set and mode is specified, use factory
if p.executorFactory != nil && mode != "" {
return p.executorFactory(mode)
}
// Otherwise use default executor
return p.executor
}
// Start starts the worker pool
func (p *Pool) Start() error {
p.mu.Lock()
defer p.mu.Unlock()
if p.started {
return fmt.Errorf("pool already started")
}
if p.executor == nil {
return fmt.Errorf("executor not set, call SetExecutor() first")
}
// Create and start workers
p.workers = make([]*Worker, p.size)
for i := 0; i < p.size; i++ {
worker := newWorker(i+1, p, &p.wg)
p.workers[i] = worker
worker.start()
}
p.started = true
return nil
}
// Stop stops the worker pool gracefully
// Waits for all running jobs to complete
func (p *Pool) Stop() error {
p.mu.Lock()
if !p.started {
p.mu.Unlock()
return nil // already stopped or never started
}
p.started = false
p.mu.Unlock()
// Stop all workers
for _, worker := range p.workers {
worker.stop()
}
// Wait for all workers to finish
p.wg.Wait()
return nil
}
// Submit submits a robot execution to the pool
// Returns execution ID if successfully queued, error otherwise
func (p *Pool) Submit(ctx *types.Context, robot *types.Robot, trigger types.TriggerType, data interface{}) (string, error) {
return p.SubmitWithMode(ctx, robot, trigger, data, "")
}
// GenerateExecID generates a new execution ID
// Exported so Manager can pre-generate IDs for tracking
func GenerateExecID() string {
return utils.NewID()
}
// SubmitWithMode submits a robot execution with specified executor mode
// executorMode: optional, overrides robot's config if provided
// Returns execution ID if successfully queued, error otherwise
// Note: This method does not support execution control (pause/resume)
func (p *Pool) SubmitWithMode(ctx *types.Context, robot *types.Robot, trigger types.TriggerType, data interface{}, executorMode types.ExecutorMode) (string, error) {
execID := GenerateExecID()
return p.submitWithIDAndMode(ctx, robot, trigger, data, execID, executorMode, nil)
}
// SubmitWithID submits a robot execution with a pre-generated execution ID
// This is used when the caller needs to track the execution before submission
func (p *Pool) SubmitWithID(ctx *types.Context, robot *types.Robot, trigger types.TriggerType, data interface{}, execID string, control types.ExecutionControl) (string, error) {
return p.submitWithIDAndMode(ctx, robot, trigger, data, execID, "", control)
}
// submitWithIDAndMode is the internal implementation that handles both cases
func (p *Pool) submitWithIDAndMode(ctx *types.Context, robot *types.Robot, trigger types.TriggerType, data interface{}, execID string, executorMode types.ExecutorMode, control types.ExecutionControl) (string, error) {
p.mu.RLock()
if !p.started {
p.mu.RUnlock()
return "", fmt.Errorf("pool not started")
}
p.mu.RUnlock()
if robot == nil {
return "", fmt.Errorf("robot cannot be nil")
}
// Create queue item with the provided ID and control
item := &QueueItem{
Robot: robot,
Ctx: ctx,
Trigger: trigger,
Data: data,
ExecutorMode: executorMode,
ExecID: execID,
Control: control,
}
// Try to add to queue
if !p.queue.Enqueue(item) {
return "", fmt.Errorf("queue full (max %d items)", p.queue.maxSize)
}
return execID, nil
}
// Running returns number of currently running jobs
func (p *Pool) Running() int {
return int(p.running.Load())
}
// Queued returns number of queued jobs
func (p *Pool) Queued() int {
return p.queue.Size()
}
// incrementRunning increments the running counter
func (p *Pool) incrementRunning() {
p.running.Add(1)
}
// decrementRunning decrements the running counter
func (p *Pool) decrementRunning() {
p.running.Add(-1)
}
// Size returns the configured pool size
func (p *Pool) Size() int {
return p.size
}
// QueueSize returns the configured queue size
func (p *Pool) QueueSize() int {
return p.queue.maxSize
}
// IsStarted returns true if the pool has been started
func (p *Pool) IsStarted() bool {
p.mu.RLock()
defer p.mu.RUnlock()
return p.started
}