From e04151e0f346abe2a98ad664f5139e6aada73d87 Mon Sep 17 00:00:00 2001 From: "prath.shenoy" Date: Mon, 28 Sep 2026 22:44:49 +0000 Subject: [PATCH] feat(stovepipe): Store terminal build ID **What**: - Add the terminal build identity to each completed request. - Persist the identity with the request lifecycle state. **Why**: - Make the selected build directly available to request consumers. - Keep the schema change additive and migration-compatible. --- stovepipe/entity/request.go | 3 ++ .../extension/storage/mysql/request_store.go | 13 ++++-- .../storage/mysql/request_store_test.go | 46 ++++++++++--------- .../storage/mysql/schema/request.sql | 15 +++--- 4 files changed, 43 insertions(+), 34 deletions(-) diff --git a/stovepipe/entity/request.go b/stovepipe/entity/request.go index efd5d64fa..00e99e8e6 100644 --- a/stovepipe/entity/request.go +++ b/stovepipe/entity/request.go @@ -116,6 +116,9 @@ type Request struct { // State is the current state of the request in the pipeline. State RequestState `json:"state"` + // TerminalBuildID is the build that established this request's terminal + // state. It is empty when no build established the terminal state. + TerminalBuildID string `json:"terminal_build_id"` // Version is the version of the object. It is used for optimistic locking. // Versioning starts at 1 and is incremented for each change to the object. Version int32 `json:"version"` diff --git a/stovepipe/extension/storage/mysql/request_store.go b/stovepipe/extension/storage/mysql/request_store.go index 0c2492b13..e9a0e0194 100644 --- a/stovepipe/extension/storage/mysql/request_store.go +++ b/stovepipe/extension/storage/mysql/request_store.go @@ -82,7 +82,7 @@ func (r *requestStore) Get(ctx context.Context, id string) (ret entity.Request, var req entity.Request err := r.db.QueryRowContext(ctx, - `SELECT id, queue, uri, state, build_strategy, base_uri, version + `SELECT id, queue, uri, state, build_strategy, base_uri, COALESCE(terminal_build_id, ''), version FROM request WHERE queue = ? AND id = ?`, r.queue, id, ).Scan( @@ -92,6 +92,7 @@ func (r *requestStore) Get(ctx context.Context, id string) (ret entity.Request, &req.State, &req.BuildStrategy, &req.BaseURI, + &req.TerminalBuildID, &req.Version, ) @@ -105,9 +106,10 @@ func (r *requestStore) Get(ctx context.Context, id string) (ret entity.Request, return req, nil } -// Update persists the mutable fields of request (uri, state, build_strategy, base_uri) if the -// oldVersion, writing newVersion. Returns ErrVersionMismatch if the stored version does not match -// (including when the request does not exist). This is a pure conditional write; the caller owns +// Update persists the mutable fields of request (URI, state, build strategy, +// base URI, terminal build ID) if the oldVersion, writing newVersion. Returns +// ErrVersionMismatch if the stored version does not match (including when the +// request does not exist). This is a pure conditional write; the caller owns // version arithmetic. func (r *requestStore) Update(ctx context.Context, request entity.Request, oldVersion, newVersion int32) (retErr error) { op := metrics.Begin(r.scope, "update", metrics.StorageLatencyBuckets) @@ -119,12 +121,13 @@ func (r *requestStore) Update(ctx context.Context, request entity.Request, oldVe result, err := r.db.ExecContext(ctx, `UPDATE request - SET uri = ?, state = ?, build_strategy = ?, base_uri = ?, version = ? + SET uri = ?, state = ?, build_strategy = ?, base_uri = ?, terminal_build_id = NULLIF(?, ''), version = ? WHERE queue = ? AND id = ? AND version = ?`, request.URI, request.State, request.BuildStrategy, request.BaseURI, + request.TerminalBuildID, newVersion, request.Queue, request.ID, diff --git a/stovepipe/extension/storage/mysql/request_store_test.go b/stovepipe/extension/storage/mysql/request_store_test.go index 0fc657cfa..ab912136e 100644 --- a/stovepipe/extension/storage/mysql/request_store_test.go +++ b/stovepipe/extension/storage/mysql/request_store_test.go @@ -110,13 +110,14 @@ func TestRequestStore_Create(t *testing.T) { func TestRequestStore_Get(t *testing.T) { want := entity.Request{ - ID: "request/monorepo/main/1", - Queue: "monorepo/main", - URI: "git://remote/monorepo/main/deadbeef", - State: entity.RequestStateProcessing, - BuildStrategy: entity.BuildStrategyFull, - BaseURI: "", - Version: 2, + ID: "request/monorepo/main/1", + Queue: "monorepo/main", + URI: "git://remote/monorepo/main/deadbeef", + State: entity.RequestStateProcessing, + BuildStrategy: entity.BuildStrategyFull, + BaseURI: "", + TerminalBuildID: "bk-1001", + Version: 2, } tests := []struct { @@ -131,9 +132,9 @@ func TestRequestStore_Get(t *testing.T) { name: "found", id: want.ID, setup: func(mock sqlmock.Sqlmock) { - rows := sqlmock.NewRows([]string{"id", "queue", "uri", "state", "build_strategy", "base_uri", "version"}). - AddRow(want.ID, want.Queue, want.URI, string(want.State), string(want.BuildStrategy), want.BaseURI, want.Version) - mock.ExpectQuery("SELECT id, queue, uri, state, build_strategy, base_uri, version"). + rows := sqlmock.NewRows([]string{"id", "queue", "uri", "state", "build_strategy", "base_uri", "terminal_build_id", "version"}). + AddRow(want.ID, want.Queue, want.URI, string(want.State), string(want.BuildStrategy), want.BaseURI, want.TerminalBuildID, want.Version) + mock.ExpectQuery("SELECT id, queue, uri, state, build_strategy, base_uri, COALESCE\\(terminal_build_id, ''\\), version"). WithArgs("monorepo/main", want.ID). WillReturnRows(rows) }, @@ -143,7 +144,7 @@ func TestRequestStore_Get(t *testing.T) { name: "not found", id: "missing", setup: func(mock sqlmock.Sqlmock) { - mock.ExpectQuery("SELECT id, queue, uri, state, build_strategy, base_uri, version"). + mock.ExpectQuery("SELECT id, queue, uri, state, build_strategy, base_uri, COALESCE\\(terminal_build_id, ''\\), version"). WithArgs("monorepo/main", "missing"). WillReturnError(sql.ErrNoRows) }, @@ -154,7 +155,7 @@ func TestRequestStore_Get(t *testing.T) { name: "query error", id: "bad", setup: func(mock sqlmock.Sqlmock) { - mock.ExpectQuery("SELECT id, queue, uri, state, build_strategy, base_uri, version"). + mock.ExpectQuery("SELECT id, queue, uri, state, build_strategy, base_uri, COALESCE\\(terminal_build_id, ''\\), version"). WithArgs("monorepo/main", "bad"). WillReturnError(fmt.Errorf("connection reset")) }, @@ -186,12 +187,13 @@ func TestRequestStore_Get(t *testing.T) { func TestRequestStore_Update(t *testing.T) { request := entity.Request{ - ID: "request/monorepo/main/1", - Queue: "monorepo/main", - URI: "git://remote/monorepo/main/deadbeef", - State: entity.RequestStateProcessing, - BuildStrategy: entity.BuildStrategyFull, - BaseURI: "", + ID: "request/monorepo/main/1", + Queue: "monorepo/main", + URI: "git://remote/monorepo/main/deadbeef", + State: entity.RequestStateProcessing, + BuildStrategy: entity.BuildStrategyFull, + BaseURI: "", + TerminalBuildID: "bk-1001", } const oldVersion, newVersion = int32(1), int32(2) @@ -205,7 +207,7 @@ func TestRequestStore_Update(t *testing.T) { name: "success", setup: func(mock sqlmock.Sqlmock) { mock.ExpectExec("UPDATE request"). - WithArgs(request.URI, request.State, request.BuildStrategy, request.BaseURI, newVersion, request.Queue, request.ID, oldVersion). + WithArgs(request.URI, request.State, request.BuildStrategy, request.BaseURI, request.TerminalBuildID, newVersion, request.Queue, request.ID, oldVersion). WillReturnResult(sqlmock.NewResult(0, 1)) }, }, @@ -213,7 +215,7 @@ func TestRequestStore_Update(t *testing.T) { name: "version mismatch", setup: func(mock sqlmock.Sqlmock) { mock.ExpectExec("UPDATE request"). - WithArgs(request.URI, request.State, request.BuildStrategy, request.BaseURI, newVersion, request.Queue, request.ID, oldVersion). + WithArgs(request.URI, request.State, request.BuildStrategy, request.BaseURI, request.TerminalBuildID, newVersion, request.Queue, request.ID, oldVersion). WillReturnResult(sqlmock.NewResult(0, 0)) }, wantErr: true, @@ -223,7 +225,7 @@ func TestRequestStore_Update(t *testing.T) { name: "exec error", setup: func(mock sqlmock.Sqlmock) { mock.ExpectExec("UPDATE request"). - WithArgs(request.URI, request.State, request.BuildStrategy, request.BaseURI, newVersion, request.Queue, request.ID, oldVersion). + WithArgs(request.URI, request.State, request.BuildStrategy, request.BaseURI, request.TerminalBuildID, newVersion, request.Queue, request.ID, oldVersion). WillReturnError(fmt.Errorf("connection reset")) }, wantErr: true, @@ -232,7 +234,7 @@ func TestRequestStore_Update(t *testing.T) { name: "rows affected error", setup: func(mock sqlmock.Sqlmock) { mock.ExpectExec("UPDATE request"). - WithArgs(request.URI, request.State, request.BuildStrategy, request.BaseURI, newVersion, request.Queue, request.ID, oldVersion). + WithArgs(request.URI, request.State, request.BuildStrategy, request.BaseURI, request.TerminalBuildID, newVersion, request.Queue, request.ID, oldVersion). WillReturnResult(sqlmock.NewErrorResult(fmt.Errorf("driver error"))) }, wantErr: true, diff --git a/stovepipe/extension/storage/mysql/schema/request.sql b/stovepipe/extension/storage/mysql/schema/request.sql index 14c766878..52ef9c8b6 100644 --- a/stovepipe/extension/storage/mysql/schema/request.sql +++ b/stovepipe/extension/storage/mysql/schema/request.sql @@ -2,12 +2,13 @@ -- VCS-agnostic commit locator; it may be empty until SourceControl resolution is wired in. -- No timestamps: created/updated times are not part of the Request entity. CREATE TABLE IF NOT EXISTS request ( - queue VARCHAR(255) NOT NULL, - id VARCHAR(255) NOT NULL, - uri VARCHAR(255) NOT NULL, - state VARCHAR(64) NOT NULL, - build_strategy VARCHAR(64) NOT NULL DEFAULT '', - base_uri VARCHAR(255) NOT NULL DEFAULT '', - version INT NOT NULL, + queue VARCHAR(255) NOT NULL, + id VARCHAR(255) NOT NULL, + uri VARCHAR(255) NOT NULL, + state VARCHAR(64) NOT NULL, + build_strategy VARCHAR(64) NOT NULL DEFAULT '', + base_uri VARCHAR(255) NOT NULL DEFAULT '', + version INT NOT NULL, + terminal_build_id VARCHAR(255) NULL, PRIMARY KEY (queue, id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;