feat(speculation): route builds through the tree via prioritize#365
Open
behinddwalls wants to merge 1 commit into
Open
feat(speculation): route builds through the tree via prioritize#365behinddwalls wants to merge 1 commit into
behinddwalls wants to merge 1 commit into
Conversation
## Summary ### Why? The speculate controller previously drove a batch's own build directly (publish to build, CAS to Speculating) while the speculation tree it maintained was pure shadow-mode bookkeeping nobody read. To make the tree the actual source of truth for what gets built, the pipeline needs to flip: speculate hands off to prioritize, and prioritize's admission decisions (Prioritized paths) are what the build stage acts on. This also completes the ownership rule this stack is building toward: the build stage becomes the sole owner of runner interaction for path-level work. Other stages (prioritize's preemptive eviction, speculate's reconcile for dead paths, both landing in a later commit) only ever record a cancel decision as intent (SpeculationPathStatusCancelling) on the tree; nothing but the build stage calls BuildRunner.Cancel. ### What? speculate.go: speculateBatch no longer publishes to build. It CASes Created/Scored batches to Speculating, then publishes the queue to prioritize on every pass (after the CAS, so a concurrent prioritize round never observes a batch as not-yet-Speculating with no later wake-up). The legacy tryFinalize step is otherwise unchanged and still only runs once a batch has reached Speculating on this or a prior pass, so merge timing is unchanged for now — tightening it to gate on the tree's own path outcome is deferred to a later commit in this stack. build.go is replaced with its tree-driven form: it loads the batch's speculation tree and, per path, either triggers a build for a Prioritized path with no build yet, or enacts a persisted Cancelling intent. Trigger dedup is on the path->build mapping (SpeculationPathBuildStore), checked before Trigger runs since it is readable up front unlike a Build row. The write order is Trigger -> Build.Create -> mapping.Create -> publish to buildsignal; a crash between Trigger and the mapping write is recovered by redelivery re-running the same path, and a lost mapping-create race defers to the winner (republishing buildsignal for its build) rather than erroring. A Prioritized path whose mapping already resolves to a non-terminal build republishes buildsignal to close the crash window between the original Create and its original publish; a terminal build is a no-op, since reconcile is what will fold that outcome into the path's status. The new Cancelling arm resolves the path's build via the same mapping, calls runner.Cancel if the build is not already terminal, and republishes buildsignal so the poll loop reacts promptly. Every lookup miss (no mapping yet, or a mapping pointing at a missing build row) is tolerated as a no-op rather than an error, leaving the intent for a later reconcile pass to settle. A missing speculation tree, by contrast, is an invariant violation — the tree is created before the batch is ever published to prioritize and is never deleted — so it surfaces as a plain error and dead-letters, failing the batch loudly instead of retrying against broken state. buildsignal.go picks up its RFC-final wording (build's ID doubling as the runner handle) plus a corrected error-classification doc paragraph — classification is per error cause via the wired classifier walk, not the per-call-site policy the old text described — with no behavior change; the test file is byte-identical to before. ## Test Plan ✅ `go build ./submitqueue/... ./service/...` ✅ `go vet ./submitqueue/... ./service/...` ✅ `bazel test //submitqueue/... //service/...` — 56/56 targets pass, including new build.go tests for the Cancelling arm (non-terminal enacts + republishes, terminal no-ops, no-mapping/dangling-mapping skip, runner error surfaces with no silent ack). ✅ `make gazelle` / `make fmt` — no diffs beyond the intended source changes. ✅ `make e2e-test` — 2/2 targets pass (stovepipe, submitqueue), confirming parity: prioritize still admits the single chain path and exactly one build runs per batch.
966e46a to
2334a19
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Why?
The speculate controller previously drove a batch's own build directly (publish to build, CAS to Speculating) while the speculation tree it maintained was pure shadow-mode bookkeeping nobody read. To make the tree the actual source of truth for what gets built, the pipeline needs to flip: speculate hands off to prioritize, and prioritize's admission decisions (Prioritized paths) are what the build stage acts on.
This also completes the ownership rule this stack is building toward: the build stage becomes the sole owner of runner interaction for path-level work. Other stages (prioritize's preemptive eviction, speculate's reconcile for dead paths, both landing in a later commit) only ever record a cancel decision as intent (SpeculationPathStatusCancelling) on the tree; nothing but the build stage calls BuildRunner.Cancel.
What?
speculate.go: speculateBatch no longer publishes to build. It CASes Created/Scored batches to Speculating, then publishes the queue to prioritize on every pass (after the CAS, so a concurrent prioritize round never observes a batch as not-yet-Speculating with no later wake-up). The legacy tryFinalize step is otherwise unchanged and still only runs once a batch has reached Speculating on this or a prior pass, so merge timing is unchanged for now — tightening it to gate on the tree's own path outcome is deferred to a later commit in this stack.
build.go is replaced with its tree-driven form: it loads the batch's speculation tree and, per path, either triggers a build for a Prioritized path with no build yet, or enacts a persisted Cancelling intent. Trigger dedup is on the path->build mapping (SpeculationPathBuildStore), checked before Trigger runs since it is readable up front unlike a Build row. The write order is Trigger -> Build.Create -> mapping.Create -> publish to buildsignal; a crash between Trigger and the mapping write is recovered by redelivery re-running the same path, and a lost mapping-create race defers to the winner (republishing buildsignal for its build) rather than erroring. A Prioritized path whose mapping already resolves to a non-terminal build republishes buildsignal to close the crash window between the original Create and its original publish; a terminal build is a no-op, since reconcile is what will fold that outcome into the path's status.
The new Cancelling arm resolves the path's build via the same mapping, calls runner.Cancel if the build is not already terminal, and republishes buildsignal so the poll loop reacts promptly. Every lookup miss (no mapping yet, or a mapping pointing at a missing build row) is tolerated as a no-op rather than an error, leaving the intent for a later reconcile pass to settle. A missing speculation tree, by contrast, is an invariant violation — the tree is created before the batch is ever published to prioritize and is never deleted — so it surfaces as a plain error and dead-letters, failing the batch loudly instead of retrying against broken state.
buildsignal.go picks up its RFC-final wording (build's ID doubling as the runner handle) plus a corrected error-classification doc paragraph — classification is per error cause via the wired classifier walk, not the per-call-site policy the old text described — with no behavior change; the test file is byte-identical to before.
Test Plan
✅
go build ./submitqueue/... ./service/...✅
go vet ./submitqueue/... ./service/...✅
bazel test //submitqueue/... //service/...— 56/56 targets pass, including new build.go tests for the Cancelling arm (non-terminal enacts + republishes, terminal no-ops, no-mapping/dangling-mapping skip, runner error surfaces with no silent ack).✅
make gazelle/make fmt— no diffs beyond the intended source changes.✅
make e2e-test— 2/2 targets pass (stovepipe, submitqueue), confirming parity: prioritize still admits the single chain path and exactly one build runs per batch.Issues
Stack