Major changes: - Add internal/logging package with field constants, context propagation, sensitive data auto-redaction, and per-component log levels - Add worker timeout constants (TimeoutQuickOp, TimeoutHealthCheck, etc.) - Extend SDLC with callback handlers, generate endpoints, and executor - Add new cookbook trees for aeries and slackpath progression - Add skeleton templates for queue, realtime, and microservices - Add worker component template with async job processing - Refactor services and handlers to use new logging infrastructure - Split component.go into component_infra.go and component_listing.go Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
389 lines
10 KiB
Go
389 lines
10 KiB
Go
package worker
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/orchard9/rdev/internal/domain"
|
|
"github.com/orchard9/rdev/internal/port"
|
|
"github.com/orchard9/rdev/internal/service"
|
|
)
|
|
|
|
// =============================================================================
|
|
// Mock implementations for worker package tests
|
|
// =============================================================================
|
|
|
|
type mockWorkQueue struct {
|
|
mu sync.Mutex
|
|
tasks map[string]*domain.WorkTask
|
|
err error
|
|
}
|
|
|
|
func newMockWorkQueue() *mockWorkQueue {
|
|
return &mockWorkQueue{tasks: make(map[string]*domain.WorkTask)}
|
|
}
|
|
|
|
func (m *mockWorkQueue) Enqueue(_ context.Context, task *domain.WorkTask) (string, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.err != nil {
|
|
return "", m.err
|
|
}
|
|
id := fmt.Sprintf("task-%d", len(m.tasks)+1)
|
|
task.ID = id
|
|
task.Status = domain.WorkTaskStatusPending
|
|
task.CreatedAt = time.Now()
|
|
m.tasks[id] = task
|
|
return id, nil
|
|
}
|
|
|
|
func (m *mockWorkQueue) Dequeue(_ context.Context, workerID string) (*domain.WorkTask, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.err != nil {
|
|
return nil, m.err
|
|
}
|
|
for _, task := range m.tasks {
|
|
if task.Status == domain.WorkTaskStatusPending {
|
|
task.Status = domain.WorkTaskStatusRunning
|
|
task.WorkerID = workerID
|
|
now := time.Now()
|
|
task.StartedAt = &now
|
|
return task, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *mockWorkQueue) Complete(_ context.Context, taskID string, result *domain.WorkResult) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
task, ok := m.tasks[taskID]
|
|
if !ok {
|
|
return domain.ErrWorkTaskNotFound
|
|
}
|
|
task.Status = domain.WorkTaskStatusCompleted
|
|
task.Result = result
|
|
now := time.Now()
|
|
task.CompletedAt = &now
|
|
return nil
|
|
}
|
|
|
|
func (m *mockWorkQueue) Fail(_ context.Context, taskID string, errMsg string) error {
|
|
return m.FailWithCode(context.Background(), taskID, errMsg, domain.WorkErrorCodeNone)
|
|
}
|
|
|
|
func (m *mockWorkQueue) FailWithCode(_ context.Context, taskID string, errMsg string, code domain.WorkErrorCode) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
task, ok := m.tasks[taskID]
|
|
if !ok {
|
|
return domain.ErrWorkTaskNotFound
|
|
}
|
|
task.RetryCount++
|
|
if task.RetryCount >= task.MaxRetries {
|
|
task.Status = domain.WorkTaskStatusFailed
|
|
task.Error = errMsg
|
|
task.ErrorCode = code
|
|
} else {
|
|
task.Status = domain.WorkTaskStatusPending
|
|
task.WorkerID = ""
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *mockWorkQueue) Cancel(_ context.Context, taskID string) error { return nil }
|
|
func (m *mockWorkQueue) GetTask(_ context.Context, taskID string) (*domain.WorkTask, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
task, ok := m.tasks[taskID]
|
|
if !ok {
|
|
return nil, domain.ErrWorkTaskNotFound
|
|
}
|
|
return task, nil
|
|
}
|
|
func (m *mockWorkQueue) ListByProject(_ context.Context, _ string, _ *domain.WorkTaskStatus, _ domain.WorkListOptions) (*domain.WorkListResult, error) {
|
|
return &domain.WorkListResult{}, nil
|
|
}
|
|
func (m *mockWorkQueue) GetStats(_ context.Context) (*domain.WorkQueueStats, error) {
|
|
return &domain.WorkQueueStats{}, nil
|
|
}
|
|
func (m *mockWorkQueue) CleanupOld(_ context.Context, _ time.Duration) (int64, error) {
|
|
return 0, nil
|
|
}
|
|
func (m *mockWorkQueue) RequeueStale(_ context.Context, _ time.Duration) (int64, error) {
|
|
return 0, nil
|
|
}
|
|
|
|
func (m *mockWorkQueue) RequeueStaleWithIDs(_ context.Context, _ time.Duration) ([]string, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
type mockWorkerRegistry struct {
|
|
mu sync.Mutex
|
|
workers map[string]*domain.Worker
|
|
err error
|
|
}
|
|
|
|
func newMockWorkerRegistry() *mockWorkerRegistry {
|
|
return &mockWorkerRegistry{workers: make(map[string]*domain.Worker)}
|
|
}
|
|
|
|
func (m *mockWorkerRegistry) Register(_ context.Context, worker *domain.Worker) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.err != nil {
|
|
return m.err
|
|
}
|
|
m.workers[worker.ID] = worker
|
|
return nil
|
|
}
|
|
|
|
func (m *mockWorkerRegistry) Heartbeat(_ context.Context, workerID string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
w, ok := m.workers[workerID]
|
|
if !ok {
|
|
return domain.ErrWorkerNotFound
|
|
}
|
|
w.LastHeartbeat = time.Now()
|
|
return nil
|
|
}
|
|
|
|
func (m *mockWorkerRegistry) UpdateStatus(_ context.Context, workerID string, status domain.WorkerStatus, taskID string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
w, ok := m.workers[workerID]
|
|
if !ok {
|
|
return domain.ErrWorkerNotFound
|
|
}
|
|
w.Status = status
|
|
w.CurrentTask = taskID
|
|
return nil
|
|
}
|
|
|
|
func (m *mockWorkerRegistry) Deregister(_ context.Context, workerID string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
delete(m.workers, workerID)
|
|
return nil
|
|
}
|
|
|
|
func (m *mockWorkerRegistry) Get(_ context.Context, workerID string) (*domain.Worker, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
w, ok := m.workers[workerID]
|
|
if !ok {
|
|
return nil, domain.ErrWorkerNotFound
|
|
}
|
|
return w, nil
|
|
}
|
|
|
|
func (m *mockWorkerRegistry) List(_ context.Context, filter port.WorkerFilter) ([]*domain.Worker, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
var result []*domain.Worker
|
|
for _, w := range m.workers {
|
|
if filter.Status != nil && w.Status != *filter.Status {
|
|
continue
|
|
}
|
|
result = append(result, w)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (m *mockWorkerRegistry) MarkStaleOffline(_ context.Context, _ time.Duration) (int, error) {
|
|
return 0, nil
|
|
}
|
|
|
|
type mockBuildAudit struct {
|
|
mu sync.Mutex
|
|
entries map[string]*domain.BuildAuditEntry
|
|
}
|
|
|
|
func newMockBuildAudit() *mockBuildAudit {
|
|
return &mockBuildAudit{entries: make(map[string]*domain.BuildAuditEntry)}
|
|
}
|
|
|
|
func (m *mockBuildAudit) Record(_ context.Context, entry *domain.BuildAuditEntry) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.entries[entry.TaskID] = entry
|
|
return nil
|
|
}
|
|
|
|
func (m *mockBuildAudit) Update(_ context.Context, taskID string, result *domain.BuildResult) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
entry, ok := m.entries[taskID]
|
|
if !ok {
|
|
return domain.ErrBuildNotFound
|
|
}
|
|
entry.Result = result
|
|
if result.Success {
|
|
entry.Status = domain.BuildStatusCompleted
|
|
} else {
|
|
entry.Status = domain.BuildStatusFailed
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *mockBuildAudit) UpdateStatus(_ context.Context, taskID string, status domain.BuildStatus, workerID string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
entry, ok := m.entries[taskID]
|
|
if !ok {
|
|
return domain.ErrBuildNotFound
|
|
}
|
|
entry.Status = status
|
|
entry.WorkerID = workerID
|
|
return nil
|
|
}
|
|
|
|
func (m *mockBuildAudit) Get(_ context.Context, taskID string) (*domain.BuildAuditEntry, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
entry, ok := m.entries[taskID]
|
|
if !ok {
|
|
return nil, domain.ErrBuildNotFound
|
|
}
|
|
return entry, nil
|
|
}
|
|
|
|
func (m *mockBuildAudit) List(_ context.Context, _ port.BuildAuditFilter) ([]*domain.BuildAuditEntry, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
type mockCodeAgent struct {
|
|
result *domain.AgentResult
|
|
err error
|
|
}
|
|
|
|
func (m *mockCodeAgent) Name() string { return "mock-agent" }
|
|
func (m *mockCodeAgent) Provider() domain.AgentProvider { return "mock" }
|
|
func (m *mockCodeAgent) Cancel(_ context.Context, _ string) error { return nil }
|
|
func (m *mockCodeAgent) Capabilities() domain.AgentCapabilities {
|
|
return domain.AgentCapabilities{Provider: "mock"}
|
|
}
|
|
func (m *mockCodeAgent) Available(_ context.Context) bool { return true }
|
|
func (m *mockCodeAgent) Execute(_ context.Context, req *domain.AgentRequest, handler domain.AgentEventHandler) (*domain.AgentResult, error) {
|
|
if handler != nil {
|
|
handler(domain.AgentEvent{
|
|
Type: domain.AgentEventOutput,
|
|
Content: "mock output for: " + req.Prompt,
|
|
})
|
|
}
|
|
if m.err != nil {
|
|
return nil, m.err
|
|
}
|
|
return m.result, nil
|
|
}
|
|
|
|
type mockCodeAgentRegistry struct {
|
|
agent port.CodeAgent
|
|
}
|
|
|
|
func (m *mockCodeAgentRegistry) Register(agent port.CodeAgent) { m.agent = agent }
|
|
func (m *mockCodeAgentRegistry) Get(_ domain.AgentProvider) port.CodeAgent { return m.agent }
|
|
func (m *mockCodeAgentRegistry) Default() port.CodeAgent { return m.agent }
|
|
func (m *mockCodeAgentRegistry) DefaultProvider() domain.AgentProvider { return "mock" }
|
|
func (m *mockCodeAgentRegistry) SetDefault(_ domain.AgentProvider) error { return nil }
|
|
func (m *mockCodeAgentRegistry) Available() []domain.AgentProvider {
|
|
return []domain.AgentProvider{"mock"}
|
|
}
|
|
func (m *mockCodeAgentRegistry) AvailableAgents(_ context.Context) []port.CodeAgent {
|
|
return []port.CodeAgent{m.agent}
|
|
}
|
|
func (m *mockCodeAgentRegistry) Count() int { return 1 }
|
|
|
|
// =============================================================================
|
|
// Mock CommandExecutor for verify tests
|
|
// =============================================================================
|
|
|
|
type mockCommandExecutor struct {
|
|
result *domain.CommandResult
|
|
err error
|
|
output []domain.OutputLine
|
|
podExists bool
|
|
podExistErr error
|
|
}
|
|
|
|
func newMockCommandExecutor() *mockCommandExecutor {
|
|
return &mockCommandExecutor{
|
|
result: &domain.CommandResult{
|
|
ExitCode: 0,
|
|
DurationMs: 100,
|
|
},
|
|
podExists: true,
|
|
}
|
|
}
|
|
|
|
func (m *mockCommandExecutor) Execute(_ context.Context, _ *domain.Command, _ string, handler domain.OutputHandler) (*domain.CommandResult, error) {
|
|
if m.err != nil {
|
|
return nil, m.err
|
|
}
|
|
// Deliver output lines to handler
|
|
for _, line := range m.output {
|
|
handler(line)
|
|
}
|
|
return m.result, nil
|
|
}
|
|
|
|
func (m *mockCommandExecutor) Cancel(_ context.Context, _ domain.CommandID) error {
|
|
return nil
|
|
}
|
|
|
|
func (m *mockCommandExecutor) PodExists(_ context.Context, _ string) (bool, error) {
|
|
return m.podExists, m.podExistErr
|
|
}
|
|
|
|
func (m *mockCommandExecutor) CheckConnection(_ context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
// =============================================================================
|
|
// Helper to build test dependencies
|
|
// =============================================================================
|
|
|
|
type testDeps struct {
|
|
queue *mockWorkQueue
|
|
registry *mockWorkerRegistry
|
|
audit *mockBuildAudit
|
|
agent *mockCodeAgent
|
|
|
|
workerSvc *service.WorkerService
|
|
workSvc *service.WorkService
|
|
buildExec *BuildExecutor
|
|
}
|
|
|
|
func newTestDeps() *testDeps {
|
|
queue := newMockWorkQueue()
|
|
registry := newMockWorkerRegistry()
|
|
audit := newMockBuildAudit()
|
|
agent := &mockCodeAgent{
|
|
result: &domain.AgentResult{
|
|
ExitCode: 0,
|
|
DurationMs: 1000,
|
|
},
|
|
}
|
|
agentRegistry := &mockCodeAgentRegistry{agent: agent}
|
|
|
|
workerSvc := service.NewWorkerService(registry, queue).
|
|
WithBuildAudit(audit)
|
|
workSvc := service.NewWorkService(queue)
|
|
|
|
buildExec := NewBuildExecutor(agentRegistry, nil, nil, nil, nil)
|
|
|
|
return &testDeps{
|
|
queue: queue,
|
|
registry: registry,
|
|
audit: audit,
|
|
agent: agent,
|
|
workerSvc: workerSvc,
|
|
workSvc: workSvc,
|
|
buildExec: buildExec,
|
|
}
|
|
}
|