summaryrefslogtreecommitdiff
path: root/internal/executor
diff options
context:
space:
mode:
Diffstat (limited to 'internal/executor')
-rw-r--r--internal/executor/executor.go38
-rw-r--r--internal/executor/executor_test.go32
2 files changed, 38 insertions, 32 deletions
diff --git a/internal/executor/executor.go b/internal/executor/executor.go
index 2505280..88a5783 100644
--- a/internal/executor/executor.go
+++ b/internal/executor/executor.go
@@ -11,6 +11,7 @@ import (
"sync"
"time"
+ "github.com/google/uuid"
"github.com/thepeterstone/claudomator/internal/event"
"github.com/thepeterstone/claudomator/internal/llm"
"github.com/thepeterstone/claudomator/internal/retry"
@@ -18,7 +19,6 @@ import (
"github.com/thepeterstone/claudomator/internal/storage"
"github.com/thepeterstone/claudomator/internal/story"
"github.com/thepeterstone/claudomator/internal/task"
- "github.com/google/uuid"
)
// Store is the subset of storage.DB methods used by the Pool.
@@ -88,8 +88,8 @@ type Pool struct {
runners map[string]Runner
store Store
logger *slog.Logger
- depPollInterval time.Duration // how often waitForDependencies polls; defaults to 5s
- requeueDelay time.Duration // how long to wait before requeuing a blocked-per-agent task; defaults to 30s
+ depPollInterval time.Duration // how often waitForDependencies polls; defaults to 5s
+ requeueDelay time.Duration // how long to wait before requeuing a blocked-per-agent task; defaults to 30s
mu sync.Mutex
active int
@@ -101,15 +101,15 @@ type Pool struct {
// so repeated round_robin tier resolutions (see selectRung) actually
// rotate across candidates rather than always picking the first one.
roleTierIndex map[string]int
- closed bool // set to true when Shutdown has been called
- resultCh chan *Result
- startedCh chan string // task IDs that just transitioned to RUNNING
- workCh chan workItem // internal bounded queue; Submit enqueues here
- doneCh chan struct{} // signals when a worker slot is freed
- workerWg sync.WaitGroup // tracks in-flight execute/executeResume goroutines
- dispatchDone chan struct{} // closed when the dispatch goroutine exits
- Classifier *Classifier
- LLM *llm.Client
+ closed bool // set to true when Shutdown has been called
+ resultCh chan *Result
+ startedCh chan string // task IDs that just transitioned to RUNNING
+ workCh chan workItem // internal bounded queue; Submit enqueues here
+ doneCh chan struct{} // signals when a worker slot is freed
+ workerWg sync.WaitGroup // tracks in-flight execute/executeResume goroutines
+ dispatchDone chan struct{} // closed when the dispatch goroutine exits
+ Classifier *Classifier
+ LLM *llm.Client
// Budget gates paid providers against a rolling window; nil disables gating.
Budget BudgetGate
}
@@ -166,9 +166,11 @@ func (p *Pool) dispatch() {
p.active++
p.mu.Unlock()
if item.exec != nil {
- p.workerWg.Add(1); go func(i workItem) { defer p.workerWg.Done(); p.executeResume(i.ctx, i.task, i.exec) }(item)
+ p.workerWg.Add(1)
+ go func(i workItem) { defer p.workerWg.Done(); p.executeResume(i.ctx, i.task, i.exec) }(item)
} else {
- p.workerWg.Add(1); go func(i workItem) { defer p.workerWg.Done(); p.execute(i.ctx, i.task) }(item)
+ p.workerWg.Add(1)
+ go func(i workItem) { defer p.workerWg.Done(); p.execute(i.ctx, i.task) }(item)
}
break
}
@@ -255,10 +257,10 @@ func (p *Pool) Cancel(taskID string) bool {
// resumablePoolStates are the task states that may be submitted for session resume.
var resumablePoolStates = map[task.State]bool{
- task.StateBlocked: true,
- task.StateTimedOut: true,
- task.StateCancelled: true,
- task.StateFailed: true,
+ task.StateBlocked: true,
+ task.StateTimedOut: true,
+ task.StateCancelled: true,
+ task.StateFailed: true,
task.StateBudgetExceeded: true,
}
diff --git a/internal/executor/executor_test.go b/internal/executor/executor_test.go
index d89a9eb..e2b9b44 100644
--- a/internal/executor/executor_test.go
+++ b/internal/executor/executor_test.go
@@ -109,7 +109,7 @@ func makeTask(id string) *task.Task {
now := time.Now().UTC()
return &task.Task{
ID: id, Name: "Test " + id,
- Agent: task.AgentConfig{Type: "claude", Instructions: "test"},
+ Agent: task.AgentConfig{Type: "claude", Instructions: "test"},
Priority: task.PriorityNormal,
Retry: task.RetryConfig{MaxAttempts: 1, Backoff: "linear"},
Tags: []string{},
@@ -1206,10 +1206,13 @@ func TestPool_Submit_ParentNotBlocked_NoTransition(t *testing.T) {
// minimalMockStore is a standalone Store implementation for unit-testing Pool
// methods that do not require a real SQLite database.
type minimalMockStore struct {
- mu sync.Mutex
- tasks map[string]*task.Task
- executions map[string]*storage.Execution
- stateUpdates []struct{ id string; state task.State }
+ mu sync.Mutex
+ tasks map[string]*task.Task
+ executions map[string]*storage.Execution
+ stateUpdates []struct {
+ id string
+ state task.State
+ }
questionUpdates []string
summaryUpdates []struct{ taskID, summary string }
changestatCalls []struct {
@@ -1245,7 +1248,7 @@ func (m *minimalMockStore) ListSubtasks(parentID string) ([]*task.Task, error) {
return nil, nil
}
func (m *minimalMockStore) ListExecutions(_ string) ([]*storage.Execution, error) { return nil, nil }
-func (m *minimalMockStore) CreateExecution(e *storage.Execution) error { return nil }
+func (m *minimalMockStore) CreateExecution(e *storage.Execution) error { return nil }
func (m *minimalMockStore) CreateExecutionAndSetRunning(e *storage.Execution) error {
return nil
}
@@ -1257,7 +1260,10 @@ func (m *minimalMockStore) UpdateTaskState(id string, newState task.State) error
return m.updateStateErr
}
m.mu.Lock()
- m.stateUpdates = append(m.stateUpdates, struct{ id string; state task.State }{id, newState})
+ m.stateUpdates = append(m.stateUpdates, struct {
+ id string
+ state task.State
+ }{id, newState})
if t, ok := m.tasks[id]; ok {
t.State = newState
}
@@ -1298,10 +1304,10 @@ func (m *minimalMockStore) UpdateExecutionChangestats(execID string, stats *task
m.mu.Unlock()
return nil
}
-func (m *minimalMockStore) RecordAgentEvent(_ storage.AgentEvent) error { return nil }
-func (m *minimalMockStore) GetProject(_ string) (*task.Project, error) { return nil, nil }
-func (m *minimalMockStore) CreateTask(_ *task.Task) error { return nil }
-func (m *minimalMockStore) CreateEvent(_ *event.Event) error { return nil }
+func (m *minimalMockStore) RecordAgentEvent(_ storage.AgentEvent) error { return nil }
+func (m *minimalMockStore) GetProject(_ string) (*task.Project, error) { return nil, nil }
+func (m *minimalMockStore) CreateTask(_ *task.Task) error { return nil }
+func (m *minimalMockStore) CreateEvent(_ *event.Event) error { return nil }
func (m *minimalMockStore) GetActiveRoleConfig(_ string) (*storage.RoleConfigRow, error) {
return nil, sql.ErrNoRows
}
@@ -1709,7 +1715,7 @@ func TestPool_MaxPerAgent_BlocksSecondTask(t *testing.T) {
runners := map[string]Runner{"claude": runner}
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
pool := NewPool(2, runners, store, logger) // pool size 2, but maxPerAgent=1
- pool.requeueDelay = 50 * time.Millisecond // speed up test
+ pool.requeueDelay = 50 * time.Millisecond // speed up test
tk1 := makeTask("mpa-1")
tk2 := makeTask("mpa-2")
@@ -1838,7 +1844,6 @@ func TestPool_ConsecutiveFailures_ResetOnSuccess(t *testing.T) {
}
}
-
// TestPool_DependsOn_NoDeadlock verifies that a task waiting for a dependency
// does NOT hold the per-agent slot, allowing the dependency to run first.
func TestPool_DependsOn_NoDeadlock(t *testing.T) {
@@ -1975,4 +1980,3 @@ func TestPool_Shutdown_TimesOut(t *testing.T) {
}
close(unblock) // cleanup
}
-