- Introduced multiple executor modes (Standard, DryRun, Sandbox) to accommodate various use cases, enhancing flexibility in execution strategies. - Updated DESIGN.md to reflect the new executor modes and their respective use cases, including detailed descriptions and configuration examples. - Revised TECHNICAL.md to outline the new executor package structure, emphasizing the modular design for future enhancements. - Enhanced the TODO.md to track the progress of executor mode implementations and related tasks. - Removed outdated executor stub files and tests, streamlining the codebase for improved maintainability. - Updated integration tests to utilize the new DryRun executor, ensuring comprehensive coverage of execution scenarios without real agent calls.
312 lines
8.2 KiB
Go
312 lines
8.2 KiB
Go
package pool_test
|
|
|
|
import (
|
|
"context"
|
|
"runtime"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/yaoapp/yao/agent/robot/executor"
|
|
"github.com/yaoapp/yao/agent/robot/pool"
|
|
"github.com/yaoapp/yao/agent/robot/types"
|
|
)
|
|
|
|
// ==================== Goroutine Leak Detection Tests ====================
|
|
|
|
// getGoroutineCount returns current number of goroutines
|
|
func getGoroutineCount() int {
|
|
return runtime.NumGoroutine()
|
|
}
|
|
|
|
// waitForGoroutineCount waits for goroutine count to stabilize
|
|
func waitForGoroutineCount(target int, timeout time.Duration) int {
|
|
deadline := time.Now().Add(timeout)
|
|
var count int
|
|
for time.Now().Before(deadline) {
|
|
count = getGoroutineCount()
|
|
if count <= target {
|
|
return count
|
|
}
|
|
runtime.Gosched()
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
return count
|
|
}
|
|
|
|
// TestPoolNoGoroutineLeak tests that pool doesn't leak goroutines after stop
|
|
func TestPoolNoGoroutineLeak(t *testing.T) {
|
|
// Get baseline goroutine count
|
|
runtime.GC()
|
|
time.Sleep(50 * time.Millisecond)
|
|
baseline := getGoroutineCount()
|
|
|
|
// Create and start pool
|
|
exec := executor.NewDryRunWithDelay(10 * time.Millisecond)
|
|
p := pool.NewWithConfig(&pool.Config{
|
|
WorkerSize: 5,
|
|
QueueSize: 100,
|
|
})
|
|
p.SetExecutor(exec)
|
|
p.Start()
|
|
|
|
// Verify workers are running
|
|
afterStart := getGoroutineCount()
|
|
assert.Greater(t, afterStart, baseline, "Should have more goroutines after start")
|
|
|
|
// Submit some jobs
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
robot := createTestRobot("robot_1", "team_1", 5, 10, 5)
|
|
for i := 0; i < 10; i++ {
|
|
p.Submit(ctx, robot, types.TriggerClock, nil)
|
|
}
|
|
|
|
// Wait for jobs to complete
|
|
time.Sleep(300 * time.Millisecond)
|
|
|
|
// Stop pool
|
|
p.Stop()
|
|
|
|
// Wait for goroutines to clean up
|
|
finalCount := waitForGoroutineCount(baseline+2, 500*time.Millisecond)
|
|
|
|
// Allow small variance (test framework goroutines)
|
|
assert.LessOrEqual(t, finalCount, baseline+2,
|
|
"Goroutine count should return to near baseline after stop (baseline=%d, final=%d)", baseline, finalCount)
|
|
}
|
|
|
|
// TestPoolMultipleStartStop tests no leak with multiple start/stop cycles
|
|
func TestPoolMultipleStartStop(t *testing.T) {
|
|
runtime.GC()
|
|
time.Sleep(50 * time.Millisecond)
|
|
baseline := getGoroutineCount()
|
|
|
|
exec := executor.NewDryRunWithDelay(5 * time.Millisecond)
|
|
|
|
for i := 0; i < 5; i++ {
|
|
p := pool.NewWithConfig(&pool.Config{
|
|
WorkerSize: 3,
|
|
QueueSize: 50,
|
|
})
|
|
p.SetExecutor(exec)
|
|
p.Start()
|
|
|
|
// Submit a few jobs
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
robot := createTestRobot("robot_1", "team_1", 5, 10, 5)
|
|
for j := 0; j < 5; j++ {
|
|
p.Submit(ctx, robot, types.TriggerClock, nil)
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
p.Stop()
|
|
}
|
|
|
|
// Wait for cleanup
|
|
finalCount := waitForGoroutineCount(baseline+2, 500*time.Millisecond)
|
|
|
|
assert.LessOrEqual(t, finalCount, baseline+2,
|
|
"Goroutine count should return to near baseline after multiple cycles (baseline=%d, final=%d)", baseline, finalCount)
|
|
}
|
|
|
|
// TestPoolStopWithoutJobs tests no leak when stopping pool with no jobs submitted
|
|
func TestPoolStopWithoutJobs(t *testing.T) {
|
|
runtime.GC()
|
|
time.Sleep(50 * time.Millisecond)
|
|
baseline := getGoroutineCount()
|
|
|
|
exec := executor.New()
|
|
p := pool.NewWithConfig(&pool.Config{
|
|
WorkerSize: 10,
|
|
QueueSize: 100,
|
|
})
|
|
p.SetExecutor(exec)
|
|
p.Start()
|
|
|
|
// Immediately stop without submitting any jobs
|
|
p.Stop()
|
|
|
|
finalCount := waitForGoroutineCount(baseline+2, 500*time.Millisecond)
|
|
|
|
assert.LessOrEqual(t, finalCount, baseline+2,
|
|
"Goroutine count should return to near baseline (baseline=%d, final=%d)", baseline, finalCount)
|
|
}
|
|
|
|
// TestPoolStopWithPendingJobs tests no leak when stopping with jobs in queue
|
|
func TestPoolStopWithPendingJobs(t *testing.T) {
|
|
runtime.GC()
|
|
time.Sleep(50 * time.Millisecond)
|
|
baseline := getGoroutineCount()
|
|
|
|
// Use slow executor so jobs stay in queue
|
|
exec := executor.NewDryRunWithDelay(500 * time.Millisecond)
|
|
p := pool.NewWithConfig(&pool.Config{
|
|
WorkerSize: 1, // only 1 worker
|
|
QueueSize: 100,
|
|
})
|
|
p.SetExecutor(exec)
|
|
p.Start()
|
|
|
|
// Submit many jobs (most will be queued)
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
robot := createTestRobot("robot_1", "team_1", 5, 50, 5)
|
|
for i := 0; i < 20; i++ {
|
|
p.Submit(ctx, robot, types.TriggerClock, nil)
|
|
}
|
|
|
|
// Stop immediately (some jobs still in queue)
|
|
time.Sleep(50 * time.Millisecond)
|
|
p.Stop()
|
|
|
|
finalCount := waitForGoroutineCount(baseline+2, 500*time.Millisecond)
|
|
|
|
assert.LessOrEqual(t, finalCount, baseline+2,
|
|
"Goroutine count should return to near baseline even with pending jobs (baseline=%d, final=%d)", baseline, finalCount)
|
|
}
|
|
|
|
// TestPoolConcurrentStartStop tests no leak with concurrent start/stop
|
|
func TestPoolConcurrentStartStop(t *testing.T) {
|
|
runtime.GC()
|
|
time.Sleep(50 * time.Millisecond)
|
|
baseline := getGoroutineCount()
|
|
|
|
exec := executor.NewDryRunWithDelay(10 * time.Millisecond)
|
|
p := pool.NewWithConfig(&pool.Config{
|
|
WorkerSize: 5,
|
|
QueueSize: 100,
|
|
})
|
|
p.SetExecutor(exec)
|
|
|
|
// Start pool
|
|
p.Start()
|
|
|
|
// Concurrent operations
|
|
done := make(chan bool, 3)
|
|
|
|
// Goroutine 1: Submit jobs
|
|
go func() {
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
robot := createTestRobot("robot_1", "team_1", 5, 10, 5)
|
|
for i := 0; i < 20; i++ {
|
|
p.Submit(ctx, robot, types.TriggerClock, nil)
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
done <- true
|
|
}()
|
|
|
|
// Goroutine 2: Check status
|
|
go func() {
|
|
for i := 0; i < 20; i++ {
|
|
_ = p.Running()
|
|
_ = p.Queued()
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
done <- true
|
|
}()
|
|
|
|
// Wait for operations
|
|
<-done
|
|
<-done
|
|
|
|
// Stop pool
|
|
p.Stop()
|
|
|
|
finalCount := waitForGoroutineCount(baseline+2, 500*time.Millisecond)
|
|
|
|
assert.LessOrEqual(t, finalCount, baseline+2,
|
|
"Goroutine count should return to near baseline after concurrent ops (baseline=%d, final=%d)", baseline, finalCount)
|
|
}
|
|
|
|
// TestWorkerGoroutinesCleanup tests that worker goroutines are properly cleaned up
|
|
func TestWorkerGoroutinesCleanup(t *testing.T) {
|
|
runtime.GC()
|
|
time.Sleep(50 * time.Millisecond)
|
|
baseline := getGoroutineCount()
|
|
|
|
exec := executor.NewDryRunWithDelay(10 * time.Millisecond)
|
|
|
|
// Create pool with many workers
|
|
p := pool.NewWithConfig(&pool.Config{
|
|
WorkerSize: 20,
|
|
QueueSize: 100,
|
|
})
|
|
p.SetExecutor(exec)
|
|
p.Start()
|
|
|
|
// Should have baseline + 20 workers
|
|
afterStart := getGoroutineCount()
|
|
assert.GreaterOrEqual(t, afterStart, baseline+20, "Should have at least 20 worker goroutines")
|
|
|
|
// Stop pool
|
|
p.Stop()
|
|
|
|
// All worker goroutines should be cleaned up
|
|
finalCount := waitForGoroutineCount(baseline+2, 500*time.Millisecond)
|
|
|
|
assert.LessOrEqual(t, finalCount, baseline+2,
|
|
"All worker goroutines should be cleaned up (baseline=%d, final=%d)", baseline, finalCount)
|
|
}
|
|
|
|
// TestPoolLongRunningJobsNoLeak tests no leak with long-running jobs
|
|
func TestPoolLongRunningJobsNoLeak(t *testing.T) {
|
|
runtime.GC()
|
|
time.Sleep(50 * time.Millisecond)
|
|
baseline := getGoroutineCount()
|
|
|
|
exec := executor.NewDryRunWithDelay(200 * time.Millisecond)
|
|
p := pool.NewWithConfig(&pool.Config{
|
|
WorkerSize: 3,
|
|
QueueSize: 100,
|
|
})
|
|
p.SetExecutor(exec)
|
|
p.Start()
|
|
|
|
// Submit jobs
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
robot := createTestRobot("robot_1", "team_1", 5, 10, 5)
|
|
for i := 0; i < 5; i++ {
|
|
p.Submit(ctx, robot, types.TriggerClock, nil)
|
|
}
|
|
|
|
// Wait for some jobs to complete
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
// Stop pool
|
|
p.Stop()
|
|
|
|
finalCount := waitForGoroutineCount(baseline+2, 500*time.Millisecond)
|
|
|
|
assert.LessOrEqual(t, finalCount, baseline+2,
|
|
"No goroutine leak after long-running jobs (baseline=%d, final=%d)", baseline, finalCount)
|
|
}
|
|
|
|
// TestQueueNoGoroutineLeak tests that queue operations don't leak goroutines
|
|
func TestQueueNoGoroutineLeak(t *testing.T) {
|
|
runtime.GC()
|
|
time.Sleep(50 * time.Millisecond)
|
|
baseline := getGoroutineCount()
|
|
|
|
// Create queue and perform many operations
|
|
pq := pool.NewPriorityQueue(1000)
|
|
|
|
// Enqueue many items
|
|
for i := 0; i < 500; i++ {
|
|
robot := createTestRobot("robot_"+string(rune('A'+i%26)), "team_1", 5, 100, 5)
|
|
pq.Enqueue(&pool.QueueItem{
|
|
Robot: robot,
|
|
Trigger: types.TriggerClock,
|
|
})
|
|
}
|
|
|
|
// Dequeue all items
|
|
for pq.Size() > 0 {
|
|
pq.Dequeue()
|
|
}
|
|
|
|
runtime.GC()
|
|
time.Sleep(50 * time.Millisecond)
|
|
finalCount := getGoroutineCount()
|
|
|
|
assert.LessOrEqual(t, finalCount, baseline+2,
|
|
"Queue operations should not leak goroutines (baseline=%d, final=%d)", baseline, finalCount)
|
|
}
|