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
feat(stovepipe): add and wire the buildsignal stage controller (#409)
## What?
Adds `stovepipe/controller/buildsignal` plus the new `Record` queue
message/proto and `TopicKeyRecord`. `Process` consumes a `BuildSignal`
(build id), loads `Build`+`Request`, no-ops if the request is terminal,
polls `buildRunner.Status`, persists a real status transition via CAS
(`BuildStore.Update`) with terminal status **write-once** (a later poll
of a flaky backend can never overwrite an already-committed terminal
status), and either reschedules itself via `PublishAfter` (5s while
`accepted`, 2s while `running`) or, once terminal, publishes to
`record`. Wires the controller into `main.go` and registers the
`buildsignal` (subscribed) and `record` (publish-only, no consumer yet)
topics.
## Test Plan
`make build`, `make test` - all clean.
## Issue
<!--
Link the issue here.
- Use 'Closes#123' if this is the final fix.
- Use 'Part of #123' or just '#123' if the feature is still in progress.
-->
- ErrNotFound -> return raw; non-retryable (storage is read-after-write consistent; see [storage README](stovepipe/extension/storage/README.md)).
24
24
- other store error -> return raw; classifier decides.
25
25
26
26
2. Load Request R = store.Get(Build.RequestID) — needed for R.Queue to resolve the build-runner.
27
-
- ErrNotFound -> retryable, like step 1: the Build's existence proves the Request write is older,
28
-
so a miss here is almost certainly a lagging read; redelivery converges. A genuinely orphaned
29
-
Build (integrity fault) still dead-letters at MaxAttempts — the same terminal outcome, without
30
-
rejecting straight to DLQ on a stale read.
27
+
- ErrNotFound -> return raw; non-retryable, same as step 1 — the Build's existence proves the
28
+
Request write is already committed, so a miss here is a storage defect, not a lagging read.
31
29
32
30
3. If R.State is terminal (superseded / recorded-green / recorded-not-green): ack and return.
33
31
- the Request is done (record already ran, or the head was superseded); stop polling.
@@ -102,19 +100,19 @@ Per `platform/errs`'s non-retryable-by-default rule (see [platform/errs/README.m
102
100
103
101
| Failure | Disposition | Why |
104
102
|---|---|---|
105
-
|`Build` not found | retryable (`errs.NewRetryableError`) |`build`'s `Create` not visible yet; redelivery converges. |
106
-
|`Request` not found | retryable (`errs.NewRetryableError`) | The Build's existence proves the Request write is older, so a miss is a stale read; a genuine orphan still dead-letters at `MaxAttempts`. |
107
103
|`Status` call | raw error; classifier decides | Deliberately left open rather than fixed either way — runner timeout/connection is transient, "runner not deployed for this queue" is not, and only a backend classifier can tell them apart. |
108
104
|`Update` CAS conflict (`ErrVersionMismatch`) | retryable | A concurrent (redelivered) writer moved the row; reload and re-check converges. |
109
105
|`PublishAfter` re-poll | retryable | The poll heartbeat; it runs only after status/persist/record all succeeded, so a transient enqueue blip is worth replaying to `MaxAttempts` before dead-lettering. |
110
106
107
+
`Build`/`Request` not found (`storage.ErrNotFound`) are **not** in this table: storage is required to be read-after-write consistent (see [storage README](stovepipe/extension/storage/README.md)), so a miss here is already the correct default (non-retryable, straight to DLQ) rather than a departure worth overriding.
108
+
111
109
Everything else — factory lookup, an `Update` store error other than a CAS conflict, and the publish to `record` — is returned raw with no override, because the default is already correct: a queue with no registered runner is a config error, and storage/queue failures dead-letter and let DLQ reconciliation recover.
112
110
113
111
## Idempotency
114
112
115
113
Every branch is safe under at-least-once redelivery:
116
114
117
-
-**Build not found** — retryable; converges as the row becomes visible.
115
+
-**Build 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.
118
116
-**Status already persisted** — a redelivery re-runs the whole algorithm from step 1, including a redundant `Status` poll (harmless — the runner reports the same thing); step 6 no-ops on the unchanged status, and the delivery proceeds to re-schedule the poll (non-terminal) or republish to `record` (terminal, idempotent). No corruption.
119
117
-**Terminal already published** — a redelivery reloads, re-polls, no-ops at step 6, republishes the same terminal signal to `record` (idempotent), and acks. Harmless.
120
118
-**`PublishAfter` failed, then retried** — the nacked delivery re-runs from step 1; there is no way to resume mid-algorithm, so it re-polls the runner too, but the row already carries the non-terminal status and step 6 no-ops. Only the final enqueue does new work.
0 commit comments