From 81d9976d6af1393a6fcc82fb9e6a6bd28611a9b1 Mon Sep 17 00:00:00 2001 From: Hiro Tamada Date: Mon, 12 Jan 2026 11:10:51 -0500 Subject: [PATCH] refactor: remove build queue for immediate build execution Replace the explicit build queue with immediate build execution. Builds now start immediately when created instead of being queued. Key changes: - Delete queue.go and queue_test.go - Remove StatusQueued - builds start with StatusBuilding - Add ErrResourcesExhausted error for resource limit detection - Add 503 response with Retry-After header to OpenAPI spec - Update manager to start builds in goroutines directly - Simplify CancelBuild to only handle running builds - Update RecoverPendingBuilds to start builds immediately - Remove queue-related metrics (queueLength, activeBuilds) - Update tests and documentation If host resources are exhausted during instance creation, the build will fail with a clear error message. The API includes a 503 response type with Retry-After header for future pre-check implementation. --- cmd/api/api/builds.go | 10 ++ go.mod | 2 +- go.sum | 2 - lib/builds/README.md | 47 ++---- lib/builds/errors.go | 3 + lib/builds/manager.go | 55 +++---- lib/builds/manager_test.go | 152 ++++++++---------- lib/builds/metrics.go | 35 ----- lib/builds/queue.go | 171 --------------------- lib/builds/queue_test.go | 230 ---------------------------- lib/builds/storage.go | 4 +- lib/builds/types.go | 1 - lib/oapi/oapi.go | 305 ++++++++++++++++++++----------------- openapi.yaml | 18 ++- 14 files changed, 290 insertions(+), 745 deletions(-) delete mode 100644 lib/builds/queue.go delete mode 100644 lib/builds/queue_test.go diff --git a/cmd/api/api/builds.go b/cmd/api/api/builds.go index deaf6ec1..be37c60d 100644 --- a/cmd/api/api/builds.go +++ b/cmd/api/api/builds.go @@ -160,6 +160,16 @@ func (s *ApiService) CreateBuild(ctx context.Context, request oapi.CreateBuildRe Code: "invalid_source", Message: err.Error(), }, nil + case errors.Is(err, builds.ErrResourcesExhausted): + return oapi.CreateBuild503JSONResponse{ + Body: oapi.Error{ + Code: "resources_exhausted", + Message: "insufficient resources for build, please retry later", + }, + Headers: oapi.CreateBuild503ResponseHeaders{ + RetryAfter: 30, + }, + }, nil default: log.ErrorContext(ctx, "failed to create build", "error", err) return oapi.CreateBuild500JSONResponse{ diff --git a/go.mod b/go.mod index 830eb39e..1376602e 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/nrednav/cuid2 v1.1.0 github.com/oapi-codegen/nethttp-middleware v1.1.2 github.com/oapi-codegen/runtime v1.1.2 + github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.1 github.com/opencontainers/runtime-spec v1.2.1 github.com/opencontainers/umoci v0.6.0 @@ -90,7 +91,6 @@ require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pkg/errors v0.9.1 // indirect diff --git a/go.sum b/go.sum index 12ceb444..3edd3725 100644 --- a/go.sum +++ b/go.sum @@ -96,8 +96,6 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU= github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y= -github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= -github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/lib/builds/README.md b/lib/builds/README.md index 4aaa7def..0c9474b0 100644 --- a/lib/builds/README.md +++ b/lib/builds/README.md @@ -7,7 +7,7 @@ The build system provides source-to-image builds inside ephemeral Cloud Hypervis ``` ┌─────────────────────────────────────────────────────────────────┐ │ Hypeman API │ -│ POST /builds → BuildManager → BuildQueue │ +│ POST /builds → BuildManager → Builder VM │ │ │ │ │ Start() → VsockHandler (port 5001) │ └─────────────────────────────────────────────────────────────────┘ @@ -53,19 +53,6 @@ The build system provides source-to-image builds inside ephemeral Cloud Hypervis | `BuildProvenance` | Audit trail for reproducibility | | `BuildPolicy` | Resource limits and network policy | -### Build Queue (`queue.go`) - -In-memory queue with configurable concurrency: - -```go -queue := NewBuildQueue(maxConcurrent) -position := queue.Enqueue(buildID, request, startFunc) -queue.Cancel(buildID) -queue.GetPosition(buildID) -``` - -**Recovery**: On startup, `listPendingBuilds()` scans disk metadata for incomplete builds and re-enqueues them in FIFO order. - ### Storage (`storage.go`) Builds are persisted to `$DATA_DIR/builds/{id}/`: @@ -87,14 +74,15 @@ Orchestrates the build lifecycle: 1. Validate request and store source 2. Write build config to disk -3. Enqueue build job -4. Create source volume from archive -5. Create config volume with `build.json` -6. Create builder VM with both volumes attached -7. Wait for build completion -8. Update metadata and cleanup +3. Create source volume from archive +4. Create config volume with `build.json` +5. Create builder VM with both volumes attached +6. Wait for build completion via vsock +7. Update metadata and cleanup + +Builds start immediately when created. If host resources are exhausted, the build will fail with an appropriate error message. -**Important**: The `Start()` method must be called to start the vsock handler for builder communication. +**Recovery**: On startup, `listPendingBuilds()` scans disk metadata for incomplete builds (building/pushing status) and restarts them. ### Cache System (`cache.go`) @@ -136,9 +124,7 @@ OpenTelemetry metrics for monitoring: | Metric | Type | Description | |--------|------|-------------| | `hypeman_build_duration_seconds` | Histogram | Build duration | -| `hypeman_builds_total` | Counter | Total builds by status/runtime | -| `hypeman_build_queue_length` | Gauge | Pending builds in queue | -| `hypeman_builds_active` | Gauge | Currently running builds | +| `hypeman_builds_total` | Counter | Total builds by status | ### Builder Agent (`builder_agent/main.go`) @@ -196,7 +182,7 @@ CMD [\"node\", \"index.js\"]" \ ```json { "id": "abc123", - "status": "queued", + "status": "building", "created_at": "2025-01-15T10:00:00Z" } ``` @@ -231,11 +217,11 @@ Builder VMs authenticate to the registry using short-lived JWT tokens: ## Build Status Flow ``` -queued → building → pushing → ready - ↘ ↗ - failed - ↑ - cancelled +building → pushing → ready + ↘ ↗ + failed + ↑ + cancelled ``` ## Security Model @@ -348,7 +334,6 @@ Each build records provenance for reproducibility: go test ./lib/builds/... -v # Test specific components -go test ./lib/builds/queue_test.go ./lib/builds/queue.go ./lib/builds/types.go -v go test ./lib/builds/cache_test.go ./lib/builds/cache.go ./lib/builds/types.go ./lib/builds/errors.go -v go test ./lib/builds/registry_token_test.go ./lib/builds/registry_token.go -v ``` diff --git a/lib/builds/errors.go b/lib/builds/errors.go index 2e8d888d..588c9c30 100644 --- a/lib/builds/errors.go +++ b/lib/builds/errors.go @@ -32,4 +32,7 @@ var ( // ErrBuildInProgress is returned when trying to cancel a build that's already complete ErrBuildInProgress = errors.New("build in progress") + + // ErrResourcesExhausted is returned when there are insufficient resources to start a build + ErrResourcesExhausted = errors.New("insufficient resources for build") ) diff --git a/lib/builds/manager.go b/lib/builds/manager.go index 1bfc40b1..8053daee 100644 --- a/lib/builds/manager.go +++ b/lib/builds/manager.go @@ -85,7 +85,6 @@ func DefaultConfig() Config { type manager struct { config Config paths *paths.Paths - queue *BuildQueue instanceManager instances.Manager volumeManager volumes.Manager secretProvider SecretProvider @@ -116,7 +115,6 @@ func NewManager( m := &manager{ config: config, paths: p, - queue: NewBuildQueue(config.MaxConcurrentBuilds), instanceManager: instanceMgr, volumeManager: volumeMgr, secretProvider: secretProvider, @@ -171,7 +169,7 @@ func (m *manager) CreateBuild(ctx context.Context, req CreateBuildRequest, sourc // Create build metadata meta := &buildMetadata{ ID: id, - Status: StatusQueued, + Status: StatusBuilding, Request: &req, CreatedAt: time.Now(), } @@ -222,17 +220,12 @@ func (m *manager) CreateBuild(ctx context.Context, req CreateBuildRequest, sourc return nil, fmt.Errorf("write build config: %w", err) } - // Enqueue the build - queuePos := m.queue.Enqueue(id, req, func() { - m.runBuild(context.Background(), id, req, policy) - }) + // Start the build immediately in background + go m.runBuild(context.Background(), id, req, policy) build := meta.toBuild() - if queuePos > 0 { - build.QueuePosition = &queuePos - } - m.logger.Info("build created", "id", id, "queue_position", queuePos) + m.logger.Info("build created", "id", id) return build, nil } @@ -386,6 +379,11 @@ func (m *manager) executeBuild(ctx context.Context, id string, req CreateBuildRe }, }) if err != nil { + // Check if this is a resource exhaustion error + errStr := err.Error() + if strings.Contains(errStr, "exceeds") && strings.Contains(errStr, "limit") { + return nil, fmt.Errorf("%w: %v", ErrResourcesExhausted, err) + } return nil, fmt.Errorf("create builder instance: %w", err) } @@ -693,14 +691,7 @@ func (m *manager) GetBuild(ctx context.Context, id string) (*Build, error) { return nil, err } - build := meta.toBuild() - - // Add queue position if queued - if meta.Status == StatusQueued { - build.QueuePosition = m.queue.GetPosition(id) - } - - return build, nil + return meta.toBuild(), nil } // ListBuilds returns all builds @@ -712,17 +703,13 @@ func (m *manager) ListBuilds(ctx context.Context) ([]*Build, error) { builds := make([]*Build, 0, len(metas)) for _, meta := range metas { - build := meta.toBuild() - if meta.Status == StatusQueued { - build.QueuePosition = m.queue.GetPosition(meta.ID) - } - builds = append(builds, build) + builds = append(builds, meta.toBuild()) } return builds, nil } -// CancelBuild cancels a pending build +// CancelBuild cancels a running build func (m *manager) CancelBuild(ctx context.Context, id string) error { meta, err := readMetadata(m.paths, id) if err != nil { @@ -730,17 +717,8 @@ func (m *manager) CancelBuild(ctx context.Context, id string) error { } switch meta.Status { - case StatusQueued: - // Remove from queue - if m.queue.Cancel(id) { - m.updateStatus(id, StatusCancelled, nil) - return nil - } - return ErrBuildInProgress // Was already picked up - case StatusBuilding, StatusPushing: - // Can't cancel a running build easily - // Would need to terminate the builder instance + // Terminate the builder instance to cancel the build if meta.BuilderInstance != nil { m.instanceManager.DeleteInstance(ctx, *meta.BuilderInstance) } @@ -936,7 +914,7 @@ func (m *manager) RecoverPendingBuilds() { meta := meta // Shadow loop variable for closure capture m.logger.Info("recovering build", "id", meta.ID, "status", meta.Status) - // Re-enqueue the build + // Start the build directly if meta.Request != nil { // Regenerate registry token since the original token may have expired // during server downtime. Token TTL is minimum 30 minutes. @@ -949,13 +927,14 @@ func (m *manager) RecoverPendingBuilds() { continue } - m.queue.Enqueue(meta.ID, *meta.Request, func() { + // Start the build in background + go func() { policy := DefaultBuildPolicy() if meta.Request.BuildPolicy != nil { policy = *meta.Request.BuildPolicy } m.runBuild(context.Background(), meta.ID, *meta.Request, &policy) - }) + }() } } diff --git a/lib/builds/manager_test.go b/lib/builds/manager_test.go index c53ac984..a0b5c5a3 100644 --- a/lib/builds/manager_test.go +++ b/lib/builds/manager_test.go @@ -254,7 +254,6 @@ func setupTestManager(t *testing.T) (*manager, *mockInstanceManager, *mockVolume mgr := &manager{ config: config, paths: p, - queue: NewBuildQueue(config.MaxConcurrentBuilds), instanceManager: instanceMgr, volumeManager: volumeMgr, secretProvider: secretProvider, @@ -281,7 +280,7 @@ func TestCreateBuild_Success(t *testing.T) { require.NoError(t, err) assert.NotEmpty(t, build.ID) - assert.Equal(t, StatusQueued, build.Status) + assert.Equal(t, StatusBuilding, build.Status) assert.NotNil(t, build.CreatedAt) // Verify source was stored @@ -339,7 +338,7 @@ func TestGetBuild_Found(t *testing.T) { require.NoError(t, err) assert.Equal(t, created.ID, build.ID) - assert.Equal(t, StatusQueued, build.Status) + assert.Equal(t, StatusBuilding, build.Status) } func TestGetBuild_NotFound(t *testing.T) { @@ -387,35 +386,6 @@ func TestListBuilds_WithBuilds(t *testing.T) { assert.Len(t, builds, 3) } -func TestCancelBuild_QueuedBuild(t *testing.T) { - // Test the queue cancellation directly to avoid race conditions - queue := NewBuildQueue(1) // Only 1 concurrent - - started := make(chan struct{}) - - // Add a blocking build to fill the single slot - queue.Enqueue("build-1", CreateBuildRequest{}, func() { - started <- struct{}{} - select {} // Block forever - }) - - // Wait for first build to start - <-started - - // Add a second build - this one should be queued - queue.Enqueue("build-2", CreateBuildRequest{}, func() {}) - - // Verify it's pending - assert.Equal(t, 1, queue.PendingCount()) - - // Cancel the queued build - cancelled := queue.Cancel("build-2") - assert.True(t, cancelled) - - // Verify it's removed from pending - assert.Equal(t, 0, queue.PendingCount()) -} - func TestCancelBuild_NotFound(t *testing.T) { mgr, _, _, tempDir := setupTestManager(t) defer os.RemoveAll(tempDir) @@ -453,7 +423,6 @@ func TestCancelBuild_AlreadyCompleted(t *testing.T) { mgr := &manager{ config: config, paths: p, - queue: NewBuildQueue(config.MaxConcurrentBuilds), tokenGenerator: NewRegistryTokenGenerator(config.RegistrySecret), logger: slog.New(slog.NewTextHandler(io.Discard, nil)), } @@ -521,35 +490,6 @@ func TestGetBuildLogs_NotFound(t *testing.T) { assert.Error(t, err) } -func TestBuildQueue_ConcurrencyLimit(t *testing.T) { - // Test the queue directly rather than through the manager - // because the manager's runBuild goroutine completes quickly with mocks - queue := NewBuildQueue(2) // Max 2 concurrent - - started := make(chan string, 5) - - // Enqueue 5 builds with blocking start functions - for i := 0; i < 5; i++ { - id := string(rune('A' + i)) - queue.Enqueue(id, CreateBuildRequest{}, func() { - started <- id - // Block until test completes - simulates long-running build - select {} - }) - } - - // Give goroutines time to start - for i := 0; i < 2; i++ { - <-started - } - - // First 2 should be active, rest should be pending - active := queue.ActiveCount() - pending := queue.PendingCount() - assert.Equal(t, 2, active, "expected 2 active builds") - assert.Equal(t, 3, pending, "expected 3 pending builds") -} - func TestUpdateStatus(t *testing.T) { // Test the updateStatus function directly using storage functions // This avoids race conditions with the build queue goroutines @@ -563,22 +503,19 @@ func TestUpdateStatus(t *testing.T) { // Create initial metadata meta := &buildMetadata{ ID: "test-build-1", - Status: StatusQueued, + Status: StatusBuilding, CreatedAt: time.Now(), } require.NoError(t, writeMetadata(p, meta)) - // Update status - meta.Status = StatusBuilding - now := time.Now() - meta.StartedAt = &now + // Update status to pushing + meta.Status = StatusPushing require.NoError(t, writeMetadata(p, meta)) // Read back and verify readMeta, err := readMetadata(p, "test-build-1") require.NoError(t, err) - assert.Equal(t, StatusBuilding, readMeta.Status) - assert.NotNil(t, readMeta.StartedAt) + assert.Equal(t, StatusPushing, readMeta.Status) } func TestUpdateStatus_WithError(t *testing.T) { @@ -593,7 +530,7 @@ func TestUpdateStatus_WithError(t *testing.T) { // Create initial metadata meta := &buildMetadata{ ID: "test-build-1", - Status: StatusQueued, + Status: StatusBuilding, CreatedAt: time.Now(), } require.NoError(t, writeMetadata(p, meta)) @@ -769,26 +706,42 @@ func TestStreamBuildEvents_NoLogs(t *testing.T) { } func TestStreamBuildEvents_WithStatusUpdate(t *testing.T) { - mgr, _, _, tempDir := setupTestManager(t) + // Test that status updates are received through event streaming + // We create build metadata directly to avoid timing issues with the build goroutine + tempDir, err := os.MkdirTemp("", "builds-test-*") + require.NoError(t, err) defer os.RemoveAll(tempDir) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + p := paths.New(tempDir) + buildID := "test-status-build" + require.NoError(t, os.MkdirAll(filepath.Join(tempDir, "builds", buildID), 0755)) - // Create a build - req := CreateBuildRequest{Dockerfile: "FROM alpine"} - sourceData := []byte("fake-tarball-data") - build, err := mgr.CreateBuild(ctx, req, sourceData) - require.NoError(t, err) + // Create metadata with building status + meta := &buildMetadata{ + ID: buildID, + Status: StatusBuilding, + CreatedAt: time.Now(), + } + require.NoError(t, writeMetadata(p, meta)) // Write some initial logs - logDir := filepath.Join(tempDir, "builds", build.ID, "logs") + logDir := filepath.Join(tempDir, "builds", buildID, "logs") require.NoError(t, os.MkdirAll(logDir, 0755)) logPath := filepath.Join(logDir, "build.log") require.NoError(t, os.WriteFile(logPath, []byte("initial log\n"), 0644)) + // Create a minimal manager for streaming + mgr := &manager{ + paths: p, + logger: slog.New(slog.NewTextHandler(io.Discard, nil)), + statusSubscribers: make(map[string][]chan BuildEvent), + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + // Stream events with follow - eventChan, err := mgr.StreamBuildEvents(ctx, build.ID, true) + eventChan, err := mgr.StreamBuildEvents(ctx, buildID, true) require.NoError(t, err) // Read events until we see the initial log @@ -802,14 +755,13 @@ eventLoop: foundInitialLog = true break eventLoop } - // Skip status events from queue (e.g. "building") case <-timeout: t.Fatal("timeout waiting for initial log event") } } // Trigger a status update to "ready" (should cause stream to close) - mgr.updateStatus(build.ID, StatusReady, nil) + mgr.updateStatus(buildID, StatusReady, nil) // Should receive "ready" status event and channel should close var readyReceived bool @@ -831,25 +783,42 @@ eventLoop: } func TestStreamBuildEvents_ContextCancellation(t *testing.T) { - mgr, _, _, tempDir := setupTestManager(t) + // Test that context cancellation properly closes the event channel + // We use a simpler approach: create build metadata directly without + // starting the actual build goroutine to avoid timing issues + tempDir, err := os.MkdirTemp("", "builds-test-*") + require.NoError(t, err) defer os.RemoveAll(tempDir) - ctx, cancel := context.WithCancel(context.Background()) + p := paths.New(tempDir) + buildID := "test-cancel-build" + require.NoError(t, os.MkdirAll(filepath.Join(tempDir, "builds", buildID), 0755)) - // Create a build - req := CreateBuildRequest{Dockerfile: "FROM alpine"} - sourceData := []byte("fake-tarball-data") - build, err := mgr.CreateBuild(ctx, req, sourceData) - require.NoError(t, err) + // Create metadata with building status + meta := &buildMetadata{ + ID: buildID, + Status: StatusBuilding, + CreatedAt: time.Now(), + } + require.NoError(t, writeMetadata(p, meta)) // Write some logs - logDir := filepath.Join(tempDir, "builds", build.ID, "logs") + logDir := filepath.Join(tempDir, "builds", buildID, "logs") require.NoError(t, os.MkdirAll(logDir, 0755)) logPath := filepath.Join(logDir, "build.log") require.NoError(t, os.WriteFile(logPath, []byte("log line\n"), 0644)) + // Create a minimal manager for streaming + mgr := &manager{ + paths: p, + logger: slog.New(slog.NewTextHandler(io.Discard, nil)), + statusSubscribers: make(map[string][]chan BuildEvent), + } + + ctx, cancel := context.WithCancel(context.Background()) + // Stream events with follow - eventChan, err := mgr.StreamBuildEvents(ctx, build.ID, true) + eventChan, err := mgr.StreamBuildEvents(ctx, buildID, true) require.NoError(t, err) // Read events until we see the log line @@ -863,7 +832,6 @@ eventLoop: foundLogLine = true break eventLoop } - // Skip status events from queue (e.g. "building") case <-timeout: t.Fatal("timeout waiting for log event") } diff --git a/lib/builds/metrics.go b/lib/builds/metrics.go index 92d3c029..f6cb35b0 100644 --- a/lib/builds/metrics.go +++ b/lib/builds/metrics.go @@ -12,8 +12,6 @@ import ( type Metrics struct { buildDuration metric.Float64Histogram buildTotal metric.Int64Counter - queueLength metric.Int64ObservableGauge - activeBuilds metric.Int64ObservableGauge } // NewMetrics creates a new Metrics instance @@ -35,27 +33,9 @@ func NewMetrics(meter metric.Meter) (*Metrics, error) { return nil, err } - queueLength, err := meter.Int64ObservableGauge( - "hypeman_build_queue_length", - metric.WithDescription("Number of builds in queue"), - ) - if err != nil { - return nil, err - } - - activeBuilds, err := meter.Int64ObservableGauge( - "hypeman_builds_active", - metric.WithDescription("Number of currently running builds"), - ) - if err != nil { - return nil, err - } - return &Metrics{ buildDuration: buildDuration, buildTotal: buildTotal, - queueLength: queueLength, - activeBuilds: activeBuilds, }, nil } @@ -68,18 +48,3 @@ func (m *Metrics) RecordBuild(ctx context.Context, status string, duration time. m.buildDuration.Record(ctx, duration.Seconds(), metric.WithAttributes(attrs...)) m.buildTotal.Add(ctx, 1, metric.WithAttributes(attrs...)) } - -// RegisterQueueCallbacks registers callbacks for queue metrics -func (m *Metrics) RegisterQueueCallbacks(queue *BuildQueue, meter metric.Meter) error { - _, err := meter.RegisterCallback( - func(ctx context.Context, observer metric.Observer) error { - observer.ObserveInt64(m.queueLength, int64(queue.PendingCount())) - observer.ObserveInt64(m.activeBuilds, int64(queue.ActiveCount())) - return nil - }, - m.queueLength, - m.activeBuilds, - ) - return err -} - diff --git a/lib/builds/queue.go b/lib/builds/queue.go deleted file mode 100644 index 2fee288b..00000000 --- a/lib/builds/queue.go +++ /dev/null @@ -1,171 +0,0 @@ -package builds - -import "sync" - -// QueuedBuild represents a build waiting to be executed -type QueuedBuild struct { - BuildID string - Request CreateBuildRequest - StartFn func() -} - -// BuildQueue manages concurrent builds with a configurable limit. -// Following the pattern from lib/images/queue.go. -// -// Design notes (see plan for full context): -// - Queue state is in-memory (lost on restart) -// - Build metadata is persisted to disk -// - On startup, pending builds are recovered via listPendingBuilds() -// -// Future migration path if needed: -// - Add BuildQueue interface with Enqueue/Dequeue/Ack/Nack -// - Implement adapters: memoryQueue, redisQueue, natsQueue -// - Use BUILD_QUEUE_BACKEND env var to select implementation -type BuildQueue struct { - maxConcurrent int - active map[string]bool - pending []QueuedBuild - mu sync.Mutex -} - -// NewBuildQueue creates a new build queue with the given concurrency limit -func NewBuildQueue(maxConcurrent int) *BuildQueue { - if maxConcurrent < 1 { - maxConcurrent = 1 - } - return &BuildQueue{ - maxConcurrent: maxConcurrent, - active: make(map[string]bool), - pending: make([]QueuedBuild, 0), - } -} - -// Enqueue adds a build to the queue. Returns queue position (0 if started immediately, >0 if queued). -// If the build is already building or queued, returns its current position without re-enqueueing. -func (q *BuildQueue) Enqueue(buildID string, req CreateBuildRequest, startFn func()) int { - q.mu.Lock() - defer q.mu.Unlock() - - // Check if already building (position 0, actively running) - if q.active[buildID] { - return 0 - } - - // Check if already in pending queue - for i, build := range q.pending { - if build.BuildID == buildID { - return i + 1 // Return existing queue position - } - } - - // Wrap the function to auto-complete - wrappedFn := func() { - defer q.MarkComplete(buildID) - startFn() - } - - build := QueuedBuild{ - BuildID: buildID, - Request: req, - StartFn: wrappedFn, - } - - // Start immediately if under concurrency limit - if len(q.active) < q.maxConcurrent { - q.active[buildID] = true - go wrappedFn() - return 0 - } - - // Otherwise queue it - q.pending = append(q.pending, build) - return len(q.pending) -} - -// MarkComplete marks a build as complete and starts the next pending build if any -func (q *BuildQueue) MarkComplete(buildID string) { - q.mu.Lock() - defer q.mu.Unlock() - - delete(q.active, buildID) - - // Start next pending build if we have capacity - if len(q.pending) > 0 && len(q.active) < q.maxConcurrent { - next := q.pending[0] - q.pending = q.pending[1:] - q.active[next.BuildID] = true - go next.StartFn() - } -} - -// GetPosition returns the queue position for a build. -// Returns nil if the build is actively running or not in queue. -func (q *BuildQueue) GetPosition(buildID string) *int { - q.mu.Lock() - defer q.mu.Unlock() - - if q.active[buildID] { - return nil // Actively running, not queued - } - - for i, build := range q.pending { - if build.BuildID == buildID { - pos := i + 1 - return &pos - } - } - - return nil // Not in queue -} - -// Cancel removes a build from the pending queue. -// Returns true if the build was cancelled, false if it was not in the queue -// (already running or not found). -func (q *BuildQueue) Cancel(buildID string) bool { - q.mu.Lock() - defer q.mu.Unlock() - - // Can't cancel if actively running - if q.active[buildID] { - return false - } - - // Find and remove from pending - for i, build := range q.pending { - if build.BuildID == buildID { - q.pending = append(q.pending[:i], q.pending[i+1:]...) - return true - } - } - - return false -} - -// IsActive returns true if the build is actively running -func (q *BuildQueue) IsActive(buildID string) bool { - q.mu.Lock() - defer q.mu.Unlock() - return q.active[buildID] -} - -// ActiveCount returns the number of actively building builds -func (q *BuildQueue) ActiveCount() int { - q.mu.Lock() - defer q.mu.Unlock() - return len(q.active) -} - -// PendingCount returns the number of queued builds -func (q *BuildQueue) PendingCount() int { - q.mu.Lock() - defer q.mu.Unlock() - return len(q.pending) -} - -// QueueLength returns the total number of builds (active + pending) -func (q *BuildQueue) QueueLength() int { - q.mu.Lock() - defer q.mu.Unlock() - return len(q.active) + len(q.pending) -} - diff --git a/lib/builds/queue_test.go b/lib/builds/queue_test.go deleted file mode 100644 index 5f5dd9af..00000000 --- a/lib/builds/queue_test.go +++ /dev/null @@ -1,230 +0,0 @@ -package builds - -import ( - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestBuildQueue_EnqueueStartsImmediately(t *testing.T) { - queue := NewBuildQueue(2) - - started := make(chan string, 2) - done := make(chan struct{}) - - // Enqueue first build - should start immediately - pos := queue.Enqueue("build-1", CreateBuildRequest{}, func() { - started <- "build-1" - <-done // Wait for signal - }) - - assert.Equal(t, 0, pos, "first build should start immediately (position 0)") - - // Wait for it to start - select { - case id := <-started: - assert.Equal(t, "build-1", id) - case <-time.After(time.Second): - t.Fatal("build-1 did not start") - } - - close(done) -} - -func TestBuildQueue_QueueWhenAtCapacity(t *testing.T) { - queue := NewBuildQueue(1) // Max 1 concurrent - - var wg sync.WaitGroup - done := make(chan struct{}) - - // Start first build - wg.Add(1) - pos1 := queue.Enqueue("build-1", CreateBuildRequest{}, func() { - wg.Done() - <-done // Block - }) - assert.Equal(t, 0, pos1) - - // Wait for first to actually start - wg.Wait() - - // Second build should be queued - pos2 := queue.Enqueue("build-2", CreateBuildRequest{}, func() {}) - assert.Equal(t, 1, pos2, "second build should be queued at position 1") - - // Third build should be queued at position 2 - pos3 := queue.Enqueue("build-3", CreateBuildRequest{}, func() {}) - assert.Equal(t, 2, pos3, "third build should be queued at position 2") - - close(done) -} - -func TestBuildQueue_DeduplicationActive(t *testing.T) { - queue := NewBuildQueue(2) - done := make(chan struct{}) - - // Start a build - queue.Enqueue("build-1", CreateBuildRequest{}, func() { - <-done - }) - - // Wait for it to become active - time.Sleep(10 * time.Millisecond) - - // Try to enqueue the same build again - should return position 0 (active) - pos := queue.Enqueue("build-1", CreateBuildRequest{}, func() {}) - assert.Equal(t, 0, pos, "re-enqueueing active build should return position 0") - - close(done) -} - -func TestBuildQueue_DeduplicationPending(t *testing.T) { - queue := NewBuildQueue(1) - done := make(chan struct{}) - - // Fill the queue - queue.Enqueue("build-1", CreateBuildRequest{}, func() { - <-done - }) - - // Add a second build to pending - pos1 := queue.Enqueue("build-2", CreateBuildRequest{}, func() {}) - assert.Equal(t, 1, pos1) - - // Try to enqueue build-2 again - should return same position - pos2 := queue.Enqueue("build-2", CreateBuildRequest{}, func() {}) - assert.Equal(t, 1, pos2, "re-enqueueing pending build should return same position") - - close(done) -} - -func TestBuildQueue_Cancel(t *testing.T) { - queue := NewBuildQueue(1) - done := make(chan struct{}) - - // Fill the queue - queue.Enqueue("build-1", CreateBuildRequest{}, func() { - <-done - }) - - // Add to pending - queue.Enqueue("build-2", CreateBuildRequest{}, func() {}) - queue.Enqueue("build-3", CreateBuildRequest{}, func() {}) - - // Cancel build-2 - cancelled := queue.Cancel("build-2") - require.True(t, cancelled, "should be able to cancel pending build") - - // Verify build-3 moved up - pos := queue.GetPosition("build-3") - require.NotNil(t, pos) - assert.Equal(t, 1, *pos, "build-3 should move to position 1") - - // Can't cancel active build - cancelled = queue.Cancel("build-1") - assert.False(t, cancelled, "should not be able to cancel active build") - - close(done) -} - -func TestBuildQueue_GetPosition(t *testing.T) { - queue := NewBuildQueue(1) - done := make(chan struct{}) - - queue.Enqueue("build-1", CreateBuildRequest{}, func() { - <-done - }) - queue.Enqueue("build-2", CreateBuildRequest{}, func() {}) - queue.Enqueue("build-3", CreateBuildRequest{}, func() {}) - - // Active build has no position (returns nil) - pos1 := queue.GetPosition("build-1") - assert.Nil(t, pos1, "active build should have no position") - - // Pending builds have positions - pos2 := queue.GetPosition("build-2") - require.NotNil(t, pos2) - assert.Equal(t, 1, *pos2) - - pos3 := queue.GetPosition("build-3") - require.NotNil(t, pos3) - assert.Equal(t, 2, *pos3) - - // Non-existent build has no position - pos4 := queue.GetPosition("build-4") - assert.Nil(t, pos4) - - close(done) -} - -func TestBuildQueue_AutoStartNextOnComplete(t *testing.T) { - queue := NewBuildQueue(1) - - started := make(chan string, 3) - var mu sync.Mutex - completionOrder := []string{} - - // Add builds - queue.Enqueue("build-1", CreateBuildRequest{}, func() { - started <- "build-1" - time.Sleep(10 * time.Millisecond) - mu.Lock() - completionOrder = append(completionOrder, "build-1") - mu.Unlock() - }) - queue.Enqueue("build-2", CreateBuildRequest{}, func() { - started <- "build-2" - time.Sleep(10 * time.Millisecond) - mu.Lock() - completionOrder = append(completionOrder, "build-2") - mu.Unlock() - }) - - // Wait for both to complete - for i := 0; i < 2; i++ { - select { - case <-started: - case <-time.After(2 * time.Second): - t.Fatal("builds did not complete in time") - } - } - - // Give time for completion - time.Sleep(50 * time.Millisecond) - - mu.Lock() - defer mu.Unlock() - assert.Equal(t, []string{"build-1", "build-2"}, completionOrder) -} - -func TestBuildQueue_Counts(t *testing.T) { - queue := NewBuildQueue(2) - - assert.Equal(t, 0, queue.ActiveCount()) - assert.Equal(t, 0, queue.PendingCount()) - assert.Equal(t, 0, queue.QueueLength()) - - done := make(chan struct{}) - queue.Enqueue("build-1", CreateBuildRequest{}, func() { <-done }) - queue.Enqueue("build-2", CreateBuildRequest{}, func() { <-done }) - - // Wait for them to start - time.Sleep(10 * time.Millisecond) - - assert.Equal(t, 2, queue.ActiveCount()) - assert.Equal(t, 0, queue.PendingCount()) - assert.Equal(t, 2, queue.QueueLength()) - - // Add a pending one - queue.Enqueue("build-3", CreateBuildRequest{}, func() {}) - - assert.Equal(t, 2, queue.ActiveCount()) - assert.Equal(t, 1, queue.PendingCount()) - assert.Equal(t, 3, queue.QueueLength()) - - close(done) -} - diff --git a/lib/builds/storage.go b/lib/builds/storage.go index 777382bb..69bec3fd 100644 --- a/lib/builds/storage.go +++ b/lib/builds/storage.go @@ -122,7 +122,7 @@ func listAllBuilds(p *paths.Paths) ([]*buildMetadata, error) { } // listPendingBuilds returns builds that need to be recovered on startup -// Returns builds with status queued/building, sorted by created_at (oldest first for FIFO) +// Returns builds with status building/pushing, sorted by created_at (oldest first for FIFO) func listPendingBuilds(p *paths.Paths) ([]*buildMetadata, error) { all, err := listAllBuilds(p) if err != nil { @@ -132,7 +132,7 @@ func listPendingBuilds(p *paths.Paths) ([]*buildMetadata, error) { var pending []*buildMetadata for _, meta := range all { switch meta.Status { - case StatusQueued, StatusBuilding, StatusPushing: + case StatusBuilding, StatusPushing: pending = append(pending, meta) } } diff --git a/lib/builds/types.go b/lib/builds/types.go index 08b05881..6208fce0 100644 --- a/lib/builds/types.go +++ b/lib/builds/types.go @@ -6,7 +6,6 @@ import "time" // Build status constants const ( - StatusQueued = "queued" StatusBuilding = "building" StatusPushing = "pushing" StatusReady = "ready" diff --git a/lib/oapi/oapi.go b/lib/oapi/oapi.go index 4171b96e..05a4314a 100644 --- a/lib/oapi/oapi.go +++ b/lib/oapi/oapi.go @@ -42,7 +42,6 @@ const ( BuildStatusCancelled BuildStatus = "cancelled" BuildStatusFailed BuildStatus = "failed" BuildStatusPushing BuildStatus = "pushing" - BuildStatusQueued BuildStatus = "queued" BuildStatusReady BuildStatus = "ready" ) @@ -153,7 +152,8 @@ type Build struct { ImageRef *string `json:"image_ref"` Provenance *BuildProvenance `json:"provenance,omitempty"` - // QueuePosition Position in build queue (only when status is queued) + // QueuePosition Deprecated - always null. Builds now start immediately. + // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set QueuePosition *int `json:"queue_position"` // StartedAt Build start timestamp @@ -3005,6 +3005,7 @@ type CreateBuildResponse struct { JSON400 *Error JSON401 *Error JSON500 *Error + JSON503 *Error } // Status returns HTTPResponse.Status @@ -4303,6 +4304,13 @@ func ParseCreateBuildResponse(rsp *http.Response) (*CreateBuildResponse, error) } response.JSON500 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON503 = &dest + } return response, nil @@ -7409,6 +7417,23 @@ func (response CreateBuild500JSONResponse) VisitCreateBuildResponse(w http.Respo return json.NewEncoder(w).Encode(response) } +type CreateBuild503ResponseHeaders struct { + RetryAfter int +} + +type CreateBuild503JSONResponse struct { + Body Error + Headers CreateBuild503ResponseHeaders +} + +func (response CreateBuild503JSONResponse) VisitCreateBuildResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Retry-After", fmt.Sprint(response.Headers.RetryAfter)) + w.WriteHeader(503) + + return json.NewEncoder(w).Encode(response.Body) +} + type CancelBuildRequestObject struct { Id string `json:"id"` } @@ -9889,143 +9914,145 @@ func (sh *strictHandler) GetVolume(w http.ResponseWriter, r *http.Request, id st // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "go9vP/6/AAAA///Bm92RVcUAAA==", + "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", + "FzVsxgAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/openapi.yaml b/openapi.yaml index bc20590f..8972b68c 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -685,7 +685,7 @@ components: BuildStatus: type: string - enum: [queued, building, pushing, ready, failed, cancelled] + enum: [building, pushing, ready, failed, cancelled] description: Build job status BuildPolicy: @@ -762,8 +762,9 @@ components: $ref: "#/components/schemas/BuildStatus" queue_position: type: integer - description: Position in build queue (only when status is queued) + description: Deprecated - always null. Builds now start immediately. nullable: true + deprecated: true image_digest: type: string description: Digest of built image (only when status is ready) @@ -2119,7 +2120,7 @@ paths: Example: [{"id": "npm_token"}, {"id": "github_token"}] responses: 202: - description: Build created and queued + description: Build created and started content: application/json: schema: @@ -2142,6 +2143,17 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + 503: + description: Service temporarily unavailable - insufficient resources + headers: + Retry-After: + description: Suggested retry delay in seconds + schema: + type: integer + content: + application/json: + schema: + $ref: "#/components/schemas/Error" /builds/{id}: get: