You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
- the row carries no scope; it is recoverable from the Request's immutable fields
58
+
(see the entity table).
57
59
- a crash between step 5 and this write orphans the triggered build (see Idempotency).
58
60
- ErrAlreadyExists -> benign (reachable only with a backend that returns deterministic ids
59
61
for retried triggers); continue to step 7.
@@ -78,7 +80,7 @@ Every branch is safe under at-least-once redelivery — with SubmitQueue's postu
78
80
-**Request not found** — non-retryable; storage's read-after-write guarantee means a miss here is a storage defect, not a lag condition to retry through.
79
81
-**Strategy not yet visible** — retryable; the producing stage's write is not visible on this reader yet.
80
82
-**Request already terminal** (step 2) — ack, no build. A redelivery after `record` finished, or after `process` superseded the head, never starts a stale build.
81
-
-**Redelivery while the Request is still in flight** (crash or failure anywhere in steps 5–8) — the redelivery re-runs from step 1, `Trigger` mints a fresh id, `Create` persists a second `Build` row, and a second poll loop starts. Harmless, in three layers: both builds target the identical `(headURI, baseURI)` scope; each `Build` polls in its own partition and `buildsignal` short-circuits the moment the Request goes terminal (its step 3); and `record`'s terminal transition is CAS-guarded, so the second verdict is a no-op. A build triggered but never persisted (crash between steps 5 and 6) is the same story minus the row: an orphan the runner finishes and nobody ever reads. Wasted CI compute, not a correctness risk — the same accepted trade as SubmitQueue.
83
+
-**Redelivery while the Request is still in flight** (crash or failure anywhere in steps 5–8) — the redelivery re-runs from step 1, `Trigger` mints a fresh id, `Create` persists a second `Build` row, and a second poll loop starts. Harmless, in three layers: both builds target the identical `(headURI, baseURI)` scope; each `Build` polls in its own partition and `buildsignal` short-circuits the moment the Request goes terminal (its step 3); and `buildsignal`'s outcome write is first-writer-wins, so the second verdict cannot flip the Request's state or overwrite the create-only validation fact. A build triggered but never persisted (crash between steps 5 and 6) is the same story minus the row: an orphan the runner finishes and nobody ever reads. Wasted CI compute, not a correctness risk — the same accepted trade as SubmitQueue.
82
84
-**Trigger / publish / other store failure** — nothing durable is left half-written that a redelivery can't reconcile; the error rejects to DLQ, and the fail-closed reconciler drives the Request terminal (see [workflow.md](doc/rfc/stovepipe/workflow.md#fail-closed-on-unprocessable-work)).
83
85
84
86
## Edge cases
@@ -128,7 +130,7 @@ The batches are **identity** — thin references carrying ids, not change conten
128
130
Stovepipe validates **one commit** against a baseline (or in full). Its `build` controller reads two opaque URIs off the `Request` and triggers:
There is no batch, no dependency list, and nothing to resolve — the URIs *are* the identity, owned by `SourceControl`. `process` already decided incremental-vs-full; `build` just reads `R.BuildStrategy`/`R.BaseURI` and acts.
@@ -153,16 +155,16 @@ type BuildRunner interface {
153
155
// Trigger starts a new build every call and mints the build's identity —
154
156
// there is no caller-supplied dedup input, matching SubmitQueue's contract
155
157
// exactly (see "Alternatives considered for the build identity" below
156
-
// for other shapes this doc considered). headURI is the commit
157
-
//under validation; baseURI is the incremental baseline (empty for a full
158
-
//build). metadata is caller annotations the runner may echo but must not
158
+
// for other shapes this doc considered). baseURI is the incremental
159
+
// baseline (empty for a full build); headURI is the commit under
160
+
//validation. metadata is caller annotations the runner may echo but must not
159
161
// depend on — empty today, but expected to carry real data eventually (e.g.
160
162
// conflict-graph info, or other upstream decisions relevant to the build)
161
163
// once a concrete need lands in either domain; the shape is deferred until
162
164
// then, not decided here. Runner-side work is async; callers learn progress
163
165
// via Status.
164
166
// Returns the runner-assigned build id, which the caller adopts as Build.ID.
The shape isn't decided here because project semantics belong to `analyze`, not `build`: how a project maps to a buildable scope (a Bazel target pattern, a directory, a service name) is implementer-specific per [workflow.md](doc/rfc/stovepipe/workflow.md#project---greenness-at-a-finer-grain). The expectation is that this stays an opaque token — following the same "identity in, resolve internally" shape already used for `headURI`/`baseURI` (owned and interpreted by `SourceControl`) — that `build` reads off the `Request`/message and hands to the runner uninterpreted, rather than a structured type `build` would have to understand:
`ProjectScope` lives in `stovepipe/entity` alongside `BuildID`/`BuildStatus`/`BuildMetadata` — projects have no SubmitQueue equivalent at all, not even a shape to mirror. Its zero value covers Phase 1 (no project — whole-repo/incremental scope only, exactly today's sketch); `analyze` is what would populate a non-zero value for Phase 2. This mirrors the additive optional field already reserved on `BuildRequest` for the same purpose (see [Queue contract additions](#queue-contract-additions)) — the wire message and the extension contract need the same new dimension, and both are deferred to the same design.
@@ -198,7 +200,7 @@ Both `Trigger` and `Status`/`Cancel` differ *in contract* between domains, even
198
200
199
201
There is exactly one build id: the runner mints it at `Trigger`, `build` adopts it as `Build.ID`, and every later call and message carries it verbatim — `Status`/`Cancel` take the same value `Trigger` returned, the queue payload is the same value, the store key is the same value. This is SubmitQueue's convention end to end. The id is opaque: no stovepipe reader parses it, derives it, or equates it with another entity's id — the trap SubmitQueue's speculate/cancel path falls into. And per the extension rules a runner keeps only transient local state, so the durable `Request` ↔ `Build` linkage lives in **our** store as `Build.RequestID`, never in the runner.
200
202
201
-
Supporting entity types: `BuildStatus`, `BuildMetadata`, and `BuildID` live in `stovepipe/entity`, shaped the same as SubmitQueue's `submitqueue/entity` equivalents but defined and duplicated locally rather than shared — `BuildStatus` is the narrow lowercase enum `"" (unknown) / accepted / running / succeeded / failed / cancelled` with an `IsTerminal()` predicate covering the last three, `BuildMetadata` is the free-form `map[string]string`, and `BuildID` is a `{ID string}` wire struct wrapping the one runner-assigned id everywhere it appears — `Trigger`'s return, `Status`/`Cancel`'s parameter, the queue payload. `stovepipe/entity/build.go` keeps what's stovepipe-specific: the `Build` entity itself (`RequestID`/`URI`/`BaseURI` alongside `ID`/`Status`/`Version`). How a target graph reaches `analyze` is out of scope for this doc — left to the `analyze` design.
203
+
Supporting entity types: `BuildStatus`, `BuildMetadata`, and `BuildID` live in `stovepipe/entity`, shaped the same as SubmitQueue's `submitqueue/entity` equivalents but defined and duplicated locally rather than shared — `BuildStatus` is the narrow lowercase enum `"" (unknown) / accepted / running / succeeded / failed / cancelled` with an `IsTerminal()` predicate covering the last three, `BuildMetadata` is the free-form `map[string]string`, and `BuildID` is a `{ID string}` wire struct wrapping the one runner-assigned id everywhere it appears — `Trigger`'s return, `Status`/`Cancel`'s parameter, the queue payload. `stovepipe/entity/build.go` keeps what's stovepipe-specific: the `Build` entity itself (`RequestID` alongside `ID`/`Status`/`Version`). How a target graph reaches `analyze` is out of scope for this doc — left to the `analyze` design.
202
204
203
205
### Alternatives considered for sharing the contract
204
206
@@ -210,7 +212,7 @@ Several shapes for sharing the `BuildRunner` contract across domains were raised
210
212
// package platform/extension/buildrunner
211
213
typeBuildRunnerinterface {
212
214
Trigger(ctx context.Context, base []entity.Batch, head entity.Batch, metadata entity.BuildMetadata) (entity.BuildID, error)
@@ -289,18 +291,18 @@ Either could be adopted independently: the idempotency token, if a backend that
289
291
290
292
## Entity and storage additions needed
291
293
292
-
**`Build` entity** (`stovepipe/entity/build.go`), following the immutable-except-`Status`/`Version` shape of `entity.Request`; `ID` and `Status` use the stovepipe-local `BuildID`/`BuildStatus` types (see the [contract sketch](#stovepipe-buildrunner-contract-design-sketch)), while `RequestID`/`URI`/`BaseURI` stay stovepipe-specific:
294
+
**`Build` entity** (`stovepipe/entity/build.go`), following the immutable-except-`Status`/`Version` shape of `entity.Request`; `ID` and `Status` use the stovepipe-local `BuildID`/`BuildStatus` types (see the [contract sketch](#stovepipe-buildrunner-contract-design-sketch)), while `RequestID` stays stovepipe-specific:
293
295
294
296
295
297
| Field | Role | Mutable? |
296
298
|---|---|---|
297
299
|`ID`| The build's own key — the runner-assigned id returned by `Trigger` (a Buildkite build number, a CI-gateway job id); opaque, never parsed or derived | no |
298
300
|`RequestID`| The `Request` this build validates (`Build`→`Request` navigation) | no |
299
-
|`URI`| Head URI being built (`== Request.URI`) | no |
300
-
|`BaseURI`| Incremental baseline; empty for full builds | no |
|`Version`|`int32` optimistic-locking version |**yes** — with `Status`|
303
303
304
+
The row deliberately carries **no scope**: `R.URI`, `R.BaseURI`, and `R.BuildStrategy` — immutable and reachable through `RequestID` — fully determine what a build ran against, and keeping one copy avoids two rows that can disagree. Phase 2 is where that stops being free: several project builds share one `RequestID`, so whatever discriminator distinguishes their rows (a project id on `Build`, or resolution through `analyze`'s own state) is part of the deferred project-scoped trigger design, alongside the per-signal message id [record.md](record.md#message-queue-additions) needs for the same reason.
0 commit comments