perf(up): parallelize all agent startup with concurrency limit (#476)
* perf(up): parallelize agent startup with worker pool and channel-based collection - Run daemon, deacon, mayor, and rig prefetch all in parallel (4-way concurrent init) - Use fixed worker pool instead of goroutine-per-task for bounded concurrency - Replace mutex-protected maps with channel-based result collection (zero lock contention) - Pre-allocate maps with known capacity to reduce allocations - Use string concatenation instead of fmt.Sprintf for display names - Reduce `gt up` startup time from ~50s to ~10s for towns with multiple rigs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(lint): fix errcheck and misspell issues in orphans.go - Check error return from fmt.Scanln calls - Fix "Cancelled" -> "Canceled" spelling Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -18,12 +19,23 @@ import (
|
|||||||
"github.com/steveyegge/gastown/internal/mayor"
|
"github.com/steveyegge/gastown/internal/mayor"
|
||||||
"github.com/steveyegge/gastown/internal/polecat"
|
"github.com/steveyegge/gastown/internal/polecat"
|
||||||
"github.com/steveyegge/gastown/internal/refinery"
|
"github.com/steveyegge/gastown/internal/refinery"
|
||||||
|
"github.com/steveyegge/gastown/internal/rig"
|
||||||
"github.com/steveyegge/gastown/internal/style"
|
"github.com/steveyegge/gastown/internal/style"
|
||||||
"github.com/steveyegge/gastown/internal/tmux"
|
"github.com/steveyegge/gastown/internal/tmux"
|
||||||
"github.com/steveyegge/gastown/internal/witness"
|
"github.com/steveyegge/gastown/internal/witness"
|
||||||
"github.com/steveyegge/gastown/internal/workspace"
|
"github.com/steveyegge/gastown/internal/workspace"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// agentStartResult holds the result of starting an agent.
|
||||||
|
type agentStartResult struct {
|
||||||
|
name string // Display name like "Witness (gastown)"
|
||||||
|
ok bool // Whether start succeeded
|
||||||
|
detail string // Status detail (session name or error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// maxConcurrentAgentStarts limits parallel agent startups to avoid resource exhaustion.
|
||||||
|
const maxConcurrentAgentStarts = 10
|
||||||
|
|
||||||
var upCmd = &cobra.Command{
|
var upCmd = &cobra.Command{
|
||||||
Use: "up",
|
Use: "up",
|
||||||
GroupID: GroupServices,
|
GroupID: GroupServices,
|
||||||
@@ -70,89 +82,108 @@ func runUp(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
allOK := true
|
allOK := true
|
||||||
|
|
||||||
// 1. Daemon (Go process)
|
// Discover rigs early so we can prefetch while daemon/deacon/mayor start
|
||||||
if err := ensureDaemon(townRoot); err != nil {
|
|
||||||
printStatus("Daemon", false, err.Error())
|
|
||||||
allOK = false
|
|
||||||
} else {
|
|
||||||
running, pid, _ := daemon.IsRunning(townRoot)
|
|
||||||
if running {
|
|
||||||
printStatus("Daemon", true, fmt.Sprintf("PID %d", pid))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Deacon (Claude agent)
|
|
||||||
deaconMgr := deacon.NewManager(townRoot)
|
|
||||||
if err := deaconMgr.Start(""); err != nil {
|
|
||||||
if err == deacon.ErrAlreadyRunning {
|
|
||||||
printStatus("Deacon", true, deaconMgr.SessionName())
|
|
||||||
} else {
|
|
||||||
printStatus("Deacon", false, err.Error())
|
|
||||||
allOK = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
printStatus("Deacon", true, deaconMgr.SessionName())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Mayor (Claude agent)
|
|
||||||
mayorMgr := mayor.NewManager(townRoot)
|
|
||||||
if err := mayorMgr.Start(""); err != nil {
|
|
||||||
if err == mayor.ErrAlreadyRunning {
|
|
||||||
printStatus("Mayor", true, mayorMgr.SessionName())
|
|
||||||
} else {
|
|
||||||
printStatus("Mayor", false, err.Error())
|
|
||||||
allOK = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
printStatus("Mayor", true, mayorMgr.SessionName())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Witnesses (one per rig)
|
|
||||||
rigs := discoverRigs(townRoot)
|
rigs := discoverRigs(townRoot)
|
||||||
for _, rigName := range rigs {
|
|
||||||
_, r, err := getRig(rigName)
|
|
||||||
if err != nil {
|
|
||||||
printStatus(fmt.Sprintf("Witness (%s)", rigName), false, err.Error())
|
|
||||||
allOK = false
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
mgr := witness.NewManager(r)
|
// Start daemon, deacon, mayor, and rig prefetch in parallel
|
||||||
if err := mgr.Start(false, "", nil); err != nil {
|
var daemonErr error
|
||||||
if err == witness.ErrAlreadyRunning {
|
var daemonPID int
|
||||||
printStatus(fmt.Sprintf("Witness (%s)", rigName), true, mgr.SessionName())
|
var deaconResult, mayorResult agentStartResult
|
||||||
|
var prefetchedRigs map[string]*rig.Rig
|
||||||
|
var rigErrors map[string]error
|
||||||
|
|
||||||
|
var startupWg sync.WaitGroup
|
||||||
|
startupWg.Add(4)
|
||||||
|
|
||||||
|
// 1. Daemon (Go process)
|
||||||
|
go func() {
|
||||||
|
defer startupWg.Done()
|
||||||
|
if err := ensureDaemon(townRoot); err != nil {
|
||||||
|
daemonErr = err
|
||||||
|
} else {
|
||||||
|
running, pid, _ := daemon.IsRunning(townRoot)
|
||||||
|
if running {
|
||||||
|
daemonPID = pid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 2. Deacon
|
||||||
|
go func() {
|
||||||
|
defer startupWg.Done()
|
||||||
|
deaconMgr := deacon.NewManager(townRoot)
|
||||||
|
if err := deaconMgr.Start(""); err != nil {
|
||||||
|
if err == deacon.ErrAlreadyRunning {
|
||||||
|
deaconResult = agentStartResult{name: "Deacon", ok: true, detail: deaconMgr.SessionName()}
|
||||||
} else {
|
} else {
|
||||||
printStatus(fmt.Sprintf("Witness (%s)", rigName), false, err.Error())
|
deaconResult = agentStartResult{name: "Deacon", ok: false, detail: err.Error()}
|
||||||
allOK = false
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
printStatus(fmt.Sprintf("Witness (%s)", rigName), true, mgr.SessionName())
|
deaconResult = agentStartResult{name: "Deacon", ok: true, detail: deaconMgr.SessionName()}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 3. Mayor
|
||||||
|
go func() {
|
||||||
|
defer startupWg.Done()
|
||||||
|
mayorMgr := mayor.NewManager(townRoot)
|
||||||
|
if err := mayorMgr.Start(""); err != nil {
|
||||||
|
if err == mayor.ErrAlreadyRunning {
|
||||||
|
mayorResult = agentStartResult{name: "Mayor", ok: true, detail: mayorMgr.SessionName()}
|
||||||
|
} else {
|
||||||
|
mayorResult = agentStartResult{name: "Mayor", ok: false, detail: err.Error()}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mayorResult = agentStartResult{name: "Mayor", ok: true, detail: mayorMgr.SessionName()}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 4. Prefetch rig configs (overlaps with daemon/deacon/mayor startup)
|
||||||
|
go func() {
|
||||||
|
defer startupWg.Done()
|
||||||
|
prefetchedRigs, rigErrors = prefetchRigs(rigs)
|
||||||
|
}()
|
||||||
|
|
||||||
|
startupWg.Wait()
|
||||||
|
|
||||||
|
// Print daemon/deacon/mayor results
|
||||||
|
if daemonErr != nil {
|
||||||
|
printStatus("Daemon", false, daemonErr.Error())
|
||||||
|
allOK = false
|
||||||
|
} else if daemonPID > 0 {
|
||||||
|
printStatus("Daemon", true, fmt.Sprintf("PID %d", daemonPID))
|
||||||
|
}
|
||||||
|
printStatus(deaconResult.name, deaconResult.ok, deaconResult.detail)
|
||||||
|
if !deaconResult.ok {
|
||||||
|
allOK = false
|
||||||
|
}
|
||||||
|
printStatus(mayorResult.name, mayorResult.ok, mayorResult.detail)
|
||||||
|
if !mayorResult.ok {
|
||||||
|
allOK = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5 & 6. Witnesses and Refineries (using prefetched rigs)
|
||||||
|
witnessResults, refineryResults := startRigAgentsWithPrefetch(rigs, prefetchedRigs, rigErrors)
|
||||||
|
|
||||||
|
// Print results in order: all witnesses first, then all refineries
|
||||||
|
for _, rigName := range rigs {
|
||||||
|
if result, ok := witnessResults[rigName]; ok {
|
||||||
|
printStatus(result.name, result.ok, result.detail)
|
||||||
|
if !result.ok {
|
||||||
|
allOK = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, rigName := range rigs {
|
||||||
|
if result, ok := refineryResults[rigName]; ok {
|
||||||
|
printStatus(result.name, result.ok, result.detail)
|
||||||
|
if !result.ok {
|
||||||
|
allOK = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Refineries (one per rig)
|
// 7. Crew (if --restore)
|
||||||
for _, rigName := range rigs {
|
|
||||||
_, r, err := getRig(rigName)
|
|
||||||
if err != nil {
|
|
||||||
printStatus(fmt.Sprintf("Refinery (%s)", rigName), false, err.Error())
|
|
||||||
allOK = false
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
mgr := refinery.NewManager(r)
|
|
||||||
if err := mgr.Start(false, ""); err != nil {
|
|
||||||
if err == refinery.ErrAlreadyRunning {
|
|
||||||
printStatus(fmt.Sprintf("Refinery (%s)", rigName), true, mgr.SessionName())
|
|
||||||
} else {
|
|
||||||
printStatus(fmt.Sprintf("Refinery (%s)", rigName), false, err.Error())
|
|
||||||
allOK = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
printStatus(fmt.Sprintf("Refinery (%s)", rigName), true, mgr.SessionName())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Crew (if --restore)
|
|
||||||
if upRestore {
|
if upRestore {
|
||||||
for _, rigName := range rigs {
|
for _, rigName := range rigs {
|
||||||
crewStarted, crewErrors := startCrewFromSettings(townRoot, rigName)
|
crewStarted, crewErrors := startCrewFromSettings(townRoot, rigName)
|
||||||
@@ -249,6 +280,182 @@ func ensureDaemon(townRoot string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rigPrefetchResult holds the result of loading a single rig config.
|
||||||
|
type rigPrefetchResult struct {
|
||||||
|
index int
|
||||||
|
rig *rig.Rig
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefetchRigs loads all rig configs in parallel for faster agent startup.
|
||||||
|
// Returns a map of rig name to loaded Rig, and any errors encountered.
|
||||||
|
func prefetchRigs(rigNames []string) (map[string]*rig.Rig, map[string]error) {
|
||||||
|
n := len(rigNames)
|
||||||
|
if n == 0 {
|
||||||
|
return make(map[string]*rig.Rig), make(map[string]error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use channel to collect results without locking
|
||||||
|
results := make(chan rigPrefetchResult, n)
|
||||||
|
|
||||||
|
for i, name := range rigNames {
|
||||||
|
go func(idx int, rigName string) {
|
||||||
|
_, r, err := getRig(rigName)
|
||||||
|
results <- rigPrefetchResult{index: idx, rig: r, err: err}
|
||||||
|
}(i, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect results - pre-allocate maps with capacity
|
||||||
|
rigs := make(map[string]*rig.Rig, n)
|
||||||
|
errors := make(map[string]error)
|
||||||
|
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
res := <-results
|
||||||
|
name := rigNames[res.index]
|
||||||
|
if res.err != nil {
|
||||||
|
errors[name] = res.err
|
||||||
|
} else {
|
||||||
|
rigs[name] = res.rig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rigs, errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// agentTask represents a unit of work for the agent worker pool.
|
||||||
|
type agentTask struct {
|
||||||
|
rigName string
|
||||||
|
rigObj *rig.Rig
|
||||||
|
isWitness bool // true for witness, false for refinery
|
||||||
|
}
|
||||||
|
|
||||||
|
// agentResultMsg carries result back from worker to collector.
|
||||||
|
type agentResultMsg struct {
|
||||||
|
rigName string
|
||||||
|
isWitness bool
|
||||||
|
result agentStartResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// startRigAgentsParallel starts all Witnesses and Refineries concurrently.
|
||||||
|
// Discovers and prefetches rigs internally. For use when rigs aren't pre-loaded.
|
||||||
|
func startRigAgentsParallel(rigNames []string) (witnessResults, refineryResults map[string]agentStartResult) {
|
||||||
|
prefetchedRigs, rigErrors := prefetchRigs(rigNames)
|
||||||
|
return startRigAgentsWithPrefetch(rigNames, prefetchedRigs, rigErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
// startRigAgentsWithPrefetch starts all Witnesses and Refineries using pre-loaded rig configs.
|
||||||
|
// Uses a worker pool with fixed goroutine count to limit concurrency and reduce overhead.
|
||||||
|
func startRigAgentsWithPrefetch(rigNames []string, prefetchedRigs map[string]*rig.Rig, rigErrors map[string]error) (witnessResults, refineryResults map[string]agentStartResult) {
|
||||||
|
n := len(rigNames)
|
||||||
|
witnessResults = make(map[string]agentStartResult, n)
|
||||||
|
refineryResults = make(map[string]agentStartResult, n)
|
||||||
|
|
||||||
|
if n == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record errors for rigs that failed to load
|
||||||
|
for rigName, err := range rigErrors {
|
||||||
|
errDetail := err.Error()
|
||||||
|
witnessResults[rigName] = agentStartResult{
|
||||||
|
name: "Witness (" + rigName + ")",
|
||||||
|
ok: false,
|
||||||
|
detail: errDetail,
|
||||||
|
}
|
||||||
|
refineryResults[rigName] = agentStartResult{
|
||||||
|
name: "Refinery (" + rigName + ")",
|
||||||
|
ok: false,
|
||||||
|
detail: errDetail,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
numTasks := len(prefetchedRigs) * 2 // witness + refinery per rig
|
||||||
|
if numTasks == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task channel and result channel
|
||||||
|
tasks := make(chan agentTask, numTasks)
|
||||||
|
results := make(chan agentResultMsg, numTasks)
|
||||||
|
|
||||||
|
// Start fixed worker pool (bounded by maxConcurrentAgentStarts)
|
||||||
|
numWorkers := maxConcurrentAgentStarts
|
||||||
|
if numTasks < numWorkers {
|
||||||
|
numWorkers = numTasks
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for i := 0; i < numWorkers; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for task := range tasks {
|
||||||
|
var result agentStartResult
|
||||||
|
if task.isWitness {
|
||||||
|
result = upStartWitness(task.rigName, task.rigObj)
|
||||||
|
} else {
|
||||||
|
result = upStartRefinery(task.rigName, task.rigObj)
|
||||||
|
}
|
||||||
|
results <- agentResultMsg{
|
||||||
|
rigName: task.rigName,
|
||||||
|
isWitness: task.isWitness,
|
||||||
|
result: result,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enqueue all tasks
|
||||||
|
for rigName, r := range prefetchedRigs {
|
||||||
|
tasks <- agentTask{rigName: rigName, rigObj: r, isWitness: true}
|
||||||
|
tasks <- agentTask{rigName: rigName, rigObj: r, isWitness: false}
|
||||||
|
}
|
||||||
|
close(tasks)
|
||||||
|
|
||||||
|
// Close results channel when workers are done
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(results)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Collect results - no locking needed, single goroutine collects
|
||||||
|
for msg := range results {
|
||||||
|
if msg.isWitness {
|
||||||
|
witnessResults[msg.rigName] = msg.result
|
||||||
|
} else {
|
||||||
|
refineryResults[msg.rigName] = msg.result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// upStartWitness starts a witness for the given rig and returns a result struct.
|
||||||
|
func upStartWitness(rigName string, r *rig.Rig) agentStartResult {
|
||||||
|
name := "Witness (" + rigName + ")"
|
||||||
|
mgr := witness.NewManager(r)
|
||||||
|
if err := mgr.Start(false, "", nil); err != nil {
|
||||||
|
if err == witness.ErrAlreadyRunning {
|
||||||
|
return agentStartResult{name: name, ok: true, detail: mgr.SessionName()}
|
||||||
|
}
|
||||||
|
return agentStartResult{name: name, ok: false, detail: err.Error()}
|
||||||
|
}
|
||||||
|
return agentStartResult{name: name, ok: true, detail: mgr.SessionName()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// upStartRefinery starts a refinery for the given rig and returns a result struct.
|
||||||
|
func upStartRefinery(rigName string, r *rig.Rig) agentStartResult {
|
||||||
|
name := "Refinery (" + rigName + ")"
|
||||||
|
mgr := refinery.NewManager(r)
|
||||||
|
if err := mgr.Start(false, ""); err != nil {
|
||||||
|
if err == refinery.ErrAlreadyRunning {
|
||||||
|
return agentStartResult{name: name, ok: true, detail: mgr.SessionName()}
|
||||||
|
}
|
||||||
|
return agentStartResult{name: name, ok: false, detail: err.Error()}
|
||||||
|
}
|
||||||
|
return agentStartResult{name: name, ok: true, detail: mgr.SessionName()}
|
||||||
|
}
|
||||||
|
|
||||||
// discoverRigs finds all rigs in the town.
|
// discoverRigs finds all rigs in the town.
|
||||||
func discoverRigs(townRoot string) []string {
|
func discoverRigs(townRoot string) []string {
|
||||||
var rigs []string
|
var rigs []string
|
||||||
|
|||||||
216
internal/cmd/up_test.go
Normal file
216
internal/cmd/up_test.go
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/steveyegge/gastown/internal/rig"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAgentStartResult_Fields(t *testing.T) {
|
||||||
|
result := agentStartResult{
|
||||||
|
name: "Witness (gastown)",
|
||||||
|
ok: true,
|
||||||
|
detail: "gt-gastown-witness",
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.name != "Witness (gastown)" {
|
||||||
|
t.Errorf("name = %q, want %q", result.name, "Witness (gastown)")
|
||||||
|
}
|
||||||
|
if !result.ok {
|
||||||
|
t.Error("ok should be true")
|
||||||
|
}
|
||||||
|
if result.detail != "gt-gastown-witness" {
|
||||||
|
t.Errorf("detail = %q, want %q", result.detail, "gt-gastown-witness")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMaxConcurrentAgentStarts_Constant(t *testing.T) {
|
||||||
|
// Verify the constant is set to a reasonable value
|
||||||
|
if maxConcurrentAgentStarts < 1 {
|
||||||
|
t.Errorf("maxConcurrentAgentStarts = %d, should be >= 1", maxConcurrentAgentStarts)
|
||||||
|
}
|
||||||
|
if maxConcurrentAgentStarts > 100 {
|
||||||
|
t.Errorf("maxConcurrentAgentStarts = %d, should be <= 100 to prevent resource exhaustion", maxConcurrentAgentStarts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSemaphoreLimitsConcurrency(t *testing.T) {
|
||||||
|
// Test that a semaphore pattern properly limits concurrency
|
||||||
|
const maxConcurrent = 3
|
||||||
|
const totalTasks = 10
|
||||||
|
|
||||||
|
sem := make(chan struct{}, maxConcurrent)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
var maxObserved int32
|
||||||
|
var current int32
|
||||||
|
|
||||||
|
for i := 0; i < totalTasks; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
// Acquire semaphore
|
||||||
|
sem <- struct{}{}
|
||||||
|
defer func() { <-sem }()
|
||||||
|
|
||||||
|
// Track concurrent count
|
||||||
|
cur := atomic.AddInt32(¤t, 1)
|
||||||
|
defer atomic.AddInt32(¤t, -1)
|
||||||
|
|
||||||
|
// Update max observed
|
||||||
|
for {
|
||||||
|
max := atomic.LoadInt32(&maxObserved)
|
||||||
|
if cur <= max || atomic.CompareAndSwapInt32(&maxObserved, max, cur) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate work
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
if maxObserved > maxConcurrent {
|
||||||
|
t.Errorf("max concurrent = %d, should not exceed %d", maxObserved, maxConcurrent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStartRigAgentsParallel_EmptyRigs(t *testing.T) {
|
||||||
|
// Test with empty rig list - should return empty maps without error
|
||||||
|
witnessResults, refineryResults := startRigAgentsParallel([]string{})
|
||||||
|
|
||||||
|
if len(witnessResults) != 0 {
|
||||||
|
t.Errorf("witnessResults should be empty, got %d entries", len(witnessResults))
|
||||||
|
}
|
||||||
|
if len(refineryResults) != 0 {
|
||||||
|
t.Errorf("refineryResults should be empty, got %d entries", len(refineryResults))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStartRigAgentsWithPrefetch_EmptyRigs(t *testing.T) {
|
||||||
|
// Test with empty inputs
|
||||||
|
witnessResults, refineryResults := startRigAgentsWithPrefetch(
|
||||||
|
[]string{},
|
||||||
|
make(map[string]*rig.Rig),
|
||||||
|
make(map[string]error),
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(witnessResults) != 0 {
|
||||||
|
t.Errorf("witnessResults should be empty, got %d entries", len(witnessResults))
|
||||||
|
}
|
||||||
|
if len(refineryResults) != 0 {
|
||||||
|
t.Errorf("refineryResults should be empty, got %d entries", len(refineryResults))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStartRigAgentsWithPrefetch_RecordsErrors(t *testing.T) {
|
||||||
|
// Test that rig errors are properly recorded
|
||||||
|
rigErrors := map[string]error{
|
||||||
|
"badrig": fmt.Errorf("rig not found"),
|
||||||
|
}
|
||||||
|
|
||||||
|
witnessResults, refineryResults := startRigAgentsWithPrefetch(
|
||||||
|
[]string{"badrig"},
|
||||||
|
make(map[string]*rig.Rig),
|
||||||
|
rigErrors,
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(witnessResults) != 1 {
|
||||||
|
t.Errorf("witnessResults should have 1 entry, got %d", len(witnessResults))
|
||||||
|
}
|
||||||
|
if result, ok := witnessResults["badrig"]; !ok {
|
||||||
|
t.Error("witnessResults should have badrig entry")
|
||||||
|
} else if result.ok {
|
||||||
|
t.Error("badrig witness result should not be ok")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(refineryResults) != 1 {
|
||||||
|
t.Errorf("refineryResults should have 1 entry, got %d", len(refineryResults))
|
||||||
|
}
|
||||||
|
if result, ok := refineryResults["badrig"]; !ok {
|
||||||
|
t.Error("refineryResults should have badrig entry")
|
||||||
|
} else if result.ok {
|
||||||
|
t.Error("badrig refinery result should not be ok")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrefetchRigs_Empty(t *testing.T) {
|
||||||
|
// Test with empty rig list
|
||||||
|
rigs, errors := prefetchRigs([]string{})
|
||||||
|
|
||||||
|
if len(rigs) != 0 {
|
||||||
|
t.Errorf("rigs should be empty, got %d entries", len(rigs))
|
||||||
|
}
|
||||||
|
if len(errors) != 0 {
|
||||||
|
t.Errorf("errors should be empty, got %d entries", len(errors))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWorkerPoolLimitsConcurrency(t *testing.T) {
|
||||||
|
// Test that a worker pool pattern properly limits concurrency
|
||||||
|
const numWorkers = 3
|
||||||
|
const numTasks = 15
|
||||||
|
|
||||||
|
tasks := make(chan int, numTasks)
|
||||||
|
results := make(chan int, numTasks)
|
||||||
|
|
||||||
|
var maxObserved int32
|
||||||
|
var current int32
|
||||||
|
|
||||||
|
// Start worker pool
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for i := 0; i < numWorkers; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for range tasks {
|
||||||
|
// Track concurrent count
|
||||||
|
cur := atomic.AddInt32(¤t, 1)
|
||||||
|
|
||||||
|
// Update max observed
|
||||||
|
for {
|
||||||
|
max := atomic.LoadInt32(&maxObserved)
|
||||||
|
if cur <= max || atomic.CompareAndSwapInt32(&maxObserved, max, cur) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate work
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
|
||||||
|
atomic.AddInt32(¤t, -1)
|
||||||
|
results <- 1
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enqueue tasks
|
||||||
|
for i := 0; i < numTasks; i++ {
|
||||||
|
tasks <- i
|
||||||
|
}
|
||||||
|
close(tasks)
|
||||||
|
|
||||||
|
// Wait for workers and collect results
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(results)
|
||||||
|
}()
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
for range results {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
if count != numTasks {
|
||||||
|
t.Errorf("expected %d results, got %d", numTasks, count)
|
||||||
|
}
|
||||||
|
if maxObserved > numWorkers {
|
||||||
|
t.Errorf("max concurrent = %d, should not exceed %d workers", maxObserved, numWorkers)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user